@shardflux/cli 0.24.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +227 -1
  3. package/dist/account-args.js +5 -0
  4. package/dist/account-pool.js +5 -1
  5. package/dist/acp-permission-elsewhere.d.ts +38 -0
  6. package/dist/acp-permission-elsewhere.js +85 -0
  7. package/dist/acp-rpc.d.ts +2 -1
  8. package/dist/acp-rpc.js +11 -2
  9. package/dist/acp.js +95 -22
  10. package/dist/agent-credentials.d.ts +3 -0
  11. package/dist/agent-credentials.js +7 -0
  12. package/dist/args.d.ts +1 -1
  13. package/dist/args.js +143 -8
  14. package/dist/board-args.d.ts +6 -0
  15. package/dist/board-args.js +155 -0
  16. package/dist/board-commands.d.ts +62 -0
  17. package/dist/board-commands.js +395 -0
  18. package/dist/codex-cloud.d.ts +30 -3
  19. package/dist/codex-cloud.js +86 -12
  20. package/dist/conversation-commands.d.ts +30 -0
  21. package/dist/conversation-commands.js +312 -9
  22. package/dist/conversation-transcript.d.ts +44 -0
  23. package/dist/conversation-transcript.js +206 -0
  24. package/dist/conversations.d.ts +5 -2
  25. package/dist/conversations.js +20 -10
  26. package/dist/device-approvals.js +4 -3
  27. package/dist/device-bridge.d.ts +42 -1
  28. package/dist/device-bridge.js +223 -12
  29. package/dist/device-folders.d.ts +58 -0
  30. package/dist/device-folders.js +228 -0
  31. package/dist/device-helper.d.ts +6 -0
  32. package/dist/device-helper.js +87 -14
  33. package/dist/drive-commands.d.ts +25 -0
  34. package/dist/drive-commands.js +201 -0
  35. package/dist/handlers.js +6 -1
  36. package/dist/main.js +3 -0
  37. package/dist/move-files.d.ts +39 -0
  38. package/dist/move-files.js +167 -0
  39. package/dist/move.js +12 -11
  40. package/dist/onboarding.d.ts +6 -2
  41. package/dist/onboarding.js +24 -6
  42. package/dist/project-git.d.ts +7 -1
  43. package/dist/project-git.js +16 -3
  44. package/dist/project-preparation.d.ts +7 -0
  45. package/dist/project-preparation.js +14 -0
  46. package/dist/project-template-setup.js +32 -11
  47. package/dist/router-commands.d.ts +22 -0
  48. package/dist/router-commands.js +179 -0
  49. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,124 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - Faster conversation starts (0.24.1): the project's dependency fingerprint is read from Git's own file list instead of walking the whole folder, so ignored and untracked trees beside the project (agent worktrees, build output) no longer hold a start on **Getting your project…**. A folder with 1.2 million such files took 32 s per start; it now takes 0.3 s, with the same fingerprint.
