@shardflux/cli 0.19.1 → 0.22.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 +278 -1
- package/README.md +225 -73
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +20 -15
- package/dist/account-pool.js +51 -97
- package/dist/account-scope.d.ts +65 -0
- package/dist/account-scope.js +246 -0
- package/dist/acp-login.d.ts +4 -3
- package/dist/acp-login.js +7 -5
- package/dist/acp-models.js +20 -15
- package/dist/acp.js +66 -13
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +86 -32
- package/dist/agent-config-files.d.ts +19 -0
- package/dist/agent-config-files.js +70 -1
- package/dist/agent-config-return.d.ts +194 -0
- package/dist/agent-config-return.js +724 -0
- package/dist/agent-config-sync.d.ts +10 -2
- package/dist/agent-config-sync.js +45 -51
- package/dist/agent-credentials.js +7 -3
- package/dist/agent-repository.d.ts +16 -3
- package/dist/agent-repository.js +149 -74
- package/dist/agent-setup.d.ts +14 -3
- package/dist/agent-setup.js +78 -36
- package/dist/agent-target-commands.js +24 -13
- package/dist/args.d.ts +1 -1
- package/dist/args.js +104 -55
- package/dist/bin.js +5 -1
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -14
- package/dist/claude-cloud-login.d.ts +24 -2
- package/dist/claude-cloud-login.js +151 -11
- package/dist/cli-proxy-forward.d.ts +2 -0
- package/dist/cli-proxy-forward.js +9 -2
- package/dist/cli-proxy-hub.d.ts +69 -25
- package/dist/cli-proxy-hub.js +231 -54
- package/dist/cli-proxy.d.ts +70 -1
- package/dist/cli-proxy.js +298 -32
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +5 -0
- package/dist/codex-cloud.js +88 -17
- package/dist/command-center-actions.js +5 -1
- package/dist/commands.js +171 -22
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +8 -5
- package/dist/conversation-terminal.d.ts +10 -2
- package/dist/conversation-terminal.js +81 -15
- package/dist/conversations.d.ts +77 -1
- package/dist/conversations.js +257 -23
- package/dist/device-bridge.d.ts +1 -1
- package/dist/device-bridge.js +6 -4
- package/dist/device-helper.js +52 -4
- package/dist/errors.d.ts +34 -1
- package/dist/errors.js +56 -5
- package/dist/filesystem-commands.d.ts +22 -3
- package/dist/filesystem-commands.js +132 -16
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +4 -1
- package/dist/initial-prompt.d.ts +8 -0
- package/dist/initial-prompt.js +43 -1
- package/dist/large-media.d.ts +14 -0
- package/dist/large-media.js +100 -0
- package/dist/linux-terminal.d.ts +5 -3
- package/dist/linux-terminal.js +6 -20
- package/dist/local-sessions.d.ts +26 -2
- package/dist/local-sessions.js +113 -24
- package/dist/machine-setup.d.ts +190 -0
- package/dist/machine-setup.js +849 -0
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +23 -13
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +63 -0
- package/dist/move-worktrees.js +201 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +116 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +59 -27
- package/dist/prepared-repository.d.ts +202 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1306 -0
- package/dist/project-preparation.d.ts +48 -0
- package/dist/project-preparation.js +304 -0
- package/dist/project-sync-manifest.d.ts +4 -0
- package/dist/project-sync-manifest.js +5 -1
- package/dist/project-sync-transfer.js +1 -1
- package/dist/project-sync.d.ts +19 -4
- package/dist/project-sync.js +31 -8
- package/dist/project-template-setup.d.ts +33 -1
- package/dist/project-template-setup.js +59 -2
- package/dist/secret-ask.d.ts +72 -0
- package/dist/secret-ask.js +351 -0
- package/dist/seed-phases.d.ts +13 -3
- package/dist/seed-phases.js +21 -3
- package/dist/setup-cache.d.ts +7 -0
- package/dist/setup-cache.js +32 -0
- package/dist/t3-launch.js +1 -4
- package/dist/t3-mode.d.ts +8 -2
- package/dist/t3-mode.js +19 -4
- package/dist/t3-phone.d.ts +4 -2
- package/dist/t3-phone.js +12 -4
- package/dist/terminal-apps.d.ts +157 -0
- package/dist/terminal-apps.js +561 -0
- package/dist/terminal-onboarding.d.ts +2 -0
- package/dist/terminal-onboarding.js +11 -1
- package/dist/this-computer.d.ts +11 -0
- package/dist/this-computer.js +36 -0
- package/dist/thread-on-computer.js +4 -1
- package/dist/update.js +11 -3
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
package/dist/agent-commands.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { linuxTerminalSession
|
|
1
|
+
import { linuxTerminalSession } from "./linux-terminal.js";
|
|
2
|
+
import { attachable, currentTerminal, openShardInTerminal, referenceFolder, reportTerminalOpen, storedTerminal } from "./terminal-apps.js";
|
|
2
3
|
import { repositoryName } from "./files-mirror.js";
|
|
3
|
-
import { prepareSetupCache } from "./setup-cache.js";
|
|
4
|
+
import { prepareSetupCache, recordSetupCacheFailure } from "./setup-cache.js";
|
|
4
5
|
import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
|
|
5
6
|
import { agentReachability } from "./agent-reachability.js";
|
|
6
7
|
import { configureCredentialFill, watchCredentialPrompts } from "./credential-prompts.js";
|
|
@@ -21,12 +22,14 @@ import { preflightProjectSeed, projectWorkspacePath, seedAgentRepository, worksp
|
|
|
21
22
|
import { agentName } from "./agent-git.js";
|
|
22
23
|
import { cloudClaudeLogin, ensureCloudClaude, loginClaudeInWorkspace, requireClaudeTerminal } from "./claude-cloud-login.js";
|
|
23
24
|
import { progress } from "./progress.js";
|
|
25
|
+
import { seedEvent, seedPhase } from "./seed-phases.js";
|
|
24
26
|
import { rememberLaunch } from "./agent-live.js";
|
|
25
27
|
import { prepareAgentMachine } from "./agent-machine.js";
|
|
26
28
|
import { installedTargets } from "./agent-targets.js";
|
|
27
29
|
import { openInFor } from "./t3-mode.js";
|
|
30
|
+
import { storedTitlePrefix } from "./client-settings.js";
|
|
28
31
|
import { openClaudeDesktopSession, openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
|
|
29
|
-
import { submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
32
|
+
import { guestFirstPrompt, INBOX_MAX_CHARS, submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
|
|
30
33
|
import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
|
|
31
34
|
import { conversationClient } from "./conversation-commands.js";
|
|
32
35
|
import { conversationTerminal, reopenConversation } from "./conversation-terminal.js";
|
|
@@ -53,7 +56,8 @@ export async function projectTemplate(ctx, remote) {
|
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
/** `place` is the repository or folder name session titles end with ("todo-app"), `task` what a new session is titled
|
|
56
|
-
* with before the guest names it from its first prompt (a first prompt line or the conversation's title)
|
|
59
|
+
* with before the guest names it from its first prompt (a first prompt line or the conversation's title), and
|
|
60
|
+
* `launch.titlePrefix` the marker titles start with when this computer chose one ("" for none; 0.20.1+). */
|
|
57
61
|
export async function agentPty(cell, app, agent, approvals, term, signal, place, setupHash, cwd, ptyName, onLaunch, deferSetup = false, conversation, task, launch) {
|
|
58
62
|
const id = `shard-${ptyName || agent}-${app}`;
|
|
59
63
|
// Arrival and changed locks queue the same durable guest job even when the
|
|
@@ -81,7 +85,8 @@ export async function agentPty(cell, app, agent, approvals, term, signal, place,
|
|
|
81
85
|
}
|
|
82
86
|
// A model and effort choice reaches only a guest whose run accepts it (the caller checked).
|
|
83
87
|
const started = await cell.exec.run(['sf-agent', 'run', app, '--name', agent, '--detach', ...conversationArgs, ...(ptyName ? ['--pty-name', ptyName] : []), ...(approvals ? ['--approvals'] : []), ...(setupHash ? ['--setup', '--setup-deps-hash', setupHash] : []), ...runFlags(launch?.choice)], {
|
|
84
|
-
|
|
88
|
+
// SF_TITLE_PREFIX: the chosen marker for a launch without a conversation record (a guest before it ignores it).
|
|
89
|
+
env: { TERM: term, ...(place ? { SF_PROJECT_NAME: place, SF_REPO_NAME: place } : {}), ...(task ? { SF_TASK_TITLE: task } : {}), ...(launch?.titlePrefix !== undefined ? { SF_TITLE_PREFIX: launch.titlePrefix } : {}), ...(setupHash && deferSetup ? { SF_SETUP_DEFERRED: '1' } : {}) }, timeoutMs: 30_000, maxOutputBytes: 4096, signal, ...(cwd ? { cwd } : {}),
|
|
85
90
|
});
|
|
86
91
|
if (started.exitCode !== 0)
|
|
87
92
|
throw new Error(`agent launch failed: ${started.stderr.trim() || `exit ${started.exitCode}`}`);
|
|
@@ -122,7 +127,12 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
122
127
|
if (app === 'codex' && typeof values.initialPrompt === 'string') {
|
|
123
128
|
await prepareInitialCodexProject(cell, cwd, `shard-${ptyName || agent}-${app}`);
|
|
124
129
|
}
|
|
125
|
-
|
|
130
|
+
// Credential fill serves the agent's later credential prompts: configured beside the first prompt, never before it.
|
|
131
|
+
const credentialFill = configureCredentialFill(ctx, ws).catch((error) => {
|
|
132
|
+
if (!ctx.signal.aborted)
|
|
133
|
+
ctx.io.stderr.write(`Credential fill is off for this session: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
134
|
+
return false;
|
|
135
|
+
});
|
|
126
136
|
// This command's --model/--effort, else the conversation's own (a relaunch keeps its model). A computer whose guest
|
|
127
137
|
// predates the choice starts the agent with its defaults, and this command says so once.
|
|
128
138
|
const help = runHelp(cell, app, ctx.signal), explicit = modelChoice(values, app);
|
|
@@ -131,16 +141,21 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
131
141
|
ctx.io.stderr.write(`${CHOICE_PENDING}\n`);
|
|
132
142
|
// A cold launch is remembered before it starts, so the listing can show it before the projection does (agent-live.ts).
|
|
133
143
|
const term = ctx.io.env.TERM && ctx.io.env.TERM !== 'dumb' ? ctx.io.env.TERM : 'xterm-256color';
|
|
134
|
-
const
|
|
144
|
+
const titlePrefix = storedTitlePrefix(ctx.store.dir);
|
|
145
|
+
const session = await agentPty(cell, app, agent, values.approvals === true, term, ctx.signal, repositoryName(ws.labels.repo_remote || project), setupHash, cwd, ptyName, () => { rememberLaunch(ctx, ws.id); progress(ctx, `Starting ${app === 'claude' ? 'Claude' : 'Codex'} in ${ws.key}…`); }, true, conversation && { id: conversation.id, session: conversation.session }, sessionTask(typeof values.initialPrompt === 'string' ? values.initialPrompt : conversation?.title), { ...(choice ? { choice } : {}), help, ...(titlePrefix !== undefined ? { titlePrefix } : {}) });
|
|
146
|
+
seedPhase('harness_launch');
|
|
147
|
+
// When the harness's terminal started, by the VM's clock (the guest may start it before this command asks).
|
|
148
|
+
if (session.created_at)
|
|
149
|
+
seedEvent('harness_started', Date.parse(session.created_at));
|
|
135
150
|
// A new conversation's guest can start the agent before this command reaches it, so the launch above never ran.
|
|
136
151
|
// Remember it either way: listing and `agents open` then read the guest until the projection lists the session.
|
|
137
152
|
rememberLaunch(ctx, ws.id);
|
|
138
153
|
// The real agent is running before any cache provisioning or mount wait.
|
|
139
|
-
const preparation = prepareSetupCache(ctx, ws).catch((error) => {
|
|
140
|
-
ctx.io.stderr.write(`Account setup cache unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
141
|
-
}).then(async () => {
|
|
154
|
+
const preparation = prepareSetupCache(ctx, ws).catch((error) => recordSetupCacheFailure(ctx, ws, error)).then(async () => {
|
|
142
155
|
if (!setupHash)
|
|
143
156
|
return;
|
|
157
|
+
// A new conversation's VM carries the setup labels before its setup starts (0.21.1+).
|
|
158
|
+
await conversation?.setupLabelled;
|
|
144
159
|
const queued = await cell.exec.run(['sf-agent', 'setup-start', '--deps-hash', setupHash], { cwd, timeoutMs: 30_000, maxOutputBytes: 4096, signal: ctx.signal });
|
|
145
160
|
if (queued.exitCode !== 0)
|
|
146
161
|
throw new Error(`project setup could not be queued: ${queued.stderr.trim() || `exit ${queued.exitCode}`}`);
|
|
@@ -151,12 +166,22 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
151
166
|
const text = values.initialPrompt;
|
|
152
167
|
// A conversation's guest delivers the prompt itself when this terminal never showed its input box (0.19.1+).
|
|
153
168
|
const queue = conversation ? () => conversationClient(ctx).conversations.send(conversation.id, text, { signal: ctx.signal }) : undefined;
|
|
154
|
-
|
|
169
|
+
// The VM's guest types it where it can (0.21.1+); this terminal types it for an older guest. A harness the guest
|
|
170
|
+
// waited for without seeing its input box gets it through the inbox, not a second wait here.
|
|
171
|
+
const guest = await guestFirstPrompt(cell, session.session_id, app, text, ws.key, ctx.signal);
|
|
172
|
+
if (guest === 'submitted')
|
|
173
|
+
delivery = 'submitted';
|
|
174
|
+
else if (guest === 'not_ready' && queue && text.length <= INBOX_MAX_CHARS) {
|
|
175
|
+
await queue();
|
|
176
|
+
delivery = 'queued';
|
|
177
|
+
}
|
|
178
|
+
else
|
|
179
|
+
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);
|
|
155
180
|
}
|
|
156
181
|
// Work that must never delay the first prompt (the laptop's agent config, 0.19.0+) starts once the agent has it.
|
|
157
182
|
const after = conversation?.afterStart?.();
|
|
158
183
|
if (!attach) {
|
|
159
|
-
await preparation;
|
|
184
|
+
await Promise.all([preparation, credentialFill]);
|
|
160
185
|
// conversation (0.16.0+): which conversation this session is, also when its computer hosts several threads.
|
|
161
186
|
// model_choice (0.17.0+): the requested model and effort, and whether this thread started with them.
|
|
162
187
|
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 } } : {}),
|
|
@@ -166,7 +191,7 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
|
|
|
166
191
|
}
|
|
167
192
|
ctx.io.stderr.write(`Shardflux · ${ws.key}\nDetach: Ctrl-] then d\n`);
|
|
168
193
|
const attached = attachTerminal({
|
|
169
|
-
...(credentialFill ? { credentialPrompts: (ask, signal) => watchCredentialPrompts(ctx, ws, ask, signal) } : {}),
|
|
194
|
+
...(await credentialFill ? { credentialPrompts: (ask, signal) => watchCredentialPrompts(ctx, ws, ask, signal) } : {}),
|
|
170
195
|
io: ctx.io, signal: ctx.signal, connect: cellConnector(cell, session.session_id, { passive: true }),
|
|
171
196
|
wake: (signal) => ws.wake({ timeoutMs: ctx.wakeTimeoutMs, signal }),
|
|
172
197
|
asleep: () => workspaceAsleep(ws),
|
|
@@ -294,12 +319,54 @@ export async function launchProjectWith(ctx, values, [path = './', explicitApp],
|
|
|
294
319
|
await prepareAgentMachine(ctx, ws);
|
|
295
320
|
return await terminal(ctx, ws, app, agent, projectKey, values, prepared?.setupHash);
|
|
296
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Whether `agents open` hands the conversation to the person's terminal app (0.20.1+) rather than attaching here:
|
|
324
|
+
* `--in terminal` with --json (an app) or `--window`, any open from a shell nobody types into (a coding agent's, or no
|
|
325
|
+
* terminal), and `--in terminal` typed in a terminal other than the chosen one.
|
|
326
|
+
*/
|
|
327
|
+
function opensInTerminalApp(ctx, values) {
|
|
328
|
+
if (values.detach === true)
|
|
329
|
+
return false;
|
|
330
|
+
const here = attachable(ctx.io);
|
|
331
|
+
if (values.in === undefined)
|
|
332
|
+
return !ctx.json && !here;
|
|
333
|
+
if (values.in !== 'terminal')
|
|
334
|
+
return false;
|
|
335
|
+
if (ctx.json || !here || values.window === true)
|
|
336
|
+
return true;
|
|
337
|
+
const stored = storedTerminal(ctx.store.dir);
|
|
338
|
+
return !!stored && stored.id !== currentTerminal(ctx.io.env);
|
|
339
|
+
}
|
|
340
|
+
/** `shard agents open <ref>` (its attach, no --in) in a new tab of the person's terminal app, in the project folder. */
|
|
341
|
+
async function openInTerminalApp(ctx, ref, values, extra = {}) {
|
|
342
|
+
const words = ['agents', 'open', ref, ...(typeof values.app === 'string' ? ['--app', values.app] : [])];
|
|
343
|
+
const result = await openShardInTerminal(ctx, words, { cwd: referenceFolder(ctx.store.dir, ctx.apiUrl, ref, ctx.io.env), window: values.window === true });
|
|
344
|
+
reportTerminalOpen(ctx, result, extra);
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* `--in auto` for a terminal conversation: the person's terminal app (a tab there, 0.20.1+), or this terminal when it
|
|
349
|
+
* is the one they use. --json without a chosen terminal app keeps the command for an app to run (a menu bar before
|
|
350
|
+
* 0.20.1 opens Terminal with it).
|
|
351
|
+
*/
|
|
352
|
+
function terminalAuto(ctx, ws, opened, values) {
|
|
353
|
+
if (ctx.json && !storedTerminal(ctx.store.dir)) {
|
|
354
|
+
out(ctx, `shard agents open ${opened}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', opened] });
|
|
355
|
+
return Promise.resolve(0);
|
|
356
|
+
}
|
|
357
|
+
const next = { ...values, in: 'terminal' };
|
|
358
|
+
if (opensInTerminalApp(ctx, next))
|
|
359
|
+
return openInTerminalApp(ctx, opened, next, { workspace: ws.data });
|
|
360
|
+
delete next.in;
|
|
361
|
+
return AGENT_HANDLERS['agents open'](ctx, next, [opened]);
|
|
362
|
+
}
|
|
297
363
|
export const AGENT_HANDLERS = {
|
|
298
364
|
async 'agents open'(ctx, values, [agent = 'main']) {
|
|
299
365
|
if (values.app !== undefined && values.app !== 'claude' && values.app !== 'codex')
|
|
300
366
|
throw new UsageError('--app must be claude or codex');
|
|
301
|
-
|
|
302
|
-
|
|
367
|
+
// The person's terminal app (0.20.1+): before any cloud work, the new tab owns the attach, credentials and prompts.
|
|
368
|
+
if (opensInTerminalApp(ctx, values))
|
|
369
|
+
return openInTerminalApp(ctx, agent, values);
|
|
303
370
|
let resolvedConversation;
|
|
304
371
|
if (/^conv_[a-z0-9]{26}$/.test(agent)) {
|
|
305
372
|
resolvedConversation = await getConversation(ctx, agent);
|
|
@@ -330,15 +397,9 @@ export const AGENT_HANDLERS = {
|
|
|
330
397
|
}
|
|
331
398
|
if (chosen === 'claude-desktop' && app === 'claude')
|
|
332
399
|
return openClaudeDesktopSession(ctx, ws, resolvedConversation);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
return 0;
|
|
337
|
-
}
|
|
338
|
-
const next = { ...values };
|
|
339
|
-
delete next.in;
|
|
340
|
-
return AGENT_HANDLERS['agents open'](ctx, next, [opened]);
|
|
341
|
-
}
|
|
400
|
+
// A terminal app attaches to the conversation's own session, Claude Code's or Codex's (Codex 0.21.1+).
|
|
401
|
+
if (chosen === 'terminal' && (app === 'claude' || app === 'codex'))
|
|
402
|
+
return terminalAuto(ctx, ws, opened, values);
|
|
342
403
|
const saved = chosen === 'codex-app' ? undefined : store.read().projects?.[project]?.target ??
|
|
343
404
|
(project.startsWith('file://') ? store.read().projects?.[new URL(project).pathname]?.target : undefined);
|
|
344
405
|
// Codex's default open implementation owns Cloud/desktop routing. PAS-XB replaces that path; do not pin
|
|
@@ -360,14 +421,7 @@ export const AGENT_HANDLERS = {
|
|
|
360
421
|
if (detected.clients.includes('claude-desktop')) {
|
|
361
422
|
return openClaudeDesktopSession(ctx, ws, resolvedConversation);
|
|
362
423
|
}
|
|
363
|
-
|
|
364
|
-
// An app runs this in the person's terminal (the menu bar opens Terminal with it).
|
|
365
|
-
out(ctx, `shard agents open ${opened}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', opened] });
|
|
366
|
-
return 0;
|
|
367
|
-
}
|
|
368
|
-
const next = { ...values };
|
|
369
|
-
delete next.in;
|
|
370
|
-
return AGENT_HANDLERS['agents open'](ctx, next, [opened]);
|
|
424
|
+
return terminalAuto(ctx, ws, opened, values);
|
|
371
425
|
}
|
|
372
426
|
if (values.in !== undefined && values.in !== 'terminal') {
|
|
373
427
|
const { HANDLERS } = await import("./handlers.js");
|
|
@@ -35,3 +35,22 @@ export declare function marketplaceSource(entry: ObjectValue): string | undefine
|
|
|
35
35
|
export declare function collectPlugins(home: string, issues: ConfigIssue[]): Promise<PluginPlan>;
|
|
36
36
|
/** Only rewrite paths to files we actually send. Missing laptop paths are never made to look portable. */
|
|
37
37
|
export declare function rewritePaths(value: unknown, home: string, remoteHome: string, files: ConfigFiles, issues: ConfigIssue[], item: string): unknown;
|
|
38
|
+
/** A conflict copy this computer keeps next to the user's file never travels, in either direction. */
|
|
39
|
+
export declare const CONFLICT_COPY: RegExp;
|
|
40
|
+
/** The user-level agent config paths that travel back from a VM: never credentials, settings, plugins or history. */
|
|
41
|
+
export declare function returnablePath(rel: string): boolean;
|
|
42
|
+
export declare const MEMORY_PATH: RegExp;
|
|
43
|
+
/** The project root Claude keys memory by: the repository's main checkout (worktrees share it), else the folder. */
|
|
44
|
+
export declare function memoryRoot(folder: string): string;
|
|
45
|
+
/** This computer's home-relative path for a VM path; undefined for memory of a VM folder that is not this project. */
|
|
46
|
+
export declare function localConfigPath(rel: string, record: {
|
|
47
|
+
worktree: string;
|
|
48
|
+
project_root: string;
|
|
49
|
+
}, localRoot: string | undefined): string | undefined;
|
|
50
|
+
/** The VM path for a local memory path, the other way round (the config copy into a new VM). */
|
|
51
|
+
export declare function vmMemoryPath(rel: string, localRoot: string, vmRoot: string): string | undefined;
|
|
52
|
+
/** This computer's memory for a project folder, under its home-relative path (copied into a VM under the VM's). */
|
|
53
|
+
export declare function collectMemory(home: string, folder: string, issues: ConfigIssue[]): Promise<{
|
|
54
|
+
root: string;
|
|
55
|
+
files: ConfigFiles;
|
|
56
|
+
}>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import { readFile, readdir, realpath, stat } from 'node:fs/promises';
|
|
2
|
-
import { join, relative } from 'node:path';
|
|
3
|
+
import { basename, dirname, join, relative } from 'node:path';
|
|
4
|
+
import { claudeProjectName } from "./local-sessions.js";
|
|
3
5
|
import { LOCAL_PATH_REASON } from "./agent-config-report.js";
|
|
4
6
|
export const object = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
5
7
|
export const text = (v) => typeof v === 'string' ? v : undefined;
|
|
@@ -54,6 +56,9 @@ export async function collectTree(source, dest, files, issues, parents = new Set
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
else if (info.isFile()) {
|
|
59
|
+
// A conflict copy (<name>.cloud.md) is this computer's to resolve; it never travels (0.20.0+).
|
|
60
|
+
if (CONFLICT_COPY.test(basename(dest)))
|
|
61
|
+
return;
|
|
57
62
|
if (info.size > maxFileBytes) {
|
|
58
63
|
issues.push({ item: dest, reason: 'file exceeds the 16 MiB config sync limit' });
|
|
59
64
|
return;
|
|
@@ -189,3 +194,67 @@ export function rewritePaths(value, home, remoteHome, files, issues, item) {
|
|
|
189
194
|
}
|
|
190
195
|
return value;
|
|
191
196
|
}
|
|
197
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
198
|
+
// Paths that travel back from a VM, and project memory on both sides (0.20.0+, agent-config-return.ts)
|
|
199
|
+
const RETURN_PATH = /^(?:\.claude\/CLAUDE\.md|\.codex\/AGENTS\.md|\.claude\/(?:skills|commands|agents|output-styles)\/.+|\.claude\/projects\/[^/]+\/memory\/.+|\.codex\/(?:prompts|skills)\/.+)$/;
|
|
200
|
+
const EXCLUDED = new Set(['.git', '.in_use', 'node_modules', '__pycache__', '.DS_Store', '.credentials.json', 'auth.json', 'projects',
|
|
201
|
+
'transcripts', 'history.jsonl', 'logs', 'cache', 'caches', '.env']);
|
|
202
|
+
/** A conflict copy this computer keeps next to the user's file never travels, in either direction. */
|
|
203
|
+
export const CONFLICT_COPY = /\.cloud(?:\.[A-Za-z0-9]+)?$/;
|
|
204
|
+
/** The user-level agent config paths that travel back from a VM: never credentials, settings, plugins or history. */
|
|
205
|
+
export function returnablePath(rel) {
|
|
206
|
+
// The claude.ai account's skills (.claude/skills/synced) and Codex's bundled ones: each computer's agent syncs those.
|
|
207
|
+
if (!RETURN_PATH.test(rel) || rel.startsWith('.codex/skills/.system/') || rel.startsWith('.claude/skills/synced/'))
|
|
208
|
+
return false;
|
|
209
|
+
const parts = rel.split('/');
|
|
210
|
+
if (parts.some(p => !p || p === '.' || p === '..'))
|
|
211
|
+
return false;
|
|
212
|
+
for (const part of parts.slice(rel.startsWith('.claude/projects/') ? 4 : 2)) {
|
|
213
|
+
if (EXCLUDED.has(part) || part.startsWith('.env.') || part.endsWith('.sf-sync'))
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
return !CONFLICT_COPY.test(parts[parts.length - 1]);
|
|
217
|
+
}
|
|
218
|
+
export const MEMORY_PATH = /^\.claude\/projects\/([^/]+)\/memory\/(.+)$/;
|
|
219
|
+
/** The project root Claude keys memory by: the repository's main checkout (worktrees share it), else the folder. */
|
|
220
|
+
export function memoryRoot(folder) {
|
|
221
|
+
try {
|
|
222
|
+
const common = execFileSync('git', ['-C', folder, 'rev-parse', '--path-format=absolute', '--git-common-dir'], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
223
|
+
if (basename(common) === '.git')
|
|
224
|
+
return dirname(common);
|
|
225
|
+
}
|
|
226
|
+
catch { /* not a repository */ }
|
|
227
|
+
return folder;
|
|
228
|
+
}
|
|
229
|
+
/** This computer's home-relative path for a VM path; undefined for memory of a VM folder that is not this project. */
|
|
230
|
+
export function localConfigPath(rel, record, localRoot) {
|
|
231
|
+
if (!returnablePath(rel))
|
|
232
|
+
return undefined;
|
|
233
|
+
const memory = MEMORY_PATH.exec(rel);
|
|
234
|
+
if (!memory)
|
|
235
|
+
return rel;
|
|
236
|
+
if (!localRoot)
|
|
237
|
+
return undefined;
|
|
238
|
+
const names = new Set([record.project_root, record.worktree].filter(Boolean).map(claudeProjectName));
|
|
239
|
+
return names.has(memory[1]) ? `.claude/projects/${claudeProjectName(localRoot)}/memory/${memory[2]}` : undefined;
|
|
240
|
+
}
|
|
241
|
+
/** The VM path for a local memory path, the other way round (the config copy into a new VM). */
|
|
242
|
+
export function vmMemoryPath(rel, localRoot, vmRoot) {
|
|
243
|
+
const memory = MEMORY_PATH.exec(rel);
|
|
244
|
+
return memory && memory[1] === claudeProjectName(localRoot) ? `.claude/projects/${claudeProjectName(vmRoot)}/memory/${memory[2]}` : undefined;
|
|
245
|
+
}
|
|
246
|
+
/** This computer's memory for a project folder, under its home-relative path (copied into a VM under the VM's). */
|
|
247
|
+
export async function collectMemory(home, folder, issues) {
|
|
248
|
+
const root = memoryRoot(folder), files = new Map();
|
|
249
|
+
const path = `.claude/projects/${claudeProjectName(root)}/memory`;
|
|
250
|
+
try {
|
|
251
|
+
await stat(join(home, path));
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (absent(error))
|
|
255
|
+
return { root, files };
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
await collectTree(join(home, path), path, files, issues);
|
|
259
|
+
return { root, files };
|
|
260
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { CellClient } from '@shardflux/sdk';
|
|
2
|
+
import type { Ctx } from './commands.js';
|
|
3
|
+
import type { Conversation, LocalConversation } from './conversations.js';
|
|
4
|
+
export declare const RETURN_ROOT = "/shardflux/project/agent-config";
|
|
5
|
+
/** The latest agent-config line ({notice, at}), for the Linux app's top-bar panel (CLI 0.20.2+). */
|
|
6
|
+
export declare const AGENT_CONFIG_NOTICE = "agent-config-notice.json";
|
|
7
|
+
/** A machine fold waits for this much quiet after the last change, and saves the machine at most this often. */
|
|
8
|
+
export declare const FOLD_QUIET_MS = 60000;
|
|
9
|
+
export declare const FOLD_INTERVAL_MS: number;
|
|
10
|
+
/** A machine VM someone is using is left alone this long before a pending fold goes ahead anyway. */
|
|
11
|
+
export declare const FOLD_DEFER_MS: number;
|
|
12
|
+
export interface ReturnedFile {
|
|
13
|
+
sha256: string;
|
|
14
|
+
mode?: string;
|
|
15
|
+
size: number;
|
|
16
|
+
history: string[];
|
|
17
|
+
at: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ReturnRecord {
|
|
20
|
+
v: 1;
|
|
21
|
+
conversation_id: string;
|
|
22
|
+
workspace_id: string;
|
|
23
|
+
app: string;
|
|
24
|
+
home: string;
|
|
25
|
+
worktree: string;
|
|
26
|
+
project_root: string;
|
|
27
|
+
seq: number;
|
|
28
|
+
updated_at: string;
|
|
29
|
+
files: Record<string, ReturnedFile>;
|
|
30
|
+
skipped?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
export type ReturnAction = 'created' | 'updated' | 'conflict' | 'trashed' | 'kept' | 'unchanged' | 'skipped';
|
|
33
|
+
export interface ReturnItem {
|
|
34
|
+
vm_path: string;
|
|
35
|
+
path?: string;
|
|
36
|
+
action: ReturnAction;
|
|
37
|
+
sha256: string;
|
|
38
|
+
local_sha256?: string;
|
|
39
|
+
copy?: string;
|
|
40
|
+
trash?: string;
|
|
41
|
+
reason?: string;
|
|
42
|
+
}
|
|
43
|
+
/** SKILL.md → SKILL.cloud.md, next to the user's own copy. */
|
|
44
|
+
export declare function conflictCopyPath(file: string): string;
|
|
45
|
+
export type Decision = 'unchanged' | 'create' | 'overwrite' | 'conflict' | 'trash' | 'keep';
|
|
46
|
+
/**
|
|
47
|
+
* `local` is this computer's content hash (undefined: no file), `cloud` the VM's ('' when the agent deleted it), and
|
|
48
|
+
* `known` every version the VM side held or was sent: when the local file is one of them, it has not changed here
|
|
49
|
+
* since, and the cloud's version may replace it.
|
|
50
|
+
*/
|
|
51
|
+
export declare function decideReturn(local: string | undefined, cloud: string, known: ReadonlySet<string>): Decision;
|
|
52
|
+
export declare function validRecord(value: unknown, id: string): value is ReturnRecord;
|
|
53
|
+
/** Moves a file to the user's Trash (macOS ~/.Trash, or the freedesktop Trash on Linux); never a hard delete. */
|
|
54
|
+
export declare function moveToTrash(file: string, env: Record<string, string | undefined>): string;
|
|
55
|
+
export interface FoldEntry {
|
|
56
|
+
vm_path: string;
|
|
57
|
+
sha256: string;
|
|
58
|
+
known: string[];
|
|
59
|
+
at: number;
|
|
60
|
+
}
|
|
61
|
+
interface ReturnStateFile {
|
|
62
|
+
v: 1;
|
|
63
|
+
/** Per conversation: the record sequence applied, and the API revision last read (a restart reads nothing again). */
|
|
64
|
+
conversations: Record<string, {
|
|
65
|
+
seq: number;
|
|
66
|
+
revision?: string;
|
|
67
|
+
files: Record<string, string>;
|
|
68
|
+
}>;
|
|
69
|
+
/** When this computer started applying, less 10 minutes (ms): conversations idle since before then are not read. */
|
|
70
|
+
enabled_at?: number;
|
|
71
|
+
machine_queue?: Record<string, FoldEntry>;
|
|
72
|
+
machine_deferred_since?: number;
|
|
73
|
+
machine_folded_at?: number;
|
|
74
|
+
}
|
|
75
|
+
export declare const journalFile: (dir: string) => string;
|
|
76
|
+
export declare function readReturnState(dir: string): ReturnStateFile;
|
|
77
|
+
export type ReturnCell = {
|
|
78
|
+
files: Pick<CellClient['files'], 'read' | 'readText'>;
|
|
79
|
+
};
|
|
80
|
+
export interface ApplyOptions {
|
|
81
|
+
home: string;
|
|
82
|
+
storeDir: string;
|
|
83
|
+
env: Record<string, string | undefined>;
|
|
84
|
+
signal?: AbortSignal;
|
|
85
|
+
/** This computer's checkout of the conversation's project (memory maps to it). */
|
|
86
|
+
folder?: string;
|
|
87
|
+
/** Hashes the config copy sent to that VM, by VM path. */
|
|
88
|
+
sent?: Record<string, string>;
|
|
89
|
+
}
|
|
90
|
+
export interface ApplyResult {
|
|
91
|
+
app: string;
|
|
92
|
+
items: ReturnItem[];
|
|
93
|
+
record?: ReturnRecord;
|
|
94
|
+
}
|
|
95
|
+
/** Applies a conversation's record; returns what happened to each path that changed since the last application. */
|
|
96
|
+
export declare function applyAgentConfigReturn(cell: ReturnCell, conversationId: string, options: ApplyOptions): Promise<ApplyResult>;
|
|
97
|
+
/**
|
|
98
|
+
* One line for a batch: "Claude saved the skill deploy-checks in the cloud: it's on this Mac now". Undefined when
|
|
99
|
+
* nothing reached this computer (an unchanged or skipped path says nothing).
|
|
100
|
+
*/
|
|
101
|
+
export declare function returnNotice(app: string, items: ReturnItem[], platform?: NodeJS.Platform): string | undefined;
|
|
102
|
+
export declare class AgentConfigReturnState {
|
|
103
|
+
readonly retries: Map<string, {
|
|
104
|
+
revision: string;
|
|
105
|
+
failures: number;
|
|
106
|
+
retryAt: number;
|
|
107
|
+
}>;
|
|
108
|
+
machine: {
|
|
109
|
+
checkedAt: number;
|
|
110
|
+
saved: boolean;
|
|
111
|
+
};
|
|
112
|
+
/** The user signed in on this computer: the session's, or the creator of its API key (refreshed every 10 minutes). */
|
|
113
|
+
identity: {
|
|
114
|
+
checkedAt: number;
|
|
115
|
+
userId?: string;
|
|
116
|
+
};
|
|
117
|
+
folding?: Promise<void>;
|
|
118
|
+
}
|
|
119
|
+
/** How often the device helper looks for agent config to bring back: its own loop, never behind a files or transcript pass. */
|
|
120
|
+
export declare const AGENT_CONFIG_POLL_MS = 2000;
|
|
121
|
+
/** With none of this user's conversations active, the full listing backs off (doubling) to this. */
|
|
122
|
+
export declare const AGENT_CONFIG_IDLE_MS = 60000;
|
|
123
|
+
/** While backed off, one cheap request (`state=working`, usually empty) this often notices a turn starting anywhere. */
|
|
124
|
+
export declare const AGENT_CONFIG_PROBE_MS = 10000;
|
|
125
|
+
/** A conversation counts as active for this long after its last activity (a turn's start or end). */
|
|
126
|
+
export declare const AGENT_CONFIG_ACTIVE_WINDOW_MS: number;
|
|
127
|
+
/**
|
|
128
|
+
* A working conversation counts as active only this long after its last activity: one left "working" by a VM that went
|
|
129
|
+
* away (staging QA FE1: 60 such, all more than a day old) must not hold every computer at the fast cadence. A longer
|
|
130
|
+
* turn still comes back, at the idle cadence.
|
|
131
|
+
*/
|
|
132
|
+
export declare const AGENT_CONFIG_WORKING_WINDOW_MS: number;
|
|
133
|
+
/** This user's conversations that are working (recently) or ended a turn within the active window. */
|
|
134
|
+
export declare function activeConversations(all: Conversation[], records: Record<string, LocalConversation>, me: string | undefined, now?: number): Conversation[];
|
|
135
|
+
export interface CadenceOptions {
|
|
136
|
+
activeMs: number;
|
|
137
|
+
idleMs: number;
|
|
138
|
+
probeMs: number;
|
|
139
|
+
}
|
|
140
|
+
export declare const CADENCE: CadenceOptions;
|
|
141
|
+
/**
|
|
142
|
+
* When the next listing is due. Active (one of this user's conversations works or just ended a turn): every 2 s.
|
|
143
|
+
* Idle: the interval doubles to 60 s, with a cheap working-only probe every 10 s and a check of this computer's own
|
|
144
|
+
* conversation records each second; either snaps it back to 2 s at once.
|
|
145
|
+
*/
|
|
146
|
+
export declare class AgentConfigCadence {
|
|
147
|
+
readonly options: CadenceOptions;
|
|
148
|
+
interval: number;
|
|
149
|
+
fullAt: number;
|
|
150
|
+
probeAt: number;
|
|
151
|
+
recordsMtime: number;
|
|
152
|
+
constructor(options?: CadenceOptions);
|
|
153
|
+
/** After a full pass: active keeps 2 s, idle doubles toward the idle ceiling. */
|
|
154
|
+
listed(active: boolean, now?: number): void;
|
|
155
|
+
snap(): void;
|
|
156
|
+
fullDue(now?: number): boolean;
|
|
157
|
+
probeDue(now?: number): boolean;
|
|
158
|
+
}
|
|
159
|
+
/** One full pass: list the account's conversations, apply what came back; true while one of this user's is active. */
|
|
160
|
+
export declare function returnAgentConfigsOnce(ctx: Ctx, state: AgentConfigReturnState): Promise<boolean>;
|
|
161
|
+
/** The cheap probe: is any of this user's conversations working right now (one request, usually an empty page)? */
|
|
162
|
+
export declare function workingProbe(ctx: Ctx, state: AgentConfigReturnState): Promise<boolean>;
|
|
163
|
+
/** The device helper's agent-config loop: activity-driven cadence, its own backoff on errors, never another loop's. */
|
|
164
|
+
export declare function runAgentConfigLoop(ctx: Ctx, state: AgentConfigReturnState, signal: AbortSignal, cadence?: AgentConfigCadence, tickMs?: number): Promise<void>;
|
|
165
|
+
/**
|
|
166
|
+
* This computer's checkout of a conversation's repository: the folder it was started from here, else the folder of
|
|
167
|
+
* another conversation of the same repository started here. A Shardflux project can hold many repositories (staging
|
|
168
|
+
* QA FE1: 31 in one project), so the project never decides. Undefined when there is none (a conversation started on
|
|
169
|
+
* the phone, the web, T3 or another computer for a repository this computer never opened): its user-level files still
|
|
170
|
+
* apply, its project memory has nowhere to go.
|
|
171
|
+
*/
|
|
172
|
+
export declare function localCheckout(c: Conversation, records: Record<string, LocalConversation>): string | undefined;
|
|
173
|
+
/**
|
|
174
|
+
* User-level config is personal. A conversation that names who started it applies only when that is this computer's
|
|
175
|
+
* user, wherever it started; a teammate's never does. One that does not (an older API, a conversation from before it
|
|
176
|
+
* was recorded) applies only when this computer started it.
|
|
177
|
+
*/
|
|
178
|
+
export declare function ownConversation(c: Pick<Conversation, 'id' | 'started_by'>, records: Record<string, LocalConversation>, me: string | undefined): boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Called by the transcript mirror's pass with the conversations it listed: every conversation of this computer's user
|
|
181
|
+
* (ownConversation), wherever it started (this computer, the phone, the web, T3, another computer). Each one's record is
|
|
182
|
+
* read once per turn (its revision; a restart reads nothing again) and applied. Conversations idle since before this
|
|
183
|
+
* computer started applying are never read. One notice line per pass, on the device helper's stdout for the menu bar.
|
|
184
|
+
*/
|
|
185
|
+
export declare function returnAgentConfigs(ctx: Ctx, records: Record<string, LocalConversation>, all: Conversation[], state: AgentConfigReturnState): Promise<string | undefined>;
|
|
186
|
+
/** Memory is per project and never part of the machine; everything else the machine delivers live. */
|
|
187
|
+
export declare function queueMachineFold(ctx: Ctx, state: AgentConfigReturnState, result: ApplyResult): Promise<void>;
|
|
188
|
+
/**
|
|
189
|
+
* Writes the queued changes into the machine VM under the same rules (a machine copy changed since is kept) and saves
|
|
190
|
+
* the machine once for the batch, after a quiet period and at most every FOLD_INTERVAL_MS. A machine VM that is running
|
|
191
|
+
* (someone may be changing it) is left alone for up to FOLD_DEFER_MS.
|
|
192
|
+
*/
|
|
193
|
+
export declare function foldIntoMachine(ctx: Ctx, state: AgentConfigReturnState, now?: number): Promise<'saved' | 'waiting' | 'nothing' | 'deferred' | 'shared'>;
|
|
194
|
+
export {};
|