@shardflux/cli 0.18.1 → 0.19.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.
- package/CHANGELOG.md +93 -1
- package/README.md +27 -0
- package/dist/account-args.js +1 -0
- package/dist/agent-commands.d.ts +1 -0
- package/dist/agent-commands.js +15 -5
- package/dist/agent-config-files.d.ts +16 -2
- package/dist/agent-config-files.js +48 -61
- package/dist/agent-config-sync.d.ts +12 -1
- package/dist/agent-config-sync.js +228 -54
- package/dist/agent-repository.d.ts +39 -9
- package/dist/agent-repository.js +309 -92
- package/dist/agent-state.d.ts +3 -0
- package/dist/agent-state.js +13 -0
- package/dist/agent-target-commands.d.ts +2 -0
- package/dist/agent-target-commands.js +45 -15
- package/dist/args.d.ts +1 -1
- package/dist/args.js +14 -2
- package/dist/conversation-router.js +7 -11
- package/dist/conversation-stash.js +64 -18
- package/dist/conversation-terminal.d.ts +1 -1
- package/dist/conversation-terminal.js +9 -10
- package/dist/conversations.d.ts +9 -2
- package/dist/conversations.js +23 -6
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/files-mirror.d.ts +1 -9
- package/dist/files-mirror.js +19 -3
- package/dist/format.js +37 -2
- package/dist/handlers.js +3 -1
- package/dist/initial-prompt.d.ts +15 -2
- package/dist/initial-prompt.js +29 -4
- package/dist/local-sessions.d.ts +60 -0
- package/dist/local-sessions.js +445 -0
- package/dist/main.js +12 -0
- package/dist/move.d.ts +44 -0
- package/dist/move.js +248 -0
- package/dist/moved-sessions.d.ts +33 -0
- package/dist/moved-sessions.js +91 -0
- package/dist/onboarding.d.ts +18 -3
- package/dist/onboarding.js +72 -46
- package/dist/project-sync-manifest.d.ts +31 -3
- package/dist/project-sync-manifest.js +192 -108
- package/dist/project-sync-transfer.d.ts +50 -4
- package/dist/project-sync-transfer.js +404 -106
- package/dist/project-sync.js +30 -33
- package/dist/seed-phases.d.ts +29 -0
- package/dist/seed-phases.js +83 -0
- package/dist/t3-mode.d.ts +7 -2
- package/dist/t3-mode.js +39 -5
- package/dist/thread-on-computer.js +4 -0
- package/dist/worktree-twin.d.ts +2 -6
- package/dist/worktree-twin.js +16 -51
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,98 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.19.1 (not yet published)
|
|
4
|
+
|
|
5
|
+
- A first prompt is never lost to a slow or unusual start. `--prompt` / `--prompt-file` types the prompt only into the
|
|
6
|
+
agent's input box, never into one of its dialogs ("❯ 1. Yes"). If the terminal never shows the input box and
|
|
7
|
+
nothing was typed, the prompt goes to the conversation's own inbox, which delivers it once the agent reads input.
|
|
8
|
+
`--json` then reports `"initial_prompt": {"submitted": false, "queued": true}` instead of failing with
|
|
9
|
+
`initial_prompt_failed`.
|
|
10
|
+
- Brings the Shardflux app for Ubuntu 0.1.3: Try it once never waits silently. A demo agent still waiting with nothing
|
|
11
|
+
said 45 s after its start gets its task once more through its conversation, and one line says so; still nothing
|
|
12
|
+
45 s later, one line says it hasn't started. The optional dependency on `@shardflux/desktop-linux-x64` / `-arm64` is
|
|
13
|
+
exactly 0.1.3.
|
|
14
|
+
|
|
15
|
+
## 0.19.0 (2026-10-11)
|
|
16
|
+
|
|
17
|
+
- A new conversation on an account without a saved machine gets its first prompt in seconds, whatever is in
|
|
18
|
+
`~/.claude` and `~/.codex`. The prompt is typed first, and this computer's Claude and Codex config is copied into
|
|
19
|
+
the VM after the start, by a detached `shard` that the starting command does not wait for (its one-line result goes
|
|
20
|
+
to `agent-config-sync.log` in the config directory). Before, the copy ran first, one request per file: a config with
|
|
21
|
+
3,000 files held the prompt back for over 5 minutes.
|
|
22
|
+
- The config copy sends a pass as one archive and one command in the VM instead of a check and a write per file
|
|
23
|
+
(3,090 files: 6.6 s including plugin installs, an unchanged copy 1 s). The rules stay the same: a file you changed
|
|
24
|
+
in the VM is never overwritten or removed, and a path that is not a regular file there is skipped.
|
|
25
|
+
- Claude Code plugins are installed by the VM's own Claude Code (`claude plugin marketplace add` and `claude plugin
|
|
26
|
+
install --scope user` for each user-scope plugin) instead of copying plugin caches and marketplace clones, so
|
|
27
|
+
Linux builds of each plugin are used. Codex's bundled `~/.codex/skills/.system` is left to the VM's Codex. A plugin
|
|
28
|
+
from a marketplace on a local folder is listed as skipped.
|
|
29
|
+
- A config file that does not parse (`~/.claude.json`, `~/.codex/config.toml`, the plugin registries) is skipped with
|
|
30
|
+
its reason; the rest of the config is still copied.
|
|
31
|
+
- A start reaches the agent about 7 s sooner: the snapshot of the working tree a conversation starts from is still
|
|
32
|
+
recorded before the agent starts, and its copy to the project's shared history now finishes in the background
|
|
33
|
+
(staging: Start to the first prompt typed 10.0 s, from 17.1 s).
|
|
34
|
+
- `shard setup --inspect`'s demo step reads "Paste the task, then Start.": the launcher opens on Daylight (sample).
|
|
35
|
+
- A conversation start tells the launcher which phase it is in (for the Mac menu bar and the Ubuntu app's phase line
|
|
36
|
+
under **Starting…**): with `SHARDFLUX_PROGRESS_FILE` naming a private file the launcher created, `shard agents new`
|
|
37
|
+
rewrites it atomically with one JSON line each time the phase changes, `{"phase":"copy_changes","line":"Copying
|
|
38
|
+
your changes…"}`. Phases: `computer` (Starting a computer…), `project` (Getting your project…), `checkout` (Checking
|
|
39
|
+
out your files…), `copy_changes` (Copying your changes…) and `harness` (Starting Claude… / Starting Codex…). Nothing
|
|
40
|
+
is written without the variable; a symlink, a file or directory others can read or write, or a file the launcher
|
|
41
|
+
already removed is never written.
|
|
42
|
+
- T3 Code (stable 0.0.45) connects with T3 Connect, once per person: `shard setup --inspect --t3-connect --json`
|
|
43
|
+
starts the sign-in, and its `t3` row carries the page and code (`t3_link`). Once it is approved the row is
|
|
44
|
+
`verified` per account (`Your projects appear in T3 Code and the T3 app.`, or `<project> is linked to your T3
|
|
45
|
+
account.` for the newest project set up for T3 Code), and every project set up for T3 Code appears in T3 Code and
|
|
46
|
+
the T3 app signed in to that account. With the sign-in, `agents targets` reports T3 Code connected for every
|
|
47
|
+
project, so a project's first thread can start in T3 Code: it is linked first, then the thread starts. Without the
|
|
48
|
+
sign-in the row offers **Connect T3 Code**; `next` still names `shard agents new --in t3 --path <project>` for
|
|
49
|
+
scripts. `shard agents new|open --in t3` says the project is linked to your T3 account instead of how to add an SSH
|
|
50
|
+
environment, and the first time a project is set up for T3 Code it opens T3 on its Connections page
|
|
51
|
+
(`t3code://app/settings/connections`) once the project is linked (on Linux through T3's registered URL handler, else
|
|
52
|
+
T3 Code opens). The project's SSH host is still configured for anyone who adds it in T3 Code themselves.
|
|
53
|
+
- Brings the Shardflux app for Ubuntu 0.1.2: T3 Code connects from the first run like Claude and Codex (one T3
|
|
54
|
+
Connect sign-in), and your own project starts in T3 Code. The optional dependency on
|
|
55
|
+
`@shardflux/desktop-linux-x64` / `-arm64` is exactly 0.1.2.
|
|
56
|
+
|
|
57
|
+
- `shard usage` shows GPU-hours on plans that include them: a `GPU-hours` line with the hours used of those included,
|
|
58
|
+
the hours past them and the part billed as overage, and while the GPU is stopped a `GPU` line naming the reason
|
|
59
|
+
(`allowance_used`, `overage_paused`, `spend_cap_reached`) and what brings it back (turn on overage, upgrade, pay the
|
|
60
|
+
invoice, raise the cap, or the period reset). Workspaces keep running meanwhile. `shard billing overage` lists the
|
|
61
|
+
GPU-hour rate, and a 402 `entitlement_required` `not_included` refusal gets an upgrade hint naming the plans that
|
|
62
|
+
include the feature (the GPU: Startup or Scale). Needs SDK 0.25.0.
|
|
63
|
+
|
|
64
|
+
- `shard move`: a Claude Code or Codex session on this computer continues in the cloud as the same session, with its
|
|
65
|
+
full history. `shard move --last` in the project folder moves its newest session (`--claude` or `--codex` to
|
|
66
|
+
choose), `--session <id>` a given one, and `--list` shows the project's sessions and where moved ones continue; `--json`
|
|
67
|
+
for scripts and apps, `--detach` to start it without opening it here. A running turn finishes here first. The
|
|
68
|
+
conversation starts from your project exactly as it is, uncommitted edits included (your working tree, index and
|
|
69
|
+
branch are untouched), its VM resumes the session (`claude --resume`, `codex resume`) with its paths moved to the
|
|
70
|
+
VM's folder, and the session's transcript is in the project space from the start. The cloud work comes back to
|
|
71
|
+
your checkout by itself, and typing in the local Claude Code copy shows one line that it now continues in the cloud.
|
|
72
|
+
Sessions from Claude Desktop's Code tab and the Codex app move the same way. Needs SDK 0.25.0.
|
|
73
|
+
- When a moved session's conversation ends, its transcript comes back to the file it left on this computer, as long as
|
|
74
|
+
nobody continued that local copy meanwhile (the copy it replaces stays in the conversation's stash).
|
|
75
|
+
- Claude Code's folder name for a long project path (above 200 characters) is the one Claude Code uses, so finished
|
|
76
|
+
conversations of such projects appear in its own history.
|
|
77
|
+
- A new conversation's VM gets your project from Git: HEAD is checked out in the VM, and only what differs from HEAD
|
|
78
|
+
on this computer is copied (changed, added and untracked files Git does not ignore, `.env*` files, Git LFS files,
|
|
79
|
+
deletions), from `git status`, not a hash of every file. A project's file count, size and history no longer refuse
|
|
80
|
+
a start (`repository_too_large` is gone): a history above 32 MiB comes from origin as a blobless partial clone (all
|
|
81
|
+
commits and trees; HEAD's files in one request; the rest backfills in the background at the lowest priority), and
|
|
82
|
+
without a reachable origin the history bundle streams from this computer in chunks of any size. The only start
|
|
83
|
+
refusal left, a folder inside another repository, comes before a VM is allocated. `SHARDFLUX_TRACE_SEED=1` prints
|
|
84
|
+
each start phase's duration on stderr.
|
|
85
|
+
- Project sync carries a project of any shape: no file-count limit and no per-file size limit. Files above 8 MiB
|
|
86
|
+
stream both ways one chunk at a time (verified against their hash, never held whole in memory), files older VMs
|
|
87
|
+
leave out of their manifest for size are hashed in the VM, and only what a VM cannot hold is reported as skipped
|
|
88
|
+
(sockets and devices, nested repositories, names with a backslash) instead of failing the sync. A path that turns
|
|
89
|
+
from a directory into a file (or back) syncs both ways.
|
|
90
|
+
- The live twin's snapshot and landing carry files and bundles of any size (streamed from and to disk); the 64 MiB
|
|
91
|
+
per-file hold and the 256 MiB transfer bound are gone.
|
|
92
|
+
- Conversation transcripts of any size mirror into the local stash (read in chunks, checked as UTF-8 and against
|
|
93
|
+
their hash); the 64 MiB mirror bound is gone. The project router copies history of any size between VMs.
|
|
94
|
+
|
|
95
|
+
## 0.18.1 (2026-10-10)
|
|
4
96
|
|
|
5
97
|
- `shard agents logs` takes a conversation id (`shard agents logs conv_… --json`) and follows that conversation's own
|
|
6
98
|
terminal: the PTY its computer recorded for it (a fork's inherited one included), also when you name the
|
package/README.md
CHANGED
|
@@ -129,6 +129,21 @@ keeps the agent's own choice. The conversation keeps its model and effort when i
|
|
|
129
129
|
adds `model_choice: {model, effort, applied}`. `shard agents targets --json` lists the choices as `models`, with menu
|
|
130
130
|
and pill text and the agent's own default, so apps never hardcode them. `--in t3` passes them to T3 Code.
|
|
131
131
|
|
|
132
|
+
Move a session from this computer to the cloud (0.19.0+): it continues there as the same Claude Code or Codex
|
|
133
|
+
session, with its full history, from your project exactly as it is.
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
shard move --last # this project's newest session (--claude or --codex to choose)
|
|
137
|
+
shard move --list # the project's sessions, and where moved ones continue
|
|
138
|
+
shard move --session 3d085e35-aa66-4866-9a53-f09719d6767c --detach --json
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
A running turn finishes here first. Uncommitted edits come along, and your working tree, index and branch stay as
|
|
142
|
+
they are. The cloud work comes back to your checkout by itself, and typing in the local Claude Code copy shows one line
|
|
143
|
+
that it continues in the cloud. Sessions from Claude Desktop's Code tab and the Codex app move the same way; in the
|
|
144
|
+
macOS menu bar, a project's recent sessions have **Move to cloud** in their ⋯ menu, and with the Shardflux MCP server
|
|
145
|
+
you can ask the agent itself to continue in the cloud.
|
|
146
|
+
|
|
132
147
|
Start a thread in T3 Code from the command line (0.17.0+):
|
|
133
148
|
|
|
134
149
|
```sh
|
|
@@ -1039,6 +1054,18 @@ names `details.reason`, and a hint says what to do (0.5.0+):
|
|
|
1039
1054
|
| `spend_cap_reached` | The spend cap is reached: raise it (`shard billing overage cap <dollars>`, at most the plan price) or upgrade. |
|
|
1040
1055
|
| `overage_paused` | A payment is past due: update the payment method (`shard billing portal --open`). |
|
|
1041
1056
|
|
|
1057
|
+
GPU-hours (0.19.0+): on plans with GPU-hours, `shard usage` adds a `GPU-hours` line with the hours used of those
|
|
1058
|
+
included (and, with overage on, the hours past them and the part billed), and a `GPU` line with the next step whenever
|
|
1059
|
+
GPU rendering needs one:
|
|
1060
|
+
|
|
1061
|
+
```text
|
|
1062
|
+
GPU-hours 3.4 of 25 used
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
`shard billing overage` lists the GPU-hour rate with the others, and a 402 `entitlement_required` refusal with reason
|
|
1066
|
+
`not_included` gets a hint naming the plans that include the feature. [/limits](https://docs.shardflux.dev/limits#gpu-hours)
|
|
1067
|
+
has the allowances and the rate.
|
|
1068
|
+
|
|
1042
1069
|
## Timing
|
|
1043
1070
|
|
|
1044
1071
|
`--timing` (0.3.0+) says where the time of an open or a wait went. After the call, `shard` prints the lifecycle
|
package/dist/account-args.js
CHANGED
|
@@ -209,6 +209,7 @@ export const ACCOUNT_COMMANDS = [
|
|
|
209
209
|
apps: { type: 'string', multiple: true, value: '<claude,codex>', description: 'The apps to verify (default: the ones found on this Mac). Repeatable or comma separated.' },
|
|
210
210
|
connect: { type: 'boolean', description: 'With --inspect: also copy this Mac’s Codex login and enable the account’s Codex remote device when Codex is picked.' },
|
|
211
211
|
'retry-codex': { type: 'boolean', description: 'With --inspect --connect: start a Codex remote device that failed or is stuck starting again, once.' },
|
|
212
|
+
't3-connect': { type: 'boolean', description: 'With --inspect: start your T3 Connect sign-in for T3 Code (0.0.45, over SSH); the t3 row carries its page and code (t3_link). After it, your projects appear in T3 Code and the T3 app (0.19.0+).' },
|
|
212
213
|
'org-name': { type: 'string', value: '<name>', description: 'Name of the organization to create when you have none.' },
|
|
213
214
|
'project-name': { type: 'string', value: '<name>', description: 'Name of the project to create when the organization has none (default Default).' },
|
|
214
215
|
'key-name': { type: 'string', value: '<name>', description: 'Name of the API key (default "shard-cli <hostname>").' },
|
package/dist/agent-commands.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export declare function terminal(ctx: Ctx, ws: Workspace, app: 'claude' | 'codex
|
|
|
23
23
|
pty?: string;
|
|
24
24
|
title?: string;
|
|
25
25
|
choice?: ModelChoice;
|
|
26
|
+
afterStart?: () => Promise<void> | undefined;
|
|
26
27
|
}): Promise<number>;
|
|
27
28
|
export declare const launchProject: Handler;
|
|
28
29
|
/** The project VM launch; with `--in t3` a first message becomes a thread on the T3 server in it (SSH mode, 0.17.0+). */
|
package/dist/agent-commands.js
CHANGED
|
@@ -17,7 +17,7 @@ import { out, resolveWorkspace } from "./commands.js";
|
|
|
17
17
|
import { savedMachine } from "./filesystem-state.js";
|
|
18
18
|
import { startAgentConfigTunnels, syncAgentConfig } from "./agent-config-sync.js";
|
|
19
19
|
import { rememberGitAgent } from "./agent-git-commands.js";
|
|
20
|
-
import { projectWorkspacePath, seedAgentRepository, workspaceRepositoryPath } from "./agent-repository.js";
|
|
20
|
+
import { preflightProjectSeed, projectWorkspacePath, seedAgentRepository, workspaceRepositoryPath } from "./agent-repository.js";
|
|
21
21
|
import { agentName } from "./agent-git.js";
|
|
22
22
|
import { cloudClaudeLogin, ensureCloudClaude, loginClaudeInWorkspace, requireClaudeTerminal } from "./claude-cloud-login.js";
|
|
23
23
|
import { progress } from "./progress.js";
|
|
@@ -28,6 +28,7 @@ import { openInFor } from "./t3-mode.js";
|
|
|
28
28
|
import { openClaudeDesktopSession, openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
29
29
|
import { submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
30
30
|
import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
31
|
+
import { conversationClient } from "./conversation-commands.js";
|
|
31
32
|
import { conversationTerminal, reopenConversation } from "./conversation-terminal.js";
|
|
32
33
|
import { CHOICE_PENDING, choiceReport, hasChoice, launchChoice, modelChoice, runFlags } from "./agent-models.js";
|
|
33
34
|
const active = (session) => session.state === 'running' || session.state === 'starting';
|
|
@@ -145,15 +146,22 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
145
146
|
throw new Error(`project setup could not be queued: ${queued.stderr.trim() || `exit ${queued.exitCode}`}`);
|
|
146
147
|
});
|
|
147
148
|
store.save(project, app);
|
|
149
|
+
let delivery;
|
|
148
150
|
if (typeof values.initialPrompt === 'string') {
|
|
149
|
-
|
|
151
|
+
const text = values.initialPrompt;
|
|
152
|
+
// A conversation's guest delivers the prompt itself when this terminal never showed its input box (0.19.1+).
|
|
153
|
+
const queue = conversation ? () => conversationClient(ctx).conversations.send(conversation.id, text, { signal: ctx.signal }) : undefined;
|
|
154
|
+
delivery = await submitInitialPrompt(signal => cellConnector(cell, session.session_id)(0, signal), text, ws.key, ctx.signal, app, bytes => cell.pty.input(session.session_id, bytes), queue);
|
|
150
155
|
}
|
|
156
|
+
// Work that must never delay the first prompt (the laptop's agent config, 0.19.0+) starts once the agent has it.
|
|
157
|
+
const after = conversation?.afterStart?.();
|
|
151
158
|
if (!attach) {
|
|
152
159
|
await preparation;
|
|
153
160
|
// conversation (0.16.0+): which conversation this session is, also when its computer hosts several threads.
|
|
154
161
|
// model_choice (0.17.0+): the requested model and effort, and whether this thread started with them.
|
|
155
|
-
ctx.io.stdout.write(json({ workspace: ws.data, session, ...(conversation ? { conversation: { id: conversation.id } } : {}), ...(
|
|
162
|
+
ctx.io.stdout.write(json({ workspace: ws.data, session, ...(conversation ? { conversation: { id: conversation.id } } : {}), ...(delivery ? { initial_prompt: delivery === 'submitted' ? { submitted: true } : { submitted: false, queued: true } } : {}),
|
|
156
163
|
...(hasChoice(explicit) ? { model_choice: choiceReport(explicit, applied) } : {}) }));
|
|
164
|
+
await after;
|
|
157
165
|
return 0;
|
|
158
166
|
}
|
|
159
167
|
ctx.io.stderr.write(`Shardflux · ${ws.key}\nDetach: Ctrl-] then d\n`);
|
|
@@ -171,7 +179,7 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
171
179
|
done();
|
|
172
180
|
}),
|
|
173
181
|
});
|
|
174
|
-
const [code] = await Promise.all([attached, preparation]);
|
|
182
|
+
const [code] = await Promise.all([attached, preparation, after]);
|
|
175
183
|
return code;
|
|
176
184
|
}
|
|
177
185
|
finally {
|
|
@@ -222,8 +230,10 @@ export async function launchProjectWith(ctx, values, [path = './', explicitApp],
|
|
|
222
230
|
const labels = projectLabels(project, agent, app);
|
|
223
231
|
const local = like ? projectIdentity(path) : project;
|
|
224
232
|
// A cloud-only --like launch must not initialize an unrelated local checkout.
|
|
225
|
-
if (!like?.labels.repo_remote || like.labels.repo_remote === (local.remote ?? `file://${local.folder}`))
|
|
233
|
+
if (!like?.labels.repo_remote || like.labels.repo_remote === (local.remote ?? `file://${local.folder}`)) {
|
|
226
234
|
await ensureProjectGit(ctx, local.folder);
|
|
235
|
+
await preflightProjectSeed(local.folder, ctx.signal);
|
|
236
|
+
}
|
|
227
237
|
const cloud = ctx.cloud();
|
|
228
238
|
let existing = await cloud.workspaces.findByKey(`${project.slug}/${agent}`, { includeDeleted: false, signal: ctx.signal });
|
|
229
239
|
const owner = agent === 'main' ? existing : like ?? await cloud.workspaces.findByKey(`${project.slug}/main`, { includeDeleted: false, signal: ctx.signal });
|
|
@@ -17,7 +17,21 @@ export declare function readObject(file: string): Promise<ObjectValue | undefine
|
|
|
17
17
|
export declare function collectTree(source: string, dest: string, files: ConfigFiles, issues: ConfigIssue[], parents?: Set<string>): Promise<void>;
|
|
18
18
|
export declare function collectUserFiles(home: string, issues: ConfigIssue[]): Promise<ConfigFiles>;
|
|
19
19
|
export declare function macBinary(bytes: Buffer): boolean;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Plugins travel as an install plan, never as files (0.19.0+): the VM's own Claude Code adds each marketplace from its
|
|
22
|
+
* source and installs each user-scope plugin (`claude plugin marketplace add` / `claude plugin install`), so plugin
|
|
23
|
+
* caches and marketplace clones (thousands of files, often built for macOS) are rebuilt there instead of copied.
|
|
24
|
+
* A marketplace with a local source cannot be rebuilt in the VM and is listed as skipped.
|
|
25
|
+
*/
|
|
26
|
+
export type PluginPlan = {
|
|
27
|
+
marketplaces: {
|
|
28
|
+
name: string;
|
|
29
|
+
source: string;
|
|
30
|
+
}[];
|
|
31
|
+
plugins: string[];
|
|
32
|
+
};
|
|
33
|
+
/** A remote marketplace source as `claude plugin marketplace add` takes it; undefined for a local one. */
|
|
34
|
+
export declare function marketplaceSource(entry: ObjectValue): string | undefined;
|
|
35
|
+
export declare function collectPlugins(home: string, issues: ConfigIssue[]): Promise<PluginPlan>;
|
|
22
36
|
/** Only rewrite paths to files we actually send. Missing laptop paths are never made to look portable. */
|
|
23
37
|
export declare function rewritePaths(value: unknown, home: string, remoteHome: string, files: ConfigFiles, issues: ConfigIssue[], item: string): unknown;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, readdir, realpath, stat } from 'node:fs/promises';
|
|
2
|
-
import { join, relative
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
3
|
import { LOCAL_PATH_REASON } from "./agent-config-report.js";
|
|
4
4
|
export const object = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
5
5
|
export const text = (v) => typeof v === 'string' ? v : undefined;
|
|
@@ -48,7 +48,7 @@ export async function collectTree(source, dest, files, issues, parents = new Set
|
|
|
48
48
|
if (info.isDirectory()) {
|
|
49
49
|
const next = new Set(parents).add(resolved);
|
|
50
50
|
for (const name of (await readdir(resolved)).sort()) {
|
|
51
|
-
if (excluded.has(name) || name.startsWith('.env.'))
|
|
51
|
+
if (excluded.has(name) || name.startsWith('.env.') || (dest === '.codex/skills' && name === '.system'))
|
|
52
52
|
continue;
|
|
53
53
|
await collectTree(join(resolved, name), `${dest}/${name}`, files, issues, next);
|
|
54
54
|
}
|
|
@@ -95,70 +95,57 @@ export function macBinary(bytes) {
|
|
|
95
95
|
return bytes.length >= 8 && bytes.readUInt32BE(4) > 0 && bytes.readUInt32BE(4) <= 20;
|
|
96
96
|
return new Set([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xbebafeca, 0xbfbafeca]).has(magic);
|
|
97
97
|
}
|
|
98
|
-
/**
|
|
99
|
-
export
|
|
100
|
-
const
|
|
98
|
+
/** A remote marketplace source as `claude plugin marketplace add` takes it; undefined for a local one. */
|
|
99
|
+
export function marketplaceSource(entry) {
|
|
100
|
+
const source = object(entry.source);
|
|
101
|
+
const kind = text(source.source);
|
|
102
|
+
const value = kind === 'github' ? text(source.repo) : kind === 'git' || kind === 'url' ? text(source.url) : undefined;
|
|
103
|
+
return value && !value.startsWith('-') && !/[\0\r\n]/.test(value) ? value : undefined;
|
|
104
|
+
}
|
|
105
|
+
async function readPluginRegistry(file, item, issues) {
|
|
106
|
+
try {
|
|
107
|
+
return await readObject(file);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof SyntaxError || /expected an object/.test(String(error))) {
|
|
111
|
+
issues.push({ item, reason: 'not valid JSON; skipped' });
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function collectPlugins(home, issues) {
|
|
118
|
+
const plan = { marketplaces: [], plugins: [] };
|
|
101
119
|
const root = join(home, '.claude/plugins');
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
const path = text(record.installPath);
|
|
114
|
-
if (!path || !under(resolve(path), join(root, 'cache'))) {
|
|
115
|
-
issues.push({ item: `plugin:${id}`, reason: 'install path is outside the plugin cache' });
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
const dest = relative(home, resolve(path));
|
|
119
|
-
const tree = new Map();
|
|
120
|
-
const before = issues.length;
|
|
121
|
-
await collectTree(path, dest, tree, issues);
|
|
122
|
-
if (issues.length !== before || [...tree.values()].some(f => macBinary(f.content))) {
|
|
123
|
-
issues.push({ item: `plugin:${id}`, reason: issues.length !== before ? 'plugin tree is incomplete' : 'contains macOS native binaries; install a Linux version' });
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
for (const [name, file] of tree)
|
|
127
|
-
files.set(name, file);
|
|
128
|
-
kept.push({ ...record, installPath: `${remoteHome}/${dest}` });
|
|
129
|
-
}
|
|
130
|
-
if (kept.length)
|
|
131
|
-
plugins[id] = kept;
|
|
132
|
-
else
|
|
133
|
-
skipped.add(id);
|
|
120
|
+
const known = await readPluginRegistry(join(root, 'known_marketplaces.json'), '.claude/plugins/known_marketplaces.json', issues);
|
|
121
|
+
const local = new Set();
|
|
122
|
+
for (const [name, raw] of Object.entries(known ?? {})) {
|
|
123
|
+
const source = marketplaceSource(object(raw));
|
|
124
|
+
if (source)
|
|
125
|
+
plan.marketplaces.push({ name, source });
|
|
126
|
+
else {
|
|
127
|
+
local.add(name);
|
|
128
|
+
issues.push({ item: `marketplace:${name}`, reason: 'local marketplace; add it in the VM with claude plugin marketplace add' });
|
|
134
129
|
}
|
|
135
|
-
files.set('.claude/plugins/installed_plugins.json', { content: Buffer.from(JSON.stringify({ ...installed, plugins }) + '\n'), mode: '0600' });
|
|
136
130
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (issues.length !== before)
|
|
152
|
-
continue;
|
|
153
|
-
// Marketplace source can contain binary packages for other platforms; the installed version is checked above.
|
|
154
|
-
for (const [name, file] of tree)
|
|
155
|
-
if (!macBinary(file.content))
|
|
156
|
-
files.set(name, file);
|
|
157
|
-
portable[id] = { ...entry, installLocation: `${remoteHome}/${dest}` };
|
|
131
|
+
const installed = await readPluginRegistry(join(root, 'installed_plugins.json'), '.claude/plugins/installed_plugins.json', issues);
|
|
132
|
+
for (const [id, records] of Object.entries(object(installed?.plugins))) {
|
|
133
|
+
const user = (Array.isArray(records) ? records : [records]).some(r => object(r).scope === 'user');
|
|
134
|
+
if (!user) {
|
|
135
|
+
issues.push({ item: `plugin:${id}`, reason: 'project/local scope travels with its repository' });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!/^[A-Za-z0-9._-]+(?:@[A-Za-z0-9._-]+)?$/.test(id)) {
|
|
139
|
+
issues.push({ item: `plugin:${id}`, reason: 'not a plugin@marketplace id' });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (local.has(id.split('@')[1] ?? '')) {
|
|
143
|
+
issues.push({ item: `plugin:${id}`, reason: 'its marketplace is local to this computer' });
|
|
144
|
+
continue;
|
|
158
145
|
}
|
|
159
|
-
|
|
146
|
+
plan.plugins.push(id);
|
|
160
147
|
}
|
|
161
|
-
return
|
|
148
|
+
return plan;
|
|
162
149
|
}
|
|
163
150
|
/** Only rewrite paths to files we actually send. Missing laptop paths are never made to look portable. */
|
|
164
151
|
export function rewritePaths(value, home, remoteHome, files, issues, item) {
|
|
@@ -28,6 +28,8 @@ export type ConfigManifest = {
|
|
|
28
28
|
hash: string;
|
|
29
29
|
created: boolean;
|
|
30
30
|
}>;
|
|
31
|
+
/** The plugin install plan last applied in the VM (0.19.0+): an unchanged plan is not installed again. */
|
|
32
|
+
plugins_hash?: string;
|
|
31
33
|
skipped: ConfigIssue[];
|
|
32
34
|
mcp: McpStatus[];
|
|
33
35
|
tunnels: ConfigTunnel[];
|
|
@@ -44,12 +46,21 @@ export type ConfigSyncResult = {
|
|
|
44
46
|
export declare function configManifests(ctx: Pick<Ctx, 'store'>): Record<string, ConfigManifest>;
|
|
45
47
|
/** `like` must not inherit generated MCP secrets restricted to the source workspace. */
|
|
46
48
|
export declare function agentConfigLikeSecrets(ctx: Ctx, reference: string): Promise<string[] | undefined>;
|
|
47
|
-
|
|
49
|
+
/** At least this many files to check go into the VM as one archive and one command instead of a stat and a write each. */
|
|
50
|
+
export declare const BULK_MIN_FILES = 8;
|
|
51
|
+
export declare function applyConfigFiles(cell: CellClient, home: string, files: ConfigFiles, previous: Record<string, ManifestFile>, trustManifest: boolean, issues: ConfigIssue[], signal: AbortSignal, deleteMissing?: boolean, progress?: (path: string, file: ManifestFile | undefined) => void, trusted?: ReadonlySet<string>): Promise<{
|
|
48
52
|
files: Record<string, ManifestFile>;
|
|
49
53
|
copied: string[];
|
|
50
54
|
removed: string[];
|
|
51
55
|
}>;
|
|
52
56
|
export declare function syncAgentConfig(ctx: Ctx, ws: Workspace): Promise<ConfigSyncResult>;
|
|
57
|
+
/**
|
|
58
|
+
* Copies the laptop's agent config into a new conversation's VM after its agent started (0.19.0+): the first prompt
|
|
59
|
+
* never waits for it. From an installed shard it runs as its own detached process (`shard agents config-sync`, its
|
|
60
|
+
* output in agent-config-sync.log of the config directory), so the command that started the conversation reports the
|
|
61
|
+
* start at once; without a runtime (tests, an embedding) it runs here and the returned promise settles when it ends.
|
|
62
|
+
*/
|
|
63
|
+
export declare function syncAgentConfigAfterStart(ctx: Ctx, ws: Workspace): Promise<void> | undefined;
|
|
53
64
|
/** Tunnels are owned by the attachment, and close even if the terminal fails or the user detaches. */
|
|
54
65
|
export declare function startAgentConfigTunnels(ctx: Ctx, ws: Workspace): Promise<{
|
|
55
66
|
stop(): Promise<void>;
|