@shardflux/cli 0.19.0 → 0.20.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 CHANGED
@@ -1,6 +1,82 @@
1
1
  # Changelog
2
2
 
3
- ## 0.19.0 (not yet published)
3
+ ## 0.20.0 (not yet published)
4
+
5
+ - **A save waits in line instead of failing** when your plan's build slots are in use: `shard machine save` and `shard
6
+ template save` print where the build is (`machine: saving version 5; queued behind 3 builds · about 40 s; …`), with
7
+ `--wait` the line updates on stderr as builds ahead finish, and `--json` carries `queue` (`position`, `slots`,
8
+ `expected_wait_seconds`). A full queue (`rate_limited`, `template_build_queue_full`) says how long until a retry
9
+ (`hint: 10 builds already wait for your plan's build slots; nothing was started. Run the same command again in about
10
+ 5 min`). Background rebases of your machine's projects no longer take your plan's slots.
11
+ - **Your machine sets itself up from this computer** (`shard machine setup --from-this-computer`): your own Claude
12
+ Code (`claude -p`), else your Codex (`codex exec`), recreates this computer's development setup on your cloud machine
13
+ with no question asked: its CLIs and tools, language runtimes at the same versions, shell config and aliases that
14
+ make sense on Linux, git identity and editor-agnostic dotfiles. It runs on your own subscription, in about 10
15
+ minutes at most (`--timeout`), then shard copies your Claude and Codex config and saves the machine (`shard machine
16
+ save --wait`). `--json` prints what was added and one line for it (`Cloud machine ready: gh, Node 24, Python 3.13, 12
17
+ skills`); `SHARDFLUX_PROGRESS_FILE` gets one `{phase, line}` per step, as for a conversation start.
18
+ - On this computer the agent may only read: Claude Code runs with only Read, Glob and Bash, in `dontAsk` mode, with
19
+ the read-only commands it needs (`brew list`, `ls`, `which`, `git config --global --get`, `<tool> --version`) and
20
+ `shard` on your machine VM pre-approved, and `~/.ssh`, `~/.aws`, keychains, `.env`, token and credential files
21
+ denied; no MCP servers, hooks or plugins of yours load. Codex runs in its workspace-write sandbox, confined to a
22
+ private temporary folder. Every `shard` the agent runs only reaches your machine VM: exec, file writes from standard
23
+ input and reads.
24
+ - Without Claude Code or Codex on this computer, your cloud Claude sets it up inside the machine VM from a list of
25
+ this computer's tools and versions (names only).
26
+ - When an agent can't finish (a usage limit, an expired sign-in, a sandbox that keeps `shard` from the machine, a
27
+ crash, its time box), the next one takes over: this computer's Claude Code, then its Codex, then your cloud Claude.
28
+ Each gets its own time box, all of them together twice it. The setup fails only when every one stopped, naming why
29
+ for each; `--json` and `--status` list the agents tried in `attempts`, and `summary.runner` names the one that set
30
+ the machine up. `--agent` runs exactly one.
31
+ - It runs once per computer and account: `--if-needed` (what the apps and the terminal onboarding run right after
32
+ their Agents step) skips a machine already set up from this computer, and a second run never starts while one is
33
+ running. `shard machine setup --status --json` shows the last one; a failure is exit 6, `machine_setup_failed`,
34
+ retryable.
35
+ - `shard agents accounts` and `shard agents usage` name Codex plans as ChatGPT does: Pro Lite and Pro Max (they read
36
+ Prolite and Promax).
37
+ - Brings the Shardflux app for Ubuntu 0.1.5: the same machine setup line in onboarding and Settings as on the Mac, and
38
+ the menu names the first run's sample Daylight (0.1.4). The optional dependency on `@shardflux/desktop-linux-x64` /
39
+ `-arm64` is exactly 0.1.5.
40
+ - What Claude or Codex saves at the user level in a cloud conversation comes back to this computer by itself: a skill,
41
+ a command, an agent, an output style, a memory, or a line in `~/.claude/CLAUDE.md` / `~/.codex/AGENTS.md`. Each turn
42
+ end, the conversation's VM records the agent's changes in the project space; the device helper the menu bar runs
43
+ applies them seconds after the turn ends, in a loop of its own every 2 s that never waits for the files mirror
44
+ (staging, 5 turns from another device: 1.8 s p50, 2.5 s max). A new file is created. A file you haven't changed
45
+ here since that VM's version is replaced. A file changed in both places (or one that another conversation's VM
46
+ started without) keeps yours and gets the cloud's version next to it
47
+ (`CLAUDE.cloud.md`). A file the agent deleted goes to the Trash. Every action is a line in
48
+ `agent-config-return.jsonl` in the config directory. Credentials, settings, plugin caches, marketplaces, transcripts
49
+ and history never travel, and neither do the claude.ai account's synced skills.
50
+ - This holds for every conversation you started, wherever you started it: this computer, the phone, the web, T3 Code
51
+ or another computer. User-level config is personal: a conversation a teammate started in the same organization never
52
+ brings its skills, memories or CLAUDE.md lines here (with an API that records who started a conversation; with an
53
+ older one, only conversations started on this computer apply). The device helper reads each one's changes once per
54
+ turn; conversations idle since before it started applying are not read.
55
+ - Project memory travels both ways. Claude keys memory by the project's folder: the VM's folder maps to this
56
+ computer's checkout of the same repository (the folder a conversation of that repository was started from here),
57
+ and a new conversation's VM gets this computer's memory for the project under its own folder, also when a saved
58
+ machine carries the rest of the config. Memory of a repository this computer has no checkout of stays in the cloud.
59
+ - With a saved machine that only you use (its organization has one member), those changes are also written into the
60
+ machine and saved once per batch (after a minute without changes, at most every 10 minutes; a running machine VM gets
61
+ up to 30 minutes first), so new conversations start with them. A machine your organization's members share gets
62
+ none of one member's config. Without a saved machine, the next config copy into a new VM carries this computer's
63
+ copy.
64
+ - The config copy's marker in the VM lists each file's hash, so the VM tells the copy's files from the agent's own
65
+ changes. A conflict copy (`*.cloud.md`) stays on this computer.
66
+
67
+ ## 0.19.1 (2026-10-11)
68
+
69
+ - A first prompt is never lost to a slow or unusual start. `--prompt` / `--prompt-file` types the prompt only into the
70
+ agent's input box, never into one of its dialogs ("❯ 1. Yes"). If the terminal never shows the input box and
71
+ nothing was typed, the prompt goes to the conversation's own inbox, which delivers it once the agent reads input.
72
+ `--json` then reports `"initial_prompt": {"submitted": false, "queued": true}` instead of failing with
73
+ `initial_prompt_failed`.
74
+ - Brings the Shardflux app for Ubuntu 0.1.3: Try it once never waits silently. A demo agent still waiting with nothing
75
+ said 45 s after its start gets its task once more through its conversation, and one line says so; still nothing
76
+ 45 s later, one line says it hasn't started. The optional dependency on `@shardflux/desktop-linux-x64` / `-arm64` is
77
+ exactly 0.1.3.
78
+
79
+ ## 0.19.0 (2026-10-11)
4
80
 
5
81
  - A new conversation on an account without a saved machine gets its first prompt in seconds, whatever is in
6
82
  `~/.claude` and `~/.codex`. The prompt is typed first, and this computer's Claude and Codex config is copied into
package/README.md CHANGED
@@ -275,7 +275,9 @@ Codex conversation gets its own VM)? `shard setup --guide` prints both paths wit
275
275
  once (a claude.ai link and a code). Codex: `codex login` on this computer. Then
276
276
  `shard setup --inspect --connect --json --apps claude,codex` (0.15.0+) checks both live, copies the Codex login and
277
277
  starts the account's Codex remote device.
278
- 3. **Build the machine** every conversation starts from. Ask your user what it should carry: languages and runtimes,
278
+ 3. **Build the machine** every conversation starts from. `shard machine setup --from-this-computer --json` (0.20.0+)
279
+ has this computer's Claude Code or Codex recreate its tools, runtimes, shell, git identity and dotfiles there and
280
+ saves it. For the rest, ask your user what it should carry: languages and runtimes,
279
281
  CLIs and tools, dotfiles and shell, secrets and credentials, editor and agent config. Then
280
282
  `shard machine open --json` (0.17.1+) prints the machine VM's key; install with
281
283
  `shard ws exec <key> -- bash -lc '...'`, write dotfiles and secret files with
@@ -1348,8 +1350,18 @@ toolchains, dependencies, services and project secrets. Both are private filesys
1348
1350
  as ordinary files. Browser logins and Claude/Codex config belong to the machine. Set up tools and secrets normally inside the VM;
1349
1351
  your agent uses the same save/rollback commands there. An agent working from your computer builds the machine with
1350
1352
  `shard machine open` (0.17.1+): it creates or resumes the machine VM without attaching and prints its key for
1351
- `shard ws exec` and `shard files write`. Every agent starts from both. Save or roll back with `shard machine save|rollback` and
1353
+ `shard ws exec` and `shard files write`. `shard machine setup --from-this-computer` (0.20.0+) does it for you: your own
1354
+ Claude Code (`claude -p`, else Codex) recreates this computer's CLIs and tools, language runtimes, shell config and
1355
+ aliases, git identity and dotfiles on the machine, on your subscription, then saves it. On this computer it only reads;
1356
+ `~/.ssh`, keychains, `.env`, token and credential files are never read. The apps and the first run start it once,
1357
+ right after you connect your agents. Every agent starts from both. Save or roll back with `shard machine save|rollback` and
1352
1358
  `shard template save|rollback`; changed files reach running agents live, software at the next start.
1359
+
1360
+ Skills, commands, agents, output styles, memories and `CLAUDE.md` / `AGENTS.md` lines that Claude or Codex saves in a
1361
+ cloud conversation come back to this computer by themselves, seconds after the turn ends, and into your saved
1362
+ machine too (0.20.0+). A file you changed here at the same time keeps your version, with the cloud's next to it
1363
+ (`CLAUDE.cloud.md`); a deleted one goes to the Trash. Project memory follows the project both ways. The menu bar says
1364
+ what arrived in one line, and `agent-config-return.jsonl` in the config directory lists every change.
1353
1365
  `shard machine save --wait` and `shard template save --wait` return once the new version is ready (0.13.3+).
1354
1366
  Software installed under the home goes into the saved image, linked back at the same path; the save line names those
1355
1367
  folders, and `--json` carries `moved_to_image` (0.18.0+).
@@ -10,6 +10,7 @@ import { CodedError } from './errors.js';
10
10
  import type { HubCloud } from './cli-proxy-hub.js';
11
11
  /** "in 2h 10m", "in 4d 8h", "in 41m"; "now" once passed. */
12
12
  export declare function resetIn(resetsAt: string | null, now: number): string | null;
13
+ export declare const planName: (planType: string) => string;
13
14
  /** Account references: the id, `<provider>:<n>` (its place in the list, from 1), the email or the label. */
14
15
  export declare function findAccount(list: AgentAccountList, ref: string): AgentAccount;
15
16
  /** The compact list: one row per account, grouped by provider. */
@@ -33,7 +33,10 @@ export function resetIn(resetsAt, now) {
33
33
  return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m`;
