@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.
- package/CHANGELOG.md +214 -1
- package/README.md +215 -75
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +19 -15
- package/dist/account-pool.js +47 -96
- package/dist/account-scope.d.ts +65 -0
- package/dist/account-scope.js +246 -0
- package/dist/acp-login.d.ts +4 -3
- package/dist/acp-login.js +7 -5
- package/dist/acp-models.js +20 -15
- package/dist/acp.js +66 -13
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +86 -32
- package/dist/agent-config-return.d.ts +46 -2
- package/dist/agent-config-return.js +110 -4
- package/dist/agent-credentials.js +7 -3
- package/dist/agent-repository.d.ts +16 -3
- package/dist/agent-repository.js +149 -74
- package/dist/agent-setup.d.ts +14 -3
- package/dist/agent-setup.js +78 -36
- package/dist/agent-target-commands.js +24 -13
- package/dist/args.d.ts +1 -1
- package/dist/args.js +94 -54
- package/dist/bin.js +5 -1
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -14
- package/dist/claude-cloud-login.d.ts +24 -2
- package/dist/claude-cloud-login.js +151 -11
- package/dist/cli-proxy-forward.d.ts +2 -0
- package/dist/cli-proxy-forward.js +9 -2
- package/dist/cli-proxy-hub.d.ts +69 -25
- package/dist/cli-proxy-hub.js +231 -54
- package/dist/cli-proxy.d.ts +70 -1
- package/dist/cli-proxy.js +298 -32
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +5 -0
- package/dist/codex-cloud.js +88 -17
- package/dist/command-center-actions.js +5 -1
- package/dist/commands.js +171 -22
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +8 -5
- package/dist/conversation-terminal.d.ts +10 -2
- package/dist/conversation-terminal.js +77 -13
- package/dist/conversations.d.ts +73 -1
- package/dist/conversations.js +257 -23
- package/dist/device-bridge.d.ts +1 -1
- package/dist/device-bridge.js +6 -4
- package/dist/device-helper.js +43 -22
- package/dist/errors.d.ts +27 -1
- package/dist/errors.js +45 -5
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +108 -54
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +3 -1
- package/dist/initial-prompt.d.ts +8 -0
- package/dist/initial-prompt.js +43 -1
- package/dist/large-media.d.ts +14 -0
- package/dist/large-media.js +100 -0
- package/dist/linux-terminal.d.ts +5 -3
- package/dist/linux-terminal.js +6 -20
- package/dist/local-sessions.d.ts +26 -2
- package/dist/local-sessions.js +113 -24
- package/dist/machine-setup.d.ts +11 -1
- package/dist/machine-setup.js +69 -22
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +11 -13
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +63 -0
- package/dist/move-worktrees.js +201 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +116 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +59 -27
- package/dist/prepared-repository.d.ts +202 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1306 -0
- package/dist/project-preparation.d.ts +48 -0
- package/dist/project-preparation.js +304 -0
- package/dist/project-sync-manifest.d.ts +4 -0
- package/dist/project-sync-manifest.js +5 -1
- package/dist/project-sync-transfer.js +1 -1
- package/dist/project-sync.d.ts +19 -4
- package/dist/project-sync.js +31 -8
- package/dist/project-template-setup.d.ts +33 -1
- package/dist/project-template-setup.js +59 -2
- package/dist/secret-ask.d.ts +72 -0
- package/dist/secret-ask.js +351 -0
- package/dist/seed-phases.d.ts +8 -1
- package/dist/seed-phases.js +17 -2
- package/dist/setup-cache.d.ts +7 -0
- package/dist/setup-cache.js +32 -0
- package/dist/t3-launch.js +1 -4
- package/dist/t3-mode.d.ts +8 -2
- package/dist/t3-mode.js +19 -4
- package/dist/t3-phone.d.ts +4 -2
- package/dist/t3-phone.js +12 -4
- package/dist/terminal-apps.d.ts +157 -0
- package/dist/terminal-apps.js +561 -0
- package/dist/terminal-onboarding.js +6 -1
- package/dist/this-computer.d.ts +11 -0
- package/dist/this-computer.js +36 -0
- package/dist/thread-on-computer.js +4 -1
- package/dist/update.js +11 -3
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
package/dist/codex-cloud.js
CHANGED
|
@@ -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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2278
|
-
const
|
|
2279
|
-
|
|
2280
|
-
|
|
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
|
|
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
|
|
2478
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
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;
|