@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/commands.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { agentReachability } from "./agent-reachability.js";
2
+ import { findOrg, resolveOrg, sessionScopeWarnings, writeWarnings } from "./account-scope.js";
2
3
  import { vaultType, vaultValue, SECRET_SECURITY_NOTE } from "./vault.js";
3
4
  /**
4
5
  * Command implementations of the project-key commands (workspaces, files, operations, secrets, templates, usage).
@@ -18,10 +19,12 @@ import { CLI_VERSION, SECRET_NAME, UUID, parseDuration, parseEnvPairs, parsePosi
18
19
  import { ExecSessionRunningError, ExecutionFailedError, ExecutionPendingError, InterruptedError, NotFoundError, UsageError } from "./errors.js";
19
20
  import { updateCheckDisabled } from "./update-check.js";
20
21
  import { withLiveAgents } from "./agent-live.js";
22
+ import { codingAgentShell } from "./agent-setup.js";
23
+ import { askInBrowser, askTimeout } from "./secret-ask.js";
21
24
  import { initTemplateYaml, recipeYaml } from "./template-yaml.js";
22
25
  import { BuildTimer, buildTimingJson, buildTimingText, progressLine } from "./timing.js";
23
26
  import { timingJson } from "./timing.js";
24
- import { table, bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, memoryGrowText, burstText, modeText, operationDetail, pairs, resizeJson, resizeText, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, durableText, exposedPortText, portTable, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, time, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
27
+ import { table, bindingsText, buildTable, buildText, changesTable, diffSummaryText, diffTable, draftText, draftStateTable, executionJson, executionSummary, fileInfoText, fileTable, languageTable, json, kv, memoryGrowText, burstText, modeText, operationDetail, suspendProgressText, pairs, resizeJson, resizeText, operationTable, packageTable, searchText, secretAccessTable, secretDetail, secretTable, secretVersionTable, sessionTable, durableText, exposedPortText, portTable, suspendRequestText, suspendWhenIdleText, templateDetailText, templateEntryText, templateFileTable, templateTable, time, usageEstimateText, usageSeriesText, usageText, workspaceDetail, workspaceTable, } from "./format.js";
25
28
  const DEFAULT_WAIT_MS = 300_000;
26
29
  const MAX_JSON_OUTPUT = 16 * 1024 * 1024;
27
30
  const MAX_STDIN = 1024 * 1024;
@@ -43,6 +46,8 @@ const cellOf = (ctx, ws) => ws.cell({
43
46
  });
44
47
  export const strs = (v) => (Array.isArray(v) ? v : typeof v === 'string' ? [v] : []);
45
48
  const MAX_SECRET_VALUE_BYTES = 65_536;
49
+ /** `files write --ask`: a secret file typed or pasted into the page (~/.npmrc, a key, a service-account JSON). */
50
+ const MAX_ASKED_FILE_BYTES = 1024 * 1024;
46
51
  /** `files patch --content-file`: a patch request is at most 7 MiB; larger files go through `files write`. */
47
52
  const MAX_PATCH_CONTENT = 7 * 1024 * 1024;
48
53
  const STOPPED_AT = { max_matches: '--max matches', budget: 'its 10 s budget', max_bytes: '4 MiB of results' };
@@ -476,6 +481,45 @@ function feedbackSender(ctx) {
476
481
  const sessionConfigured = Boolean(ctx.io.env.SHARDFLUX_SESSION_TOKEN?.trim()) || ctx.store.profile(ctx.apiUrl).session !== undefined;
477
482
  return keyConfigured || !sessionConfigured ? ctx.cloud() : ctx.account();
478
483
  }
484
+ /**
485
+ * An organization-level read (`shard usage`; 0.20.2+). The API key reads its own organization. Another organization
486
+ * (--org) answers 404 to a project key, so that read is made with the signed-in session, which sees every organization
487
+ * the person is in; without a session the error says which organization the key belongs to and how to switch. No key
488
+ * but a session: the session, for --org or the default organization.
489
+ */
490
+ async function readOrg(ctx, orgFlag, read) {
491
+ const signedIn = ctx.sessionSource() !== null;
492
+ const hasKey = Boolean(ctx.io.env.SHARDFLUX_API_KEY?.trim()) || ctx.store.profile(ctx.apiUrl).api_key !== undefined;
493
+ const bySession = async () => {
494
+ const account = ctx.account();
495
+ return read(account, (orgFlag !== undefined ? await findOrg(account, orgFlag) : await resolveOrg(ctx, account, {})).id);
496
+ };
497
+ if (!hasKey && signedIn)
498
+ return bySession();
499
+ if (orgFlag === undefined)
500
+ return read(ctx.cloud(), (await principal(ctx)).organization_id);
501
+ if (!UUID.test(orgFlag)) {
502
+ if (signedIn)
503
+ return bySession();
504
+ throw new UsageError('--org must be an organization id (UUID)', 'or sign in (shard auth login) to name it by slug');
505
+ }
506
+ try {
507
+ return await read(ctx.cloud(), orgFlag);
508
+ }
509
+ catch (err) {
510
+ if (!(err instanceof ShardfluxApiError && err.status === 404))
511
+ throw err;
512
+ const p = await principal(ctx);
513
+ // Retry only a cross-org read. A real 404 in the key's own org must keep its error and credential boundary.
514
+ if (p.organization_id === orgFlag)
515
+ throw err;
516
+ if (signedIn)
517
+ return bySession();
518
+ const envKey = ctx.apiKey().source === 'env';
519
+ const key = envKey ? `The API key in SHARDFLUX_API_KEY (${p.api_key.key_id})` : `The saved API key ${p.api_key.key_id}`;
520
+ throw new UsageError(`${key} belongs to organization ${p.organization_id}, so it cannot read organization ${orgFlag}.`, `sign in to read any of your organizations: shard auth login (or switch the key: shard setup --org ${orgFlag} --new-key)${envKey ? '; after saving the new key, unset SHARDFLUX_API_KEY so shard uses it' : ''}`);
521
+ }
522
+ }
479
523
  /** Contract bounds of the feedback context fields shard sends (characters). */