34
34
  }
35
35
  const pct = (w) => `${Math.round(w.used_percent)}%`;
36
- const plan = (a) => a.kind === 'api_key' ? 'API key' : a.plan_type ? a.plan_type.charAt(0).toUpperCase() + a.plan_type.slice(1) : '';
36
+ /** The plan as its provider names it (apps/api/src/agent-accounts/model.ts PERSONAL_PLANS): "promax" is Pro Max, not Promax. */
37
+ const PLAN_NAMES = { free: 'Free', go: 'Go', plus: 'Plus', pro: 'Pro', prolite: 'Pro Lite', promax: 'Pro Max', max: 'Max' };
38
+ export const planName = (planType) => PLAN_NAMES[planType] ?? planType.charAt(0).toUpperCase() + planType.slice(1);
39
+ const plan = (a) => a.kind === 'api_key' ? 'API key' : a.plan_type ? planName(a.plan_type) : '';
37
40
  const threads = (n) => `${n} ${n === 1 ? 'thread' : 'threads'}`;
38
41
  const name = (a) => a.email && a.label !== a.email ? `${a.label} (${a.email})` : a.label;
39
42
  function bar(used, width = 10) {
@@ -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,8 +146,12 @@ 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
- await submitInitialPrompt(signal => cellConnector(cell, session.session_id)(0, signal), values.initialPrompt, ws.key, ctx.signal, app, bytes => cell.pty.input(session.session_id, bytes));
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
  }
