@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.
- package/CHANGELOG.md +312 -1
- package/README.md +242 -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-journal.js +58 -6
- package/dist/acp-login.d.ts +13 -5
- package/dist/acp-login.js +37 -8
- package/dist/acp-models.d.ts +6 -1
- package/dist/acp-models.js +29 -19
- package/dist/acp.js +120 -14
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +110 -42
- 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.d.ts +20 -1
- package/dist/agent-target-commands.js +73 -31
- package/dist/agent-targets.js +6 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +95 -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-hub.d.ts +61 -25
- package/dist/cli-proxy-hub.js +202 -54
- package/dist/cli-proxy.d.ts +44 -1
- package/dist/cli-proxy.js +177 -28
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +14 -4
- package/dist/codex-cloud.js +135 -31
- package/dist/codex-linux.d.ts +14 -0
- package/dist/codex-linux.js +69 -0
- package/dist/command-center-actions.js +18 -5
- package/dist/command-center-model.d.ts +2 -0
- package/dist/command-center-model.js +6 -1
- package/dist/command-center.js +3 -3
- package/dist/commands.js +173 -24
- 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 +78 -13
- package/dist/conversations.d.ts +86 -1
- package/dist/conversations.js +306 -23
- package/dist/desktop-linux.js +16 -3
- 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 +29 -2
- package/dist/errors.js +50 -7
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +133 -57
- 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/menubar.d.ts +6 -5
- package/dist/menubar.js +38 -28
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +69 -0
- package/dist/move-worktrees.js +252 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +111 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +63 -29
- package/dist/prepared-repository.d.ts +204 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1307 -0
- package/dist/project-preparation.d.ts +62 -0
- package/dist/project-preparation.js +316 -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 +22 -4
- package/dist/project-sync.js +34 -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.d.ts +2 -1
- package/dist/t3-launch.js +12 -6
- 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/t3-proxy.d.ts +81 -0
- package/dist/t3-proxy.js +325 -0
- package/dist/terminal-apps.d.ts +168 -0
- package/dist/terminal-apps.js +574 -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.d.ts +2 -0
- package/dist/update.js +43 -9
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- 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
|
-
|
|
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
|
},
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -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
|
-
/**
|
|
30
|
-
|
|
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,
|
|
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' &&
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
}
|