Writing a skill

A skill is a directory: skill.json + index.ts. The only function that matters is plan().

Your first skill in 15 minutes

This whole flow is automated in scripts/onboarding-test.ts: it packs the tarball, installs it into a throwaway prefix, and runs exactly these steps. If the checklist and the product ever disagree, that test goes red.

# 0. prerequisites: Node 22+ on PATH (the shim calls `node`)
deskshell --version

# 1. scaffold — lands in ~/.deskshell/skills/<name>, NOT in any repo
deskshell new hello-report
# → creates skill.json + index.ts, then prints the next steps

# 2. edit ONE thing: open index.ts and change plan()
#    (the template already lists files and writes report.md)

# 3. does it show up?
deskshell list | Select-String hello-report

# 4. preview: nothing is touched
deskshell run hello-report --root .\some-folder --mode preview

# 5. apply, then undo — this is the whole trust story in two commands
deskshell run hello-report --root .\some-folder --mode apply
deskshell undo --last --root .\some-folder

Expected preview shape:

汇总:...
步骤:
  write:report  [ok]

Three things that trip people up (all fixed, all covered by the test):

| Symptom | Cause | Fix | | --- | --- | --- | | '"node"' is not recognized | Node installed but not on PATH | add its bin/install dir to PATH, then reopen the shell | | deskshell new created a packages/skills folder next to your files | you are on an older version | upgrade; new now writes to your user skill dir, use --in-repo when contributing to this repo | | --last needs a positive integer | older version required a count | deskshell undo --last now means "the last one"; --last 3 still works |

Minimal template

import type { Action, Skill } from "../../core/src/types.ts";

export const skill: Skill = {
  manifest: {
    name: "hello-move",
    version: "0.1.0",
    description: "Move a.txt into docs/",
    entry: "./index.ts",
    capabilities: [
      { id: "fs.write", scope: "{{root}}", reason: "move a file inside the root" },
    ],
  },
  async plan({ root }) {
    return [
      {
        id: "mkdir:docs",
        kind: "fs.mkdir",
        path: `${root}/docs`,
        risk: "low",
        post: [{ kind: "path.exists", path: `${root}/docs` }],
        undo: { kind: "fs.rmdir", path: `${root}/docs` },
      },
      {
        id: "move:a",
        kind: "fs.move",
        from: `${root}/a.txt`,
        to: `${root}/docs/a.txt`,
        risk: "low",
        pre: [{ kind: "path.exists", path: `${root}/a.txt` }],
        post: [{ kind: "path.exists", path: `${root}/docs/a.txt` }],
        undo: { kind: "fs.move", from: `${root}/docs/a.txt`, to: `${root}/a.txt` },
      },
    ];
  },
};

Three authoring guidelines

Provide a post-assertion for every action. Check that the target state actually holds. Protocol v0 keeps post optional for compatibility; the kernel validates supplied assertions but does not require one.

Provide an undo for every reversible action. Missing/null undo and irreversible: true require explicit confirmation before apply. Confirmed irreversible actions cannot be recovered and must not be represented by a placeholder undo. For reversible deletion, use a move into a trash folder. fs.delete without an undo is rejected even with confirmation.

Never touch system APIs. No fs.writeFileSync, no child_process, no UIA inside a skill. Produce Actions and let the kernel execute them.

Reading the screen

plan(input, ctx) receives a context with ctx.driver:

async plan({ root }, ctx) {
  const tree = await ctx.driver!.tree({ titleRegex: "Edge" }, 8);
  const tabs = walk(tree).filter((n) => n.controlType === "TabItem");
  // turn the observation into Actions
}

Observation is read-only and produces no side effects. Every mutation must come back as an Action.

Platform gating

{ "platforms": ["win32"] }

Omitting it means all platforms. Declaring it makes deskshell list mark the skill unavailable elsewhere — better than failing at runtime.

Common mistakes

Debugging

node bin/deskshell.ts run my-skill --mode preview

Preview prints risk and undoable per step — the fastest self-check you have.