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
- pre without post: the failure surfaces downstream, and you can't tell which step broke.
- Missing/null
undo: apply requires explicit irreversible-action confirmation;fs.deletewithout an undo remains prohibited. Preview shows the step as non-undoable. - relative paths: always use
path.join(input.root, ...), and never escape the root. - side effects inside
plan(): plan runs twice (preview + apply); any write happens twice.
Debugging
node bin/deskshell.ts run my-skill --mode preview
Preview prints risk and undoable per step — the fastest self-check you have.