@shardflux/cli 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +214 -1
  2. package/README.md +215 -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-login.d.ts +4 -3
  10. package/dist/acp-login.js +7 -5
  11. package/dist/acp-models.js +20 -15
  12. package/dist/acp.js +66 -13
  13. package/dist/agent-commands.d.ts +4 -1
  14. package/dist/agent-commands.js +86 -32
  15. package/dist/agent-config-return.d.ts +46 -2
  16. package/dist/agent-config-return.js +110 -4
  17. package/dist/agent-credentials.js +7 -3
  18. package/dist/agent-repository.d.ts +16 -3
  19. package/dist/agent-repository.js +149 -74
  20. package/dist/agent-setup.d.ts +14 -3
  21. package/dist/agent-setup.js +78 -36
  22. package/dist/agent-target-commands.js +24 -13
  23. package/dist/args.d.ts +1 -1
  24. package/dist/args.js +94 -54
  25. package/dist/bin.js +5 -1
  26. package/dist/browser.d.ts +13 -2
  27. package/dist/browser.js +30 -14
  28. package/dist/claude-cloud-login.d.ts +24 -2
  29. package/dist/claude-cloud-login.js +151 -11
  30. package/dist/cli-proxy-forward.d.ts +2 -0
  31. package/dist/cli-proxy-forward.js +9 -2
  32. package/dist/cli-proxy-hub.d.ts +69 -25
  33. package/dist/cli-proxy-hub.js +231 -54
  34. package/dist/cli-proxy.d.ts +70 -1
  35. package/dist/cli-proxy.js +298 -32
  36. package/dist/client-settings.d.ts +39 -0
  37. package/dist/client-settings.js +91 -0
  38. package/dist/codex-cloud.d.ts +5 -0
  39. package/dist/codex-cloud.js +88 -17
  40. package/dist/command-center-actions.js +5 -1
  41. package/dist/commands.js +171 -22
  42. package/dist/conversation-commands.d.ts +5 -0
  43. package/dist/conversation-commands.js +8 -5
  44. package/dist/conversation-terminal.d.ts +10 -2
  45. package/dist/conversation-terminal.js +77 -13
  46. package/dist/conversations.d.ts +73 -1
  47. package/dist/conversations.js +257 -23
  48. package/dist/device-bridge.d.ts +1 -1
  49. package/dist/device-bridge.js +6 -4
  50. package/dist/device-helper.js +43 -22
  51. package/dist/errors.d.ts +27 -1
  52. package/dist/errors.js +45 -5
  53. package/dist/filesystem-commands.d.ts +16 -9
  54. package/dist/filesystem-commands.js +108 -54
  55. package/dist/format.d.ts +2 -1
  56. package/dist/format.js +10 -1
  57. package/dist/handlers.js +3 -1
  58. package/dist/initial-prompt.d.ts +8 -0
  59. package/dist/initial-prompt.js +43 -1
  60. package/dist/large-media.d.ts +14 -0
  61. package/dist/large-media.js +100 -0
  62. package/dist/linux-terminal.d.ts +5 -3
  63. package/dist/linux-terminal.js +6 -20
  64. package/dist/local-sessions.d.ts +26 -2
  65. package/dist/local-sessions.js +113 -24
  66. package/dist/machine-setup.d.ts +11 -1
  67. package/dist/machine-setup.js +69 -22
  68. package/dist/machine-status.d.ts +67 -0
  69. package/dist/machine-status.js +140 -0
  70. package/dist/main.js +11 -13
  71. package/dist/move-worktrees-restore.py +144 -0
  72. package/dist/move-worktrees.d.ts +63 -0
  73. package/dist/move-worktrees.js +201 -0
  74. package/dist/move.d.ts +10 -1
  75. package/dist/move.js +116 -16
  76. package/dist/onboarding.d.ts +11 -7
  77. package/dist/onboarding.js +59 -27
  78. package/dist/prepared-repository.d.ts +202 -0
  79. package/dist/prepared-repository.js +860 -0
  80. package/dist/prepared-repository.py +1306 -0
  81. package/dist/project-preparation.d.ts +48 -0
  82. package/dist/project-preparation.js +304 -0
  83. package/dist/project-sync-manifest.d.ts +4 -0
  84. package/dist/project-sync-manifest.js +5 -1
  85. package/dist/project-sync-transfer.js +1 -1
  86. package/dist/project-sync.d.ts +19 -4
  87. package/dist/project-sync.js +31 -8
  88. package/dist/project-template-setup.d.ts +33 -1
  89. package/dist/project-template-setup.js +59 -2
  90. package/dist/secret-ask.d.ts +72 -0
  91. package/dist/secret-ask.js +351 -0
  92. package/dist/seed-phases.d.ts +8 -1
  93. package/dist/seed-phases.js +17 -2
  94. package/dist/setup-cache.d.ts +7 -0
  95. package/dist/setup-cache.js +32 -0
  96. package/dist/t3-launch.js +1 -4
  97. package/dist/t3-mode.d.ts +8 -2
  98. package/dist/t3-mode.js +19 -4
  99. package/dist/t3-phone.d.ts +4 -2
  100. package/dist/t3-phone.js +12 -4
  101. package/dist/terminal-apps.d.ts +157 -0
  102. package/dist/terminal-apps.js +561 -0
  103. package/dist/terminal-onboarding.js +6 -1
  104. package/dist/this-computer.d.ts +11 -0
  105. package/dist/this-computer.js +36 -0
  106. package/dist/thread-on-computer.js +4 -1
  107. package/dist/update.js +11 -3
  108. package/dist/worktree-twin.d.ts +10 -2
  109. package/dist/worktree-twin.js +27 -9
  110. package/package.json +5 -5
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { CodedError, UsageError } from "./errors.js";
3
3
  import { json } from "./format.js";