480
524
  const FEEDBACK_CONTEXT_MAX = { workspace: 200, 'request-id': 200, 'error-code': 100, command: 2000 };
481
525
  /**
@@ -879,7 +923,10 @@ export const KEY_HANDLERS = {
879
923
  },
880
924
  async whoami(ctx) {
881
925
  const p = await principal(ctx);
882
- out(ctx, principalText(p), p);
926
+ // Signed in too (0.20.2+): distinguish departed defaults/keys from an accessible key in another default org.
927
+ const warnings = await sessionScopeWarnings(ctx, { keyId: p.api_key.key_id, source: ctx.apiKey().source, organizationId: p.organization_id, projectId: p.project_id });
928
+ writeWarnings(ctx, warnings);
929
+ out(ctx, principalText(p), warnings.length ? { ...p, warnings } : p);
883
930
  return 0;
884
931
  },
885
932
  async version(ctx, values) {
@@ -893,7 +940,16 @@ export const KEY_HANDLERS = {
893
940
  const category = oneOf(values, 'category', FEEDBACK_CATEGORIES);
894
941
  const context = feedbackContext(ctx, values);
895
942
  const message = await feedbackMessage(ctx, values, words);
896
- const r = await feedbackSender(ctx).sendFeedback({ message, ...(category ? { category } : {}), context });
943
+ if (values.yes !== true) {
944
+ const { requestId, errorCode, ...fields } = context;
945
+ const preview = { message, category: category ?? 'other', context: { ...fields, ...(requestId ? { request_id: requestId } : {}), ...(errorCode ? { error_code: errorCode } : {}) } };
946
+ out(ctx, `Feedback preview for the Shardflux team (not sent):\n${json(preview)}Show the exact message and context to the user. Send the same content with --yes only after explicit authorization.\n`, {
947
+ sent: false, authorization_required: true, preview,
948
+ note: 'Not sent. Show the exact message and context to the user; use --yes only after explicit authorization.',
949
+ });
950
+ return 0;
951
+ }
952
+ const r = await feedbackSender(ctx).sendFeedback({ message, category: category ?? 'other', context });
897
953
  const human = r.duplicate
898
954
  ? `Already received: the same message was sent in the last 24 hours (feedback ${r.id}); not emailed again.\n`
899
955
  : `Sent to the Shardflux team (feedback ${r.id}). Thank you.\n`;
@@ -917,20 +973,18 @@ export const KEY_HANDLERS = {
917
973
  out(ctx, usageSeriesText(series), series);
918
974
  return 0;
919
975
  }
920
- const org = str(values.org) ?? (await principal(ctx)).organization_id;
921
- if (!UUID.test(org))
922
- throw new UsageError(`--org must be an organization id (UUID)`);
976
+ const org = str(values.org);
923
977
  if (values.estimate === true) {
924
- const estimate = await ctx.cloud().usage.estimate(org);
978
+ const estimate = await readOrg(ctx, org, (client, id) => client.usage.estimate(id));
925
979
  out(ctx, usageEstimateText(estimate), estimate);
926
980
  return 0;
927
981
  }
928
982
  if (values.series === true) {
929
- const series = await ctx.cloud().usage.series(org, range);
983
+ const series = await readOrg(ctx, org, (client, id) => client.usage.series(id, range));
930
984
  out(ctx, usageSeriesText(series), series);
931
985
  return 0;
932
986
  }
933
- const summary = await ctx.cloud().request('GET', `/v1/organizations/${encodeURIComponent(org)}/usage/summary`);
987
+ const summary = await readOrg(ctx, org, (client, id) => client.request('GET', `/v1/organizations/${encodeURIComponent(id)}/usage/summary`));
934
988
  out(ctx, usageText(summary), summary);
935
989
  return 0;
936
990
  },
@@ -1050,7 +1104,7 @@ export const KEY_HANDLERS = {
1050
1104
  })));
1051
1105
  }
1052
1106
  if (s.state === 'starting' || s.state === 'running') {
1053
- throw new ExecSessionRunningError(`Exec session ${s.session_id} is still ${s.state}.`, s, `follow it to its end with: shard ws exec-read ${ref} ${s.session_id} --follow`);
1107
+ throw new ExecSessionRunningError(`Exec session ${s.session_id} is still ${s.state}${s.error ? `: ${s.error}` : ''}.`, s, `follow it to its end with: shard ws exec-read ${ref} ${s.session_id} --follow`);
1054
1108
  }
1055
1109
  const result = { exitCode: s.exit_code ?? null, termSignal: s.term_signal ?? null, timedOut: s.timed_out ?? false, canceled: s.canceled ?? false };
1056
1110
  if (!ctx.json && result.exitCode === null && !result.timedOut && !(result.termSignal !== null && result.termSignal > 0)) {
@@ -1067,7 +1121,7 @@ export const KEY_HANDLERS = {
1067
1121
  const s = await cellOf(ctx, ws).exec.cancel(sessionId, graceMs);
1068
1122
  const ended = s.state !== 'starting' && s.state !== 'running';
1069
1123
  const status = !ended ? `${s.state} (cancel sent)` : s.canceled ? `canceled${s.term_signal ? ` (signal ${s.term_signal})` : ''}` : `already ${s.state}${typeof s.exit_code === 'number' ? ` (exit code ${s.exit_code})` : ''}`;
1070
- out(ctx, `exec session ${s.session_id}: ${status}\n`, s);
1124
+ out(ctx, `exec session ${s.session_id}: ${status}${s.error ? `: ${s.error}` : ''}${!ended || s.error?.startsWith('cancel accepted; execution drain') ? `; follow with: shard ws exec-read ${ref} ${s.session_id} --follow` : ''}\n`, s);
1071
1125
  return 0;
1072
1126
  },
1073
1127
  async 'workspaces exec-input'(ctx, values, [ref, sessionId, data]) {
@@ -1334,11 +1388,38 @@ export const KEY_HANDLERS = {
1334
1388
  },
1335
1389
  async 'files write'(ctx, values, [ref, path]) {
1336
1390
  const ifRev = ifRevision(values);
1337
- const data = await readInput(ctx, str(values.from) ?? '-', 1024 * 1024 * 1024, 'input');
1391
+ // --ask (0.20.1+): a secret file's contents come from the person, on a page shard opens; the agent never sees them.
1392
+ const ask = values.ask === true;
1393
+ if (ask && str(values.from) !== undefined)
1394
+ throw new UsageError('use --ask or --from, not both');
1395
+ const data = ask ? null : await readInput(ctx, str(values.from) ?? '-', 1024 * 1024 * 1024, 'input');
1338
1396
  const ws = await resolveWorkspace(ctx, ref);
1339
1397
  fileFirstOnly(ws, ifRev !== undefined ? ['--if-revision'] : []);
1340
1398
  const cell = cellOf(ctx, ws);
1341
- const r = await cell.files.write(path, data, { append: values.append === true, createParents: values.parents !== false, ...(ifRev !== undefined ? { ifTreeRevision: ifRev } : {}) });
1399
+ // A new file written from the page is the owner's only (0600); an existing file keeps its mode.
1400
+ const write = (bytes) => cell.files.write(path, bytes, { append: values.append === true, createParents: values.parents !== false, ...(ask ? { mode: '0600' } : {}), ...(ifRev !== undefined ? { ifTreeRevision: ifRev } : {}) });
1401
+ const r = data !== null ? await write(data) : (await askInBrowser(ctx, {
1402
+ kind: 'file',
1403
+ title: `Write ${path}`,
1404
+ purpose: `Saved to ${path} in ${ws.key}. Your coding agent never sees it.`,
1405
+ label: 'File contents',
1406
+ submit: 'Save',
1407
+ done: 'Saved',
1408
+ failed: 'Not saved',
1409
+ doing: `to type the contents of ${path}`,
1410
+ maxBytes: MAX_ASKED_FILE_BYTES,
1411
+ timeoutMs: askTimeout(values),
1412
+ use: async (text) => {
1413
+ // Text files end with a newline; the browser's text area drops a pasted one.
1414
+ const bytes = Buffer.from(text.endsWith('\n') ? text : `${text}\n`, 'utf8');
1415
+ try {
1416
+ return await write(bytes);
1417
+ }
1418
+ finally {
1419
+ bytes.fill(0);
1420
+ }
1421
+ },
1422
+ })).result;
1342
1423
  // File-first: the tree revision the write published (or the current one when nothing changed), to chain --if-revision.
1343
1424
  const tree = treeRevisionAfter(ws, cell);
1344
1425
  out(ctx, `wrote ${r.bytes_written} bytes to ${r.path}${r.sha256 ? ` (sha256 ${r.sha256})` : ''}${tree !== null ? `, tree revision ${tree}` : ''}\n`, tree !== null ? { ...r, tree_revision: tree } : r);
@@ -2272,12 +2353,18 @@ export const KEY_HANDLERS = {
2272
2353
  const scope = scopeFlag(values);
2273
2354
  const perms = permissionParams(values, scope === null ? 'project' : 'organization', 'create');
2274
2355
  const description = str(values.description);
2275
- const value = await readSecretValue(ctx, values);
2356
+ const value = await readSecretValue(ctx, values, name);
2276
2357
  const cloud = ctx.cloud();
2277
- const base = { name: name, value, ...(description !== undefined ? { description } : {}) };
2278
- const s = scope === null
2279
- ? await cloud.secrets.create((await principal(ctx)).project_id, { ...base, ...perms })
2280
- : await cloud.secrets.createOrganization(scope, { ...base, ...perms });
2358
+ const project = scope === null ? (await principal(ctx)).project_id : null;
2359
+ const create = (secret) => {
2360
+ const base = { name: name, value: secret, ...(description !== undefined ? { description } : {}) };
2361
+ return project !== null ? cloud.secrets.create(project, { ...base, ...perms }) : cloud.secrets.createOrganization(scope, { ...base, ...perms });
2362
+ };
2363
+ const s = value !== null ? await create(value) : await askSecretValue(ctx, values, {
2364
+ title: `Add ${name}`,
2365
+ purpose: `Saved to your ${project !== null ? 'project' : 'organization'}’s secrets as ${name}. Your coding agent never sees it.`,
2366
+ name: name,
2367
+ }, create);
2281
2368
  out(ctx, `Created ${s.scope} secret ${s.name} (${s.id}), version ${s.current_version}. The value is stored encrypted and is never shown.\n${secretDetail(s)}`, s);
2282
2369
  return 0;
2283
2370
  },
@@ -2326,9 +2413,14 @@ export const KEY_HANDLERS = {
2326
2413
  return 0;
2327
2414
  },
2328
2415
  async 'secrets rotate'(ctx, values, [ref]) {
2329
- const value = await readSecretValue(ctx, values);
2416
+ const value = await readSecretValue(ctx, values, ref);
2330
2417
  const s = await resolveSecret(ctx, values, ref);
2331
- const r = await ctx.cloud().secrets.rotate(s.id, value);
2418
+ const rotate = (secret) => ctx.cloud().secrets.rotate(s.id, secret);
2419
+ const r = value !== null ? await rotate(value) : await askSecretValue(ctx, values, {
2420
+ title: `New value for ${s.name}`,
2421
+ purpose: `Replaces ${s.name} in your ${s.scope === 'project' ? 'project' : 'organization'}’s secrets; older values are erased. Your coding agent never sees it.`,
2422
+ name: s.name,
2423
+ }, rotate);
2332
2424
  out(ctx, `Rotated ${r.secret.name} to version ${r.version.version}; older values were erased. Processes that already read the old value keep it.\n`, r);
2333
2425
  return 0;
2334
2426
  },
@@ -2382,6 +2474,27 @@ export const KEY_HANDLERS = {
2382
2474
  out(ctx, operationDetail(op), op);
2383
2475
  return 0;
2384
2476
  },
2477
+ async 'operations progress'(ctx, _values, [id]) {
2478
+ if (!UUID.test(id))
2479
+ throw new UsageError('<operation-id> must be a UUID');
2480
+ const progress = await ctx.cloud().workspaces.suspendProgress(id, { signal: ctx.signal });
2481
+ out(ctx, suspendProgressText(progress), { progress });
2482
+ return 0;
2483
+ },
2484
+ async 'operations cancel'(ctx, values, [id]) {
2485
+ if (!UUID.test(id))
2486
+ throw new UsageError('<operation-id> must be a UUID');
2487
+ if (values.wait === true) {
2488
+ const operation = await waited(ctx, `Suspension cancellation for ${id} continues server side.`, () => ctx.cloud().workspaces.cancelSuspend(id, { signal: ctx.signal, timeoutMs: waitTimeout(values) }));
2489
+ const progress = await ctx.cloud().workspaces.suspendProgress(id, { signal: ctx.signal });
2490
+ out(ctx, suspendProgressText(progress), { operation, progress });
2491
+ }
2492
+ else {
2493
+ const result = await ctx.cloud().workspaces.requestCancelSuspend(id, { signal: ctx.signal });
2494
+ out(ctx, suspendProgressText(result.progress), result);
2495
+ }
2496
+ return 0;
2497
+ },
2385
2498
  async 'operations wait'(ctx, values, [id]) {
2386
2499
  if (!UUID.test(id))
2387
2500
  throw new UsageError(`<operation-id> must be a UUID (got "${id}")`);
@@ -2474,12 +2587,26 @@ function permissionParams(values, scope, mode) {
2474
2587
  return out;
2475
2588
  }
2476
2589
  /**
2477
- * The secret value from --from-file or standard input (never from arguments): UTF-8 text, one trailing
2478
- * newline removed, 1..65536 bytes. Never echoed.
2590
+ * The secret value (never from arguments): --from-file, piped standard input, or a no-echo prompt on a terminal. Null
2591
+ * when the person types it on the page shard opens instead (0.20.1+): --ask, or a coding agent's shell with nothing
2592
+ * piped (a terminal there is not the person's).
2479
2593
  */
