@shardflux/cli 0.20.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +312 -1
  2. package/README.md +242 -75
  3. package/dist/account-args.js +7 -2
  4. package/dist/account-commands.js +56 -106
  5. package/dist/account-pool.d.ts +19 -15
  6. package/dist/account-pool.js +47 -96
  7. package/dist/account-scope.d.ts +65 -0
  8. package/dist/account-scope.js +246 -0
  9. package/dist/acp-journal.js +58 -6
  10. package/dist/acp-login.d.ts +13 -5
  11. package/dist/acp-login.js +37 -8
  12. package/dist/acp-models.d.ts +6 -1
  13. package/dist/acp-models.js +29 -19
  14. package/dist/acp.js +120 -14
  15. package/dist/agent-commands.d.ts +4 -1
  16. package/dist/agent-commands.js +110 -42
  17. package/dist/agent-config-return.d.ts +46 -2
  18. package/dist/agent-config-return.js +110 -4
  19. package/dist/agent-credentials.js +7 -3
  20. package/dist/agent-repository.d.ts +16 -3
  21. package/dist/agent-repository.js +149 -74
  22. package/dist/agent-setup.d.ts +14 -3
  23. package/dist/agent-setup.js +78 -36
  24. package/dist/agent-target-commands.d.ts +20 -1
  25. package/dist/agent-target-commands.js +73 -31
  26. package/dist/agent-targets.js +6 -1
  27. package/dist/args.d.ts +1 -1
  28. package/dist/args.js +95 -54
  29. package/dist/bin.js +5 -1
  30. package/dist/browser.d.ts +13 -2
  31. package/dist/browser.js +30 -14
  32. package/dist/claude-cloud-login.d.ts +24 -2
  33. package/dist/claude-cloud-login.js +151 -11
  34. package/dist/cli-proxy-hub.d.ts +61 -25
  35. package/dist/cli-proxy-hub.js +202 -54
  36. package/dist/cli-proxy.d.ts +44 -1
  37. package/dist/cli-proxy.js +177 -28
  38. package/dist/client-settings.d.ts +39 -0
  39. package/dist/client-settings.js +91 -0
  40. package/dist/codex-cloud.d.ts +14 -4
  41. package/dist/codex-cloud.js +135 -31
  42. package/dist/codex-linux.d.ts +14 -0
  43. package/dist/codex-linux.js +69 -0
  44. package/dist/command-center-actions.js +18 -5
  45. package/dist/command-center-model.d.ts +2 -0
  46. package/dist/command-center-model.js +6 -1
  47. package/dist/command-center.js +3 -3
  48. package/dist/commands.js +173 -24
  49. package/dist/conversation-commands.d.ts +5 -0
  50. package/dist/conversation-commands.js +8 -5
  51. package/dist/conversation-terminal.d.ts +10 -2
  52. package/dist/conversation-terminal.js +78 -13
  53. package/dist/conversations.d.ts +86 -1
  54. package/dist/conversations.js +306 -23
  55. package/dist/desktop-linux.js +16 -3
  56. package/dist/device-bridge.d.ts +1 -1
  57. package/dist/device-bridge.js +6 -4
  58. package/dist/device-helper.js +43 -22
  59. package/dist/errors.d.ts +29 -2
  60. package/dist/errors.js +50 -7
  61. package/dist/filesystem-commands.d.ts +16 -9
  62. package/dist/filesystem-commands.js +133 -57
  63. package/dist/format.d.ts +2 -1
  64. package/dist/format.js +10 -1
  65. package/dist/handlers.js +3 -1
  66. package/dist/initial-prompt.d.ts +8 -0
  67. package/dist/initial-prompt.js +43 -1
  68. package/dist/large-media.d.ts +14 -0
  69. package/dist/large-media.js +100 -0
  70. package/dist/linux-terminal.d.ts +5 -3
  71. package/dist/linux-terminal.js +6 -20
  72. package/dist/local-sessions.d.ts +26 -2
  73. package/dist/local-sessions.js +113 -24
  74. package/dist/machine-setup.d.ts +11 -1
  75. package/dist/machine-setup.js +69 -22
  76. package/dist/machine-status.d.ts +67 -0
  77. package/dist/machine-status.js +140 -0
  78. package/dist/main.js +11 -13
  79. package/dist/menubar.d.ts +6 -5
  80. package/dist/menubar.js +38 -28
  81. package/dist/move-worktrees-restore.py +144 -0
  82. package/dist/move-worktrees.d.ts +69 -0
  83. package/dist/move-worktrees.js +252 -0
  84. package/dist/move.d.ts +10 -1
  85. package/dist/move.js +111 -16
  86. package/dist/onboarding.d.ts +11 -7
  87. package/dist/onboarding.js +63 -29
  88. package/dist/prepared-repository.d.ts +204 -0
  89. package/dist/prepared-repository.js +860 -0
  90. package/dist/prepared-repository.py +1307 -0
  91. package/dist/project-preparation.d.ts +62 -0
  92. package/dist/project-preparation.js +316 -0
  93. package/dist/project-sync-manifest.d.ts +4 -0
  94. package/dist/project-sync-manifest.js +5 -1
  95. package/dist/project-sync-transfer.js +1 -1
  96. package/dist/project-sync.d.ts +22 -4
  97. package/dist/project-sync.js +34 -8
  98. package/dist/project-template-setup.d.ts +33 -1
  99. package/dist/project-template-setup.js +59 -2
  100. package/dist/secret-ask.d.ts +72 -0
  101. package/dist/secret-ask.js +351 -0
  102. package/dist/seed-phases.d.ts +8 -1
  103. package/dist/seed-phases.js +17 -2
  104. package/dist/setup-cache.d.ts +7 -0
  105. package/dist/setup-cache.js +32 -0
  106. package/dist/t3-launch.d.ts +2 -1
  107. package/dist/t3-launch.js +12 -6
  108. package/dist/t3-mode.d.ts +8 -2
  109. package/dist/t3-mode.js +19 -4
  110. package/dist/t3-phone.d.ts +4 -2
  111. package/dist/t3-phone.js +12 -4
  112. package/dist/t3-proxy.d.ts +81 -0
  113. package/dist/t3-proxy.js +325 -0
  114. package/dist/terminal-apps.d.ts +168 -0
  115. package/dist/terminal-apps.js +574 -0
  116. package/dist/terminal-onboarding.js +6 -1
  117. package/dist/this-computer.d.ts +11 -0
  118. package/dist/this-computer.js +36 -0
  119. package/dist/thread-on-computer.js +4 -1
  120. package/dist/update.d.ts +2 -0
  121. package/dist/update.js +43 -9
  122. package/dist/worktree-twin.d.ts +10 -2
  123. package/dist/worktree-twin.js +27 -9
  124. package/package.json +5 -5
