@shardflux/cli 0.22.0 → 0.23.0
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.
- package/CHANGELOG.md +98 -0
- package/README.md +37 -10
- package/dist/acp-journal.js +58 -6
- package/dist/acp-login.d.ts +10 -3
- package/dist/acp-login.js +31 -4
- package/dist/acp-models.d.ts +6 -1
- package/dist/acp-models.js +21 -16
- package/dist/acp.js +60 -7
- package/dist/agent-commands.js +29 -15
- package/dist/agent-target-commands.d.ts +20 -1
- package/dist/agent-target-commands.js +49 -18
- package/dist/agent-targets.js +6 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +8 -7
- package/dist/cli-proxy-forward.d.ts +0 -2
- package/dist/cli-proxy-forward.js +2 -9
- package/dist/cli-proxy-hub.d.ts +22 -30
- package/dist/cli-proxy-hub.js +65 -94
- package/dist/cli-proxy.d.ts +12 -38
- package/dist/cli-proxy.js +51 -168
- package/dist/codex-cloud.d.ts +9 -4
- package/dist/codex-cloud.js +47 -14
- package/dist/codex-linux.d.ts +14 -0
- package/dist/codex-linux.js +69 -0
- package/dist/command-center-actions.js +13 -4
- package/dist/command-center-model.d.ts +2 -0
- package/dist/command-center-model.js +6 -1
- package/dist/command-center.js +3 -3
- package/dist/commands.js +2 -2
- package/dist/conversation-terminal.js +3 -2
- package/dist/conversations.d.ts +14 -1
- package/dist/conversations.js +55 -6
- package/dist/desktop-linux.js +16 -3
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +5 -2
- package/dist/filesystem-commands.js +25 -3
- package/dist/menubar.d.ts +6 -5
- package/dist/menubar.js +38 -28
- package/dist/move-worktrees.d.ts +13 -7
- package/dist/move-worktrees.js +68 -17
- package/dist/move.js +20 -25
- package/dist/onboarding.js +4 -2
- package/dist/prepared-repository.d.ts +2 -0
- package/dist/prepared-repository.js +1 -1
- package/dist/prepared-repository.py +3 -2
- package/dist/project-preparation.d.ts +14 -0
- package/dist/project-preparation.js +16 -4
- package/dist/project-sync.d.ts +3 -0
- package/dist/project-sync.js +4 -1
- package/dist/t3-launch.d.ts +2 -1
- package/dist/t3-launch.js +11 -2
- package/dist/t3-proxy.d.ts +81 -0
- package/dist/t3-proxy.js +325 -0
- package/dist/terminal-apps.d.ts +11 -0
- package/dist/terminal-apps.js +18 -5
- package/dist/update.d.ts +2 -0
- package/dist/update.js +32 -6
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,103 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.23.0 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
- Codex Cloud enable and setup report when a ChatGPT sign-in is required, with `codex login` and `shard setup` as the next steps. Setup offers the sign-in action.
|
|
6
|
+
|
|
7
|
+
- On macOS, the installed CLI records a matching stable Node.js path for the menu bar, so Homebrew upgrades and
|
|
8
|
+
cleanup keep its actions available.
|
|
9
|
+
- On Linux, the desktop records the installed Node.js and CLI paths for its actions and existing autostart entry,
|
|
10
|
+
keeping them available after the launching shell's fnm multishell links expire.
|
|
11
|
+
- Opening a moved Claude or Codex conversation by its full key resolves the conversation from any folder, including
|
|
12
|
+
`--in auto` in the desktop menus and explicit app/web targets. A computer key opens its bound conversation's session.
|
|
13
|
+
- Move to cloud moves the session's checkout, uncommitted edits included, in any repository: also one with other
|
|
14
|
+
local branches, other worktrees or a worktree whose folder is gone. Those stay on this computer, so `shard move`,
|
|
15
|
+
the menu bar's and the Linux app's **Move to cloud** and the MCP tool go straight ahead without a scope choice
|
|
16
|
+
(`--this-worktree-only` is now the default). When the session also worked in another worktree, the move names it
|
|
17
|
+
in one line and `--json` lists it as `other_worktrees`. `--preserve-all-worktrees` skips registrations whose folder
|
|
18
|
+
is gone (`stale_registrations` in `--plan`); their branches move with the other local refs.
|
|
19
|
+
- Open Codex Cloud chats in the browser on every platform. `agents open --original` opens the original history; `--history <thread-id>` opens any earlier linked chat. JSON includes all previous history links.
|
|
20
|
+
|
|
21
|
+
- Opening a conversation from a coding agent's shell (Codex, Claude Code, Cursor) opens exactly one terminal, and the
|
|
22
|
+
conversation attaches there. The terminal no longer inherits the agent shell's variables, and a terminal Shardflux
|
|
23
|
+
opened never opens another, so it no longer reopens terminals in a loop that steals focus. A Codex Cloud conversation
|
|
24
|
+
opens in Codex before any terminal is considered, also when given its computer's key.
|
|
25
|
+
- Inherits completed forward-helper verification reuse from SDK 0.27.0 on a reused workspace client; each ownership epoch and helper version is verified again.
|
|
26
|
+
- Historical template file-list GETs wait for checksum-verified cache reconstruction, honoring Retry-After within the SDK admission deadline. File history and contents remain stored; POSTs never replay for file_list_indexing.
|
|
27
|
+
|
|
28
|
+
- Plan-derived API budgets (0.23.0+, SDK 0.27.0): `--json` error output includes `rate_limit` and `retry_after_seconds`; the text hint names the shared scope and delay. API request pacing follows the SDK admission timeout and preserves the request body. Reuse workspace credentials and reduce status polling when the returned budget asks for a delay.
|
|
29
|
+
|
|
30
|
+
- T3 Code conversations on Shardflux follow your T3 Code proxy setup (CLIProxyAPI, or any Claude- or
|
|
31
|
+
OpenAI-compatible proxy). What your T3 Code Claude provider runs with (its environment variables, and Claude Code's
|
|
32
|
+
settings.json `env`: `ANTHROPIC_BASE_URL`, its token, models and headers) reaches the conversation's Claude Code, and
|
|
33
|
+
your Codex `model_provider` (its `base_url`, `wire_api` and the key `env_key` names) reaches its Codex. CLIProxyAPI
|
|
34
|
+
on this computer (`127.0.0.1:8317`, or the port it had before `shard cli-proxy import`) is reached through your
|
|
35
|
+
hosted CLIProxyAPI, as on this computer; a proxy at another address is used as written. A Claude Code conversation
|
|
36
|
+
that goes through your proxy with its own token starts without a Shardflux Claude sign-in. T3 Code's default
|
|
37
|
+
provider is the one followed while it is enabled.
|
|
38
|
+
- `shard agents new --in t3` and `shard agents open t3` give the VM's own T3 Code (SSH and the T3 app) the same Claude
|
|
39
|
+
Code and Codex provider settings, with tokens in its secret store.
|
|
40
|
+
- Sign in on the Shardflux provider in T3 Code (`shard acp --login`) offers to move the CLIProxyAPI your T3 Code setup
|
|
41
|
+
uses into your account, so cloud conversations use it too. Claude Code that goes through it is connected then.
|
|
42
|
+
- Your hosted CLIProxyAPI routes requests across your logins exactly as its config says (round-robin, fill-first,
|
|
43
|
+
session affinity): `shard cli-proxy import` brings every login as it is on your computer, enabled or disabled, and
|
|
44
|
+
nothing changes them afterwards. `shard cli-proxy use <login>` serves a provider with that one login; `shard
|
|
45
|
+
cli-proxy use --all` uses every login again. `--use` on import is the same choice for a login you move.
|
|
46
|
+
- `ws exec-cancel` displays pending/unverified kernel drain and the follow command; `exec-read` keeps the real foreground state and reason.
|
|
47
|
+
- `shard template save` and `shard template` on your computer use the newest conversation this checkout started whose
|
|
48
|
+
VM is still there (a restored one too), else the project's workspace from before conversations. They no longer
|
|
49
|
+
refuse with "Run shard ./ for this project" after `shard ./`.
|
|
50
|
+
- `shard agents logs <conversation>` of a Codex Cloud conversation says its output is in its Codex thread and names
|
|
51
|
+
the commands that show it: `shard agents open <id>` opens the thread, `shard c wait <id>` waits for its reply and
|
|
52
|
+
`shard c get <id>` shows it.
|
|
53
|
+
- `shard agents new --in terminal --prompt <text>` (or `--prompt-file`, `--image`) starts a new conversation with that
|
|
54
|
+
first message, as without `--in`; a new Codex conversation says it opens in Codex Cloud. Before, it was refused
|
|
55
|
+
with a message that recommended the same command. A first message with `--name`, `--like`, an agent argument or
|
|
56
|
+
another `--in` target names the option it cannot be combined with.
|
|
57
|
+
- `shard projects sync add <path> --wait` reports a project setup that stopped as `project_setup_failed` (it was
|
|
58
|
+
`internal_error`): the message names the step and what it needs, such as a lockfile to commit,
|
|
59
|
+
`details.step` and `details.error_code` identify it, and the hint gives the way forward.
|
|
60
|
+
- `shard agents open` (and Open in the menus) of a Codex Cloud chat that switching the active Codex account or turning
|
|
61
|
+
Codex Cloud off and on stopped continues it in a new Codex chat of the same conversation, on its computer and branch,
|
|
62
|
+
instead of reporting that its executor was removed.
|
|
63
|
+
- ACP journal followers retry temporary lifecycle interruptions with bounded backoff and their saved sequence, without resubmitting a turn; permanent loss stops promptly.
|
|
64
|
+
- SSH inherits the SDK cold helper race repair.
|
|
65
|
+
- Inherits SDK 0.27.0 safe replay of truncated JSON lifecycle and exec answers.
|
|
66
|
+
- Uses SDK 0.27.0: workspace exec retries a lost start response with the same session ID.
|
|
67
|
+
|
|
68
|
+
- A T3 Code (`shard acp`) conversation starts the way a terminal one does: from its prepared revision, sending only
|
|
69
|
+
what the project cache lacks (nothing for a returning project), so its session is ready ~2.5–3 s sooner. Large files
|
|
70
|
+
still on their way stay out of the first file reconcile and land by themselves.
|
|
71
|
+
- A T3 Code (`shard acp`) conversation's first prompt reaches the agent ~1 s sooner: the agent's session starts while
|
|
72
|
+
the project files are reconciled, instead of after. `SHARDFLUX_TRACE_SEED=1` also marks each step of that
|
|
73
|
+
connection (`acp_*`). Each message to the agent up to 192 KiB (every one but a long prompt) now takes one request
|
|
74
|
+
to your VM instead of four.
|
|
75
|
+
- `shard agents open t3` and `shard agents new --in t3` with T3 Code 0.0.45 bring T3 up only once the workspace's T3
|
|
76
|
+
Connect link is linked: T3 lists the project only then. A link in progress is waited for (at most 30 s, within
|
|
77
|
+
`--wake-timeout`). A refused link is the `t3_link_failed` error with the API's remedy (for T3 Connect's environment
|
|
78
|
+
limit, Settings → Connections, with `details.recovery`); a link still in progress is `t3_link_pending`. Both carry
|
|
79
|
+
`workspace_key`, `conversation_id` and any started `t3_thread_id`, so the menu bar and the Linux app offer Open for
|
|
80
|
+
what exists rather than a new start. Before, `--json` answered `opened: true` with `t3_link.status: linking`.
|
|
81
|
+
- `shard agents open t3 <conversation>` for a Codex Cloud conversation opens its existing Codex chat, as
|
|
82
|
+
`shard agents open` does, instead of setting its VM up in T3 Code, which cannot show that chat. It neither wakes the
|
|
83
|
+
VM nor needs T3 Code.
|
|
84
|
+
- The Codex app on Linux: OpenAI's ChatGPT app for Linux (`chatgpt.desktop`) or a community build, found as the desktop
|
|
85
|
+
entry that handles codex://. `shard agents open <conversation>` and `shard agents open codex-app <conversation>` open
|
|
86
|
+
a Codex Cloud conversation's existing thread in it, through that entry, and `agents open codex-app` of a Cloud
|
|
87
|
+
conversation opens its thread on macOS too (it set up an SSH environment before). Its SSH environments stay a macOS
|
|
88
|
+
target.
|
|
89
|
+
- On a computer without the Codex app, opening or starting a Codex Cloud conversation says where its chat is,
|
|
90
|
+
whether it is waiting for your answer, and how to answer and follow it from here (`shard conversations send <id>
|
|
91
|
+
"<answer>"`, `shard conversations get <id>`), instead of failing to open its codex:// link. `--json` adds
|
|
92
|
+
`codex_app` and `reply_command`.
|
|
93
|
+
- The command center (`shard` with no arguments) lists a Codex Cloud chat as **needs you** or **working** from Codex
|
|
94
|
+
even while its computer is parked, and Enter opens it in Codex without waking that computer. Includes the Linux
|
|
95
|
+
desktop 0.1.10 payloads: Codex Cloud chats in the top-bar panel.
|
|
96
|
+
- `shard menubar start`, `stop` and `status` act on the Shardflux app for this config directory wherever it runs from
|
|
97
|
+
(menu bar 0.8.4+, one app per config directory), and an update stops that app before the new one starts, so no
|
|
98
|
+
older app stays behind. A start that races another one for the same directory hands over without opening the
|
|
99
|
+
launcher.
|
|
100
|
+
|
|
3
101
|
## 0.22.0 (2026-10-13)
|
|
4
102
|
|
|
5
103
|
- **Starts send only what changed.** A new conversation's project travels as Git packs into a project cache its VMs
|
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @shardflux/cli: `shard`
|
|
2
2
|
|
|
3
|
+
Historical template file lists: reads queue checksum-verified reconstruction when a cold list is absent from the SQL cache. TypeScript SDK 0.27.0+ / Python 0.23.0+ wait full Retry-After for retryable API 409 file_list_indexing on GET/HEAD within the admission timeout. Template objects and version history stay available.
|
|
4
|
+
|
|
5
|
+
Plan-derived API budgets (0.23.0+, SDK 0.27.0): `--json` error output includes `rate_limit` and `retry_after_seconds`; the text hint names the shared scope and delay. API request pacing follows the SDK admission timeout and preserves the request body. Reuse workspace credentials and reduce status polling when the returned budget asks for a delay.
|
|
6
|
+
|
|
7
|
+
Exec survives a dropped connection (0.23.0+): a lost start response is retried with the same session ID and reattaches to the command that already started, and a cut-off JSON answer is retried within the same safe retry boundary.
|
|
8
|
+
|
|
3
9
|
Suspension controls arrive in 0.22.0 (next coordinated release). `shard operations progress <operation-id>` reads the actual phase, host deadline, cancellation window and safe next action. `shard operations cancel <operation-id>` requests cancellation; `--wait --timeout 5m` waits for confirmed `suspend_canceled` recovery of the original workspace. Asynchronous acceptance is labeled separately. The owning host refuses cancellation outside validated preparation; after capture, wait for the outcome and resume if it succeeded. These commands never reset, replace or launch a provider turn. `--json` preserves the reported values; unavailable progress is explicit.
|
|
4
10
|
|
|
5
11
|
The command line for [Shardflux](https://shardflux.dev): serverless VMs for AI agents. Open a
|
|
@@ -114,9 +120,13 @@ and Codex, skipping every step that is already done (0.15.0+). Editors that laun
|
|
|
114
120
|
run the same sign-in as `shard --login` (0.22.0+).
|
|
115
121
|
With T3 Code 0.0.45, `shard agents new --in t3` adds the project as an SSH environment instead.
|
|
116
122
|
|
|
123
|
+
ACP journal read reattachment uses bounded backoff through temporary lifecycle interruptions (0.23.0+). It keeps the saved sequence and never resubmits an uncertain turn.
|
|
124
|
+
|
|
117
125
|
In CLI 0.22.0+, `shard setup --inspect` shows a stopped T3 Connect workspace link and its remedy without restarting
|
|
118
|
-
the link or replacing your approved sign-in.
|
|
119
|
-
|
|
126
|
+
the link or replacing your approved sign-in. `shard agents open t3` brings T3 Code up once the workspace is linked
|
|
127
|
+
(0.23.0+): a refused link answers `t3_link_failed` with what to do (for T3 Connect's environment limit, remove an
|
|
128
|
+
environment in T3 Code → Settings → Connections, `details.recovery`), and a link still in progress after 30 seconds
|
|
129
|
+
answers `t3_link_pending`. Both name the workspace and conversation: open it again with `shard agents open t3`.
|
|
120
130
|
|
|
121
131
|
Start another thread on a computer that is already set up (0.16.0+): it shares that conversation's installed
|
|
122
132
|
packages, caches, running services, ports and files, and works on its own branch and worktree.
|
|
@@ -155,18 +165,20 @@ shard move --list # the project's sessions, and where moved one
|
|
|
155
165
|
shard move --session 3d085e35-aa66-4866-9a53-f09719d6767c --detach --json
|
|
156
166
|
```
|
|
157
167
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
The session's checkout moves, uncommitted edits included; the repository's other worktrees and local branches stay
|
|
169
|
+
on this computer (0.23.0+; `--this-worktree-only` says so explicitly). When the session also worked in another
|
|
170
|
+
worktree, the move names it in one line (`other_worktrees` in `--json`). To bring every registered worktree, all
|
|
171
|
+
local refs, indices and ignored files too, inspect the exact paths and file bytes with
|
|
172
|
+
`shard move --last --plan --json`, then move with `--preserve-all-worktrees` (0.22.0+). Preserve-all includes ignored dependency/build caches; tar headers add to the displayed file bytes and the VM
|
|
162
173
|
and project space need free disk for the copies. It never follows symlinks into unrelated sibling folders.
|
|
163
174
|
The source checkout stays untouched. Cloud worktrees keep their staged/unstaged state; the selected tree runs on
|
|
164
175
|
the conversation's branch while its original branch and HEAD remain in the recovery archive. Git location
|
|
165
176
|
pointers are repaired for the VM. The result's `preserved.snapshot` names an immutable migration-time snapshot
|
|
166
177
|
in the project space. Its `restore.py manifest.json <empty-folder>` command restores the original repository
|
|
167
178
|
state into a new folder and refuses to overwrite continued work. It is a recovery snapshot, not an ongoing
|
|
168
|
-
backup of later edits in other worktrees.
|
|
169
|
-
|
|
179
|
+
backup of later edits in other worktrees. External object alternates, nested repositories and special files need
|
|
180
|
+
resolution before preserve-all can upload; a registration whose folder is gone has nothing to copy and is listed as
|
|
181
|
+
`stale_registrations`.
|
|
170
182
|
|
|
171
183
|
A running turn finishes here first. Uncommitted edits come along, and your working tree, index and branch stay as
|
|
172
184
|
they are. The cloud work comes back to your checkout by itself, and typing in the local Claude Code copy shows one line
|
|
@@ -187,7 +199,8 @@ The first message becomes a T3 thread and T3 comes to the front. With the Shardf
|
|
|
187
199
|
cloud VM; with T3 Code 0.0.45 it runs on the T3 server in the project's VM, or with `--on` on that computer's.
|
|
188
200
|
`--image` attaches images. `shard agents targets --json` shows T3 Code's mode, where each app's new conversations
|
|
189
201
|
open, and every place each app can open (`open_in_targets`, 0.22.0+); `shard agents open t3 <conversation>` brings its
|
|
190
|
-
thread back
|
|
202
|
+
thread back, and a Codex Cloud conversation's chat opens in your browser (0.23.0+). Answer a Codex Cloud
|
|
203
|
+
conversation with `shard conversations send <id> "<answer>"` (0.23.0+ names it when you open one). Codex opens in a terminal app on macOS too (0.22.0+): `shard agents targets --open-in codex=terminal`.
|
|
191
204
|
|
|
192
205
|
`shard settings --title-prefix <text|none|default>` (0.22.0+) sets the marker your Claude conversations' titles start
|
|
193
206
|
with in claude.ai and the Claude app (default `☁`: "☁ Fix login bug · todo-app"); `shard settings` shows it with where
|
|
@@ -317,7 +330,8 @@ answer) or their product (it runs VMs for its customers through the API, SDK or
|
|
|
317
330
|
|
|
318
331
|
2. **Connect Claude and Codex.** Ask which they use (Claude Code and Codex / Claude Code / Codex). `shard agents
|
|
319
332
|
credentials login claude --json` opens the claude.ai sign-in and a page for its code in their browser (0.22.0+);
|
|
320
|
-
Codex uses this computer's
|
|
333
|
+
Codex Cloud uses this computer's ChatGPT sign-in (`codex login`); copy it with `shard setup`.
|
|
334
|
+
`shard setup --inspect --connect --json --apps claude,codex`
|
|
321
335
|
(0.15.0+) checks both live, copies the Codex login and starts the account's Codex remote device.
|
|
322
336
|
3. **Set up the machine** every conversation starts from: ask "Set up your cloud machine from this computer?" (Yes /
|
|
323
337
|
Not now), then `shard machine setup --from-this-computer --json` (0.20.0+): this computer's Claude Code or Codex
|
|
@@ -1596,3 +1610,16 @@ Explicit `agents new --in terminal`, `--in t3`, and `--like` launches keep their
|
|
|
1596
1610
|
Save progress (0.22.0+): Machine and template saves show source size and an estimated duration, then queued/build state and phase updates immediately. With `--json`, progress events go to stderr and the final stdout document includes `preflight` and `phases`. Failure and timeout output retain the latest counters and `details.last_progress`, with the build id and recovery action. If an older API cannot expose the build, JSON stderr emits `save_started` with the version/build id and leaves the phase unknown. Missing final host reports preserve the last observed progress.
|
|
1597
1611
|
|
|
1598
1612
|
Desktop launch cards (0.22.0+): the bundled desktop apps clear and close the launcher on submission, then show each pending start in a layered card stack near the top of the display. **Show all launches** expands individual statuses and recovery.
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
### Pending execution cancellation (0.23.0+)
|
|
1616
|
+
|
|
1617
|
+
On guests advertising `exec_cancel_pending.v1`, successful cancellation can return the actual `running` state
|
|
1618
|
+
with `canceled: true` while the kernel holds a killed process. The execution's `error` explains pending drain,
|
|
1619
|
+
and becomes unverified after 30 seconds without claiming an exit or freeing ownership. Read/follow that
|
|
1620
|
+
session until its real final status; repeat cancellation is idempotent. An already exited foreground keeps
|
|
1621
|
+
its result while descendant drain can remain pending. TS/Python agent tools and MCP expose the reason as
|
|
1622
|
+
`status_reason` and keep the exit code null while active. CLI prints the reason and follow command.
|
|
1623
|
+
Older clients read the same session response fields.
|
|
1624
|
+
|
|
1625
|
+
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.
|
package/dist/acp-journal.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
import { ShardfluxApiError, ShardfluxProtocolError } from '@shardflux/sdk';
|
|
3
5
|
import { AcpRpc } from "./acp-rpc.js";
|
|
6
|
+
import { reattachDelayMs } from "./agent-terminal.js";
|
|
7
|
+
const RECONNECT_BUDGET_MS = 120_000;
|
|
8
|
+
function retryableRead(error) {
|
|
9
|
+
if (error instanceof ShardfluxApiError)
|
|
10
|
+
return error.retryable || ['stale_epoch', 'token_expired', 'workspace_not_running', 'workspace_busy'].includes(error.code);
|
|
11
|
+
return error instanceof TypeError || error instanceof ShardfluxProtocolError && error.status === 0;
|
|
12
|
+
}
|
|
13
|
+
// An input this small (every RPC but a long prompt) travels in its command's start request: one request instead of a
|
|
14
|
+
// start, a stdin frame, an output stream and a cancel (0.23.0+).
|
|
15
|
+
const ONE_SHOT_INPUT = 192 << 10;
|
|
4
16
|
// InputACP's private, same-uid socket receipt protocol. Stdin avoids Linux's 128 KiB per-argument limit.
|
|
5
17
|
export const ACP_INPUT_SCRIPT = `import json, os, socket, sys
|
|
6
18
|
payload = sys.stdin.buffer.readline((2 << 20) + 1024)
|
|
@@ -70,6 +82,19 @@ export class AcpJournal {
|
|
|
70
82
|
async #deliver(inputId, message) {
|
|
71
83
|
const dir = `/home/user/.local/share/shardflux/acp/${this.#conversation}`;
|
|
72
84
|
const payload = Buffer.from(`{"mode":"input","input_id":${JSON.stringify(inputId)},"message":${message}}\n`);
|
|
85
|
+
if (payload.length <= ONE_SHOT_INPUT) {
|
|
86
|
+
const r = await this.#cell.exec.run(['python3', '-c', ACP_INPUT_SCRIPT, dir], { cwd: this.#cwd, stdin: payload, timeoutMs: 15_000, maxOutputBytes: 65536, signal: this.#signal });
|
|
87
|
+
let receipt;
|
|
88
|
+
try {
|
|
89
|
+
receipt = JSON.parse(r.stdout);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
receipt = undefined;
|
|
93
|
+
}
|
|
94
|
+
if (r.exitCode !== 0 || receipt?.accepted !== true)
|
|
95
|
+
throw new Error('ACP delivery is unverified; load this session before continuing.');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
73
98
|
const session = await this.#cell.exec.start({ argv: ['python3', '-c', ACP_INPUT_SCRIPT, dir], cwd: this.#cwd, stdin_open: true, timeout_ms: 15_000 }, this.#signal);
|
|
74
99
|
let offset = 0, stdout = '';
|
|
75
100
|
try {
|
|
@@ -167,17 +192,22 @@ export class AcpJournal {
|
|
|
167
192
|
const signal = AbortSignal.any([this.#signal, reader.signal]);
|
|
168
193
|
try {
|
|
169
194
|
let failures = 0;
|
|
195
|
+
let outageStarted;
|
|
170
196
|
while (this.#wanted() && !signal.aborted) {
|
|
171
197
|
const decoder = new StringDecoder('utf8');
|
|
172
198
|
let tail = '', offset = 0;
|
|
199
|
+
let interruption;
|
|
173
200
|
try {
|
|
174
201
|
const session = await this.#cell.exec.start({ argv: ['sf-agent', 'acp', 'attach', '--conversation', this.#conversation, '--from-seq', String(this.#cursor.from_seq), '--follow'], cwd: this.#cwd, timeout_ms: 0 }, signal);
|
|
175
202
|
this.#follower = session.session_id;
|
|
176
203
|
for await (const event of await this.#cell.exec.output(session.session_id, { follow: true, signal })) {
|
|
177
|
-
if (event.type === 'error')
|
|
178
|
-
|
|
204
|
+
if (event.type === 'error') {
|
|
205
|
+
if (event.error)
|
|
206
|
+
throw new ShardfluxApiError(502, event.error, 'cell');
|
|
207
|
+
throw new Error('ACP journal failed without an error envelope');
|
|
208
|
+
}
|
|
179
209
|
if (event.type === 'exit')
|
|
180
|
-
throw new
|
|
210
|
+
throw new ShardfluxProtocolError('ACP journal detached', 0, 'cell');
|
|
181
211
|
if (event.type !== 'output' || event.stream !== 'stdout' || !event.data)
|
|
182
212
|
continue;
|
|
183
213
|
const bytes = Buffer.from(event.data, 'base64'), at = event.offset ?? offset;
|
|
@@ -189,8 +219,12 @@ export class AcpJournal {
|
|
|
189
219
|
for (let newline; (newline = tail.indexOf('\n')) >= 0;) {
|
|
190
220
|
const line = tail.slice(0, newline);
|
|
191
221
|
tail = tail.slice(newline + 1);
|
|
222
|
+
const before = this.#cursor.from_seq;
|
|
192
223
|
await this.#entry(line);
|
|
193
|
-
|
|
224
|
+
if (this.#cursor.from_seq > before) {
|
|
225
|
+
failures = 0;
|
|
226
|
+
outageStarted = undefined;
|
|
227
|
+
}
|
|
194
228
|
}
|
|
195
229
|
if (Buffer.byteLength(tail) > 4 << 20)
|
|
196
230
|
throw new Error('ACP journal frame exceeds 4 MiB');
|
|
@@ -198,17 +232,35 @@ export class AcpJournal {
|
|
|
198
232
|
return;
|
|
199
233
|
}
|
|
200
234
|
if (!signal.aborted)
|
|
201
|
-
throw new
|
|
235
|
+
throw new ShardfluxProtocolError('ACP journal connection ended', 0, 'cell');
|
|
202
236
|
}
|
|
203
237
|
catch (error) {
|
|
204
238
|
if (signal.aborted)
|
|
205
239
|
break;
|
|
206
|
-
if (
|
|
240
|
+
if (!retryableRead(error))
|
|
207
241
|
throw error;
|
|
242
|
+
outageStarted ??= performance.now();
|
|
243
|
+
if (performance.now() - outageStarted >= RECONNECT_BUDGET_MS)
|
|
244
|
+
throw new Error('ACP journal reconnect timed out; load this session to continue.', { cause: error });
|
|
245
|
+
failures++;
|
|
246
|
+
interruption = error;
|
|
208
247
|
}
|
|
209
248
|
finally {
|
|
210
249
|
await this.#detach();
|
|
211
250
|
}
|
|
251
|
+
// Retrying only the read follower cannot submit a second turn. Keep
|
|
252
|
+
// the original sequence/prompt cursor and stop waits on editor exit.
|
|
253
|
+
// A stopped reader (pause, idle) ends the wait as it ends an attach:
|
|
254
|
+
// the loop exits without closing the editor's connection.
|
|
255
|
+
if (this.#wanted() && !signal.aborted && interruption !== undefined) {
|
|
256
|
+
try {
|
|
257
|
+
await delay(Math.min(reattachDelayMs(interruption, failures), RECONNECT_BUDGET_MS - (performance.now() - outageStarted)), undefined, { signal });
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (!signal.aborted)
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
212
264
|
}
|
|
213
265
|
}
|
|
214
266
|
catch (error) {
|
package/dist/acp-login.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Ctx } from './commands.js';
|
|
2
|
+
import type { ImportResult, LocalCliProxy } from './cli-proxy.js';
|
|
2
3
|
/**
|
|
3
4
|
* The ACP terminal sign-in method. An editor runs it in its own terminal when the user chooses it, then reconnects:
|
|
4
5
|
* T3 Code appends these args to the configured `shard acp` (`shard acp --login`); ACP registry clients run the agent
|
|
@@ -13,9 +14,15 @@ export declare const ACP_LOGIN_METHOD: {
|
|
|
13
14
|
};
|
|
14
15
|
/** Whether an API key is configured (no request; a revoked key surfaces as auth_required on its first call). */
|
|
15
16
|
export declare function acpSignedIn(ctx: Ctx): boolean;
|
|
17
|
+
/** How the sign-in finds and moves this computer's CLIProxyAPI (tests replace both). */
|
|
18
|
+
export interface AcpLoginDeps {
|
|
19
|
+
detect?: (env: Record<string, string | undefined>) => Promise<LocalCliProxy | null>;
|
|
20
|
+
move?: (ctx: Ctx) => Promise<ImportResult>;
|
|
21
|
+
}
|
|
16
22
|
/**
|
|
17
23
|
* `shard acp --login`: everything a first conversation needs, each step skipped when it is already done: the
|
|
18
|
-
* account (signed up or in with this computer's Codex login, else an email), its API key, the Codex login copy
|
|
19
|
-
*
|
|
24
|
+
* account (signed up or in with this computer's Codex login, else an email), its API key, the Codex login copy, the
|
|
25
|
+
* user's T3 Code proxy setup (a CLIProxyAPI on this computer, moved into the account when they say so) and the cloud
|
|
26
|
+
* Claude sign-in (not needed when Claude Code goes through that proxy with its own token).
|
|
20
27
|
*/
|
|
21
|
-
export declare function runAcpLogin(ctx: Ctx): Promise<number>;
|
|
28
|
+
export declare function runAcpLogin(ctx: Ctx, deps?: AcpLoginDeps): Promise<number>;
|
package/dist/acp-login.js
CHANGED
|
@@ -6,6 +6,8 @@ import { describeError } from "./errors.js";
|
|
|
6
6
|
import { cloudClaudeLogin, loginClaudeInWorkspace } from "./claude-cloud-login.js";
|
|
7
7
|
import { syncAgentCredentials } from "./agent-credentials.js";
|
|
8
8
|
import { thisComputer } from "./this-computer.js";
|
|
9
|
+
import { detectLocalCliProxy, importCliProxy } from "./cli-proxy.js";
|
|
10
|
+
import { accountVmProxy, readT3ProxySetup } from "./t3-proxy.js";
|
|
9
11
|
/**
|
|
10
12
|
* The ACP terminal sign-in method. An editor runs it in its own terminal when the user chooses it, then reconnects:
|
|
11
13
|
* T3 Code appends these args to the configured `shard acp` (`shard acp --login`); ACP registry clients run the agent
|
|
@@ -46,10 +48,11 @@ async function accountReady(ctx) {
|
|
|
46
48
|
const say = (ctx, text) => { ctx.io.stderr.write(text); };
|
|
47
49
|
/**
|
|
48
50
|
* `shard acp --login`: everything a first conversation needs, each step skipped when it is already done: the
|
|
49
|
-
* account (signed up or in with this computer's Codex login, else an email), its API key, the Codex login copy
|
|
50
|
-
*
|
|
51
|
+
* account (signed up or in with this computer's Codex login, else an email), its API key, the Codex login copy, the
|
|
52
|
+
* user's T3 Code proxy setup (a CLIProxyAPI on this computer, moved into the account when they say so) and the cloud
|
|
53
|
+
* Claude sign-in (not needed when Claude Code goes through that proxy with its own token).
|
|
51
54
|
*/
|
|
52
|
-
export async function runAcpLogin(ctx) {
|
|
55
|
+
export async function runAcpLogin(ctx, deps = {}) {
|
|
53
56
|
const { HANDLERS } = await import("./handlers.js");
|
|
54
57
|
const home = ctx.io.env.HOME ?? homedir();
|
|
55
58
|
const codex = existsSync(join(home, '.codex', 'auth.json'));
|
|
@@ -99,7 +102,31 @@ export async function runAcpLogin(ctx) {
|
|
|
99
102
|
await syncAgentCredentials(ctx.cloud(), { home, redact: (value) => ctx.addSecret(value) });
|
|
100
103
|
say(ctx, `Codex: connected with the login on ${thisComputer()}.\n`);
|
|
101
104
|
}
|
|
102
|
-
|
|
105
|
+
// T3 Code's own providers go through CLIProxyAPI on this computer: cloud threads follow once it is in the account.
|
|
106
|
+
let plan = accountVmProxy(ctx);
|
|
107
|
+
if (plan.importable.length) {
|
|
108
|
+
const local = await (deps.detect ?? ((env) => detectLocalCliProxy({ env })))(ctx.io.env).catch(() => null);
|
|
109
|
+
if (local?.logins.length) {
|
|
110
|
+
const answer = (await ctx.input.answer('\nYour T3 Code setup uses CLIProxyAPI on this computer. Move it into your Shardflux account so cloud conversations use it too? Your logins and routing come along, and this computer keeps http://127.0.0.1:8317. [Y/n] '))?.trim().toLowerCase() ?? 'n';
|
|
111
|
+
if (answer === '' || answer.startsWith('y')) {
|
|
112
|
+
try {
|
|
113
|
+
// The answer above is the consent to stop the local copy too.
|
|
114
|
+
const moved = await (deps.move ?? ((c) => importCliProxy(c, { values: { yes: true } })))(ctx);
|
|
115
|
+
plan = accountVmProxy(ctx);
|
|
116
|
+
say(ctx, `CLIProxyAPI: in your account, ${moved.logins.active.length} login${moved.logins.active.length === 1 ? '' : 's'} serving. Cloud conversations use it.\n`);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
say(ctx, `CLIProxyAPI: ${describeError(error).message} Run: shard cli-proxy import\n`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else
|
|
123
|
+
say(ctx, 'CLIProxyAPI: stays on this computer; cloud conversations use your Shardflux sign-ins. Move it any time: shard cli-proxy import\n');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const claude = readT3ProxySetup({ env: ctx.io.env }).claude;
|
|
127
|
+
if (plan.claudeCredential && claude)
|
|
128
|
+
say(ctx, `Claude Code: connected through ${claude.local ? 'your CLIProxyAPI' : new URL(claude.url).host}.\n`);
|
|
129
|
+
else if (await cloudClaudeLogin(ctx))
|
|
103
130
|
say(ctx, 'Claude Code: connected.\n');
|
|
104
131
|
else {
|
|
105
132
|
const answer = (await ctx.input.answer('\nConnect Claude Code now? It opens a Claude sign-in page. [Y/n] '))?.trim().toLowerCase() ?? 'n';
|
package/dist/acp-models.d.ts
CHANGED
|
@@ -24,11 +24,16 @@ export declare class AcpModelCache {
|
|
|
24
24
|
read(scope: AcpScope): AcpCatalog;
|
|
25
25
|
write(scope: AcpScope, catalog: AcpCatalog): void;
|
|
26
26
|
}
|
|
27
|
-
export declare function discoverAcpCatalog(ctx: Ctx, ws: Workspace, root: string): Promise<AcpCatalog>;
|
|
27
|
+
export declare function discoverAcpCatalog(ctx: Ctx, ws: Workspace, root: string, env?: Record<string, string>): Promise<AcpCatalog>;
|
|
28
28
|
export interface AcpPreparation {
|
|
29
29
|
allocated(conversation: Conversation, workspace: WorkspaceView): void;
|
|
30
30
|
ready(workspace: Workspace): void;
|
|
31
31
|
prepared(): void;
|
|
32
|
+
/** The user's T3 Code proxy setup (t3-proxy.ts): the adapters' variables, and the VM files they read, placed first. */
|
|
33
|
+
proxy?: {
|
|
34
|
+
env: Record<string, string>;
|
|
35
|
+
ready(workspace: Workspace): Promise<void>;
|
|
36
|
+
};
|
|
32
37
|
}
|
|
33
38
|
export interface AcpLifecycle {
|
|
34
39
|
prepare(ctx: Ctx, folder: string, name: string, callbacks: AcpPreparation): Promise<AcpCatalog>;
|
package/dist/acp-models.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, posix } from 'node:path';
|
|
4
4
|
import { projectIdentity } from "./project-identity.js";
|
|
5
|
-
import { localChanges
|
|
6
|
-
import {
|
|
5
|
+
import { localChanges } from "./agent-repository.js";
|
|
6
|
+
import { createConversationRecord, conversationWorkspace, seedConversation } from "./conversations.js";
|
|
7
|
+
import { seedPhase } from "./seed-phases.js";
|
|
7
8
|
import { AgentStateStore } from "./agent-state.js";
|
|
8
9
|
import { objectValue } from "./acp-rpc.js";
|
|
9
10
|
import { nativeCurrentModel, nativeModelOptions } from "./acp-catalog.js";
|
|
10
|
-
import { sendTwinSeed } from "./worktree-twin.js";
|
|
11
11
|
/** No prompts, login, logout or credential writes. Every child process group is
|
|
12
12
|
* terminated, including initialization failures and interrupted clients. */
|
|
13
13
|
export const ACP_MODELS_SCRIPT = String.raw `
|
|
@@ -115,9 +115,9 @@ export class AcpModelCache {
|
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
export async function discoverAcpCatalog(ctx, ws, root) {
|
|
118
|
+
export async function discoverAcpCatalog(ctx, ws, root, env = {}) {
|
|
119
119
|
const result = await ws.cell().exec.run(['node', '-e', ACP_MODELS_SCRIPT], { cwd: root, timeoutMs: 55_000,
|
|
120
|
-
signal: ctx.signal, maxOutputBytes: 1 << 20, env: { SF_ACP_MODEL_DISCOVERY: '1' } });
|
|
120
|
+
signal: ctx.signal, maxOutputBytes: 1 << 20, env: { ...env, SF_ACP_MODEL_DISCOVERY: '1' } });
|
|
121
121
|
if (result.exitCode !== 0)
|
|
122
122
|
throw new Error(`Guest model discovery failed: ${result.stderr.trim()}`);
|
|
123
123
|
const discovered = JSON.parse(result.stdout), catalog = {};
|
|
@@ -140,25 +140,30 @@ export const ACP_LIFECYCLE = { async prepare(ctx, folder, name, callbacks) {
|
|
|
140
140
|
const changes = localChanges(folder, ctx.signal);
|
|
141
141
|
changes.catch(() => undefined);
|
|
142
142
|
const ws = await conversationWorkspace(ctx, c);
|
|
143
|
+
seedPhase('vm_running');
|
|
143
144
|
callbacks.ready(ws);
|
|
144
|
-
await ws.cell().files.mkdir(c.worktree_path, { parents: true });
|
|
145
|
-
const background = [];
|
|
146
145
|
let initialReady, initialFailed;
|
|
147
146
|
const initialFiles = new Promise((resolve, reject) => { initialReady = resolve; initialFailed = reject; });
|
|
148
147
|
// The adapters read the project's own agent settings when it has any: discovery then waits for those files. A
|
|
149
|
-
// project without them is discovered while it seeds (0.21.1+)
|
|
148
|
+
// project without them is discovered while it seeds (0.21.1+), beside the worktree rather than in it (a prepared
|
|
149
|
+
// worktree is moved into place whole): the catalog depends on the account, not the files.
|
|
150
150
|
const projectSettings = ACP_INITIAL_FILES.some(path => existsSync(join(folder, path)));
|
|
151
|
+
// The same seed as a terminal start (0.22.0+): the prepared revision, only what the project cache lacks, or the copy
|
|
152
|
+
// seed. Media land before the reply and those still on their way stay out of the first file reconcile (acp.ts).
|
|
151
153
|
const seeded = (async () => {
|
|
152
|
-
await
|
|
153
|
-
initialFiles: ACP_INITIAL_FILES, onInitialFiles: initialReady });
|
|
154
|
-
await sendTwinSeed(ctx.store.dir, ws.cell(), c, folder, ctx.signal);
|
|
155
|
-
await checkoutConversationBranch(ctx, ws.cell(), c);
|
|
156
|
-
await Promise.all(background);
|
|
154
|
+
await seedConversation(ctx, c, ws, folder, undefined, changes, undefined, { initialFiles: ACP_INITIAL_FILES, onInitialFiles: initialReady, landNow: true });
|
|
157
155
|
callbacks.prepared();
|
|
158
156
|
})();
|
|
159
157
|
void seeded.catch(initialFailed);
|
|
160
|
-
const
|
|
161
|
-
|
|
158
|
+
const proxied = callbacks.proxy?.ready(ws) ?? Promise.resolve();
|
|
159
|
+
const models = (async () => {
|
|
160
|
+
await proxied;
|
|
161
|
+
if (projectSettings) {
|
|
162
|
+
await initialFiles;
|
|
163
|
+
return discoverAcpCatalog(ctx, ws, c.worktree_path, callbacks.proxy?.env);
|
|
164
|
+
}
|
|
165
|
+
return discoverAcpCatalog(ctx, ws, posix.dirname(c.worktree_path), callbacks.proxy?.env);
|
|
166
|
+
})();
|
|
162
167
|
// Observe both failures immediately. Never start a second VM, and never delete
|
|
163
168
|
// the user's conversation VM as catalog cleanup.
|
|
164
169
|
const results = await Promise.allSettled([seeded, models]);
|