6
+ - A folder without Git that `shard` makes a repository of (a first chat, `shard projects link`) starts with its files as
7
+ the first commit (0.25.0+; `.gitignore` respected, large media stay untracked), so a conversation starts from the
8
+ same commit as the folder and its work comes back by fast-forward, as in any repository. Before, an empty first
9
+ commit left every file to the starting snapshot and the landing asked the agent to merge. A repository of your own
10
+ keeps its staged and untracked files as they are.
11
+ - Device requests (0.25.0+) are answered between turns and as a turn ends; only an archived conversation refuses them.
12
+ - Background preparation of a project with no build or test command ends ready (0.25.0+): there is nothing to check,
13
+ so `shard projects sync list` and the folders `shard projects link` adds no longer show an error.
14
+ - `shard move` (0.25.0+) brings the files the session used outside the project: files you @-mentioned or named in a
15
+ prompt, files it read or edited, and the files a handed file names (a `~/Downloads` handoff and what it points to).
16
+ Each goes to the same place under the VM's home, and this computer's home path leads there, so the history's paths
17
+ open in the cloud. Before, only the checkout and the transcript moved, and a moved session stopped at a handoff it
18
+ could no longer open. Key and sign-in stores (`~/.ssh`, `~/.aws`, `.env` files, private keys) stay here. Up to 64
19
+ files, 64 MiB in all, 16 MiB each; the command names what it brings and what stays, and `--json` adds `files` and
20
+ `files_left`.
21
+
22
+ - Tunnel recovery (0.24.1, SDK 0.27.3): a foreground reverse tunnel replaces its guest helper automatically when replay is lost. Existing TCP peers reset; fresh connections reach the new listener. SSH and device port-forward connections close cleanly for a new connection instead of looping on an unrecoverable cursor.
23
+ - Folders on this computer (0.25.0+): an agent can list a folder here (`dir_list`: names, kinds, sizes and Git
24
+ remotes, never file contents), make one of your projects from it (`project_link`, with an optional first task), and
25
+ move a file to or from anywhere here with an absolute or `~/` path. Nothing runs before you approve it: **Allow once**,
26
+ or **Always for this folder**, which covers that kind of request for that folder and what is inside it, from any of
27
+ your conversations. A path outside every such folder asks again, and commands never run under a folder grant. The
28
+ background helper now answers for your signed-in project (and, signed in without a key, every project you can reach),
29
+ so a home conversation or an Orchestrator task can ask too, and reads every project's requests in one poll.
30
+ - `shard projects link <path> [--prompt <text>] [--app claude|codex]` (0.25.0+) makes a local folder one of your
31
+ projects: prepared in the background and kept as this computer's live copy; `--prompt` also starts a conversation
32
+ there. `--json`: `{project: {project_id, repo_remote, folder, name, conversation_id?}}`.
33
+ - `shard device-helper` (0.25.0+) asks the desktop app first when it serves `device-approver.sock` in the config folder,
34
+ so approvals show there as Needs-you items; otherwise the menu bar, an editor (ACP) or `shard device approve` as before.
35
+
36
+ - `shard route <text...>` and `shard ask <question...>` (0.25.0+, SDK 0.28.0): the board's input bar in a terminal.
37
+ `shard route` prints where the input goes (its project or your home) and whether it is a task, a question or a steer
38
+ for an open task, with each task in your words and its title when the input asks for several; `shard ask` streams
39
+ the answer to a quick question about your work as it is written. `--task <task-id>` names the selected task, `--org`
40
+ and `--project` the scope (signed in: your organization's projects; with `SHARDFLUX_API_KEY` set: that key's
41
+ project), and `--json` prints the API's `RouterResult` or the whole answer.
42
+ - The board (0.25.0+, SDK 0.28.0): `shard board` shows what waits on you first, then every task in Now, Next and
43
+ Later with its state, title or words and project; `--follow` prints each change as it happens, until Ctrl-C
44
+ (`--json`: the board, then one feed answer per line). `shard task add <words...>` (`--next`, `--later`, `--first`,
45
+ `--after <task-id>`, `--home`, `--project`), `show`, `edit` (`--text`, `--title`, `--now|--next|--later`,
46
+ `--first|--after`, `--keep`, `--clear`), `rm`, `steer` and `clear-done` manage tasks in your own words, and `shard
47
+ task show` lists each report with its evidence. `shard needs-you` lists everything your agents wait on, and `shard
48
+ needs-you answer <id>` answers any of it (`allow|always|deny`, `--everywhere`, `--pick`, `--text`,
49
+ `land|keep|discard`, `--keep <task-id>`). `shard approvals` shows the approval mode and always-allow rules; `set
50
+ <ask|routine|trusted|default> [--project <id>|--default]`, `allow <tool> <pattern...> [--project <id>|--everywhere]`
51
+ and `remove <rule-id>` change them.
52
+
53
+ - Project drive (0.25.0+, SDK 0.28.0): `shard drive put <local-file> [<drive-path>]` saves a file to the project's
54
+ drive as a new version and prints its path and sha256 (the drive path defaults to the file's name; `-` reads standard
55
+ input; `--content-type`); `shard drive get <path>` writes its bytes to standard output or with `-o` to a file, checked
56
+ against their SHA-256, and `--sha256` reads exactly that version, also after the path was overwritten or removed;
57
+ `shard drive ls [prefix]` lists the current files (size, saved time, type, path; `--limit`, `--cursor`, `--all`);
58
+ `shard drive rm <path>`. The API key's project or `--project`; signed in, any of your projects by id or slug.
59
+ `--json` everywhere.
60
+ - Model template (0.25.0+, SDK 0.28.0): `shard models` prints which harness, model and effort each role of a task
61
+ (master, builder, hard, light, reviewer) runs with, until when a role waits for its account's next usage window and
62
+ what the other provider would run now; `--harness` and `--attempt` show a running task's and a retry's. `shard models
63
+ primary claude|codex|clear` sets the project's primary provider, `--org <id|slug>` the organization's default.
64
+ - `shard conversations list` and `shard conversations feed` (0.25.0+, SDK 0.28.0): signed in without an API key, every
65
+ project you work in, across your organizations, on one cursor; `--org <id|slug>` narrows them to one organization
66
+ (and uses your sign-in when you have one). With an API key they cover its project, as before.
67
+
68
+ - `shard conversations answer <id> <permission-id> --pick <option>` (0.25.0+) answers the agent's question (Claude Code's
69
+ AskUserQuestion) with exactly the picked option; repeat `--pick` for a multi-select question, `--pick
70
+ "<question>=<option>"` when it asks several; `deny` declines it.
71
+
72
+ - `shard acp`: a conversation computer on a newer template keeps the models the session offered until its live list
73
+ arrives, so the editor's first model choice is taken; before, the list went empty for a few seconds after
74
+ `session/new` and `session/set_model` answered "This model is no longer available".
75
+
76
+ - Codex accounts (0.25.0+, SDK 0.28.0): `shard agents open` of a Codex Cloud conversation checks the Codex app on this
77
+ computer first. When the app is signed out or signed in to another ChatGPT account than the thread's, it says so in
78
+ one line with the account to sign in as and the thread's web link, instead of opening a link the app cannot show.
79
+ Earlier threads are listed with their accounts (`Earlier history: <url> (account)`). `--json` adds `account`,
80
+ `codex_app_login`, `account_mismatch` (`line`, `sign_in_as`, `web_url`) and each earlier thread's `account`.
81
+ `shard agents accounts use` of a Codex account says when the Codex app here is signed in to another account
82
+ (`--json`: `codex_app`).
83
+ - Codex sign-in (0.25.0+, SDK 0.28.0): when Codex Cloud refuses the copied ChatGPT login, `shard setup --inspect`
84
+ says so in one line with the next step: `codex login` (Sign in to Codex again), then, once this computer has the new
85
+ sign-in, Copy sign-in (`shard agents credentials sync`); `--connect` copies it. `shard ./ codex` and
86
+ `shard agents codex enable` copy this computer's login and continue when it is new, or stop at once with
87
+ `codex_sign_in_required` and that line. The Codex remote device is never restarted for a sign-in (its Cloud threads
88
+ keep running), and one starting again with the new login reads as connecting.
89
+
90
+
91
+ - `shard conversations start` starts a conversation without a folder: from the project hub (`--repo`, `--branch`), or with `--home` a coordinator on your machine that spawns threads in the project's repositories. `--app`, `--model`, `--effort`, `--title`, `--project`; `--prompt` sends the first message, with `--file` attachments. Prints the conversation's id, key and state (`--json`: the conversation, its workspace and the message).
92
+
93
+ - `shard conversations send <id> <text> --file <path>` (repeatable, up to 20 files of 64 MiB each) puts each file in the conversation's computer before the message, which lists their paths, and prints `Attached <path>` for each. The text may be left out when files are attached, and unquoted words are sent as one message.
94
+
95
+ - Home conversations list as `home` in `shard conversations list` and `get`, and open without a repository in `/home/user`.
96
+
97
+ - `shard acp`: a permission prompt answered through the API (the Shard client, `shard conversations answer`, MCP)
98
+ while the editor shows it is withdrawn from the editor (`$/cancel_request`) and the turn goes on in the editor; before,
99
+ the editor saw nothing more of the turn until it answered its own prompt.
100
+
101
+ - Permission requests (0.25.0+, SDK 0.28.0): `shard conversations permissions <id>` lists what a conversation's agent
102
+ asked permission for before running a tool (Claude Code, Codex and ACP agents), oldest first, with `--state`,
103
+ `--all` and `--json`; `shard conversations answer <id> <permission-id> allow|always|deny` answers one.
104
+ `shard conversations get` names the requests a `needs_you` conversation waits on (`Waiting on: Run npm test`).
105
+
106
+ - `shard conversations watch <id>` shows a conversation's terminal live and read-only: its last 256 KiB, then the agent's
107
+ output as it works, until its session exits or Ctrl-C. It never types into the terminal and never keeps the computer
108
+ awake while the agent is idle.
109
+ - `shard conversations get <id>` prints one line per port a process listens on in the conversation's computer: the
110
+ port, the program, and its URL once exposed (or `127.0.0.1 only`). `--json` includes `pty_id`, `listening_ports` and
111
+ `listening_ports_at`.
112
+
113
+ - `shard conversations feed` (0.25.0+): each conversation of the project as it changes, until interrupted; `--json`
114
+ prints one `{conversation, cursor}` object per line, `--cursor` continues after one. `shard conversations list
115
+ --updated-after <time>` filters by `updated_at`.
116
+ - Conversation titles in human output are the agent's own short title (`title_short`) once it has one.
117
+
118
+ - `shard acp` opens a conversation by its id: `session/load` with a `conv_…` session id attaches an ACP client to that conversation's live session in its computer, with its history replayed by the agent and no local folder synced.
119
+
120
+ - `shard conversations transcript <id>` prints a conversation’s transcript, the agent’s own Claude Code or Codex JSONL byte for byte, for working, parked and finished conversations alike (`--offset`, `--session`; `--follow` keeps printing new lines). `shard conversations events <id>` follows it live: state changes, new transcript lines, tool calls and the reply as it streams; `--json` prints one event per line.
121
+
3
122
  ## 0.24.0