2480
- async function readSecretValue(ctx, values) {
2594
+ async function readSecretValue(ctx, values, name) {
2481
2595
  const file = str(values['from-file']);
2596
+ if (values.ask === true) {
2597
+ if (file !== undefined)
2598
+ throw new UsageError('use --ask or --from-file, not both');
2599
+ return null;
2600
+ }
2601
+ const agentShell = codingAgentShell(ctx.io.env);
2602
+ if (file === undefined && ctx.io.stdin.isTTY) {
2603
+ if (agentShell)
2604
+ return null;
2605
+ return secretText(await ctx.input.secret(`${name} (typing is hidden): `, 'the secret value', 'pipe the value in, or use --ask'));
2606
+ }
2482
2607
  const bytes = await readInput(ctx, file ?? '-', MAX_SECRET_VALUE_BYTES + 2, file === undefined ? 'secret value (standard input)' : '--from-file');
2608
+ if (file === undefined && bytes.length === 0 && agentShell)
2609
+ return null;
2483
2610
  let text;
2484
2611
  try {
2485
2612
  text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
@@ -2490,18 +2617,40 @@ async function readSecretValue(ctx, values) {
2490
2617
  finally {
2491
2618
  bytes.fill(0);
2492
2619
  }
2620
+ return secretText(text);
2621
+ }
2622
+ /** A secret value as stored: one trailing newline removed, 1..65536 UTF-8 bytes, no NUL. */
2623
+ function secretText(value) {
2624
+ let text = value;
2493
2625
  if (text.endsWith('\r\n'))
2494
2626
  text = text.slice(0, -2);
2495
2627
  else if (text.endsWith('\n'))
2496
2628
  text = text.slice(0, -1);
2497
2629
  if (text.length === 0)
2498
- throw new UsageError('the secret value is empty: pipe it on standard input or pass --from-file <file>');
2630
+ throw new UsageError('the secret value is empty: pipe it on standard input, pass --from-file <file> or use --ask');
2499
2631
  if (Buffer.byteLength(text, 'utf8') > MAX_SECRET_VALUE_BYTES)
2500
2632
  throw new UsageError(`the secret value exceeds ${MAX_SECRET_VALUE_BYTES} bytes`);
2501
2633
  if (text.includes('\u0000'))
2502
2634
  throw new UsageError('the secret value must not contain NUL characters');
2503
2635
  return text;
2504
2636
  }
2637
+ /** The secret's value from the page shard opens, handed straight to `save` (create or rotate); the page shows its outcome. */
2638
+ async function askSecretValue(ctx, values, page, save) {
2639
+ const asked = await askInBrowser(ctx, {
2640
+ kind: 'secret',
2641
+ title: page.title,
2642
+ purpose: page.purpose,
2643
+ label: 'Value',
2644
+ submit: 'Save',
2645
+ done: 'Saved',
2646
+ failed: 'Not saved',
2647
+ doing: `to type ${page.name}`,
2648
+ maxBytes: MAX_SECRET_VALUE_BYTES + 2,
2649
+ timeoutMs: askTimeout(values),
2650
+ use: (value) => save(secretText(value)),
2651
+ });
2652
+ return asked.result;
2653
+ }
2505
2654
  /** `<id|name>`: a UUID is tried as an id first; otherwise the live secret of that name in the scope (project, or --org). */
2506
2655
  async function resolveSecret(ctx, values, ref) {
2507
2656
  const cloud = ctx.cloud();
@@ -5,4 +5,9 @@ export declare function questionLine(c: {
5
5
  state: string;
6
6
  question?: string | null;
7
7
  }, max?: number): string | undefined;
8
+ /** A conversation's title as people read it: "New conversation" while it is still named by its generated id, before a
9
+ * prompt names it; a name someone chose stays (0.21.1+). The menu bar's AgentNaming.isRawIdentifier rule. */
10
+ export declare function displayTitle(c: {
11
+ title: string;
12
+ }): string;
8
13
  export declare const CONVERSATION_HANDLERS: Record<string, Handler>;
@@ -11,17 +11,20 @@ export function questionLine(c, max = 100) {
11
11
  const text = c.state === 'needs_you' ? c.question?.replace(/\s+/g, ' ').trim() : undefined;
12
12
  return !text ? undefined : text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text;
13
13
  }
14
+ /** A conversation's title as people read it: "New conversation" while it is still named by its generated id, before a
15
+ * prompt names it; a name someone chose stays (0.21.1+). The menu bar's AgentNaming.isRawIdentifier rule. */
16
+ export function displayTitle(c) { return !c.title || /^(conv[-_])?[a-z0-9]{26}$/.test(c.title) ? 'New conversation' : c.title; }
14
17
  export const CONVERSATION_HANDLERS = {
15
- async 'conversations resume'(ctx, _values, [id]) { const r = await conversationClient(ctx).conversations.resume(id, { signal: ctx.signal }); out(ctx, `${r.conversation.title}: resumed.\n`, r); return 0; },
18
+ async 'conversations resume'(ctx, _values, [id]) { const r = await conversationClient(ctx).conversations.resume(id, { signal: ctx.signal }); out(ctx, `${displayTitle(r.conversation)}: resumed.\n`, r); return 0; },
16
19
  async 'conversations reader'(ctx, _values, [id]) { const r = await conversationClient(ctx).conversations.spaceReader(id, { signal: ctx.signal }); out(ctx, `${r.workspace.workspace_key}\n`, r); return 0; },
17
20
  async 'conversations list'(ctx, values) {
18
21
  const state = str(values.state);
19
22
  const page = await conversationClient(ctx).conversations.list({ projectId: str(values.project), state, parentId: str(values.parent), cursor: str(values.cursor), limit: values.limit ? Number(values.limit) : undefined });
20
23
  // A conversation that needs you adds a fifth column: why (0.18.0+).
21
- out(ctx, page.data.map(c => { const q = questionLine(c); return `${c.id}\t${c.title}\t${c.state}\t${c.branch}${q ? `\t${q}` : ''}`; }).join('\n') + '\n', page);
24
+ out(ctx, page.data.map(c => { const q = questionLine(c); return `${c.id}\t${displayTitle(c)}\t${c.state}\t${c.branch}${q ? `\t${q}` : ''}`; }).join('\n') + '\n', page);
22
25
  return 0;
23
26
  },
24
- async 'conversations get'(ctx, _values, [id]) { const c = await conversationClient(ctx).conversations.get(id); const q = questionLine(c, 200); out(ctx, `${c.title}\n${c.state}\n${c.branch}\n${q ? `${q}\n` : ''}`, c); return 0; },
27
+ async 'conversations get'(ctx, _values, [id]) { const c = await conversationClient(ctx).conversations.get(id); const q = questionLine(c, 200); out(ctx, `${displayTitle(c)}\n${c.state}\n${c.branch}\n${q ? `${q}\n` : ''}`, c); return 0; },
25
28
  // A finished conversation whose VM was removed restores into a fresh one, then takes the message (APIs that do; 0.18.1+).
26
29
  async 'conversations send'(ctx, _values, [id, text]) {
27
30
  const r = await conversationClient(ctx).conversations.send(id, text, { signal: ctx.signal });
@@ -34,14 +37,14 @@ export const CONVERSATION_HANDLERS = {
34
37
  throw new UsageError('--timeout must be 0..900 seconds.');
35
38
  const until = str(values.until)?.split(',');
36
39
  const r = await conversationClient(ctx).conversations.wait(id, { until, timeoutSeconds: seconds, signal: ctx.signal });
37
- out(ctx, 'timeout' in r ? 'Still working.\n' : `${r.conversation.title}: ${r.event.kind}\n${r.event.question ?? r.event.result?.summary ?? ''}\n`, r);
40
+ out(ctx, 'timeout' in r ? 'Still working.\n' : `${displayTitle(r.conversation)}: ${r.event.kind}\n${r.event.question ?? r.event.result?.summary ?? ''}\n`, r);
38
41
  return 0;
39
42
  },
40
43
  async 'conversations land'(ctx, values, [id, action]) {
41
44
  if (!['merge', 'pr', 'keep', 'discard'].includes(action))
42
45
  throw new UsageError('Choose merge, pr, keep or discard.');
43
46
  const r = await conversationClient(ctx).conversations.land(id, action, { into: str(values.into), signal: ctx.signal });
44
- out(ctx, `${r.title}: ${r.landed.how}\n`, r);
47
+ out(ctx, `${displayTitle(r)}: ${r.landed.how}\n`, r);
45
48
  return 0;
46
49
  },
47
50
  async 'files pull'(ctx, _values, [conversation, path]) {
@@ -2,8 +2,13 @@ import type { Workspace } from '@shardflux/sdk';
2
2
  import type { Ctx, Handler } from './commands.js';
3
3
  import type { Values } from './args.js';
4
4
  import type { Conversation } from './conversations.js';
5
- /** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
6
- export declare function conversationTerminal(ctx: Ctx, ws: Workspace, c: Conversation, project: string, values: Values, afterStart?: () => Promise<void> | undefined): Promise<number>;
5
+ import type { ConversationSetup } from './project-template-setup.js';
6
+ /**
7
+ * Every terminal entry point uses the durable conversation's native harness, worktree and session. A new conversation
8
+ * in a repository without a project template for its dependencies sets its worktree up once its agent has started
9
+ * (`setup`, 0.21.1+).
10
+ */
11
+ export declare function conversationTerminal(ctx: Ctx, ws: Workspace, c: Conversation, project: string, values: Values, afterStart?: () => Promise<void> | undefined, setup?: ConversationSetup): Promise<number>;
7
12
  /**
8
13
  * A conversation and its VM, for an app to open (Claude Desktop, claude.ai, T3 Code, the menu bar). A conversation
9
14
  * whose VM was removed after it finished restores first (0.18.1+): a fresh VM from its saved branch and transcript, the
@@ -17,7 +22,10 @@ export declare function reopenConversation(ctx: Ctx, c: Conversation, values: Va
17
22
  /**
18
23
  * `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
19
24
  * Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
25
+ * From a shell nobody types into (a coding agent's, or none) a terminal conversation opens in a new tab of the
26
+ * person's terminal app (0.20.1+, agents targets --terminal).
20
27
  */
21
28
  export declare const launchProjectCommand: Handler;
29
+ export declare function forwardedFlags(values: Values): string[];
22
30
  /** Select a durable conversation, then use the harness's native terminal for all interaction. */
23
31
  export declare const launchConversationProject: Handler;
@@ -1,4 +1,6 @@
1
1
  import { linuxTerminalSession } from "./linux-terminal.js";
2
+ import { resolve } from 'node:path';
3
+ import { attachable, handedToTerminal, openShardInTerminal, reportTerminalOpen } from "./terminal-apps.js";
2
4
  import { randomUUID } from 'node:crypto';
3
5
  import { AgentStateStore } from "./agent-state.js";
4
6
  import { projectIdentity, projectLabels } from "./project-identity.js";
@@ -7,11 +9,12 @@ import { modelChoice } from "./agent-models.js";
7
9
  import { terminal } from "./agent-commands.js";
8
10
  import { cloudClaudeLogin, loginClaudeInWorkspace, requireClaudeTerminal } from "./claude-cloud-login.js";
9
11
  import { syncAgentCredentials } from "./agent-credentials.js";
10
- import { prepareProjectTemplate } from "./project-template-setup.js";
12
+ import { labelConversationSetup, prepareProjectTemplate, readConversationSetup } from "./project-template-setup.js";
11
13
  import { projectWorkspacePath, seedAgentRepository } from "./agent-repository.js";
12
14
  import { syncAgentConfig, syncAgentConfigAfterStart } from "./agent-config-sync.js";
13
15
  import { savedMachine } from "./filesystem-state.js";
14
16
  import { ensureMenubar, MenubarManager } from "./menubar.js";
17
+ import { ensureProjectGit } from "./project-git.js";
15
18
  import { UsageError } from "./errors.js";
16
19
  import { localResumeTerminal } from "./conversation-local-resume.js";
17
20
  import { agentReachability } from "./agent-reachability.js";
@@ -24,16 +27,21 @@ import { openClaudeDesktopSession, remoteControlUrl } from "./agent-target-comma
24
27
  import { openUrl } from "./browser.js";
25
28
  import { out } from "./commands.js";
26
29
  import { progress } from "./progress.js";
30
+ import { seedPhase } from "./seed-phases.js";
27
31
  /** This computer's agent config, copied after the start; with a saved machine (which carries it) just the project memory. */
28
32
  const afterStartConfig = (ctx, ws, machine) => () => syncAgentConfigAfterStart(ctx, ws, machine ? { memoryOnly: true } : {});
29
- /** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
30
- export async function conversationTerminal(ctx, ws, c, project, values, afterStart) {
33
+ /**
34
+ * Every terminal entry point uses the durable conversation's native harness, worktree and session. A new conversation
35
+ * in a repository without a project template for its dependencies sets its worktree up once its agent has started
36
+ * (`setup`, 0.21.1+).
37
+ */
38
+ export async function conversationTerminal(ctx, ws, c, project, values, afterStart, setup) {
31
39
  if (c.route === 'codex-cloud')
32
40
  return openCodexConversation(ctx, c);
33
41
  await restoreConversationBinding(ctx, c, ws);
34
42
  // A thread on another conversation's computer has its own PTY there, never the VM's own (0.16.0+).
35
43
  const pty = ws.labels.shard_conversation && ws.labels.shard_conversation !== c.id ? conversationName(c) : undefined;
36
- return terminal(ctx, ws, c.harness, conversationName(c), project, values, undefined, c.worktree_path, { id: c.id, session: c.harness_session_id, title: c.title, ...(pty ? { pty } : {}), choice: recordedChoice(c), ...(afterStart ? { afterStart } : {}) });
44
+ return terminal(ctx, ws, c.harness, conversationName(c), project, values, setup?.hash, c.worktree_path, { id: c.id, session: c.harness_session_id, title: c.title, ...(pty ? { pty } : {}), choice: recordedChoice(c), ...(afterStart ? { afterStart } : {}), ...(setup ? { setupLabelled: setup.labelled } : {}) });
37
45
  }
38
46
  /**
39
47
  * A conversation and its VM, for an app to open (Claude Desktop, claude.ai, T3 Code, the menu bar). A conversation
@@ -58,20 +66,59 @@ export async function reopenConversation(ctx, c, values) {
58
66
  /**
59
67
  * `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
60
68
  * Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
69
+ * From a shell nobody types into (a coding agent's, or none) a terminal conversation opens in a new tab of the
70
+ * person's terminal app (0.20.1+, agents targets --terminal).
61
71
  */
62
72
  export const launchProjectCommand = async (ctx, values, [path = './', explicit]) => {
63
73
  const named = explicit ?? (values.claude === true ? 'claude' : values.codex === true ? 'codex' : undefined);
74
+ // A terminal this CLI opened for it runs it there, never in yet another one (0.23.0+).
75
+ const typing = attachable(ctx.io), here = typing || ctx.json || handedToTerminal(ctx.io.env);
64
76
  if (values.resume !== true && values.review !== true) {
65
77
  const project = projectIdentity(path);
66
78
  const app = named === 'claude' || named === 'codex' ? named : new AgentStateStore(ctx.store.dir).app(project.remote ?? project.folder);
67
- const where = process.platform === 'linux' && ctx.io.stdin.isTTY ? 'terminal' : openInChoices(ctx, project.slug)[app];
79
+ const where = process.platform === 'linux' && typing ? 'terminal' : openInChoices(ctx, project.slug)[app];
68
80
  if (where === 't3')
69
81
  return launchT3(ctx, values, path, app);
70
82
  if (values.on === undefined && app === 'claude' && (where === 'claude-desktop' || where === 'web'))
71
83
  return startConversation(ctx, values, [path, ...(explicit ? [explicit] : [])], where);
84
+ if (!here && where === 'terminal')
85
+ return projectInTerminalApp(ctx, values, path, named);
72
86
  }
87
+ else if (!here)
88
+ return projectInTerminalApp(ctx, values, path, named);
73
89
  return launchConversationProject(ctx, values, [path, ...(explicit ? [explicit] : [])]);
74
90
  };
91
+ /** Flags `shard ./` hands to the terminal tab that runs it; files become absolute paths (the tab starts in the project). */
92
+ const NOT_FORWARDED = new Set(['json', 'api-url', 'agent-label', 'wake', 'wake-timeout', 'help', 'version', 'timing', 'reachable', 'claude', 'codex']);
93
+ export function forwardedFlags(values) {
94
+ const flags = [];
95
+ for (const [key, value] of Object.entries(values)) {
96
+ if (NOT_FORWARDED.has(key) || value === undefined)
97
+ continue;
98
+ for (const v of Array.isArray(value) ? value : [value]) {
99
+ if (v === true)
100
+ flags.push(`--${key}`);
101
+ else if (v === false)
102
+ flags.push(`--no-${key}`);
103
+ else if (typeof v === 'string') {
104
+ if ((key === 'prompt-file' || key === 'image') && v === '-')
105
+ throw new UsageError(`--${key} - reads standard input, which a new terminal tab cannot: pass a file, or start it detached with --json.`);
106
+ flags.push(`--${key}`, key === 'prompt-file' || key === 'image' ? resolve(v) : v);
107
+ }
108
+ }
109
+ }
110
+ return flags;
111
+ }
112
+ /** `shard ./` with its flags in a new tab of the person's terminal app, in the project folder: the tab owns the start,
113
+ * any sign-in and the attach, as if the person typed it there. */
114
+ async function projectInTerminalApp(ctx, values, path, app) {
115
+ const result = await openShardInTerminal(ctx, ['./', ...(app ? [app] : []), ...forwardedFlags(values)], { cwd: resolve(path) });
116
+ if (!result.opened) {
117
+ throw new UsageError(`shard ./ opens Claude Code or Codex in a terminal, and none opened here${result.reason ? `: ${result.reason.replace(/\.$/, '')}` : ''}.`, 'ask your user to run shard ./ in their own terminal, or start the conversation detached: shard ./ --json --prompt "<task>" (shard ./ codex --json ... for Codex)');
118
+ }
119
+ reportTerminalOpen(ctx, result);
120
+ return 0;
121
+ }
75
122
  /** Select a durable conversation, then use the harness's native terminal for all interaction. */
76
123
  export const launchConversationProject = (ctx, values, args) => startConversation(ctx, values, args);
77
124
  async function startConversation(ctx, values, [path = './', explicit], openIn) {
@@ -114,17 +161,31 @@ async function startConversation(ctx, values, [path = './', explicit], openIn) {
114
161
  }
115
162
  if (app === 'codex' && !linuxTerminalSession(ctx, values))
116
163
  return launchCodexCloud(ctx, values, path, images);
117
- await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
118
- const signedIn = app === 'claude' ? await cloudClaudeLogin(ctx) : true;
119
- if (!signedIn)
120
- requireClaudeTerminal(ctx);
164
+ seedPhase('cli_start');
165
+ // Agent credentials and the Claude sign-in are checked beside the project's local work (its Git state and snapshot),
166
+ // and both pass before a VM is requested (0.21.1+).
167
+ const signIn = (async () => {
168
+ await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
169
+ const signed = app === 'claude' ? await cloudClaudeLogin(ctx) : true;
170
+ if (!signed)
171
+ requireClaudeTerminal(ctx);
172
+ seedPhase('claude_login');
173
+ return signed;
174
+ })();
175
+ signIn.catch(() => undefined);
176
+ // Whether this conversation sets the project up: hashed from the files it starts from (in a Git repository, as its VM
177
+ // reads them), and asked while its VM starts.
178
+ await ensureProjectGit(ctx, project.folder);
179
+ const setup = readConversationSetup(ctx, project);
180
+ seedPhase('project_git');
121
181
  let created;
122
182
  try {
123
- created = await createConversation(ctx, project.folder, app, 'terminal', undefined, choice);
183
+ created = await createConversation(ctx, project.folder, app, 'terminal', undefined, choice, undefined, setup, signIn);
124
184
  }
125
185
  catch (e) {
126
186
  if (!conversationsUnavailable(e))
127
187
  throw e;
188
+ const signedIn = await signIn;
128
189
  // Optional migration: preserve the native terminal on the old control plane, without reviving main sync.
129
190
  const name = `c-${randomUUID().slice(0, 12)}`;
130
191
  const prepared = await prepareProjectTemplate(ctx, project, undefined);
@@ -142,6 +203,9 @@ async function startConversation(ctx, values, [path = './', explicit], openIn) {
142
203
  return terminal(ctx, ws, app, name, project.remote ?? project.folder, values, prepared.setupHash);
143
204
  }
144
205
  const { conversation: c, workspace: ws } = created;
206
+ const signedIn = await signIn;
207
+ const setupStart = labelConversationSetup(ctx, ws, c.worktree_path, await setup);
208
+ seedPhase('setup_label');
145
209
  if (values.computer === true)
146
210
  await ws.setComputerUse(true);
147
211
  await uploadInitialImages(ws.cell(), images, ctx.signal);
@@ -152,17 +216,18 @@ async function startConversation(ctx, values, [path = './', explicit], openIn) {
152
216
  // Without a saved machine the laptop's Claude and Codex config is copied in, after the agent has its first prompt.
153
217
  const afterStart = afterStartConfig(ctx, ws, !!(await savedMachine(ctx))?.template);
154
218
  await ensureMenubar(ctx.store.dir, ctx.apiUrl, new MenubarManager({ home: ctx.io.env.HOME, signal: ctx.signal }));
219
+ seedPhase('menubar');
155
220
  if (openIn) {
156
221
  // The agent starts detached in its VM; the conversation then opens in Claude Desktop or claude.ai (Remote Control).
157
222
  const quiet = { ...ctx, io: { ...ctx.io, stdout: { write: () => true } } };
158
- await conversationTerminal(quiet, ws, c, project.remote ?? project.folder, { ...values, detach: true }, afterStart);
223
+ await conversationTerminal(quiet, ws, c, project.remote ?? project.folder, { ...values, detach: true }, afterStart, setupStart);
159
224
  if (openIn === 'claude-desktop')
160
225
  return openClaudeDesktopSession(ctx, ws, c);
161
226
  const url = await remoteControlUrl(ctx, ws, c);
162
- if (!ctx.json && !openUrl(url, ctx.io.env))
227
+ if (!ctx.json && !(await openUrl(url, ctx.io.env)))
163
228
  throw new Error(`could not open ${url}`);
164
229
  out(ctx, `${url}\n`, { target: 'web', workspace: ws.data, conversation_id: c.id, url });
165
230
  return 0;
166
231
  }
167
- return conversationTerminal(ctx, ws, c, project.remote ?? project.folder, values, afterStart);
232
+ return conversationTerminal(ctx, ws, c, project.remote ?? project.folder, values, afterStart, setupStart);
168
233
  }