package/dist/move.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { Ctx, Handler } from './commands.js';
3
3
  import type { Values } from './args.js';
4
4
  import type { Conversation } from './conversations.js';
5
5
  import type { LocalSession, SessionHarness, SessionState } from './local-sessions.js';
6
+ import type { WorktreeMigration } from './move-worktrees.js';
6
7
  type Env = Record<string, string | undefined>;
7
8
  /** The project a folder belongs to: its Git repository's top, or the folder itself. */
8
9
  export declare function projectRoot(folder: string): string;
@@ -15,10 +16,17 @@ type MoveCell = Pick<ReturnType<Workspace['cell']>, 'exec' | 'files'>;
15
16
  * project space is mounted, as the conversation's durable copy with the same checkpoint metadata the guest writes
16
17
  * (transcripts/<id>/meta.json), so the session survives the VM from its first minute.
17
18
  */
18
- export declare function placeTranscript(cell: MoveCell, c: Pick<Conversation, 'id' | 'harness' | 'worktree_path'>, s: LocalSession, bytes: Buffer, folder: string, signal?: AbortSignal): Promise<{
19
+ export declare function placeTranscript(cell: MoveCell, c: Pick<Conversation, 'id' | 'harness' | 'worktree_path'>, s: LocalSession, bytes: Buffer, folder: string, signal?: AbortSignal, worktrees?: WorktreeMigration): Promise<{
19
20
  native: string;
20
21
  durable: boolean;
21
22
  }>;
23
+ /** Every working directory the whole transcript names in its record metadata, never in message or tool content. */
24
+ export declare function transcriptCwds(bytes: Buffer): string[];
25
+ /**
26
+ * The project a move goes to: --path when the session's records confirm it worked there (placeTranscript checks), the
27
+ * current folder's project when it worked there, else the project of where it works now.
28
+ */
29
+ export declare function moveFolder(s: Pick<LocalSession, 'cwd' | 'cwds'>, here: string, explicit: boolean): string;
22
30
  export declare function moveSession(ctx: Ctx, values: Values): Promise<number>;
23
31
  /** `shard move --list`: this project's sessions on this computer, and where each moved one continues. */
24
32
  export declare function listMovable(ctx: Pick<Ctx, 'store' | 'apiUrl'>, values: Values, env: Env, now?: number): {
@@ -30,6 +38,7 @@ export declare function listMovable(ctx: Pick<Ctx, 'store' | 'apiUrl'>, values:
30
38
  updated_at: string;
31
39
  source: string;
32
40
  cwd: string;
41
+ cwds: string[];
33
42
  running: boolean;
34
43
  turn: "working" | "idle";
35
44
  moved: {
package/dist/move.js CHANGED
@@ -31,8 +31,9 @@ import { openInChoices } from "./t3-mode.js";
31
31
  import { openClaudeDesktopSession, remoteControlUrl } from "./agent-target-commands.js";
32
32
  import { openUrl } from "./browser.js";
33
33
  import { SYNC_CHUNK_BYTES } from "./project-sync-transfer.js";
34
- import { completeRecords, continuedAfter, findLocalSession, homeRelativeTranscript, listLocalSessions, readTranscript, rewriteSessionPaths, sessionState } from "./local-sessions.js";
34
+ import { completeRecords, continuedAfter, findLocalSession, homeRelativeTranscript, listLocalSessions, readTranscript, recordCwd, rewriteSessionPaths, sessionState } from "./local-sessions.js";
35
35
  import { installDivergenceNotice, movedKey, movedSession, movedSessions, rememberMoved } from "./moved-sessions.js";
36
+ import { captureWorktreeMigration, inspectWorktreeMigration, migrationScope, preserveWorktreeMigration, referencedWorktrees, rewriteWorktreeTranscript } from "./move-worktrees.js";
36
37
  const VM_HOME = '/home/user';
37
38
  const SPACE = '/shardflux/project';
38
39
  /** A transcript above this moves with the repository's history bound. */
@@ -95,14 +96,27 @@ async function writeChunks(cell, path, bytes) {
95
96
  * project space is mounted, as the conversation's durable copy with the same checkpoint metadata the guest writes
96
97
  * (transcripts/<id>/meta.json), so the session survives the VM from its first minute.
97
98
  */
98
- export async function placeTranscript(cell, c, s, bytes, folder, signal) {
99
- const sessionCwd = realOr(s.cwd), inside = relative(folder, sessionCwd);
100
- if (inside.startsWith('..'))
101
- throw new UsageError('The session ran outside this project folder; run shard move in its project.');
102
- let moved = rewriteSessionPaths(bytes, folder, c.worktree_path);
103
- if (s.cwd !== sessionCwd && !s.cwd.startsWith(`${folder}/`) && s.cwd !== folder) {
104
- // The transcript recorded a path through a link (/tmp for /private/tmp): move that spelling too.
105
- moved = rewriteSessionPaths(moved, s.cwd.slice(0, s.cwd.length - inside.length).replace(/\/$/, ''), c.worktree_path);
99
+ export async function placeTranscript(cell, c, s, bytes, folder, signal, worktrees) {
100
+ // The project is confirmed by the session's own records (0.20.2+): any of them working in the folder, not only the
101
+ // first (a session started in a scratch folder that went on in this repository). Paths are moved for this folder;
102
+ // records of a folder outside it keep their bytes.
103
+ const here = confirmedCwds(s, bytes, folder, worktrees);
104
+ let moved = bytes;
105
+ if (worktrees) {
106
+ // Normalize linked spellings confirmed by record metadata before mapping
107
+ // longest registered prefixes. Scratch records outside the family stay intact.
108
+ for (const tree of [...worktrees.trees].sort((a, b) => b.source.length - a.source.length)) {
109
+ const spellings = here.filter(cwd => isInside(tree.source, realOr(cwd))).map(cwd => linkedSpelling(cwd, tree.source));
110
+ for (const spelled of new Set(spellings.filter((p) => !!p)))
111
+ moved = rewriteSessionPaths(moved, spelled, tree.source);
112
+ }
113
+ moved = rewriteWorktreeTranscript(moved, worktrees, c.worktree_path);
114
+ }
115
+ else {
116
+ moved = rewriteSessionPaths(moved, folder, c.worktree_path);
117
+ // A path recorded through a link (/tmp for /private/tmp): move that spelling too.
118
+ for (const spelled of new Set(here.map(cwd => linkedSpelling(cwd, folder)).filter((p) => !!p)))
119
+ moved = rewriteSessionPaths(moved, spelled, c.worktree_path);
106
120
  }
107
121
  const homeRelative = homeRelativeTranscript(s, c.worktree_path);
108
122
  const native = `${VM_HOME}/${homeRelative}`;
@@ -125,6 +139,52 @@ const realOr = (path) => { try {
125
139
  catch {
126
140
  return resolve(path);
127
141
  } };
142
+ const isInside = (folder, path) => path === folder || path.startsWith(folder.endsWith('/') ? folder : `${folder}/`);
143
+ /** The folder as `cwd` spells it through a link (/tmp/x for /private/tmp/x), or undefined when it spells it plainly. */
144
+ function linkedSpelling(cwd, folder) {
145
+ if (isInside(folder, cwd))
146
+ return undefined;
147
+ const inside = relative(folder, realOr(cwd));
148
+ if (inside && !cwd.endsWith(`/${inside}`))
149
+ return undefined;
150
+ return inside ? cwd.slice(0, cwd.length - inside.length - 1) : cwd;
151
+ }
152
+ /** Every working directory the whole transcript names in its record metadata, never in message or tool content. */
153
+ export function transcriptCwds(bytes) {
154
+ const out = new Set();
155
+ for (const line of bytes.toString('utf8').split('\n')) {
156
+ try {
157
+ const r = JSON.parse(line);
158
+ if (!r || typeof r !== 'object' || Array.isArray(r))
159
+ continue;
160
+ const cwd = recordCwd(r);
161
+ if (cwd)
162
+ out.add(cwd);
163
+ }
164
+ catch { /* a partial record */ }
165
+ }
166
+ return [...out];
167
+ }
168
+ function confirmedCwds(s, bytes, folder, worktrees) {
169
+ const roots = [folder, ...(worktrees?.trees.map(t => t.source) ?? [])];
170
+ const here = [...new Set([s.cwd, ...s.cwds, ...transcriptCwds(bytes)])].filter(cwd => roots.some(root => isInside(root, realOr(cwd))));
171
+ if (!here.length)
172
+ throw new UsageError('The session ran outside this project folder; run shard move in its project, or pass --path with the repository it worked in.');
173
+ return here;
174
+ }
175
+ /**
176
+ * The project a move goes to: --path when the session's records confirm it worked there (placeTranscript checks), the
177
+ * current folder's project when it worked there, else the project of where it works now.
178
+ */
179
+ export function moveFolder(s, here, explicit) {
180
+ if (explicit)
181
+ return here;
182
+ const root = (cwd) => (existsSync(cwd) ? projectRoot(cwd) : here);
183
+ if (isInside(here, realOr(s.cwd)))
184
+ return root(s.cwd);
185
+ const inHere = [...s.cwds].reverse().find(cwd => isInside(here, realOr(cwd)));
186
+ return inHere ? root(inHere) : root(s.cwd);
187
+ }
128
188
  async function alreadyMoved(ctx, m) {
129
189
  try {
130
190
  const c = await getConversation(ctx, m.conversation_id);
@@ -140,19 +200,49 @@ export async function moveSession(ctx, values) {
140
200
  const env = { ...process.env, ...ctx.io.env };
141
201
  const here = projectRoot(typeof values.path === 'string' ? values.path : process.cwd());
142
202
  const s = chooseSession(values, env, here);
143
- const folder = typeof values.path === 'string' ? here : projectRoot(existsSync(s.cwd) ? s.cwd : here);
203
+ const folder = moveFolder(s, here, typeof values.path === 'string');
144
204
  const prior = movedSession(ctx.store.dir, ctx.apiUrl, s.harness, s.session_id);
145
- if (prior) {
205
+ if (prior && values.plan !== true) {
146
206
  const live = await alreadyMoved(ctx, prior);
147
207
  if (live) {
148
208
  out(ctx, `This session already continues in the cloud as ${prior.key}. Open it: shard agents open ${prior.key}\n`, { already: true, moved: prior, conversation: live });
149
209
  return 0;
150
210
  }
151
211
  }
152
- await waitForTurnEnd(ctx, s, env);
212
+ // A normal move continues the session from this checkout, uncommitted edits included; the repository's other
213
+ // worktrees and local branches stay on this computer, untouched (as `this-worktree-only` says explicitly).
214
+ // --preserve-all-worktrees copies them too.
215
+ const preserveAll = values['preserve-all-worktrees'] === true;
216
+ if (preserveAll && values['this-worktree-only'] === true)
217
+ throw new UsageError('Choose --preserve-all-worktrees or --this-worktree-only.');
218
+ if (values.plan !== true) {
219
+ // A local MCP caller is still in its turn: an unrelated folder is refused while it can still answer.
220
+ if (!preserveAll)
221
+ confirmedCwds(s, completeRecords(readTranscript(s.file)), folder);
222
+ await waitForTurnEnd(ctx, s, env);
223
+ }
153
224
  const bytes = completeRecords(readTranscript(s.file));
154
225
  if (bytes.length > TRANSCRIPT_LIMIT)
155
226
  throw new UsageError('This session’s transcript is above the 256 MiB move bound.');
227
+ const plan = preserveAll || values.plan === true ? await inspectWorktreeMigration(folder, bytes, ctx.signal) : null;
228
+ // Refuse before syncing credentials, creating a VM or snapshotting an unrelated
229
+ // folder. An explicit all-worktree scope also accepts record-confirmed linked owners.
230
+ confirmedCwds(s, bytes, folder, plan ?? undefined);
231
+ let elsewhere = [];
232
+ if (plan) {
233
+ await captureWorktreeMigration(plan, ctx.signal);
234
+ const scope = migrationScope(plan);
235
+ if (values.plan === true) {
236
+ out(ctx, `${JSON.stringify(scope, null, 2)}\n`, scope);
237
+ return 0;
238
+ }
239
+ ctx.io.stderr.write(`Preserving ${plan.trees.length} registered worktrees, ${plan.files} files, ${plan.bytes} file bytes (archives add headers). Includes ignored files and Git metadata:\n${plan.trees.map(t => ` ${t.source}`).join('\n')}\nSymlink targets and unrelated sibling folders are not uploaded.\n`);
240
+ }
241
+ else if (values['this-worktree-only'] !== true) {
242
+ elsewhere = await referencedWorktrees(folder, bytes, ctx.signal);
243
+ if (elsewhere.length && !ctx.json)
244
+ ctx.io.stderr.write(`This session also worked in ${elsewhere.length === 1 ? 'another worktree' : `${elsewhere.length} other worktrees`} of this repository (${elsewhere.join(', ')}); ${elsewhere.length === 1 ? 'it stays' : 'they stay'} on this computer. shard move --preserve-all-worktrees copies every worktree.\n`);
245
+ }
156
246
  const sha256 = createHash('sha256').update(bytes).digest('hex');
157
247
  if (!ctx.json)
158
248
  ctx.io.stderr.write(`Moving ${s.harness === 'claude' ? 'Claude Code' : 'Codex'} session “${s.title}” to the cloud…\n`);
@@ -162,11 +252,16 @@ export async function moveSession(ctx, values) {
162
252
  requireClaudeTerminal(ctx);
163
253
  let created;
164
254
  let placed;
255
+ let preserved;
165
256
  try {
166
257
  created = await createConversation(ctx, folder, s.harness, 'terminal', undefined, {}, {
167
258
  session_id: s.session_id, title: s.title,
259
+ ...(values['preserve-all-worktrees'] === true && plan ? { preserveWorktrees: true,
260
+ seedRepository: async (ws, c) => { preserved = await preserveWorktreeMigration(ws.cell(), plan, c, ctx.signal); } } : {}),
168
261
  // The workspace's cell handle is shared with the seed that follows: it stays open.
169
- beforeSeed: async (ws, c) => { placed = await placeTranscript(ws.cell(), c, s, bytes, folder, ctx.signal); },
262
+ beforeSeed: async (ws, c) => {
263
+ placed = await placeTranscript(ws.cell(), c, s, bytes, folder, ctx.signal, values['preserve-all-worktrees'] === true && plan ? plan : undefined);
264
+ },
170
265
  });
171
266
  }
172
267
  catch (e) {
@@ -198,7 +293,7 @@ export async function moveSession(ctx, values) {
198
293
  if (!attach) {
199
294
  const quiet = { ...ctx, io: { ...ctx.io, stdout: { write: () => true } } };
200
295
  await conversationTerminal(quiet, ws, c, place, { ...values, detach: true });
201
- out(ctx, `Moved to the cloud as ${c.key}: the same session, with its history. Open it: shard agents open ${c.key}\n`, { moved, conversation: c, workspace: ws.data, ...(placed ? { transcript: placed } : {}) });
296
+ out(ctx, `Moved to the cloud as ${c.key}: the same session, with its history. Open it: shard agents open ${c.key}\n`, { moved, conversation: c, workspace: ws.data, ...(placed ? { transcript: placed } : {}), ...(preserved ? { preserved } : {}), ...(elsewhere.length ? { other_worktrees: elsewhere } : {}) });
202
297
  return 0;
203
298
  }
204
299
  const where = openInChoices(ctx, project.slug)[s.harness];
@@ -208,7 +303,7 @@ export async function moveSession(ctx, values) {
208
303
  if (where === 'claude-desktop')
209
304
  return openClaudeDesktopSession(ctx, ws, c);
210
305
  const url = await remoteControlUrl(ctx, ws, c);
211
- if (!openUrl(url, ctx.io.env))
306
+ if (!(await openUrl(url, ctx.io.env)))
212
307
  throw new Error(`could not open ${url}`);
213
308
  out(ctx, `${url}\n`, { moved, conversation: c, url });
214
309
  return 0;
@@ -223,7 +318,7 @@ export function listMovable(ctx, values, env, now = Date.now()) {
223
318
  const sessions = listLocalSessions(folder, env, { harness: harnessOf(values), limit: 10 }).map(s => {
224
319
  const m = moved[movedKey(s.harness, s.session_id)];
225
320
  const state = sessionState(s, env, now);
226
- return { harness: s.harness, session_id: s.session_id, title: s.title, updated_at: s.updated_at, source: s.source, cwd: s.cwd,
321
+ return { harness: s.harness, session_id: s.session_id, title: s.title, updated_at: s.updated_at, source: s.source, cwd: s.cwd, cwds: s.cwds,
227
322
  running: state.running, turn: state.turn,
228
323
  moved: m ? { conversation_id: m.conversation_id, key: m.key, moved_at: m.moved_at, diverged: continuedAfter(s, m.bytes) } : null };
229
324
  });
@@ -1,6 +1,7 @@
1
1
  import type { Ctx } from './commands.js';
2
2
  import type { Values } from './args.js';
3
3
  import type { CodexConnectorView, CodexHealth } from './codex-cloud.js';
4
+ import type { MachineRow } from './machine-status.js';
4
5
  /**
5
6
  * The first-run demo's task: it shows the agent's own computer at work. The user copies it and pastes it into the
6
7
  * launcher themselves; Daylight's own CLAUDE.md and AGENTS.md say how to run it and test it in the desktop browser.
@@ -13,7 +14,7 @@ export type AgentApp = 'claude' | 'codex';
13
14
  * rest (Claude Code's installer uses ~/.local/bin).
14
15
  */
15
16
  export declare function installDirs(home: string): string[];
16
- /** Claude Code or Codex is on this Mac: its home folder (~/.claude, ~/.codex) or its command on PATH or in an install folder. */
17
+ /** Claude Code or Codex is on this computer: its home folder (~/.claude, ~/.codex) or its command on PATH or in an install folder. */
17
18
  export declare function foundApps(env: NodeJS.ProcessEnv): Promise<AgentApp[]>;
18
19
  /** --apps claude,codex (repeatable or comma separated); unknown names are refused. */
19
20
  export declare function parseApps(value: unknown): AgentApp[] | undefined;
@@ -71,32 +72,35 @@ export interface T3SignInRow {
71
72
  * appears in T3 Code and the T3 app by itself. `startSignIn` (`--t3-connect`) starts that sign-in. Null when T3 Code
72
73
  * is not installed.
73
74
  */
74
- export declare function t3Connection(ctx: Ctx, signedIn: boolean, appDirs?: string[], platform?: NodeJS.Platform, startSignIn?: boolean): Promise<Connection | null>;
75
+ export declare function t3Connection(ctx: Ctx, signedIn: boolean, appDirs?: string[], platform?: NodeJS.Platform, startSignIn?: boolean, scope?: Values): Promise<Connection | null>;
75
76
  /** Claude: the account login is ready and Claude itself accepts it right now. */
76
77
  export declare function claudeConnection(row: CredentialRow | undefined): Omit<Connection, 'id' | 'picked' | 'found'>;
77
- /** Codex: this Mac's Codex login is copied and the account's Codex remote device (one executor) is connected. */
78
+ /** Codex: this computer's Codex login is copied and the account's Codex remote device (one executor) is connected. */
78
79
  export declare function codexConnection(local: boolean, synced: boolean, connector: CodexConnectorView | null, health: CodexHealth): Omit<Connection, 'id' | 'picked' | 'found'>;
79
80
  /**
80
81
  * The onboarding report, for an agent setting itself up from chat and for the menu bar app. It verifies every
81
82
  * connection live: the Shardflux account, Claude (Claude itself accepts the login) and Codex (the login is copied and
82
- * the account's Codex remote device is connected), for the apps picked (--apps, else saved, else found on this Mac).
83
+ * the account's Codex remote device is connected), for the apps picked (--apps, else saved, else found on this computer).
83
84
  * With --connect it also does what needs no sign-in: copies the Codex login and enables the Codex remote device.
84
85
  */
85
- export declare function onboardingReport(ctx: Ctx, values: Values): Promise<{
86
+ export declare function onboardingReport(ctx: Ctx, values: Values, platform?: NodeJS.Platform): Promise<{
86
87
  version: number;
87
88
  api_url: string;
88
89
  apps: AgentApp[];
89
90
  found_apps: AgentApp[];
90
91
  connections: Connection[];
91
92
  ready: boolean;
93
+ connections_ready: boolean;
94
+ machine_ready: boolean | null;
92
95
  account_error: string | null;
96
+ machine: MachineRow | null;
93
97
  demo: {
94
98
  prompt: string;
95
99
  project: string;
96
100
  prepare: string[];
97
101
  };
98
102
  activity: {
99
- state: "running" | "failed" | "suspended" | "deleted" | "creating" | "starting" | "suspending" | "resuming" | "forking" | "stopping" | "deleting";
103
+ state: "running" | "failed" | "creating" | "starting" | "suspending" | "suspended" | "resuming" | "forking" | "stopping" | "deleting" | "deleted";
100
104
  workspace: {
101
105
  id: string;
102
106
  key: string;
@@ -180,7 +184,7 @@ interface ProcessRow {
180
184
  export declare function activeCommands(processes: ProcessRow[]): string[];
181
185
  /** Commands and listening ports of a running VM. Never wakes a parked computer. */
182
186
  export declare function liveActivity(ctx: Ctx, ref: string): Promise<{
183
- state: "running" | "failed" | "suspended" | "deleted" | "creating" | "starting" | "suspending" | "resuming" | "forking" | "stopping" | "deleting";
187
+ state: "running" | "failed" | "creating" | "starting" | "suspending" | "suspended" | "resuming" | "forking" | "stopping" | "deleting" | "deleted";
184
188
  workspace: {
185
189
  id: string;
186
190
  key: string;
@@ -6,12 +6,15 @@ import { resolveWorkspace } from "./commands.js";
6
6
  import { AgentStateStore } from "./agent-state.js";
7
7
  import { describeError } from "./errors.js";
8
8
  import { t3PhoneLink } from "./t3-phone.js";
9
+ import { t3NeedsProject, t3SignInProject } from "./account-scope.js";
9
10
  import { readAgentCredentials, syncAgentCredentials } from "./agent-credentials.js";
10
11
  import { getConversation } from "./conversations.js";
11
12
  import { codexHealth, readCodexConnector, restartCodexConnector } from "./codex-cloud.js";
12
13
  import { applicationDirs } from "./agent-targets.js";
13
14
  import { configureT3Provider } from "./t3-provider.js";
14
15
  import { t3Install } from "./t3-mode.js";
16
+ import { inspectMachineRow, rowText } from "./machine-status.js";
17
+ import { launcherShortcut } from "./this-computer.js";
15
18
  /**
16
19
  * The first-run demo's task: it shows the agent's own computer at work. The user copies it and pastes it into the
17
20
  * launcher themselves; Daylight's own CLAUDE.md and AGENTS.md say how to run it and test it in the desktop browser.
@@ -37,7 +40,7 @@ export function installDirs(home) {
37
40
  return [join(home, '.local', 'bin'), join(home, '.claude', 'local'), join(home, '.npm-global', 'bin'), join(home, '.bun', 'bin'),
38
41
  join(home, '.volta', 'bin'), join(home, 'Library', 'pnpm')];
39
42
  }
40
- /** Claude Code or Codex is on this Mac: its home folder (~/.claude, ~/.codex) or its command on PATH or in an install folder. */
43
+ /** Claude Code or Codex is on this computer: its home folder (~/.claude, ~/.codex) or its command on PATH or in an install folder. */
41
44
  export async function foundApps(env) {
42
45
  const home = env.HOME ?? homedir();
43
46
  const onPath = async (name) => {
@@ -69,7 +72,7 @@ export function parseApps(value) {
69
72
  * appears in T3 Code and the T3 app by itself. `startSignIn` (`--t3-connect`) starts that sign-in. Null when T3 Code
70
73
  * is not installed.
71
74
  */
72
- export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.io.env), platform = process.platform, startSignIn = false) {
75
+ export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.io.env), platform = process.platform, startSignIn = false, scope = {}) {
73
76
  // The same install every T3 command uses (macOS and Linux): a build with ACP providers first.
74
77
  const app = t3Install(ctx.io.env, platform, appDirs);
75
78
  if (!app)
@@ -90,37 +93,52 @@ export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.
90
93
  // Stable T3: one T3 Connect sign-in per person (the API's, per account), then each project appears by itself. `next`
91
94
  // is for scripts: a project is set up for T3 Code with it.
92
95
  const ssh = { ...base, mode: 'ssh', next: shard(['agents', 'new', '--in', 't3', '--path', '<project>']) };
96
+ const selection = ['org', 'project'].flatMap(key => typeof scope[key] === 'string' ? [`--${key}`, scope[key]] : []);
93
97
  const offer = { ...ssh, state: 'action', fact: 'Your projects appear in T3 Code and the T3 app once connected.',
94
- action: { title: 'Connect T3 Code', command: shard(['setup', '--inspect', '--t3-connect', '--json']) } };
98
+ action: { title: 'Connect T3 Code', command: shard(['setup', '--inspect', '--t3-connect', '--json', ...selection]) } };
95
99
  if (!signedIn)
96
100
  return offer;
97
- const cloud = ctx.cloud();
101
+ let projectId;
98
102
  let status;
99
103
  try {
100
- status = await cloud.t3.status();
104
+ // Status and sign-in must use the same person and organization. The key may belong to another organization
105
+ // (or another member); its connected state cannot decide whether this person's selected sign-in is connected.
106
+ if (ctx.sessionSource() !== null) {
107
+ projectId = await t3SignInProject(ctx, scope);
108
+ status = await ctx.account().t3.status(projectId ? { projectId } : {});
109
+ }
110
+ else
111
+ status = await ctx.cloud().t3.status();
101
112
  }
102
113
  catch {
103
114
  return { ...ssh, state: 'unknown', fact: 'T3 Connect could not be checked just now.', action: { title: 'Retry', retry: true } };
104
115
  }
105
116
  // Remembered for `agents targets`: with the sign-in, every project can start in T3 Code (t3Status).
106
- try {
107
- new AgentStateStore(ctx.store.dir).saveT3Connected(ctx.apiUrl, status.connected);
117
+ // A one-off --org/--project check must not replace the default target's remembered status.
118
+ if (selection.length === 0) {
119
+ try {
120
+ new AgentStateStore(ctx.store.dir).saveT3Connected(ctx.apiUrl, status.connected);
121
+ }
122
+ catch { /* the next check writes it */ }
108
123
  }
109
- catch { /* the next check writes it */ }
110
124
  if (!status.connected) {
111
125
  if (!startSignIn)
112
126
  return offer;
113
- const signIn = await startT3SignIn(ctx);
127
+ const signIn = await startT3SignIn(ctx, projectId);
114
128
  if (signIn === 'connected')
115
- return t3Connection(ctx, signedIn, appDirs, platform, false);
129
+ return t3Connection(ctx, signedIn, appDirs, platform, false, scope);
116
130
  if (signIn.needs_login)
117
131
  return { ...offer, fact: 'Sign in to Shardflux on this computer to connect T3 Code.', t3_link: signIn };
118
132
  return { ...offer, fact: 'Confirm the code on the page that opened. This updates on its own.', t3_link: signIn,
119
133
  action: { title: 'Connect T3 Code', url: signIn.sign_in.verification_uri } };
120
134
  }
121
135
  const connected = { ...ssh, state: 'verified', fact: 'Your projects appear in T3 Code and the T3 app.' };
136
+ // Explicit account selection is a status check, not consent to relink the default computer's last workspace.
137
+ if (selection.length > 0)
138
+ return connected;
122
139
  // The newest project set up for T3 Code from this computer (agents.json; a targets.json entry from an older CLI still
123
- // counts): where it is. One set up before T3 Connect, or whose link failed, is linked now: it is the person's choice.
140
+ // counts): where it is. One set up before T3 Connect is linked now. An existing error stays visible until the
141
+ // person explicitly requests another link; a periodic inspection must not restart permanent refusals.
124
142
  const recorded = new AgentStateStore(ctx.store.dir).t3Environments(ctx.apiUrl).at(-1);
125
143
  const manifest = await jsonFile(join(ctx.store.dir, 'targets.json'));
126
144
  const entries = Array.isArray(manifest?.entries) ? manifest.entries : [];
@@ -131,7 +149,9 @@ export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.
131
149
  try {
132
150
  const ws = await resolveWorkspace(ctx, key);
133
151
  const known = status.links.find(l => l.workspace_id === ws.id);
134
- const link = !known || known.status === 'error' || known.status === 'needs_sign_in' ? (await t3PhoneLink(ctx, ws.id)).link : known;
152
+ if (known?.status === 'error')
153
+ return { ...ssh, state: 'info', fact: known.last_error ?? 'T3 Connect could not link this workspace. Check T3 Connect, then open this project in T3 Code again to retry.' };
154
+ const link = !known || known.status === 'needs_sign_in' ? (await t3PhoneLink(ctx, ws.id, ws.data.project_id)).link : known;
135
155
  // Linked to the person's T3 account: T3 Code and the T3 app list it once they are signed in to that account.
136
156
  if (link.status === 'linked')
137
157
  return { ...connected, fact: `${link.label} is linked to your T3 account.` };
@@ -149,19 +169,21 @@ export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.
149
169
  }
150
170
  /**
151
171
  * Starts the person's T3 Connect sign-in (a pending one comes back again). It needs this computer's Shardflux session,
152
- * not only an API key. `connected` when the account already has one (409 `t3_signed_in`).
172
+ * not only an API key. `connected` when the account already has one (409 `t3_signed_in`). The sign-in is per
173
+ * organization: it is scoped by --project/--org, else the API key's project, else the saved default (0.20.2+; before,
174
+ * a person in two organizations got 422 project_id is required).
153
175
  */
154
- async function startT3SignIn(ctx) {
155
- if (ctx.store.profile(ctx.apiUrl).session?.state !== 'active')
176
+ async function startT3SignIn(ctx, projectId) {
177
+ if (ctx.sessionSource() === null)
156
178
  return { status: 'needs_sign_in', label: '', needs_login: true };
157
179
  try {
158
- const flow = await ctx.account().t3.signIn();
180
+ const flow = await ctx.account().t3.signIn(projectId ? { projectId } : {});
159
181
  return { status: 'needs_sign_in', label: '', sign_in: { verification_uri: flow.verification_uri, user_code: flow.user_code, expires_at: flow.expires_at } };
160
182
  }
161
183
  catch (error) {
162
184
  if (error instanceof ShardfluxApiError && describeError(error).code === 't3_signed_in')
163
185
  return 'connected';
164
- throw error;
186
+ throw t3NeedsProject(error) ?? error;
165
187
  }
166
188
  }
167
189
  const shard = (words) => ['shard', ...words];
@@ -176,7 +198,7 @@ export function claudeConnection(row) {
176
198
  return { state: 'action', fact: 'Claude no longer accepts this sign-in.', action: { ...signIn, title: 'Sign in to Claude again' } };
177
199
  return { state: 'verified', fact: 'Signed in. Works in every conversation, on web and phone.' };
178
200
  }
179
- /** Codex: this Mac's Codex login is copied and the account's Codex remote device (one executor) is connected. */
201
+ /** Codex: this computer's Codex login is copied and the account's Codex remote device (one executor) is connected. */
180
202
  export function codexConnection(local, synced, connector, health) {
181
203
  const enable = { title: 'Connect Codex', command: shard(['agents', 'codex', 'enable']) };
182
204
  if (!synced) {
@@ -198,14 +220,14 @@ export function codexConnection(local, synced, connector, health) {
198
220
  /**
199
221
  * The onboarding report, for an agent setting itself up from chat and for the menu bar app. It verifies every
200
222
  * connection live: the Shardflux account, Claude (Claude itself accepts the login) and Codex (the login is copied and
201
- * the account's Codex remote device is connected), for the apps picked (--apps, else saved, else found on this Mac).
223
+ * the account's Codex remote device is connected), for the apps picked (--apps, else saved, else found on this computer).
202
224
  * With --connect it also does what needs no sign-in: copies the Codex login and enables the Codex remote device.
203
225
  */
204
- export async function onboardingReport(ctx, values) {
226
+ export async function onboardingReport(ctx, values, platform = process.platform) {
205
227
  const saved = await jsonFile(resolve(ctx.store.dir, 'onboarding.json'));
206
228
  const state = saved?.api_url === ctx.apiUrl ? saved : undefined;
207
229
  const found = await foundApps(ctx.io.env);
208
- // An empty saved pick means nothing was chosen yet: what is found on this Mac is pre-ticked.
230
+ // An empty saved pick means nothing was chosen yet: what is found on this computer is pre-ticked.
209
231
  const savedApps = Array.isArray(state?.apps) && state.apps.length ? parseApps(state.apps.filter(v => typeof v === 'string')) : undefined;
210
232
  const apps = parseApps(values.apps) ?? savedApps ?? found;
211
233
  const connect = values.connect === true;
@@ -256,7 +278,7 @@ export async function onboardingReport(ctx, values) {
256
278
  let read = await readCodexConnector(ctx);
257
279
  health = codexHealth(read.connector, read.workspaceState);
258
280
  const synced = credentials.some(row => row.app === 'codex');
259
- // One executor per account, enabled as `shard setup` does when this Mac is signed in to Codex. --retry-codex starts
281
+ // One executor per account, enabled as `shard setup` does when this computer is signed in to Codex. --retry-codex starts
260
282
  // a failed or stuck one again, once.
261
283
  if (connect && synced && (health === 'missing' || health === 'disabled')) {
262
284
  read = { connector: await ctx.cloud().codex.enable({ signal: ctx.signal }), workspaceState: null };
@@ -279,24 +301,33 @@ export async function onboardingReport(ctx, values) {
279
301
  const claude = { id: 'claude', picked: apps.includes('claude'), found: found.includes('claude'),
280
302
  ...(!connected ? signIn : credentialError !== undefined ? failure(credentialError) : claudeConnection(credentials.find(row => row.app === 'claude'))) };
281
303
  const codex = { id: 'codex', picked: apps.includes('codex'), found: found.includes('codex'),
282
- ...(!connected ? signIn : (credentialError ?? codexError) !== undefined ? failure(credentialError ?? codexError)
283
- : codexConnection(localCodex, credentials.some(row => row.app === 'codex'), connector, health)) };
284
- const t3 = await t3Connection(ctx, connected, undefined, undefined, values['t3-connect'] === true);
304
+ ...(!connected ? signIn : codexError instanceof ShardfluxApiError && codexError.reason === 'codex_chatgpt_login_required'
305
+ ? { state: 'action', fact: codexError.message, action: { title: 'Sign in with ChatGPT', command: ['codex', 'login'] } }
306
+ : (credentialError ?? codexError) !== undefined ? failure(credentialError ?? codexError)
307
+ : codexConnection(localCodex, credentials.some(row => row.app === 'codex'), connector, health)) };
308
+ const t3 = await t3Connection(ctx, connected || ctx.sessionSource() !== null, undefined, undefined, values['t3-connect'] === true, values);
285
309
  const connections = [account, claude, codex, ...(t3 ? [t3] : [])];
310
+ // The cloud machine (0.20.2+), the same row `shard machine setup --status` reports: whether what is installed carries
311
+ // over to new conversations. It never decides `ready`: conversations start without a saved machine too.
312
+ const machine = connected ? await inspectMachineRow(ctx) : null;
313
+ const shortcut = launcherShortcut(ctx.store.dir, platform);
314
+ const press = (what) => (shortcut ? `Press ${shortcut}, ${what}` : `Open a new conversation, ${what}`);
286
315
  // Ready: signed in and at least one agent app connected; the other can be connected later (Settings, or this again).
287
316
  const ready = account.state === 'verified' && [claude, codex].some(c => c.state === 'verified');
288
317
  const done = (value) => value === true || (typeof value === 'string' && value.length > 0);
289
318
  return {
290
- version: 3, api_url: ctx.apiUrl, apps, found_apps: found, connections, ready, account_error: accountError,
319
+ version: 3, api_url: ctx.apiUrl, apps, found_apps: found, connections, ready, connections_ready: ready,
320
+ machine_ready: machine && !['unknown', 'unavailable'].includes(machine.state) ? machine.state === 'ready' && machine.version !== null && machine.version > 0 : null,
321
+ account_error: accountError, machine,
291
322
  demo: { prompt: DEMO_PROMPT, project: 'Daylight (sample)', prepare: shard(['setup', '--prepare-sample', '--json']) },
292
323
  activity: values.activity === true && typeof values.workspace === 'string' ? await liveActivity(ctx, values.workspace) : null,
293
324
  steps: [
294
325
  { id: 'account', title: 'Sign in to Shardflux', status: connected ? 'complete' : 'available', command: shard(['signup']) },
295
326
  { id: 'connect', title: 'Connect Claude, Codex or both', status: ready ? 'complete' : 'available' },
296
327
  { id: 'demo', title: 'Try it once: your agent’s own computer', status: done(state?.demo_finished) ? 'complete' : 'available',
297
- action: 'Copy the task and press ⌥⇧Space. Paste the task, then Start. The agent installs what it needs, starts it and tests it in its own browser.' },
328
+ action: `Copy the task. ${press('paste the task, then Start.')} The agent installs what it needs, starts it and tests it in its own browser.` },
298
329
  { id: 'own', title: 'Your own project', status: done(state?.own_key) ? 'complete' : 'available',
299
- action: 'Press ⌥⇧Space, pick your project and describe what you want.' },
330
+ action: press('pick your project and describe what you want.') },
300
331
  ],
301
332
  };
302
333
  }
@@ -305,7 +336,10 @@ export function onboardingText(report) {
305
336
  const names = { account: 'Shardflux', claude: 'Claude', codex: 'Codex', t3: 'T3 Code' };
306
337
  const mark = { verified: '✓', action: '!', pending: '…', unknown: '?', info: '·' };
307
338
  const lines = report.connections.filter(c => c.id === 'account' || c.picked || c.id === 't3').map(c => `${mark[c.state]} ${names[c.id]}: ${c.fact}${c.action?.command ? ` (${c.action.command.join(' ')})` : ''}`);
308
- return `${lines.join('\n')}\n${report.ready ? `Ready. Try it once: ${report.demo.prompt}` : 'Not ready yet: finish the steps above, then run this again.'}\n`;
339
+ const machineMark = { ready: '✓', saving: '…', failed: '!', none: '·', unknown: '?', unavailable: '?' };
340
+ if (report.machine)
341
+ lines.push(`${machineMark[report.machine.state]} ${rowText(report.machine)}`);
342
+ return `${lines.join('\n')}\n${report.ready ? `Agent connections ready. Try it once: ${report.demo.prompt}` : 'Connections not ready yet: finish the steps above, then run this again.'}\n`;
309
343
  }
310
344
  /** What a person recognises, most telling first: installs, then how the app is run, then what it runs. */
311
345
  const RANKS = [/^(sudo|apt|apt-get|dpkg)$/, /^(npm|npx|pnpm|yarn|bun)$/, /^(node|python[0-9.]*|uv|vite|next|deno|docker|cargo|go|make)$/, /^(pip[0-9.]*|inotifywait)$/];