4
4
  import { syncAgentCredentials } from "./agent-credentials.js";
5
- import { createConversation, getConversation, recordedChoice, resumeConversation } from "./conversations.js";
5
+ import { createConversation, conversationWorkspace, getConversation, localConversations, recordedChoice, resumeConversation, seedConversation } from "./conversations.js";
6
6
  import { CHOICE_PENDING, choiceReport, hasChoice, modelChoice } from "./agent-models.js";
7
7
  import { AgentStateStore } from "./agent-state.js";
8
8
  import { ensureProjectGit } from "./project-git.js";
@@ -128,24 +128,68 @@ export async function launchCodexCloud(ctx, values, path, images = []) {
128
128
  await ensureProjectGit(ctx, project.folder);
129
129
  await gitRunner(project.folder, { ...process.env, ...ctx.io.env }, ctx.signal)(['rev-parse', 'HEAD']);
130
130
  await ensureCodexCloud(ctx);
131
- const created = await createConversation(ctx, path, 'codex', 'codex-cloud', typeof values.name === 'string' ? values.name : undefined, choice);
132
- const c = created.conversation;
133
- // --computer: the conversation VM's desktop is on before Codex starts, so the thread gets the computer tools.
134
- if (values.computer === true)
135
- await created.workspace.setComputerUse(true);
136
- // The thread's tools run in this VM, so its first message can name the images by their VM paths.
137
- await uploadInitialImages(created.workspace.cell(), images, ctx.signal);
138
- // Idempotency follows the durable conversation, including a lost response.
139
- await ctx.cloud().codex.admit(c.id, { idempotencyKey: `codex-${c.id}`, signal: ctx.signal });
140
- const task = await ctx.cloud().codex.waitTask(c.id, { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal });
141
- new AgentStateStore(ctx.store.dir).save(project.remote ?? project.folder, 'codex');
142
- const submitted = typeof values.initialPrompt === 'string';
143
- if (submitted)
144
- await submitCloudInitialPrompt(ctx, c, values.initialPrompt, created.workspace.key);
145
- await output(ctx, c, task, submitted, choice);
146
- return 0;
131
+ const patch = (id, update) => new AgentStateStore(ctx.store.dir).update(doc => {
132
+ const records = doc.conversations;
133
+ const r = records[id];
134
+ if (r?.cloud_start)
135
+ r.cloud_start = { ...r.cloud_start, ...update };
136
+ });
137
+ let release;
138
+ try {
139
+ const created = await createConversation(ctx, path, 'codex', 'codex-cloud', typeof values.name === 'string' ? values.name : undefined, choice, undefined, undefined, undefined, {
140
+ accepted(c) {
141
+ new AgentStateStore(ctx.store.dir).update(doc => {
142
+ const records = doc.conversations;
143
+ records[c.id].cloud_start = { ...(typeof values.initialPrompt === 'string' ? { prompt: values.initialPrompt } : {}), computer: values.computer === true, attachments_pending: images.length > 0, state: 'accepted', heartbeat_at: Date.now() };
144
+ });
145
+ release = carryStart(ctx, c.id);
146
+ },
147
+ async beforeSeed(ws, c) {
148
+ if (values.computer === true)
149
+ await ws.setComputerUse(true);
150
+ await uploadInitialImages(ws.cell(), images, ctx.signal);
151
+ patch(c.id, { attachments_pending: false });
152
+ },
153
+ });
154
+ const c = created.conversation;
155
+ // --computer: the conversation VM's desktop is on before Codex starts, so the thread gets the computer tools.
156
+ // The thread's tools run in this VM, so its first message can name the images by their VM paths.
157
+ // Idempotency follows the durable conversation, including a lost response.
158
+ await ctx.cloud().codex.admit(c.id, { idempotencyKey: `codex-${c.id}`, signal: ctx.signal });
159
+ const task = await ctx.cloud().codex.waitTask(c.id, { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal });
160
+ new AgentStateStore(ctx.store.dir).save(project.remote ?? project.folder, 'codex');
161
+ const submitted = typeof values.initialPrompt === 'string';
162
+ if (submitted)
163
+ await submitCloudInitialPrompt(ctx, c, values.initialPrompt, created.workspace.key);
164
+ patch(c.id, { state: 'active', submitted, prompt: undefined });
165
+ await output(ctx, c, task, submitted, choice);
166
+ return 0;
167
+ }
168
+ finally {
169
+ release?.();
170
+ }
171
+ }
172
+ /**
173
+ * While this process carries an accepted start it says so every 10 s (0.22.0+): the device helper resumes only a start
174
+ * nobody carries, never one a foreground command is still running.
175
+ */
176
+ function carryStart(ctx, id) {
177
+ const beat = () => new AgentStateStore(ctx.store.dir).update(doc => { const r = doc.conversations[id]; if (r?.cloud_start)
178
+ r.cloud_start.heartbeat_at = Date.now(); });
179
+ beat();
180
+ const timer = setInterval(beat, 10_000);
181
+ timer.unref?.();
182
+ return () => clearInterval(timer);
147
183
  }