4
123
 
5
124
  - Finish all active device port-forward cleanup and close the local listener and cell before returning the original tunnel-close error. Repeated stop and abort keep the same error and promise.
package/README.md CHANGED
@@ -144,6 +144,21 @@ shard ./ --on fix-login --review # a read-only Claude reviewer of it
144
144
  the new thread. Codex threads are Codex Cloud threads, on the web and your phone too. `shard agents list` shows each
145
145
  thread's conversation and computer.
146
146
 
147
+ Start a conversation without a folder (0.25.0+): its repository is checked out from the project's saved branches, or
148
+ with `--home` a coordinator starts on your machine and spawns threads in the project's repositories.
149
+
150
+ ```sh
151
+ shard conversations start --prompt "Add CSV export to the invoices page" --file mockup.png
152
+ shard conversations start --home --title "Plan the billing work"
153
+ shard conversations send conv_6qz2dmu4kd3xa7v5l2hgmfnpjy "Here is the failing run" --file ci.log
154
+ ```
155
+
156
+ `--repo` and `--branch` choose the repository and the branch it starts from (default: the project's most recently
157
+ active repository, from `main`, then `master`); `--app`, `--model` and `--effort` work as for `shard ./`. It prints the
158
+ conversation's id, key and state; `shard agents open <id>` opens it. `--file` (repeatable, up to 20 files of 64 MiB
159
+ each) puts each file in the conversation's computer before the message and prints where it landed; `--json` returns
160
+ the stored paths.
161
+
147
162
  Choose the model and reasoning effort a new thread starts with (0.17.0+):
148
163
 