151
156
  // Work that must never delay the first prompt (the laptop's agent config, 0.19.0+) starts once the agent has it.
152
157
  const after = conversation?.afterStart?.();
@@ -154,7 +159,7 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
154
159
  await preparation;
155
160
  // conversation (0.16.0+): which conversation this session is, also when its computer hosts several threads.
156
161
  // model_choice (0.17.0+): the requested model and effort, and whether this thread started with them.
157
- ctx.io.stdout.write(json({ workspace: ws.data, session, ...(conversation ? { conversation: { id: conversation.id } } : {}), ...(typeof values.initialPrompt === 'string' ? { initial_prompt: { submitted: true } } : {}),
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 } } : {}),
158
163
  ...(hasChoice(explicit) ? { model_choice: choiceReport(explicit, applied) } : {}) }));
159
164
  await after;
160
165
  return 0;
@@ -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,150 @@
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
+ /** A machine fold waits for this much quiet after the last change, and saves the machine at most this often. */
6
+ export declare const FOLD_QUIET_MS = 60000;
7
+ export declare const FOLD_INTERVAL_MS: number;
8
+ /** A machine VM someone is using is left alone this long before a pending fold goes ahead anyway. */
9
+ export declare const FOLD_DEFER_MS: number;
10
+ export interface ReturnedFile {
11
+ sha256: string;
12
+ mode?: string;
13
+ size: number;
14
+ history: string[];
15
+ at: string;
16
+ }
17
+ export interface ReturnRecord {
18
+ v: 1;
19
+ conversation_id: string;
20
+ workspace_id: string;
21
+ app: string;
22
+ home: string;
23
+ worktree: string;
24
+ project_root: string;
25
+ seq: number;
26
+ updated_at: string;
27
+ files: Record<string, ReturnedFile>;
28
+ skipped?: Record<string, string>;
29
+ }
30
+ export type ReturnAction = 'created' | 'updated' | 'conflict' | 'trashed' | 'kept' | 'unchanged' | 'skipped';
31
+ export interface ReturnItem {
32
+ vm_path: string;
33
+ path?: string;
34
+ action: ReturnAction;
35
+ sha256: string;
36
+ local_sha256?: string;
37
+ copy?: string;
38
+ trash?: string;
39
+ reason?: string;
40
+ }
41
+ /** SKILL.md → SKILL.cloud.md, next to the user's own copy. */
42
+ export declare function conflictCopyPath(file: string): string;
43
+ export type Decision = 'unchanged' | 'create' | 'overwrite' | 'conflict' | 'trash' | 'keep';
44
+ /**
45
+ * `local` is this computer's content hash (undefined: no file), `cloud` the VM's ('' when the agent deleted it), and
46
+ * `known` every version the VM side held or was sent: when the local file is one of them, it has not changed here
47
+ * since, and the cloud's version may replace it.
48
+ */
49
+ export declare function decideReturn(local: string | undefined, cloud: string, known: ReadonlySet<string>): Decision;
50
+ export declare function validRecord(value: unknown, id: string): value is ReturnRecord;
51
+ /** Moves a file to the user's Trash (macOS ~/.Trash, or the freedesktop Trash on Linux); never a hard delete. */
52
+ export declare function moveToTrash(file: string, env: Record<string, string | undefined>): string;
53
+ export interface FoldEntry {
54
+ vm_path: string;
55
+ sha256: string;
56
+ known: string[];
57
+ at: number;
58
+ }
59
+ interface ReturnStateFile {
60
+ v: 1;
61
+ /** Per conversation: the record sequence applied, and the API revision last read (a restart reads nothing again). */
62
+ conversations: Record<string, {
63
+ seq: number;
64
+ revision?: string;
65
+ files: Record<string, string>;
66
+ }>;
67
+ /** When this computer started applying, less 10 minutes (ms): conversations idle since before then are not read. */
68
+ enabled_at?: number;
69
+ machine_queue?: Record<string, FoldEntry>;
70
+ machine_deferred_since?: number;
71
+ machine_folded_at?: number;
72
+ }
73
+ export declare const journalFile: (dir: string) => string;
74
+ export declare function readReturnState(dir: string): ReturnStateFile;
75
+ export type ReturnCell = {
76
+ files: Pick<CellClient['files'], 'read' | 'readText'>;
77
+ };
78
+ export interface ApplyOptions {
79
+ home: string;
80
+ storeDir: string;
81
+ env: Record<string, string | undefined>;
82
+ signal?: AbortSignal;
83
+ /** This computer's checkout of the conversation's project (memory maps to it). */
84
+ folder?: string;
85
+ /** Hashes the config copy sent to that VM, by VM path. */
86
+ sent?: Record<string, string>;
87
+ }
88
+ export interface ApplyResult {
89
+ app: string;
90
+ items: ReturnItem[];
91
+ record?: ReturnRecord;
92
+ }
93
+ /** Applies a conversation's record; returns what happened to each path that changed since the last application. */
94
+ export declare function applyAgentConfigReturn(cell: ReturnCell, conversationId: string, options: ApplyOptions): Promise<ApplyResult>;
95
+ /**
96
+ * One line for a batch: "Claude saved the skill deploy-checks in the cloud: it's on this Mac now". Undefined when
97
+ * nothing reached this computer (an unchanged or skipped path says nothing).
98
+ */
99
+ export declare function returnNotice(app: string, items: ReturnItem[], platform?: NodeJS.Platform): string | undefined;
100
+ export declare class AgentConfigReturnState {
101
+ readonly retries: Map<string, {
102
+ revision: string;
103
+ failures: number;
104
+ retryAt: number;
105
+ }>;
106
+ machine: {
107
+ checkedAt: number;
108
+ saved: boolean;
109
+ };
110
+ /** The user signed in on this computer: the session's, or the creator of its API key (refreshed every 10 minutes). */
111
+ identity: {
112
+ checkedAt: number;
113
+ userId?: string;
114
+ };
115
+ folding?: Promise<void>;
116
+ }
117
+ /** How often the device helper looks for agent config to bring back: its own loop, never behind a files or transcript pass. */
118
+ export declare const AGENT_CONFIG_POLL_MS = 2000;
119
+ /** One pass of the device helper's own agent-config loop: list the account's conversations and apply what came back. */
120
+ export declare function returnAgentConfigsOnce(ctx: Ctx, state: AgentConfigReturnState): Promise<string | undefined>;
121
+ /**
122
+ * This computer's checkout of a conversation's repository: the folder it was started from here, else the folder of
123
+ * another conversation of the same repository started here. A Shardflux project can hold many repositories (staging
124
+ * QA FE1: 31 in one project), so the project never decides. Undefined when there is none (a conversation started on
125
+ * the phone, the web, T3 or another computer for a repository this computer never opened): its user-level files still
126
+ * apply, its project memory has nowhere to go.
127
+ */
128
+ export declare function localCheckout(c: Conversation, records: Record<string, LocalConversation>): string | undefined;
129
+ /**
130
+ * User-level config is personal. A conversation that names who started it applies only when that is this computer's
131
+ * user, wherever it started; a teammate's never does. One that does not (an older API, a conversation from before it
132
+ * was recorded) applies only when this computer started it.
133
+ */
134
+ export declare function ownConversation(c: Pick<Conversation, 'id' | 'started_by'>, records: Record<string, LocalConversation>, me: string | undefined): boolean;
135
+ /**
136
+ * Called by the transcript mirror's pass with the conversations it listed: every conversation of this computer's user
137
+ * (ownConversation), wherever it started (this computer, the phone, the web, T3, another computer). Each one's record is
138
+ * read once per turn (its revision; a restart reads nothing again) and applied. Conversations idle since before this
139
+ * computer started applying are never read. One notice line per pass, on the device helper's stdout for the menu bar.
140
+ */
141
+ export declare function returnAgentConfigs(ctx: Ctx, records: Record<string, LocalConversation>, all: Conversation[], state: AgentConfigReturnState): Promise<string | undefined>;
142
+ /** Memory is per project and never part of the machine; everything else the machine delivers live. */
143
+ export declare function queueMachineFold(ctx: Ctx, state: AgentConfigReturnState, result: ApplyResult): Promise<void>;
144
+ /**
145
+ * Writes the queued changes into the machine VM under the same rules (a machine copy changed since is kept) and saves
146
+ * the machine once for the batch, after a quiet period and at most every FOLD_INTERVAL_MS. A machine VM that is running
147
+ * (someone may be changing it) is left alone for up to FOLD_DEFER_MS.
148
+ */
149
+ export declare function foldIntoMachine(ctx: Ctx, state: AgentConfigReturnState, now?: number): Promise<'saved' | 'waiting' | 'nothing' | 'deferred' | 'shared'>;
150
+ export {};