184
+ /** A start whose carrying process said so in the last 45 s. */
185
+ export const startCarried = (start) => Date.now() - (start.heartbeat_at ?? 0) < 45_000;
148
186
  export async function openCodexConversation(ctx, c) {
187
+ const local = localConversations(ctx)[c.id];
188
+ if (local?.cloud_start && local.cloud_start.state !== 'active' && !c.ended_at) {
189
+ const task = await resumeCloudStartup(ctx, c);
190
+ await output(ctx, c, task, local.cloud_start.prompt !== undefined, recordedChoice(c));
191
+ return 0;
192
+ }
149
193
  if (c.state === 'done' || c.state === 'archived' || c.ended_at) {
150
194
  c = await resumeConversation(ctx, c);
151
195
  await ctx.cloud().codex.admit(c.id, { idempotencyKey: `codex-resume-${c.id}-${c.workspace_id}`, signal: ctx.signal });
@@ -162,6 +206,33 @@ export async function openCodexConversation(ctx, c) {
162
206
  await output(ctx, c, task);
163
207
  return 0;
164
208
  }
209
+ /** Continues the same accepted identity, never creates a replacement VM/chat. */
210
+ export async function resumeCloudStartup(ctx, c) {
211
+ const local = localConversations(ctx)[c.id], start = local?.cloud_start;
212
+ if (!start)
213
+ throw new UsageError('This conversation has no local accepted startup.');
214
+ if (start.attachments_pending)
215
+ throw new CodedError('initial_images_pending', 'This accepted conversation still needs its initial images. Resume from the launcher with the retained images.', undefined, { conversation_id: c.id, workspace_key: c.key });
216
+ const release = carryStart(ctx, c.id);
217
+ try {
218
+ const ws = await conversationWorkspace(ctx, c);
219
+ // The same seed the start began: its own captured revision (never a newer checkout), or the copy seed.
220
+ await seedConversation(ctx, c, ws, local.folder);
221
+ await ctx.cloud().codex.admit(c.id, { idempotencyKey: `codex-${c.id}`, signal: ctx.signal });
222
+ const task = await ctx.cloud().codex.waitTask(c.id, { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal });
223
+ if (start.prompt !== undefined && !start.submitted)
224
+ await submitCloudInitialPrompt(ctx, c, start.prompt, ws.key);
225
+ new AgentStateStore(ctx.store.dir).update(doc => {
226
+ const records = doc.conversations;
227
+ if (records[c.id]?.cloud_start)
228
+ records[c.id].cloud_start = { ...start, state: 'active', submitted: start.prompt !== undefined || start.submitted, prompt: undefined, error: undefined };
229
+ });
230
+ return task;
231
+ }
232
+ finally {
233
+ release();
234
+ }
235
+ }
165
236
  export const CODEX_CLOUD_HANDLERS = {
166
237
  'agents codex status': async (ctx) => { ctx.io.stdout.write(json(await ctx.cloud().codex.connector())); return 0; },
167
238
  'agents codex enable': async (ctx) => { ctx.io.stdout.write(json(await ensureCodexCloud(ctx))); return 0; },
@@ -69,7 +69,11 @@ export const CENTER_HANDLERS = {
69
69
  if (values.name !== undefined || !ref || values.path !== undefined) {
70
70
  return launchProject(ctx, { ...values, ...(ref && values.like === undefined ? { like: ref } : {}) }, [str(values.path) ?? './', ...(str(values.app) ? [str(values.app)] : [])]);
71
71
  }
72
- return agentAction(ctx, 'open', { ...values, in: 'terminal' }, [ref]);
72
+ // The agent's own terminal session: attached here, started detached with --json, or (without a terminal here) a
73
+ // tab of the person's terminal app, as agents open does (0.20.1+).
74
+ const next = { ...values };
75
+ delete next.in;
76
+ return agentAction(ctx, 'open', next, [ref]);
73
77
  },
74
78
  async 'agents fork'(ctx, values, [ref, name]) {
75
79
  if (!ref || !name || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(name))
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
  },
@@ -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;