149
164
  ```sh
@@ -235,6 +250,8 @@ shard conversations try <conversation>
235
250
  Your branch opens in `.shard/worktrees/<name>`, syncs both ways, and its dev servers appear at localhost.
236
251
  Ctrl-C stops local sync. Your main checkout stays available for your own work.
237
252
  Device tools ask for approval on your computer before preparing the conversation’s local worktree. With CLI 0.15.0 and menu bar 0.3.0, Review shows the exact command, file direction, paths or port mapping; access outside the conversation folder only offers Allow once. Live transcripts stay in
253
+
254
+ From 0.25.0, an agent can also list a folder on your computer, make one of your projects from it, and move a file to or from anywhere on it by an absolute or `~/` path. Each asks first: **Allow once**, or **Always for this folder**, which covers that kind of request for that folder and everything inside it. `shard projects link <path> [--prompt <text>]` does the same from a terminal: the folder is prepared in the background, kept as this computer’s live copy, and `--prompt` starts a conversation there.
238
255
  `~/.config/shardflux/conversations/`; ended sessions enter Claude Code’s or Codex’s native history. Transcript files up to 64 MiB are supported (0.14.0+).
239
256
  From 0.14.3, transcript copying runs independently of device approvals. Unknown entries are skipped, and failed copies retry with backoff; diagnostics name the conversation and affected entry.
240
257
 
@@ -687,6 +704,7 @@ shows the status; `shard version --check` asks now (`--json`: `{"cli", "sdk", "u
687
704
  | `version [--check]` | Print the CLI and SDK versions and the update status (`--check` asks the API now). |
688
705
  | `usage [--org <id>] [--estimate \| --series \| --workspace <id\|key>] [--from T] [--to T] [--granularity hour\|day] [--meter M]` | Usage summary for the current period: plan, meters and allowances. `--estimate`: the bill so far and projected allowance use; `--series`: usage over time; `--workspace`: one workspace's usage over time (0.5.0+). The summary includes the [overage](#usage-and-overage) block (0.5.1+). |
689
706
  | `feedback [--yes] [--category C] [--workspace W] [--request-id ID] [--error-code CODE] [--command TEXT] [--from-file F\|-] <message...>` | Preview feedback; `--yes` sends after explicit user authorization (0.22.0+). See [Feedback](#feedback-preview-and-user-approval). |
707
+ | `route [--task ID] [--org O] [--project P] <text...>`, `ask [--task ID] [--org O] [--project P] <question...>` | The board's input bar (0.25.0+): where a line of input goes (its project or your home; a task, a question or a steer, split per task), and a quick question about your work, answered as it is written. |
690
708
  | `workspaces open <key> --template <slug> [--secret NAME]... [--input NAME=value]... [--input-file F] [--lifetime persistent\|session] [--mode processful\|file-first] [--cpu-millis N --memory-mib N --disk-gib N] [--allocation-mode fixed\|elastic] [--memory-held N] [--no-wait] [--timeout 5m] [--timing]` | Open by key. Creates the workspace on first use and reconnects or resumes it afterwards, never resetting it. Waits until it is ready (the template's start commands and services included) unless you pass `--no-wait`. `--secret` binds secret names (replacing the binding of an existing workspace). `--input` (0.4.0+) passes the template's text inputs (replacing those of an existing workspace); `--input-file` reads `NAME=value` lines or a JSON object. `--lifetime session`: the workspace is discarded when the session ends (`workspaces close` or the idle timeout), and the key then opens a new workspace. `--mode file-first` (0.5.0+) opens a [file-first workspace](#file-first-workspaces); the default is processful for a new key and the stored mode for an existing one. `--allocation-mode elastic` (0.8.0+): [elastic memory](#elastic-memory-080). |
691
709
  | `workspaces list [--state S] [--prefix P] [--lifetime persistent\|session\|any] [--purpose standard\|template_draft\|template_test\|any] [--include-deleted] [--all] [--limit N] [--cursor C]` | One page, or every page with `--all`. The next cursor is printed on stderr. By default only persistent standard workspaces are listed. A file-first workspace's state reads `running (file-first)` (0.5.0+). |
692
710
  | `workspaces get <id\|key>` | One workspace (deleted ones included), with the startup of its template's start commands and services (0.4.0+): a failed step shows its exit code, reason and output tail. Its mode, and a file-first workspace's tree revision (0.5.0+). Its memory (0.8.0+): `fixed 4096 MiB`, or `elastic: promised …, held …, plugged +… MiB`. The template version whose [immutable paths](#immutable-paths-080) it has mounted (0.8.0+): `immutable paths v3 (mounted read-only)`. |
@@ -1446,7 +1464,7 @@ A message from claude.ai or the Claude app wakes a waiting Claude conversation a
1446
1464
 
1447
1465
  Run `shard ./` in your project to start Claude Code or Codex, usable immediately while setup runs in the background.
1448
1466
 
1449
- New project folders are ready to use: `shard ./` and `shard agents new` initialize Git on `main` when needed and create an empty initial commit (CLI 0.14.2+). Existing files and staged work stay yours; the initial commit contains no files. Repositories without commits keep their current branch. If Git identity is missing, that one commit uses `Shardflux <noreply@shardflux.dev>` without changing your Git configuration.
1467
+ New project folders are ready to use: `shard ./` and `shard agents new` initialize Git on `main` when needed (CLI 0.14.2+). From 0.25.0 a folder `shard` makes a repository of gets its files as the first commit (`.gitignore` respected; large media stay untracked), so a conversation's work comes back by fast-forward; a repository of your own without commits keeps its staged and untracked files behind an empty initial commit. Repositories without commits keep their current branch. If Git identity is missing, that one commit uses `Shardflux <noreply@shardflux.dev>` without changing your Git configuration.
1450
1468
 
1451
1469
  Machine saves require a machine source VM (`labels.shard_layer = "machine"`). On your computer, run
1452
1470
  `shard machine open --json`, make changes in that VM, then `shard machine save --wait`. The computer's CLI
@@ -1559,6 +1577,40 @@ account, make another one active first; your own you can always remove. `--json`
1559
1577
 
1560
1578
  From 0.14.0, `shard conversations list|get|send|wait|land|move` manages project conversations. `shard c` is the short form. Conversations retain their branch and transcript as their VM binding changes.
1561
1579
 
1580
+ Watch an agent work: `shard conversations watch <id>` shows the conversation's terminal live, read-only. It starts
1581
+ with the last 256 KiB the agent printed, then streams everything it prints, until its session exits or you press
1582
+ Ctrl-C. Nothing you type reaches the agent, and watching never keeps its computer awake while the agent is idle.
1583
+ `shard conversations get <id>` lists the servers running in the conversation's computer, one line per port, with
1584
+ each port's URL once exposed (`shard ws port expose`); `--json` returns them as `listening_ports`, and the terminal as
1585
+ `pty_id`.
1586
+
1587
+ ```sh
1588
+ shard conversations watch conv_a7k2m4q6s3t5v7x2z4b6d3f5h7
1589
+ shard conversations get conv_a7k2m4q6s3t5v7x2z4b6d3f5h7
1590
+ # Fix login
1591
+ # working
1592
+ # shard/fix-login
1593
+ # 5173 node https://5173-<handle>.shardflux.app (exposed)
1594
+ # 5432 postgres 127.0.0.1 only
1595
+ ```
1596
+
1597
+ `shard conversations transcript <id>` (0.25.0+) prints the conversation's transcript: the agent's own Claude Code or
1598
+ Codex JSONL, byte for byte, for a working, parked or finished conversation alike, without waking its VM. `--follow`
1599
+ keeps printing lines as the agent writes them; `--offset` starts at a byte offset and `--session` reads an earlier
1600
+ session. `shard conversations events <id>` (0.25.0+) follows the conversation live: its state, new transcript lines,
1601
+ tool calls and the reply as it streams; `--json` prints one event per line.
1602
+
1603
+ ```sh
1604
+ shard c transcript conv_k3m5q7s2u4w6y3b5d7f2h4j6ln > session.jsonl
1605
+ shard c transcript conv_k3m5q7s2u4w6y3b5d7f2h4j6ln --follow | jq -c 'select(.type=="assistant")'
1606
+ shard c events conv_k3m5q7s2u4w6y3b5d7f2h4j6ln
1607
+ # state: working
1608
+ # Looking at the login test.
1609
+ # tool: Read login.test.ts (pending)
1610
+ # tool: Read login.test.ts (completed)
1611
+ # state: done
1612
+ ```
1613
+
1562
1614
 
1563
1615
  Desktop project connections (0.14.0+): `shard agents open codex-app <project>/<workspace>` and
1564
1616
  `shard agents open claude-desktop <project>/<workspace>` register **Shardflux · <project>** in the app.
@@ -1568,6 +1620,49 @@ Conversation JSON includes `join_mode` and the worktree used by reviewers and wo
1568
1620
 
1569
1621
  `shard conversations resume <id>` continues a saved conversation in a fresh VM under the same identity (0.14.0+). `shard ./ --resume` uses this same path. `shard conversations reader <project-id>` opens a small read-only workspace for saved branches and transcripts; the Mac transcript mirror opens it automatically when no project VM is live. Adaptive idle reclaims the reader.
1570
1622
 
1623
+ ### Route input and ask about your work (0.25.0+)
1624
+
1625
+ `shard route` reads a line of input the way the board's input bar does: which project it belongs to (or your home),
1626
+ whether it is a new task, a question or a steer for an open task, and each task in your own words when it asks for
1627
+ several. `shard ask` answers a quick question about your work from the board, printing the answer as it is written.
1628
+
1629
+ ```sh
1630
+ shard route fix the hero image on the landing page and add a GPU row to the pricing table
1631
+ # 2 tasks in site
1632
+ # 1. Fix the landing page hero image
1633
+ # fix the hero image on the landing page
1634
+ # 2. Add a GPU pricing row
1635
+ # add a GPU row to the pricing table
1636
+ shard route --task task_healthzhealthzhealthzhealt return 503 instead of 500 when the database is down
1637
+ # steer task_healthzhealthzhealthzhealt in status-board
1638
+ # return 503 instead of 500 when the database is down
1639
+ shard ask what is the healthz work doing right now
1640
+ # Your “Add /healthz endpoint” agent is working on shard/healthz with 2 commits and was last active 4 minutes ago. …
1641
+ ```
1642
+
1643
+ Signed in, both read every project of your organization (`--org <id|slug>` when you belong to several, `--project` to
1644
+ lean to one); with `SHARDFLUX_API_KEY` set they read that key's project. `--task` names the selected task. `--json`
1645
+ prints the API's object: the route (`kind`, `parts`, `task_ref`, `uncertain`, …) or the whole answer (`text`,
1646
+ `needs_work`, `sources`, …). An answer that needs work ends with "Make it a task?". Neither starts work or wakes a
1647
+ computer.
1648
+
1649
+ ### Permission requests (0.25.0+)
1650
+
1651
+ When a conversation's agent asks before it runs a tool (Claude Code, Codex or an ACP agent), the conversation is
1652
+ `needs_you` and `shard conversations get` names what it waits on. Answer it from any terminal:
1653
+
1654
+ ```sh
1655
+ shard c permissions <conversation-id> --state pending
1656
+ # 01a1300c-… pending Run npm test npm test
1657
+ shard c answer <conversation-id> <permission-id> allow # or always, deny
1658
+ # Allowed once: Run npm test
1659
+ ```
1660
+
1661
+ Each line is the request id, its state, what the agent wants to do, the command, path or URL, and the decision once
1662
+ answered. Without `--state` every request is listed, oldest first; `--all` reads every page and `--json` prints `data`
1663
+ and `nextCursor`. `always` lets the agent's harness stop asking for this kind of call. The agent goes on with its turn
1664
+ as soon as you answer, and a parked computer wakes to take the answer.
1665
+
1571
1666
  ### Starting from your working state (0.22.0+)
1572
1667
 
1573
1668
  A new conversation starts from your worktree as it is: committed history, uncommitted edits, untracked files and your
@@ -1608,6 +1703,52 @@ With CLI 0.23.0+, a full conversation key or id opens that same Claude or Codex
1608
1703
 
1609
1704
  Use `shard agents open <workspace> --in auto --json` to resolve where a conversation opens: a saved target first; then, for Claude (0.15.1+), the conversation itself in Claude Desktop when it is installed (`{"target":"claude-desktop","url":"claude://code/session_…","app_bundle_id":…}`), else Claude Code in the terminal (`{"target":"terminal","command":["shard","agents","open","<workspace>"]}`; without `--json` it attaches in this terminal). Codex Cloud opens its current thread in your browser (CLI 0.23.0+). claude.ai opens with `--in web`. The caller opens the `url` or app, or runs the command in a terminal. Explicit `--in terminal` retains terminal attachment. The menu uses this resolver for new and existing conversations, without provider URLs in Swift.
1610
1705
 
1706
+ ## The board (0.25.0+)
1707
+
1708
+ The board is one list of what you asked your agents for, the same on every device: what waits on you first, then every
1709
+ task in Now, Next and Later, each in your own words. The agent that does the work seals a report with its evidence, and
1710
+ reading the board never wakes a computer. The board commands use your project API key: the board is its project's.
1711
+
1712
+ ```sh
1713
+ shard task add Fix the wake race in resume # your words, kept exactly; the project's master starts the work
1714
+ # task_h7g4d7tqyv2pz3a2npyo4rpgzq now queued
1715
+ shard board
1716
+ # Needs you
1717
+ # perm_01a1300c-… Run npm test? npm test
1718
+ #
1719
+ # Now
1720
+ # task_h7g4d7tqyv2pz3a2npyo4rpgzq working Fix the wake race in resume 01a1415f-…
1721
+ shard needs-you answer perm_01a1300c-… always # allow, always or deny
1722
+ # Always allowed: Run npm test (rule rule_ym4xywjgj7vvvrhkqlduotkvb4)
1723
+ shard task show task_h7g4d7tqyv2pz3a2npyo4rpgzq # the plan, then each report with its evidence
1724
+ shard board --follow # each change as it happens, until Ctrl-C
1725
+ ```
1726
+
1727
+ **Tasks.** `shard task add <words...>` adds a task to Now (`--next`, `--later`), last in its bucket unless `--first` or
1728
+ `--after <task-id>` places it; `--home` runs a personal task on the project's machine home. `shard task edit <id>`
1729
+ changes the words (`--text`, and the task's master hears it), the title (`--title`), the bucket (`--now`, `--next`,
1730
+ `--later`) and the order (`--first`, `--after`), keeps an agent's suggestion as your own (`--keep`) or clears the task
1731
+ (`--clear`; `--no-clear` brings it back). `shard task steer <id> <words...>` sends steering words to the task's master,
1732
+ `shard task rm <id>` deletes the task, and `shard task clear-done` clears every finished one. `shard task show <id>`
1733
+ prints the task, its plan and each report: milestone, outcome and evidence (`tests 12/12 (pnpm test)`).
1734
+
1735
+ **Needs you.** `shard needs-you` lists permission requests, questions, credential requests, finished work to land and
1736
+ Later tasks to keep, oldest first. `shard needs-you answer <id>` answers any of them: `allow`, `always` (also adds the
1737
+ always-allow rule for the project; `--everywhere` for every project) or `deny` for a permission request, `--pick` for an
1738
+ agent's question in one, `--text` for a question, `land`, `keep` or `discard` for finished work, and `--keep <task-id>`
1739
+ (or `keep`, `discard`) for Later tasks. The agent goes on as soon as you answer.
1740
+
1741
+ **Approvals.** The approval mode answers your agents' permission requests before they reach you: `ask` sends every one
1742
+ to you, `routine` (the default) approves tests, builds, reads and installs, `trusted` approves everything except
1743
+ deploys, pushes to main and spending. `shard approvals` shows the modes and rules. `shard approvals set <mode>` sets the
1744
+ project's mode (`--project <id>`; `default` puts it back on the default mode), `shard approvals allow <tool>
1745
+ <pattern...>` always allows a tool for commands that start with the pattern (`shard approvals allow Bash pnpm test`),
1746
+ and `shard approvals remove <rule-id>` removes a rule. Requests already waiting that a new mode or rule covers are
1747
+ answered at once.
1748
+
1749
+ Each line is one item, tab-separated, id first. `--json` prints the API's objects; `shard board --follow --json` prints
1750
+ the board on its first line, then one feed answer per line.
1751
+
1611
1752
  ## Codex Cloud (0.15.0+)
1612
1753
 
1613
1754
  New Codex conversations use `shard agents new --app codex --path <directory>` or `shard ./ codex`. Each gets a fresh conversation VM and a genuine Codex Cloud thread. `shard agents open <conversation-id>` opens the existing thread without waking its VM. These paths create no SSH device entries and need no pairing.
@@ -1633,10 +1774,95 @@ Older clients read the same session response fields.
1633
1774
 
1634
1775
  Cloud history (0.23.0+): `shard agents open <conversation-id> --original` opens the original chat; `--history <thread-id>` opens an earlier linked chat. `--json` includes `previous_threads` with each history URL. Sign in with the Codex account that owns the selected chat.
1635
1776
 
1777
+ Codex accounts (0.25.0+): before opening a Cloud thread, `shard agents open` checks which ChatGPT account the Codex app on this computer is signed in to. When it is not the thread's account, it names the account to sign in as and prints the thread's web link. Earlier threads are listed with their accounts.
1778
+
1636
1779
  ## Complete conversation lists (0.23.1+)
1637
1780
 
1781
+ `shard conversations feed` (0.25.0+) prints each conversation of the project the moment it changes, until you stop it:
1782
+ `<id> <title> <state>`, or with `--json` one `{conversation, cursor}` object per line (`--cursor` continues after
1783
+ it). `shard conversations list --updated-after <time>` lists only conversations changed since then. Titles show the
1784
+ agent's own short name for a conversation once it has one (`title_short`).
1785
+
1786
+ Signed in (`shard auth login`) without an API key, `shard conversations list` and `shard conversations feed` (0.25.0+)
1787
+ cover every project you work in, across your organizations, with one cursor; `--org <id|slug>` narrows them to one
1788
+ organization and `--project` to one project. With an API key they cover the key's project, and `--org` uses your
1789
+ sign-in when you have one:
1790
+
1791
+ ```sh
1792
+ shard c list --org acme --state needs_you
1793
+ shard c feed --org acme --json
1794
+ ```
1795
+
1638
1796
  `shard conversations list --all` reads every page, keeping the selected project, state and parent filters. `--json` returns `data` and `nextCursor`. Linux desktop 0.1.11+ reads all conversation and computer pages and shows the selected project key’s organization, usage, spend and connector status in Settings.
1639
1797
 
1798
+ ## Project drive (0.25.0+)
1799
+
1800
+ `shard drive` keeps a project's files: the screenshots, logs, reports and notes your agents and people save and read by
1801
+ path, the same from a phone, a script or another agent whether the project's computers are working, asleep or gone.
1802
+ Paths are relative to the project space root, the names agents see under `/shardflux/project`
1803
+ (`tasks/t_ab12/evidence/healthz.png`, `BOARD.md`).
1804
+
1805
+ ```sh
1806
+ shard drive put ./out/healthz.png tasks/t_ab12/evidence/healthz.png
1807
+ # Saved tasks/t_ab12/evidence/healthz.png (48.2 KB, image/png)
1808
+ # sha256 <sha256>
1809
+ shard drive put BOARD.md # saved as BOARD.md
1810
+ git diff | shard drive put - tasks/t_ab12/review.diff
1811
+ shard drive ls tasks/t_ab12/
1812
+ # SIZE SAVED TYPE PATH
1813
+ # 48.2 KB 2026-10-15 23:13:47Z image/png tasks/t_ab12/evidence/healthz.png
1814
+ # 24 bytes 2026-10-15 23:13:48Z text/markdown; charset=utf-8 tasks/t_ab12/notes.md
1815
+ shard drive get tasks/t_ab12/evidence/healthz.png -o healthz.png
1816
+ shard drive get tasks/t_ab12/notes.md --sha256 <sha256> # exactly that version
1817
+ shard drive rm tasks/t_ab12/notes.md
1818
+ ```
1819
+
1820
+ - Every `put` is a new version named by the SHA-256 of its bytes, printed after the path; saving the bytes that are
1821
+ already current keeps that version. `get --sha256` reads any version, also after its path was overwritten or
1822
+ removed, so a report that names a file's sha256 always opens the bytes it was written with.
1823
+ - `get` writes the bytes to standard output, or to a file with `-o`, once they match their SHA-256; with `--json` it
1824
+ returns `content_base64` with `sha256`, `size` and `content_type`.
1825
+ - `put <local-file> [<drive-path>]`: the drive path defaults to the file's name, and `-` reads standard input. The
1826
+ type comes from the extension (`.png` image/png, `.md` text/markdown) unless `--content-type` names one. `--json`
1827
+ returns the saved `file`.
1828
+ - `ls [prefix]` lists the current files by path, 200 to a page (`--limit` up to 1000, `--cursor`, `--all`); `--json`
1829
+ returns `data` (each file with its `sha256`) and `next_cursor`.
1830
+ - The drive is the API key's project, or `--project <id>`. Signed in (`shard auth login`), `--project` takes any of
1831
+ your projects by id or slug, and without an API key the drive is your default project's.
1832
+
1833
+ ## Model template (0.25.0+)
1834
+
1835
+ `shard models` shows which harness, model and effort each role of a task runs with, from the Claude and Codex accounts
1836
+ your organization connected: master plans and coordinates, builder does the work, hard takes the hardest parts, light
1837
+ the small ones and reviewer reviews with a fresh context.
1838
+
1839
+ ```sh
1840
+ shard models
1841
+ # Primary: claude (connected first), secondary: codex
1842
+ # Connected: claude (max), codex (pro)
1843
+ #
1844
+ # ROLE HARNESS MODEL EFFORT WAITS UNTIL
1845
+ # master claude opus medium (plans high) -
1846
+ # builder claude opus high -
1847
+ # hard claude fable high -
1848
+ # light codex gpt-6.1-sol low -
1849
+ # reviewer codex gpt-6.1-sol medium -
1850
+ shard models primary codex # this project's primary
1851
+ shard models primary claude --org acme # the organization's default
1852
+ shard models primary clear
1853
+ ```
1854
+
1855
+ - A role's harness, model and effort go straight to `shard conversations start --app <harness> --model <model>
1856
+ --effort <effort>` (`default`: leave out `--effort`).
1857
+ - `--harness claude|codex` keeps a running task's master, builder and hard work on its provider; `--attempt <n>` (1 to
1858
+ 10) shows the template for a retry, on the other provider or one tier up.
1859
+ - A role whose account waits for its next usage window shows until when, and what the other provider would run now,
1860
+ to offer the person; nothing switches on its own.
1861
+ - The primary provider is the project's choice when it has one, else the organization's, else the provider connected
1862
+ first. `shard models primary` sets the project's (owners and admins, or the project's API key), and with `--org` the
1863
+ organization's default (owners and admins, signed in); `clear` removes the choice. `--json` returns the template:
1864
+ `primary`, `primary_source`, `secondary`, `connected` and `roles`.
1865
+
1640
1866
  ## Default shared volumes (0.24.0+)
1641
1867
 
1642
1868
  ```sh
@@ -269,6 +269,11 @@ export const ACCOUNT_COMMANDS = [
269
269
  options: { confirm: { type: 'string', value: '<slug>', description: 'The organization’s slug, typed out (required).' }, ...YES('the deletion') },
270
270
  },
271
271
  { path: ['projects', 'list'], summary: 'List the organization’s projects (* marks the default)', positionals: [], options: { ...ORG } },
272
+ { path: ['projects', 'link'], summary: 'Make a local folder one of your projects, prepared in the background and kept as this computer’s live copy (0.25.0+)', positionals: [{ name: 'path', required: true }], options: {
273
+ prompt: { type: 'string', value: '<text>', description: 'Also start a conversation in that folder with this first message.' },
274
+ 'prompt-file': { type: 'string', value: '<file|->', description: 'Read that first message from a UTF-8 file or stdin (-), up to 256 KiB.' },
275
+ app: { type: 'string', value: '<claude|codex>', description: 'Agent app of that conversation (default claude).' },
276
+ } },
272
277
  { path: ['projects', 'sync', 'list'], summary: 'Show selected local projects and their preparation state', positionals: [], options: {} },
273
278
  { path: ['projects', 'sync', 'add'], summary: 'Prepare and keep this selected local project synced', positionals: [{ name: 'path', required: true }], options: { wait: { type: 'boolean', description: 'Prepare now and wait for its published environment; otherwise the device helper prepares it in the background.' } } },
274
279
  { path: ['projects', 'sync', 'pause'], summary: 'Pause background preparation of this local project', positionals: [{ name: 'path', required: true }], options: {} },
@@ -2,6 +2,7 @@ import { out, resolveWorkspace } from "./commands.js";
2
2
  import { UsageError } from "./errors.js";
3
3
  import { claudeLoginUrls, requireClaudeTerminal, runWorkspaceLogin } from "./claude-cloud-login.js";
4
4
  import { openMachine } from "./filesystem-commands.js";
5
+ import { codexAppAccountNotice } from "./codex-cloud.js";
5
6
  const PROVIDERS = { claude: 'Claude', codex: 'ChatGPT / Codex' };
6
7
  const WINDOW_LABELS = { five_hour: '5-hour', seven_day: 'Weekly', seven_day_opus: 'Weekly Opus', seven_day_sonnet: 'Weekly Sonnet', primary: 'Primary', secondary: 'Secondary' };
7
8
  /** A window's length as a title, as Claude's windows read: "Weekly", "5-hour", "2-day", "90-minute". */
@@ -245,7 +246,10 @@ export const ACCOUNT_POOL_HANDLERS = {
245
246
  const follows = account.provider === 'claude'
246
247
  ? 'New conversations start with it; running ones continue on it, and Remote Control turns back on by itself.'
247
248
  : `New conversations and every running conversation VM use it; Codex Cloud moves to it${previous ? ` (Cloud threads started on ${name(previous)} stay in that ChatGPT account)` : ''}.`;
248
- out(ctx, previous || !isActive(account) ? `${name(active)} is the active ${PROVIDERS[account.provider]} account. ${follows}\n` : `${name(active)} is already the active ${PROVIDERS[account.provider]} account.\n`, { ...list, account: active });
249
+ // codex_app (0.25.0+): the Codex app here opens the account's Cloud threads only once it is signed in to it.
250
+ const app = account.provider === 'codex' ? await codexAppAccountNotice(ctx, active) : null;
251
+ out(ctx, (previous || !isActive(account) ? `${name(active)} is the active ${PROVIDERS[account.provider]} account. ${follows}\n` : `${name(active)} is already the active ${PROVIDERS[account.provider]} account.\n`)
252
+ + (app?.line ? `${app.line}\n` : ''), { ...list, account: active, ...(account.provider === 'codex' ? { codex_app: app ? { login: app.login, notice: app.line } : null } : {}) });
249
253
  return 0;
250
254
  },
251
255
  };
@@ -0,0 +1,38 @@
1
+ import type { ConversationPermission } from '@shardflux/sdk';
2
+ import type { ObjectValue } from './acp-rpc.js';
3
+ /** The part of the SDK client the watch reads: GET /v1/conversations/{id}/permissions (0.28.0+). */
4
+ export interface PermissionsReader {
5
+ conversations: {
6
+ permissions(id: string, p: {
7
+ state?: ConversationPermission['state'];
8
+ limit?: number;
9
+ cursor?: string;
10
+ }): Promise<{
11
+ data: ConversationPermission[];
12
+ nextCursor: string | null;
13
+ }>;
14
+ permission(id: string, permissionId: string): Promise<ConversationPermission>;
15
+ };
16
+ }
17
+ /** How often the watch reads the API while the editor's permission prompt is open (it never touches the VM). */
18
+ export declare const ELSEWHERE_POLL_MS = 1500;
19
+ /** The tool and command, path or URL the guest records for an ACP permission request (services/guest-agent ACPPermissionAsk). */
20
+ export declare function requestIdentity(params: ObjectValue): {
21
+ tool: string | null;
22
+ detail: string;
23
+ };
24
+ /**
25
+ * While the editor shows the agent's permission prompt, the journal relay is paused, so nothing else of the turn reaches
26
+ * the editor. The same request is listed through the API, where a person can answer it too (the Shard client, the CLI,
27
+ * MCP). This resolves with the request once it leaves `pending` that way (answered through the API, or closed because
28
+ * the agent no longer waits), so the relay can withdraw the editor's prompt and go on with the turn. The request is the
29
+ * newest one of the conversation with the prompt's tool and command, path or URL that was reported as the prompt opened
30
+ * or still waits. It resolves null on an API without permission requests (404, 503) or when `signal` ends the prompt;
31
+ * other errors are retried.
32
+ */
33
+ export declare function answeredElsewhere(client: PermissionsReader, conversationId: string, params: ObjectValue, openedAt: number, signal: AbortSignal, pollMs?: number): Promise<ConversationPermission | null>;
34
+ /**
35
+ * The editor-side answer matching a request answered elsewhere: the option of its decision's kind (as the guest picks
36
+ * it), or a cancelled outcome. The agent already has the first answer; this one only completes the relayed request.
37
+ */
38
+ export declare function outcomeFor(p: ConversationPermission, params: ObjectValue): ObjectValue;
@@ -0,0 +1,85 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { ShardfluxApiError } from '@shardflux/sdk';
3
+ import { objectValue } from "./acp-rpc.js";
4
+ /** How often the watch reads the API while the editor's permission prompt is open (it never touches the VM). */
5
+ export const ELSEWHERE_POLL_MS = 1500;
6
+ /**
7
+ * The guest reports a request as it journals it, just before the editor's prompt opens: one reported up to this long
8
+ * before (clock difference with the API included) is this prompt's; an older one counts only while it still waits (an
9
+ * editor reconnecting to a request that is still open).
10
+ */
11
+ const REPORTED_BEFORE_MS = 10_000;
12
+ /** The tool and command, path or URL the guest records for an ACP permission request (services/guest-agent ACPPermissionAsk). */
13
+ export function requestIdentity(params) {
14
+ const call = objectValue(params.toolCall), raw = objectValue(call.rawInput), str = (v) => typeof v === 'string' ? v : '';
15
+ const location = Array.isArray(call.locations) ? str(objectValue(call.locations[0]).path) : '';
16
+ const tool = str(call.name) || str(objectValue(objectValue(call._meta).claudeCode).toolName);
17
+ return { tool: tool || null, detail: str(raw.command) || location || str(raw.file_path) || str(raw.url) };
18
+ }
19
+ /**
20
+ * While the editor shows the agent's permission prompt, the journal relay is paused, so nothing else of the turn reaches
21
+ * the editor. The same request is listed through the API, where a person can answer it too (the Shard client, the CLI,
22
+ * MCP). This resolves with the request once it leaves `pending` that way (answered through the API, or closed because
23
+ * the agent no longer waits), so the relay can withdraw the editor's prompt and go on with the turn. The request is the
24
+ * newest one of the conversation with the prompt's tool and command, path or URL that was reported as the prompt opened
25
+ * or still waits. It resolves null on an API without permission requests (404, 503) or when `signal` ends the prompt;
26
+ * other errors are retried.
27
+ */
28
+ export async function answeredElsewhere(client, conversationId, params, openedAt, signal, pollMs = ELSEWHERE_POLL_MS) {
29
+ const want = requestIdentity(params);
30
+ let request;
31
+ for (;;) {
32
+ try {
33
+ await delay(pollMs, undefined, { signal });
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ try {
39
+ if (!request) {
40
+ // Every state, every page: an answer through the API can land before the first read.
41
+ let mine;
42
+ for (let cursor, pages = 0; pages < 20; pages++) {
43
+ const page = await client.conversations.permissions(conversationId, { limit: 200, ...(cursor ? { cursor } : {}) });
44
+ for (const p of page.data) {
45
+ if (p.detail === want.detail && (p.tool ?? null) === want.tool && (p.state === 'pending' || Date.parse(p.created_at) >= openedAt - REPORTED_BEFORE_MS))
46
+ mine = p;
47
+ }
48
+ if (!page.nextCursor)
49
+ break;
50
+ cursor = page.nextCursor;
51
+ }
52
+ if (!mine)
53
+ continue;
54
+ if (mine.state !== 'pending')
55
+ return mine;
56
+ request = mine.id;
57
+ continue;
58
+ }
59
+ const p = await client.conversations.permission(conversationId, request);
60
+ if (p.state !== 'pending')
61
+ return p;
62
+ }
63
+ catch (error) {
64
+ if (signal.aborted)
65
+ return null;
66
+ if (error instanceof ShardfluxApiError && (error.status === 404 || error.status === 503))
67
+ return null;
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * The editor-side answer matching a request answered elsewhere: the option of its decision's kind (as the guest picks
73
+ * it), or a cancelled outcome. The agent already has the first answer; this one only completes the relayed request.
74
+ */
75
+ export function outcomeFor(p, params) {
76
+ const options = (Array.isArray(params.options) ? params.options : []).map(objectValue);
77
+ const wanted = p.decision === 'allow_once' ? ['allow_once', 'allow_always'] : p.decision === 'allow_always' ? ['allow_always', 'allow_once']
78
+ : p.decision === 'deny' ? ['reject_once', 'reject_always'] : [];
79
+ for (const kind of wanted) {
80
+ const option = options.find(o => o.kind === kind);
81
+ if (typeof option?.optionId === 'string')
82
+ return { outcome: { outcome: 'selected', optionId: option.optionId } };
83
+ }
84
+ return { outcome: { outcome: 'cancelled' } };
85
+ }
package/dist/acp-rpc.d.ts CHANGED
@@ -34,7 +34,8 @@ export declare class AcpRpc {
34
34
  get busy(): boolean;
35
35
  get closed(): boolean;
36
36
  feed(bytes: Uint8Array): void;
37
- request(method: string, params: ObjectValue, timeoutMs?: number, signal?: AbortSignal): Promise<unknown>;
37
+ /** `cancelOnAbort`: an abort also tells the peer the request is withdrawn (`$/cancel_request`), so an editor drops its prompt. */
38
+ request(method: string, params: ObjectValue, timeoutMs?: number, signal?: AbortSignal, cancelOnAbort?: boolean): Promise<unknown>;
38
39
  notify(method: string, params: ObjectValue): Promise<void>;
39
40
  close(error?: Error): void;
40
41
  drain(): Promise<void>;
package/dist/acp-rpc.js CHANGED
@@ -86,7 +86,8 @@ export class AcpRpc {
86
86
  pending.resolve(message.result);
87
87
  this.onIdle();
88
88
  }
89
- request(method, params, timeoutMs = 120_000, signal) {
89
+ /** `cancelOnAbort`: an abort also tells the peer the request is withdrawn (`$/cancel_request`), so an editor drops its prompt. */
90
+ request(method, params, timeoutMs = 120_000, signal, cancelOnAbort = false) {
90
91
  if (this.#closed)
91
92
  return Promise.reject(this.#closed);
92
93
  if (signal?.aborted)
@@ -95,7 +96,15 @@ export class AcpRpc {
95
96
  return new Promise((resolve, reject) => {
96
97
  const cleanup = () => signal?.removeEventListener('abort', abort);
97
98
  const timer = timeoutMs > 0 ? setTimeout(() => { this.pending.delete(id); cleanup(); reject(new RpcError(-32000, 'Request timed out', { code: 'timeout', fix: 'Try the message again.' })); this.onIdle(); }, timeoutMs) : undefined;
98
- const abort = () => { this.pending.delete(id); clearTimeout(timer); cleanup(); reject(signal?.reason instanceof Error ? signal.reason : new Error('Request interrupted')); this.onIdle(); };
99
+ const abort = () => {
100
+ this.pending.delete(id);
101
+ clearTimeout(timer);
102
+ cleanup();
103
+ if (cancelOnAbort && !this.#closed)
104
+ void this.#write({ jsonrpc: '2.0', method: '$/cancel_request', params: { requestId: id } }).catch(() => undefined);
105
+ reject(signal?.reason instanceof Error ? signal.reason : new Error('Request interrupted'));
106
+ this.onIdle();
107
+ };
99
108
  this.pending.set(id, { resolve, reject, timer, cleanup });
100
109
  signal?.addEventListener('abort', abort, { once: true });
101
110
  void this.#write({ jsonrpc: '2.0', id, method, params }).catch((error) => { this.pending.delete(id); clearTimeout(timer); cleanup(); reject(error instanceof Error ? error : new Error(String(error))); this.onIdle(); });