| name | vscode-sendkeys |
| description | Drive a running VS Code (this fork) from an agent — create files, run commands, type into the editor, go to a line, ctrl+click / go-to-definition, run terminal commands — by dropping action files into a spool directory. Use when the user says "drive vscode", "sendkeys to vscode", "have the agent control vscode", "run these actions in the editor", "open this file and jump to line N", or wires a session to control the IDE. Producer CLI is `vsc-sendkeys.cjs`; the listener is the bundled `vscode-sendkeys` extension. Full design in SENDKEYS_SPEC.md. |
vscode-sendkeys — agent driver for VS Code
Spool-directory protocol: you drop small action files into the spool dir; the
bundled vscode-sendkeys extension consumes each file and runs its lines through
VS Code's real command/editor pipeline (same path as the Command Palette).
Works out of the box — the extension always watches ~/.vscode-sendkeys
(auto-created) with zero setup; VSCODE_SENDKEYS_DIR only overrides the
location. Fire-and-forget by default; optional result channel when you need the
return value. Design + rationale: SENDKEYS_SPEC.md.
1. Launch the app (the skill invokes it — one command)
This skill is the core entrypoint: it launches the app itself via the
code-agent command (symlinked into ~/.local/bin, so it's on PATH from any
directory). The driver is on by default — any launch of this fork watches
~/.vscode-sendkeys — so code-agent just launches this fork's dev build (and
sets the spool dir explicitly for clarity):
code-agent
code-agent dir
code-agent status
First time on a fresh clone: one command installs prerequisites (nvm + the
pinned node), builds from source, and puts code-agent on your PATH. It's one
cross-platform Node script (code-agent-setup.cjs) — via Task or plain node,
same on macOS/Linux/Windows:
task setup
node code-agent-setup.cjs setup
Individual steps also exist: task prereqs / task build / task install
(or node code-agent-setup.cjs <step>). code-agent build delegates to the
same script.
Override the spool dir or enable logging by exporting before launch:
VSCODE_SENDKEYS_DIR=/some/dir VSCODE_SENDKEYS_LOG=1 code-agent.
Confirm it's watching: Command Palette → "Sendkeys: Show Watcher Status"
shows watching: <dir> (matching code-agent dir). No env, no arming, no
code-agent needed — even a plain scripts/code.sh launch watches the default
spool. (Verified out-of-the-box live: no VSCODE_SENDKEYS_DIR, plain launch,
plain CLI → actions landed on disk.)
Platforms
code-agent works on all three OSes — pick the matching entrypoint:
| OS | Command | Wraps | Put on PATH via |
|---|
| macOS / Linux | code-agent | scripts/code.sh | symlink ~/.local/bin/code-agent → repo code-agent |
| Windows | code-agent.cmd | scripts\code.bat | add the repo dir to PATH, or call code-agent.cmd by full path |
Both take the same subcommands (build / status / dir / -- <folder>) and
the same env vars (Windows default spool dir is %USERPROFILE%\.vscode-sendkeys).
The producer CLI is platform-neutral — always node vsc-sendkeys.cjs … — and the
listener extension is plain Node, so the whole driver runs unchanged on any OS.
Under the hood the launcher just sets VSCODE_SENDKEYS_DIR and execs the
platform's scripts/code.{sh,bat} — you can still launch manually with
VSCODE_SENDKEYS_DIR=… ./scripts/code.sh (or set VSCODE_SENDKEYS_DIR=… && scripts\code.bat on Windows) if needed.
2. Send actions (the CLI)
vsc-sendkeys.cjs (repo root, dependency-free). --dir defaults to
$VSCODE_SENDKEYS_DIR, so once armed you can omit it.
Multiple actions in one atomic, ordered file — the normal case:
node vsc-sendkeys.cjs do \
"open:src/vs/editor/browser/coreCommands.ts" \
"goto:100:5" \
"def:100:5" \
"type:// reviewed"
Each verb:payload runs in order, in one file, atomically (no torn reads, no
interleaving with another producer). This is what you generate from an agent.
One action: node vsc-sendkeys.cjs send "cmd:workbench.action.files.save"
Build up incrementally, then push:
node vsc-sendkeys.cjs create "tmp/demo.ts" "export const x = 1"
node vsc-sendkeys.cjs goto 1:14
node vsc-sendkeys.cjs push
Run terminal / shell commands (integrated terminal named sendkeys):
node vsc-sendkeys.cjs send "run:npm run compile"
3. Action verbs
verb:payload | Does | Under the hood |
|---|
create:<relpath> | Create + open an empty file | workspace.fs.writeFile → show |
open:<relpath> | Open an existing file | showTextDocument |
cmd:<commandId> | Run any VS Code command | commands.executeCommand(id) |
type:<text> | Type into the focused editor (autocomplete/brackets fire) — see focus note | core type command |
insert:<text> | Insert at cursor, focus-independent (reliable when unattended) | editor.edit |
goto:<line[:col]> | Move cursor to 1-based line/col + reveal | selection + revealRange |
def:<line:col> / ctrlclick:<line:col> | The ctrl+click gesture — go to definition | goto + editor.action.revealDefinition |
run:<shell cmd> | Run in integrated terminal | terminal.sendText |
save | Persist the target editor (focus-independent) | TextDocument.save() |
key:<chord> | Common chords (ctrl+s, cmd+shift+p, enter, …) | mapped → command |
For cmd with arguments use the incremental verb with a JSON array, e.g.
node vsc-sendkeys.cjs cmd workbench.action.terminal.sendSequence '[{"text":"ls\\n"}]'.
create with a body writes it straight to disk: create <relpath> <body...>
(or create:<relpath> + insert: + save to build it through the editor).
type vs insert (verified live). type routes through the real typing
pipeline and only lands in the focused editor — perfect interactively (VS
Code in the foreground), but a freshly-launched or background window may not
hold keyboard focus, so type can silently no-op there. For unattended /
headless runs use insert (focus-independent) and save to persist.
The extension also remembers the last created/opened editor, so a
create → insert → save batch stays consistent even without focus.
4. Get results back (optional)
Add --result. Tag lines you care about with @id and the CLI prints a JSON
map of { ok, value?, error? }, then returns:
node vsc-sendkeys.cjs --result do "open:src/main.ts" "@d def:20:10"
Without --result nothing is written back (pure fire-and-forget).
5. Rules & gotchas
\n is the line separator — a type: line can't contain a literal Enter;
send key:enter (or a separate action) for a newline.
- Ordering is per file. Actions in one
do/push run top-to-bottom and are
awaited, so open → goto → def see consistent editor state. Across
separate pushes, ordering is by filename (timestamped), not guaranteed
against a concurrent producer.
- At-most-once: the watcher deletes a file before running it — a crash
won't replay it. Don't rely on retries.
- Never append into the spool dir directly. The CLI always stages to a
dotfile and
rename()s in (atomic). Hand-rolled producers must do the same.
key: is best-effort for uncommon chords — prefer cmd:<commandId> when
you know the command id (it's exact and always works).
6. Verify it's working
node vsc-sendkeys.cjs send "cmd:workbench.action.showCommands"
node vsc-sendkeys.cjs do "create:tmp/sk-ok.txt" "type:it works"
ls "$VSCODE_SENDKEYS_DIR"
Files
extensions/vscode-sendkeys/ — the listener (plain JS, no build; reload window to pick up edits).
vsc-sendkeys.cjs — the producer CLI (this skill's node … calls).
SENDKEYS_SPEC.md — design, protocol, and the optional core hook for full key: chord parity.