@shardflux/cli 0.19.1 → 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 +278 -1
- package/README.md +225 -73
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +20 -15
- package/dist/account-pool.js +51 -97
- 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-files.d.ts +19 -0
- package/dist/agent-config-files.js +70 -1
- package/dist/agent-config-return.d.ts +194 -0
- package/dist/agent-config-return.js +724 -0
- package/dist/agent-config-sync.d.ts +10 -2
- package/dist/agent-config-sync.js +45 -51
- 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 +104 -55
- 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 +81 -15
- package/dist/conversations.d.ts +77 -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 +52 -4
- package/dist/errors.d.ts +34 -1
- package/dist/errors.js +56 -5
- package/dist/filesystem-commands.d.ts +22 -3
- package/dist/filesystem-commands.js +132 -16
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +4 -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 +190 -0
- package/dist/machine-setup.js +849 -0
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +23 -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 +13 -3
- package/dist/seed-phases.js +21 -3
- 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.d.ts +2 -0
- package/dist/terminal-onboarding.js +11 -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/device-bridge.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export declare class DeviceGrants {
|
|
|
63
63
|
allows(api: string, project: string, root: string, kind: string, outside: boolean): boolean;
|
|
64
64
|
save(api: string, project: string, root: string, kind: string): Promise<void>;
|
|
65
65
|
}
|
|
66
|
-
export declare function deviceSummary(r: DeviceRequest): string;
|
|
66
|
+
export declare function deviceSummary(r: DeviceRequest, platform?: NodeJS.Platform): string;
|
|
67
67
|
/** Structured local approval details; the summary remains for older menu bar and ACP clients. */
|
|
68
68
|
export declare function deviceApproval(r: DeviceRequest, c: Conversation, target: {
|
|
69
69
|
path: string;
|
package/dist/device-bridge.js
CHANGED
|
@@ -14,6 +14,7 @@ import { UsageError } from "./errors.js";
|
|
|
14
14
|
import { conversationClient } from "./conversation-commands.js";
|
|
15
15
|
import { withTargetLock } from "./target-lock.js";
|
|
16
16
|
import { checkConversationWorktree, rememberConversationWorktree } from "./conversation-worktree.js";
|
|
17
|
+
import { yourComputer } from "./this-computer.js";
|
|
17
18
|
const LIMIT = 256 << 20, OUTPUT = (192 << 10) - 128;
|
|
18
19
|
const inside = (root, target) => target === root || target.startsWith(root + sep);
|
|
19
20
|
const safeId = (value) => { if (!/^[a-zA-Z0-9_-]{1,96}$/.test(value))
|
|
@@ -67,14 +68,15 @@ export class DeviceGrants {
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
|
-
export function deviceSummary(r) {
|
|
71
|
+
export function deviceSummary(r, platform = process.platform) {
|
|
72
|
+
const yours = yourComputer(platform);
|
|
71
73
|
if (tryLocallyAction(r))
|
|
72
74
|
return tryLocallyAction(r) === 'stop' ? 'Stop this conversation’s local sync and ports.' : 'Try this conversation in an isolated local worktree with two-way sync and localhost ports.';
|
|
73
75
|
if (r.kind === 'exec')
|
|
74
|
-
return `Run on
|
|
76
|
+
return `Run on ${yours}: ${JSON.stringify(r.exec?.argv)} · directory ${r.exec?.cwd_relative ?? '.'}`;
|
|
75
77
|
if (r.files)
|
|
76
|
-
return `${r.kind === 'files_pull' ? 'Share with conversation' :
|
|
77
|
-
return `${r.kind === 'port_forward' ?
|
|
78
|
+
return `${r.kind === 'files_pull' ? 'Share with conversation' : `Save on ${yours}`}: ${r.files.local_path_relative} ↔ ${r.files.remote_path}`;
|
|
79
|
+
return `${r.kind === 'port_forward' ? `Open conversation preview on ${yours}` : `Connect conversation to ${yours}`}: ${r.port?.remote_port} ↔ localhost:${r.port?.local_port}`;
|
|
78
80
|
}
|
|
79
81
|
/** Structured local approval details; the summary remains for older menu bar and ACP clients. */
|
|
80
82
|
export function deviceApproval(r, c, target, folder) {
|
package/dist/device-helper.js
CHANGED
|
@@ -4,15 +4,17 @@ import { ConversationMirrorState, mirrorProjectConversations } from "./conversat
|
|
|
4
4
|
import { FilesMirrorState, mirrorFilesOnce } from "./files-mirror.js";
|
|
5
5
|
import { withTargetLock } from "./target-lock.js";
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import { conversationsUnavailable } from "./conversations.js";
|
|
8
|
-
import { localConversations } from "./conversations.js";
|
|
7
|
+
import { completeDeferredPreparation, conversationsUnavailable, localConversations } from "./conversations.js";
|
|
9
8
|
import { randomUUID } from 'node:crypto';
|
|
10
9
|
import { readFileSync } from 'node:fs';
|
|
11
10
|
import { writePrivateFile } from "./credentials.js";
|
|
12
11
|
import { conversationClient } from "./conversation-commands.js";
|
|
13
12
|
import { Readable } from 'node:stream';
|
|
14
13
|
import { runWorktreeTwin } from "./worktree-twin-engine.js";
|
|
14
|
+
import { AgentConfigReturnState, runAgentConfigLoop } from "./agent-config-return.js";
|
|
15
15
|
import { askLocalDeviceApproval } from "./device-approvals.js";
|
|
16
|
+
import { resumeCloudStartup, startCarried } from "./codex-cloud.js";
|
|
17
|
+
import { runProjectPreparation } from "./project-preparation.js";
|
|
16
18
|
/** The menu bar owns this helper and its stdin; VM data can request actions but cannot answer approvals. */
|
|
17
19
|
export async function runDeviceHelper(ctx, options = {}) {
|
|
18
20
|
const abort = new AbortController(), signal = AbortSignal.any([ctx.signal, abort.signal]);
|
|
@@ -55,7 +57,7 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
55
57
|
ctx.io.stdout.write(JSON.stringify({ type: 'approval_resolved', request_id: approval.request_id }) + '\n'); });
|
|
56
58
|
};
|
|
57
59
|
const working = new Map();
|
|
58
|
-
const mirrored = new ConversationMirrorState(), files = new FilesMirrorState();
|
|
60
|
+
const mirrored = new ConversationMirrorState(), files = new FilesMirrorState(), config = new AgentConfigReturnState();
|
|
59
61
|
const deviceFile = join(ctx.store.dir, 'device-id');
|
|
60
62
|
let deviceId;
|
|
61
63
|
try {
|
|
@@ -67,11 +69,53 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
67
69
|
}
|
|
68
70
|
const executor = new DeviceExecutor(ctx, ask, deviceId);
|
|
69
71
|
const report = (e) => { ctx.io.stderr.write(`Device bridge: ${e instanceof Error ? e.message : String(e)}\n`); };
|
|
70
|
-
let mirroring, twin;
|
|
72
|
+
let mirroring, twin, returning, preparing, projects;
|
|
71
73
|
try {
|
|
72
74
|
return await withTargetLock(join(ctx.store.dir, 'device-helper-lock'), async () => {
|
|
73
75
|
// Finished conversations land in their local branches (worktree-twin-engine.ts); its own loop and backoff.
|
|
74
76
|
twin = runWorktreeTwin(ctx, deviceId, signal).catch(report);
|
|
77
|
+
// Skills, memories and CLAUDE.md lines saved in the cloud come back on their own cadence (agent-config-return.ts):
|
|
78
|
+
// never behind a files-mirror pass, which takes minutes for an account with hundreds of conversations. Every 2 s
|
|
79
|
+
// while one of this user's conversations is active, backing off to 60 s when none is.
|
|
80
|
+
returning = runAgentConfigLoop(ctx, config, signal).catch(report);
|
|
81
|
+
projects = runProjectPreparation(ctx).catch(report);
|
|
82
|
+
// Starts outlive the foreground command (0.22.0+): an accepted Codex Cloud start resumes with its own revision
|
|
83
|
+
// and idempotent admission and message; large media that followed a first prompt land while the VM is awake.
|
|
84
|
+
preparing = (async () => {
|
|
85
|
+
const failures = new Map();
|
|
86
|
+
while (!signal.aborted) {
|
|
87
|
+
let deferred = false;
|
|
88
|
+
for (const local of Object.values(localConversations(ctx))) {
|
|
89
|
+
if (local.conversation.ended_at || (failures.get(local.conversation.id) ?? 0) > Date.now())
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
// A start a foreground command still carries is its own: resumed only once nobody does.
|
|
93
|
+
if (local.cloud_start && local.cloud_start.state !== 'active' && !local.cloud_start.attachments_pending) {
|
|
94
|
+
if (!startCarried(local.cloud_start))
|
|
95
|
+
await resumeCloudStartup(ctx, local.conversation);
|
|
96
|
+
}
|
|
97
|
+
else if (local.preparation?.state === 'deferred') {
|
|
98
|
+
// A parked VM is never woken for this: its media land the next time it is awake.
|
|
99
|
+
if (await completeDeferredPreparation(ctx, local.conversation))
|
|
100
|
+
failures.delete(local.conversation.id);
|
|
101
|
+
else {
|
|
102
|
+
deferred = true;
|
|
103
|
+
failures.set(local.conversation.id, Date.now() + 15_000);
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
failures.delete(local.conversation.id);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (signal.aborted)
|
|
111
|
+
break;
|
|
112
|
+
report(error);
|
|
113
|
+
failures.set(local.conversation.id, Date.now() + 60_000);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await delay(deferred ? 5_000 : 30_000, undefined, { signal }).catch(() => undefined);
|
|
117
|
+
}
|
|
118
|
+
})();
|
|
75
119
|
// A slow or failed transcript copy must not delay heartbeats, request polling or approvals.
|
|
76
120
|
mirroring = (async () => {
|
|
77
121
|
let failures = 0;
|
|
@@ -133,7 +177,11 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
133
177
|
await executor.close();
|
|
134
178
|
await Promise.all([...working.values()]);
|
|
135
179
|
await mirroring;
|
|
180
|
+
await returning;
|
|
181
|
+
await config.folding;
|
|
136
182
|
await twin;
|
|
183
|
+
await preparing;
|
|
184
|
+
await projects;
|
|
137
185
|
await input.catch(() => undefined);
|
|
138
186
|
}
|
|
139
187
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -58,9 +58,21 @@ export declare class TermsNotAcceptedError extends CodedError {
|
|
|
58
58
|
export declare class AuthConfigError extends HintedError {
|
|
59
59
|
name: string;
|
|
60
60
|
}
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* A workspace key that no workspace of this project has (or another name that resolves to nothing). `details` keeps the
|
|
63
|
+
* API's reason when shard rewords its 404 (0.20.1+: `filesystem_not_found` from shard template delete).
|
|
64
|
+
*/
|
|
62
65
|
export declare class NotFoundError extends HintedError {
|
|
63
66
|
name: string;
|
|
67
|
+
readonly details: Record<string, unknown> | undefined;
|
|
68
|
+
constructor(message: string, hint?: string, details?: Record<string, unknown>);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* `shard machine setup --from-this-computer` (0.20.0+) ran but did not save a machine: the agent failed, ran past its
|
|
72
|
+
* time box, or the save failed. Retryable: the same command runs it again (exit 6).
|
|
73
|
+
*/
|
|
74
|
+
export declare class MachineSetupError extends HintedError {
|
|
75
|
+
name: string;
|
|
64
76
|
}
|
|
65
77
|
/** `auth login` of a two-factor account without a code: the pending sign-in is saved; `shard auth mfa` completes it (exit 4). */
|
|
66
78
|
export declare class MfaPendingError extends HintedError {
|
|
@@ -140,6 +152,27 @@ export interface ErrorInfo {
|
|
|
140
152
|
*/
|
|
141
153
|
export declare function redact(text: string, ...secrets: (string | null | undefined)[]): string;
|
|
142
154
|
/** The `hint:` a CLI error carries itself, if any. */
|
|
155
|
+
/**
|
|
156
|
+
* After a save that failed (0.20.2+): the version in use and the source VM are as they were, so the same save can run
|
|
157
|
+
* again; the build's log says what happened.
|
|
158
|
+
*/
|
|
159
|
+
export declare function saveRecovery(level: 'machine' | 'template', inUse: number, buildId?: string, failure?: SaveFailure): string;
|
|
160
|
+
/** What a failed save reports about its build (SDK 0.26.0+ on an API that carries them). */
|
|
161
|
+
export interface SaveFailure {
|
|
162
|
+
code?: string | undefined;
|
|
163
|
+
retryable?: boolean | undefined;
|
|
164
|
+
details?: Record<string, unknown> | undefined;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A save larger than the plan's workspace disk (0.21.1+): resource_limit with details.limit template_org_bytes_max;
|
|
168
|
+
* `limit` is the plan's workspace disk and `actual` the version's size, in bytes.
|
|
169
|
+
*/
|
|
170
|
+
export declare function planDiskLimit(failure: SaveFailure | undefined): {
|
|
171
|
+
limit: number;
|
|
172
|
+
actual: number;
|
|
173
|
+
} | undefined;
|
|
174
|
+
/** Bytes as the plan names a disk: `2 GiB`, `2.5 GiB`. */
|
|
175
|
+
export declare const gib: (bytes: number) => string;
|
|
143
176
|
export declare function ownHint(err: unknown): string | undefined;
|
|
144
177
|
export declare function describeError(err: unknown): ErrorInfo;
|
|
145
178
|
/** Maps an error to the documented exit code. */
|
package/dist/errors.js
CHANGED
|
@@ -83,9 +83,24 @@ export class TermsNotAcceptedError extends CodedError {
|
|
|
83
83
|
export class AuthConfigError extends HintedError {
|
|
84
84
|
name = 'AuthConfigError';
|
|
85
85
|
}
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* A workspace key that no workspace of this project has (or another name that resolves to nothing). `details` keeps the
|
|
88
|
+
* API's reason when shard rewords its 404 (0.20.1+: `filesystem_not_found` from shard template delete).
|
|
89
|
+
*/
|
|
87
90
|
export class NotFoundError extends HintedError {
|
|
88
91
|
name = 'NotFoundError';
|
|
92
|
+
details;
|
|
93
|
+
constructor(message, hint, details) {
|
|
94
|
+
super(message, hint);
|
|
95
|
+
this.details = details;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `shard machine setup --from-this-computer` (0.20.0+) ran but did not save a machine: the agent failed, ran past its
|
|
100
|
+
* time box, or the save failed. Retryable: the same command runs it again (exit 6).
|
|
101
|
+
*/
|
|
102
|
+
export class MachineSetupError extends HintedError {
|
|
103
|
+
name = 'MachineSetupError';
|
|
89
104
|
}
|
|
90
105
|
/** `auth login` of a two-factor account without a code: the pending sign-in is saved; `shard auth mfa` completes it (exit 4). */
|
|
91
106
|
export class MfaPendingError extends HintedError {
|
|
@@ -168,6 +183,34 @@ export function redact(text, ...secrets) {
|
|
|
168
183
|
return out;
|
|
169
184
|
}
|
|
170
185
|
/** The `hint:` a CLI error carries itself, if any. */
|
|
186
|
+
/**
|
|
187
|
+
* After a save that failed (0.20.2+): the version in use and the source VM are as they were, so the same save can run
|
|
188
|
+
* again; the build's log says what happened.
|
|
189
|
+
*/
|
|
190
|
+
export function saveRecovery(level, inUse, buildId, failure) {
|
|
191
|
+
const kept = inUse > 0 ? `version ${inUse} stays in use and ` : '';
|
|
192
|
+
const vm = level === 'machine' ? 'machine' : 'project';
|
|
193
|
+
// Larger than the plan's workspace disk (0.21.1+): the same save fails again until the VM or the plan changes.
|
|
194
|
+
const disk = planDiskLimit(failure);
|
|
195
|
+
if (disk) {
|
|
196
|
+
const open = level === 'machine' ? ' (shard machine open)' : '';
|
|
197
|
+
return `${kept}your ${vm} VM is untouched; delete what it does not need there${open}, such as caches and build output, then run shard ${level} save --wait; or upgrade to a plan with more than ${gib(disk.limit)} of disk: shard billing plans`;
|
|
198
|
+
}
|
|
199
|
+
if (failure?.retryable === false)
|
|
200
|
+
return `${kept}your ${vm} VM is untouched; change what the message names in it, then run shard ${level} save --wait`;
|
|
201
|
+
const log = buildId ? `; build log: shard templates builds log ${buildId}` : '';
|
|
202
|
+
return `${kept}your ${vm} VM is untouched; run shard ${level} save --wait again (it saves from the same VM)${log}`;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* A save larger than the plan's workspace disk (0.21.1+): resource_limit with details.limit template_org_bytes_max;
|
|
206
|
+
* `limit` is the plan's workspace disk and `actual` the version's size, in bytes.
|
|
207
|
+
*/
|
|
208
|
+
export function planDiskLimit(failure) {
|
|
209
|
+
const d = failure?.details;
|
|
210
|
+
return failure?.code === 'resource_limit' && d?.limit === 'template_org_bytes_max' && typeof d.value === 'number' && typeof d.actual === 'number' ? { limit: d.value, actual: d.actual } : undefined;
|
|
211
|
+
}
|
|
212
|
+
/** Bytes as the plan names a disk: `2 GiB`, `2.5 GiB`. */
|
|
213
|
+
export const gib = (bytes) => `${Number((bytes / 2 ** 30).toFixed(2))} GiB`;
|
|
171
214
|
export function ownHint(err) {
|
|
172
215
|
return err instanceof HintedError ? err.hint : undefined;
|
|
173
216
|
}
|
|
@@ -253,10 +296,14 @@ export function describeError(err) {
|
|
|
253
296
|
if (err instanceof TemplateBuildTimeoutError)
|
|
254
297
|
return { code: 'timeout', message: `${err.message}; follow it with: shard templates builds get ${err.build.id}`, last_state: err.build.state };
|
|
255
298
|
// machine/template save --wait (0.13.3+): the save ended without its version, or is still building at the deadline.
|
|
256
|
-
|
|
257
|
-
|
|
299
|
+
// 0.20.2+: details.build_id, and a failure's recovery as `fix` (the hint line).
|
|
300
|
+
// 0.21.1+: the build's failure (error_code and its details, e.g. limit/value/actual) and whether the same save can
|
|
301
|
+
// succeed (retryable false: larger than the plan's workspace disk, or another refusal that repeats until the VM changes).
|
|
302
|
+
if (err instanceof FilesystemSaveFailedError) {
|
|
303
|
+
return { code: 'filesystem_save_failed', message: err.message, retryable: err.retryable, fix: saveRecovery(err.state.level, err.state.version, err.buildId, err), details: { ...(err.details ?? {}), ...(err.code ? { error_code: err.code } : {}), level: err.state.level, version: err.version, ...(err.buildId ? { build_id: err.buildId } : {}), ...(err.progress ? { last_progress: err.progress } : {}) } };
|
|
304
|
+
}
|
|
258
305
|
if (err instanceof FilesystemSaveTimeoutError)
|
|
259
|
-
return { code: 'timeout', message: `${err.message}; check with shard ${err.state.level}`, last_state: err.state.state, details: { level: err.state.level, version: err.version } };
|
|
306
|
+
return { code: 'timeout', message: `${err.message}; check with shard ${err.state.level}`, last_state: err.state.state, details: { level: err.state.level, version: err.version, ...(err.buildId ? { build_id: err.buildId } : {}), ...(err.progress ? { last_progress: err.progress } : {}) } };
|
|
260
307
|
if (err instanceof TemplateUploadError)
|
|
261
308
|
return { code: 'upload_failed', message: err.message, ...(err.status ? { status: err.status } : {}), details: { sha256: err.sha256, ...(err.code ? { storage_code: err.code } : {}) } };
|
|
262
309
|
if (err instanceof AuthConfigError)
|
|
@@ -264,7 +311,9 @@ export function describeError(err) {
|
|
|
264
311
|
if (err instanceof MfaPendingError)
|
|
265
312
|
return { code: 'mfa_required', message: err.message };
|
|
266
313
|
if (err instanceof NotFoundError)
|
|
267
|
-
return { code: 'not_found', message: err.message };
|
|
314
|
+
return { code: 'not_found', message: err.message, ...(err.details ? { details: err.details } : {}) };
|
|
315
|
+
if (err instanceof MachineSetupError)
|
|
316
|
+
return { code: 'machine_setup_failed', message: err.message, retryable: true };
|
|
268
317
|
if (err instanceof InterruptedError) {
|
|
269
318
|
return { code: 'interrupted', message: err.message, ...(err.operationId ? { operation_id: err.operationId } : {}), ...(err.executionId ? { execution_id: err.executionId } : {}) };
|
|
270
319
|
}
|
|
@@ -294,6 +343,8 @@ export function exitCodeFor(err, interrupted = false) {
|
|
|
294
343
|
return EXIT.operationFailed;
|
|
295
344
|
if (err instanceof NotFoundError)
|
|
296
345
|
return EXIT.notFound;
|
|
346
|
+
if (err instanceof MachineSetupError)
|
|
347
|
+
return EXIT.operationFailed;
|
|
297
348
|
if (err instanceof OperationTimeoutError || err instanceof CheckoutTimeoutError || isNamed(err, 'TimeoutError'))
|
|
298
349
|
return EXIT.timeout;
|
|
299
350
|
if (err instanceof OperationFailedError || err instanceof DurabilityLostError || err instanceof ExecutionFailedError || err instanceof CheckoutEndedError)
|
|
@@ -1,12 +1,31 @@
|
|
|
1
|
-
import type { AgentFilesystem, Workspace } from '@shardflux/sdk';
|
|
1
|
+
import type { AgentFilesystem, AgentFilesystemDeleted, Workspace } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx, Handler } from './commands.js';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* One line per status: what is in use and what is saving or failed, its platform base and setup cache. A project
|
|
5
|
+
* template unused for 30 days (rebase_deferred, 0.20.1+) follows the machine when the next conversation starts.
|
|
6
|
+
*/
|
|
4
7
|
export declare function filesystemLine(state: AgentFilesystem & {
|
|
5
8
|
cache_scope?: string;
|
|
6
9
|
}): string;
|
|
10
|
+
/** A build's place while it waits for one of the plan's build slots (API: TemplateBuild.queue). */
|
|
11
|
+
export interface BuildQueue {
|
|
12
|
+
position: number;
|
|
13
|
+
slots: number;
|
|
14
|
+
expected_wait_seconds: number;
|
|
15
|
+
}
|
|
16
|
+
/** The pending build's queue, if it waits for a slot; nothing when the API predates it or the build cannot be read. */
|
|
17
|
+
export declare function pendingQueue(ctx: Ctx, state: AgentFilesystem): Promise<BuildQueue | undefined>;
|
|
18
|
+
/** Within one phase of a waited save, a changed counter line is printed at most this often (0.20.2+). */
|
|
19
|
+
export declare const SAVE_PROGRESS_LINE_MS = 30000;
|
|
7
20
|
/** The save's one line about software under the home that went into the image (0.18.0+), as the in-VM shard says it. */
|
|
8
21
|
export declare function movedLine(state: AgentFilesystem, level: 'machine' | 'template'): string;
|
|
9
|
-
/**
|
|
22
|
+
/** `shard template delete` (0.20.1+): what went, in one line. A binding-only delete has no template. */
|
|
23
|
+
export declare function deletedLine(deleted: AgentFilesystemDeleted): string;
|
|
24
|
+
/**
|
|
25
|
+
* The VM `shard machine save` saves: the machine VM of the version in use, else the one the last save came from. A
|
|
26
|
+
* deleted one is not there (0.20.2+: the machine row offers Save again only while it is).
|
|
27
|
+
*/
|
|
28
|
+
export declare function machineSaveSource(ctx: Ctx, state: AgentFilesystem): Promise<Workspace | null>;
|
|
10
29
|
/** The account's machine VM, created or resumed; `sync: false` skips the first-open config sync (an account sign-in). */
|
|
11
30
|
export declare function openMachine(ctx: Ctx, state?: AgentFilesystem, opts?: {
|
|
12
31
|
sync?: boolean;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { FilesystemSaveFailedError, FilesystemSaveTimeoutError, SavePhases, ShardfluxApiError, savePreflightText, saveProgressText } from '@shardflux/sdk';
|
|
1
3
|
import { codingAgentShell } from "./agent-setup.js";
|
|
2
4
|
import { waitTimeout } from "./commands.js";
|
|
3
|
-
import { UsageError } from "./errors.js";
|
|
5
|
+
import { NotFoundError, UsageError } from "./errors.js";
|
|
4
6
|
import { projectIdentity, normalizeRemote } from "./project-identity.js";
|
|
5
7
|
import { AGENT_HANDLERS } from "./agent-commands.js";
|
|
6
8
|
import { readSetupProgress } from "./assisted-setup.js";
|
|
@@ -10,9 +12,13 @@ function remote() {
|
|
|
10
12
|
const project = projectIdentity('./');
|
|
11
13
|
return { project, repoRemote: project.remote ?? normalizeRemote(`file://${project.folder}`) };
|
|
12
14
|
}
|
|
13
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* One line per status: what is in use and what is saving or failed, its platform base and setup cache. A project
|
|
17
|
+
* template unused for 30 days (rebase_deferred, 0.20.1+) follows the machine when the next conversation starts.
|
|
18
|
+
*/
|
|
14
19
|
export function filesystemLine(state) {
|
|
15
|
-
const
|
|
20
|
+
const rebase = state.rebase_deferred ? ', follows your machine on next use' : state.rebase_pending ? ', rebase pending' : '';
|
|
21
|
+
const base = state.platform_base ? `; platform ${state.platform_base.slug}@${state.platform_base.version}${rebase}` : '';
|
|
16
22
|
return usageLine(state) + base + (state.cache_scope ? `; cache: ${state.cache_scope}` : '');
|
|
17
23
|
}
|
|
18
24
|
function usageLine(state) {
|
|
@@ -29,17 +35,87 @@ function output(ctx, state) {
|
|
|
29
35
|
ctx.io.stdout.write(ctx.json ? JSON.stringify(state) + '\n' : `${filesystemLine(state)}\n`);
|
|
30
36
|
return 0;
|
|
31
37
|
}
|
|
32
|
-
/**
|
|
33
|
-
async function
|
|
38
|
+
/** The pending build's queue, if it waits for a slot; nothing when the API predates it or the build cannot be read. */
|
|
39
|
+
export async function pendingQueue(ctx, state) {
|
|
40
|
+
if (!state.pending)
|
|
41
|
+
return undefined;
|
|
42
|
+
try {
|
|
43
|
+
const build = await ctx.cloud().templates.builds.get(state.organization_id, state.pending.build_id);
|
|
44
|
+
return build.queue ?? undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Within one phase of a waited save, a changed counter line is printed at most this often (0.20.2+). */
|
|
51
|
+
export const SAVE_PROGRESS_LINE_MS = 30_000;
|
|
52
|
+
/**
|
|
53
|
+
* Save output: the preflight line (what the save copies and about how long it takes, 0.20.2+), then one line naming
|
|
54
|
+
* the version being saved and its state (its place in the queue while the plan's build slots are in use, 0.20.0+), or
|
|
55
|
+
* with --wait one stderr line per phase with its counters, and the saved version. --json: one document at the end
|
|
56
|
+
* (`phases` with --wait, `preflight` when known), the progress as {"event":"save_progress"} lines on stderr.
|
|
57
|
+
*/
|
|
58
|
+
async function saved(ctx, values, save, preflight, level) {
|
|
34
59
|
if (values.wait !== true && values.timeout !== undefined)
|
|
35
60
|
throw new UsageError('--timeout needs --wait');
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
const pf = await preflight();
|
|
62
|
+
if (pf && !ctx.json)
|
|
63
|
+
progress(ctx, savePreflightText(pf));
|
|
64
|
+
else if (pf && values.wait === true)
|
|
65
|
+
ctx.io.stderr.write(`${JSON.stringify({ event: 'save_preflight', ...pf })}\n`);
|
|
66
|
+
const phases = new SavePhases();
|
|
67
|
+
// The first line names the version; later lines say what changed: a new phase or place in the queue at once, newer
|
|
68
|
+
// counters within a phase at most every SAVE_PROGRESS_LINE_MS.
|
|
69
|
+
let announced = false;
|
|
70
|
+
let last = { line: '', milestone: '', at: 0 };
|
|
71
|
+
const onProgress = (p, final = false) => {
|
|
72
|
+
phases.update(p);
|
|
73
|
+
const line = saveProgressText(p);
|
|
74
|
+
const milestone = JSON.stringify([p.state, p.progress?.phase ?? null, p.queue?.position ?? null, p.queue?.slots ?? null]);
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
if (announced && (line === last.line || (!final && milestone === last.milestone && now - last.at < SAVE_PROGRESS_LINE_MS)))
|
|
77
|
+
return;
|
|
78
|
+
if (ctx.json)
|
|
79
|
+
ctx.io.stderr.write(`${JSON.stringify({ event: 'save_progress', ...p })}\n`);
|
|
80
|
+
else
|
|
81
|
+
progress(ctx, announced ? line : `${level}: saving version ${p.version} · ${line}`);
|
|
82
|
+
announced = true;
|
|
83
|
+
last = { line, milestone, at: now };
|
|
84
|
+
};
|
|
85
|
+
const wait = values.wait === true ? { timeoutMs: waitTimeout(values, 600_000), onProgress, onChange: (state) => {
|
|
86
|
+
// An API that does not show the build: the version alone.
|
|
87
|
+
if (!announced && state.state === 'building' && state.pending) {
|
|
88
|
+
// The filesystem says saving; the unavailable build view cannot tell us queued versus running.
|
|
89
|
+
if (ctx.json)
|
|
90
|
+
ctx.io.stderr.write(`${JSON.stringify({ event: 'save_started', level, version: state.pending.version, build_id: state.pending.build_id })}\n`);
|
|
91
|
+
else
|
|
92
|
+
progress(ctx, `${level}: saving version ${state.pending.version}`);
|
|
93
|
+
announced = true;
|
|
94
|
+
}
|
|
95
|
+
} } : undefined;
|
|
96
|
+
const document = (state, extra = {}) => ({ ...state, ...extra, ...(wait ? { phases: phases.list() } : {}), ...(pf ? { preflight: pf } : {}) });
|
|
97
|
+
let state;
|
|
98
|
+
try {
|
|
99
|
+
state = await save({ signal: ctx.signal, ...(wait ? { wait } : {}) });
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if ((err instanceof FilesystemSaveTimeoutError || err instanceof FilesystemSaveFailedError) && err.progress)
|
|
103
|
+
onProgress(err.progress, true);
|
|
104
|
+
// --json: stdout still gets its one document, with how far the save got (still building at a timeout).
|
|
105
|
+
if (ctx.json && (err instanceof FilesystemSaveTimeoutError || err instanceof FilesystemSaveFailedError)) {
|
|
106
|
+
if (err instanceof FilesystemSaveFailedError)
|
|
107
|
+
phases.end();
|
|
108
|
+
ctx.io.stdout.write(`${JSON.stringify(document(err.state))}\n`);
|
|
109
|
+
}
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
phases.end();
|
|
113
|
+
const queue = wait ? undefined : await pendingQueue(ctx, state);
|
|
39
114
|
if (ctx.json)
|
|
40
|
-
return output(ctx, state);
|
|
115
|
+
return output(ctx, document(state, queue ? { queue } : {}));
|
|
41
116
|
const moved = movedLine(state, level);
|
|
42
|
-
|
|
117
|
+
const queued = queue ? `; ${saveProgressText({ version: state.pending?.version ?? 0, build_id: state.pending?.build_id ?? '', state: 'queued', queue })}` : '';
|
|
118
|
+
ctx.io.stdout.write(wait ? `${level}: version ${state.version} saved${moved}\n` : state.pending ? `${filesystemLine(state)}${queued}; shard ${level} shows when it is ready${moved}\n` : `${filesystemLine(state)}${moved}\n`);
|
|
43
119
|
return 0;
|
|
44
120
|
}
|
|
45
121
|
/** The save's one line about software under the home that went into the image (0.18.0+), as the in-VM shard says it. */
|
|
@@ -49,6 +125,12 @@ export function movedLine(state, level) {
|
|
|
49
125
|
return '';
|
|
50
126
|
return `; ${moved.length} large tool folder${moved.length === 1 ? '' : 's'} went into the ${level} image (${moved.join(', ')}); they keep working at the same paths`;
|
|
51
127
|
}
|
|
128
|
+
/** `shard template delete` (0.20.1+): what went, in one line. A binding-only delete has no template. */
|
|
129
|
+
export function deletedLine(deleted) {
|
|
130
|
+
const versions = deleted.template ? ` (${deleted.template.versions} saved version${deleted.template.versions === 1 ? '' : 's'})` : '';
|
|
131
|
+
const builds = deleted.canceled_builds > 0 ? `, stopped ${deleted.canceled_builds} build${deleted.canceled_builds === 1 ? '' : 's'}` : '';
|
|
132
|
+
return `template: deleted${versions}${builds}; new conversations start from your machine until setup saves a new one`;
|
|
133
|
+
}
|
|
52
134
|
const rollbackTo = (value) => {
|
|
53
135
|
if (value === undefined)
|
|
54
136
|
return undefined;
|
|
@@ -57,7 +139,17 @@ const rollbackTo = (value) => {
|
|
|
57
139
|
throw new UsageError('--to must be a saved version number');
|
|
58
140
|
return n;
|
|
59
141
|
};
|
|
60
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* The VM `shard machine save` saves: the machine VM of the version in use, else the one the last save came from. A
|
|
144
|
+
* deleted one is not there (0.20.2+: the machine row offers Save again only while it is).
|
|
145
|
+
*/
|
|
146
|
+
export async function machineSaveSource(ctx, state) {
|
|
147
|
+
const cloud = ctx.cloud();
|
|
148
|
+
const ws = await cloud.workspaces.findByKey(`machine/${state.organization_id}/v${state.version}`, { includeDeleted: false, signal: ctx.signal }) ??
|
|
149
|
+
(state.workspace_id ? await cloud.workspaces.get(state.workspace_id).catch((e) => { if (e instanceof ShardfluxApiError && e.status === 404)
|
|
150
|
+
return null; throw e; }) : null);
|
|
151
|
+
return ws && !ws.data.deleted_at ? ws : null;
|
|
152
|
+
}
|
|
61
153
|
/** The account's machine VM, created or resumed; `sync: false` skips the first-open config sync (an account sign-in). */
|
|
62
154
|
export async function openMachine(ctx, state, opts = {}) {
|
|
63
155
|
const cloud = ctx.cloud();
|
|
@@ -99,12 +191,10 @@ export const FILESYSTEM_HANDLERS = {
|
|
|
99
191
|
},
|
|
100
192
|
async 'machine save'(ctx, values) {
|
|
101
193
|
const cloud = ctx.cloud();
|
|
102
|
-
const
|
|
103
|
-
const ws = await cloud.workspaces.findByKey(`machine/${state.organization_id}/v${state.version}`, { signal: ctx.signal }) ??
|
|
104
|
-
(state.workspace_id ? await cloud.workspaces.get(state.workspace_id) : null);
|
|
194
|
+
const ws = await machineSaveSource(ctx, await cloud.machine.get({ signal: ctx.signal }));
|
|
105
195
|
if (!ws)
|
|
106
196
|
throw new UsageError('Open your machine and change it first, then run shard machine save', 'shard machine in a terminal, or shard machine open --json for an agent (0.17.1+)');
|
|
107
|
-
return saved(ctx, values, options => cloud.machine.save({ workspaceId: ws.id }, options), 'machine');
|
|
197
|
+
return saved(ctx, values, options => cloud.machine.save({ workspaceId: ws.id }, options), () => cloud.machine.preflight({ workspaceId: ws.id }, { signal: ctx.signal }), 'machine');
|
|
108
198
|
},
|
|
109
199
|
async 'machine rollback'(ctx, values) {
|
|
110
200
|
return output(ctx, await ctx.cloud().machine.rollback({ to: rollbackTo(values.to) }, { signal: ctx.signal }));
|
|
@@ -123,9 +213,35 @@ export const FILESYSTEM_HANDLERS = {
|
|
|
123
213
|
const ws = await ctx.cloud().workspaces.findByKey(`${project.slug}/main`, { signal: ctx.signal });
|
|
124
214
|
if (!ws)
|
|
125
215
|
throw new UsageError('Run shard ./ for this project before saving its template');
|
|
126
|
-
return saved(ctx, values, options => ctx.cloud().templates.saveProject({ repoRemote, workspaceId: ws.id }, options), 'template');
|
|
216
|
+
return saved(ctx, values, options => ctx.cloud().templates.saveProject({ repoRemote, workspaceId: ws.id }, options), () => ctx.cloud().templates.preflightProject({ workspaceId: ws.id }, { signal: ctx.signal }), 'template');
|
|
127
217
|
},
|
|
128
218
|
async 'template rollback'(ctx, values) {
|
|
129
219
|
return output(ctx, await ctx.cloud().templates.rollbackProject({ repoRemote: remote().repoRemote, to: rollbackTo(values.to) }, { signal: ctx.signal }));
|
|
130
220
|
},
|
|
221
|
+
/** `shard template delete` (0.20.1+): asks first in a terminal; elsewhere it needs --yes. */
|
|
222
|
+
async 'template delete'(ctx, values) {
|
|
223
|
+
const { repoRemote } = remote();
|
|
224
|
+
if (values.yes !== true) {
|
|
225
|
+
if (!ctx.input.interactive)
|
|
226
|
+
throw new UsageError(`refusing to delete without --yes (the project template of ${repoRemote} and its saved versions are deleted; new conversations start from your machine until setup saves a new one)`, 'run: shard template delete --yes');
|
|
227
|
+
const answer = (await ctx.input.answer(`Delete the project template for ${repoRemote}? Its saved versions go; new conversations start from your machine until setup saves a new one. [y/N] `))?.trim() ?? '';
|
|
228
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
229
|
+
ctx.io.stderr.write('Kept the project template.\n');
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
let deleted;
|
|
234
|
+
try {
|
|
235
|
+
// One key per run: a retry after a dropped connection replays the same delete.
|
|
236
|
+
deleted = await ctx.cloud().templates.deleteProject({ repoRemote }, { idempotencyKey: randomUUID(), signal: ctx.signal });
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
if (err instanceof ShardfluxApiError && err.status === 404 && err.details?.reason === 'filesystem_not_found') {
|
|
240
|
+
throw new NotFoundError(`template: nothing saved yet for ${repoRemote}, so there is nothing to delete`, 'setup saves it after shard ./, or run shard template save', { reason: 'filesystem_not_found', repo_remote: repoRemote });
|
|
241
|
+
}
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
ctx.io.stdout.write(ctx.json ? JSON.stringify(deleted) + '\n' : `${deletedLine(deleted)}\n`);
|
|
245
|
+
return 0;
|
|
246
|
+
},
|
|
131
247
|
};
|
package/dist/format.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentSession, ApiKey, AuditEvent, AuthSessionInfo, BillingCatalog, BillingInvoicePage, BillingSubscription, DataExport, DraftState, ExecutionChange, ExecutionResult, FileInfo, EgressPolicyVersion, Invitation, Member, Organization, Project, ProjectEgressPolicy, Volume, VolumeAttachment, WorkspaceEgressPolicy, FileList, FileSearchResponse, Operation, Secret, SecretAccessEvent, SecretVersion, SpendPolicy, SuspendRequest, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, TemplateVersion, WorkspaceChange, WorkspaceSecretBindings, WorkspaceMemory, WorkspaceView, MemoryGrow, BurstSummary, ResizeResult, ExposePortResult, ExposedPort } from '@shardflux/sdk';
|
|
1
|
+
import type { AgentSession, ApiKey, AuditEvent, AuthSessionInfo, BillingCatalog, BillingInvoicePage, BillingSubscription, DataExport, DraftState, ExecutionChange, ExecutionResult, FileInfo, EgressPolicyVersion, Invitation, Member, Organization, Project, ProjectEgressPolicy, Volume, VolumeAttachment, WorkspaceEgressPolicy, FileList, FileSearchResponse, Operation, Secret, SecretAccessEvent, SecretVersion, SpendPolicy, SuspendRequest, SuspendProgress, TemplateBuild, TemplateDetail, TemplateDiffEntry, TemplateDraft, TemplateFileEntry, TemplateLanguages, TemplatePackagePage, TemplateSettings, TemplateSummary, TemplateVersion, WorkspaceChange, WorkspaceSecretBindings, WorkspaceMemory, WorkspaceView, MemoryGrow, BurstSummary, ResizeResult, ExposePortResult, ExposedPort } from '@shardflux/sdk';
|
|
2
2
|
import type { ErrorInfo } from './errors.js';
|
|
3
3
|
/** A workspace as the port commands name it. */
|
|
4
4
|
interface PortWorkspace {
|
|
@@ -70,6 +70,7 @@ export declare function operationTable(ops: readonly Operation[]): string;
|
|
|
70
70
|
*/
|
|
71
71
|
export declare function durableText(op: Operation): string;
|
|
72
72
|
export declare function operationDetail(op: Operation): string;
|
|
73
|
+
export declare function suspendProgressText(p: SuspendProgress): string;
|
|
73
74
|
export declare function sessionTable(sessions: readonly AgentSession[]): string;
|
|
74
75
|
/** `files stat`: one path's metadata; the revision only when requested and returned. */
|
|
75
76
|
export declare function fileInfoText(f: FileInfo): string;
|
package/dist/format.js
CHANGED
|
@@ -300,6 +300,15 @@ export function operationDetail(op) {
|
|
|
300
300
|
['updated', time(op.updated_at)],
|
|
301
301
|
['completed', time(op.completed_at)],
|
|
302
302
|
['error', err ? `${scalar(err.code)}${err.message ? `: ${scalar(err.message)}` : ''}` : '-'],
|
|
303
|
+
]) + (op.progress ? `\n${suspendProgressText(op.progress)}` : '');
|
|
304
|
+
}
|
|
305
|
+
export function suspendProgressText(p) {
|
|
306
|
+
return kv([
|
|
307
|
+
['operation', p.operation_id], ['phase', p.phase], ['operation state', p.state], ['workspace state', p.workspace_state],
|
|
308
|
+
['cancelable', p.cancelable ? 'yes' : 'no'], ['progress supported', p.supported ? 'yes' : 'unavailable'], ['next action', p.safe_action],
|
|
309
|
+
...(p.phase_started_at ? [['phase started', p.phase_started_at]] : []),
|
|
310
|
+
...(p.deadline_at ? [['host deadline', p.deadline_at]] : []),
|
|
311
|
+
...(p.accepted === true ? [['cancel request', 'accepted; use the operation state to confirm recovery']] : []),
|
|
303
312
|
]);
|
|
304
313
|
}
|
|
305
314
|
export function sessionTable(sessions) {
|
|
@@ -546,7 +555,7 @@ export function feedbackCommand(e) {
|
|
|
546
555
|
if (SHELL_SAFE.test(code))
|
|
547
556
|
parts.push(`--error-code ${code}`);
|
|
548
557
|
parts.push('"<what you expected>"');
|
|
549
|
-
return parts.join(' ');
|
|
558
|
+
return `${parts.join(' ')} # preview only; show the message and context, then use --yes only after explicit user authorization`;
|
|
550
559
|
}
|
|
551
560
|
/**
|
|
552
561
|
* One stderr line: `error: <message> [code=... status=... request_id=...]` plus a hint when there is one, and the
|
package/dist/handlers.js
CHANGED
|
@@ -23,4 +23,7 @@ import { CLI_PROXY_HANDLERS } from "./cli-proxy.js";
|
|
|
23
23
|
import { ACCOUNT_POOL_HANDLERS } from "./account-pool.js";
|
|
24
24
|
import { MOVE_HANDLERS } from "./move.js";
|
|
25
25
|
import { AGENT_CONFIG_HANDLERS } from "./agent-config-sync.js";
|
|
26
|
-
|
|
26
|
+
import { MACHINE_SETUP_HANDLERS } from "./machine-setup.js";
|
|
27
|
+
import { SETTINGS_HANDLERS } from "./client-settings.js";
|
|
28
|
+
import { PROJECT_PREPARATION_HANDLERS } from "./project-preparation.js";
|
|
29
|
+
export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...MACHINE_SETUP_HANDLERS, ...SETTINGS_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS };
|
package/dist/initial-prompt.d.ts
CHANGED
|
@@ -30,6 +30,14 @@ export declare const INBOX_MAX_CHARS = 65536;
|
|
|
30
30
|
* options. The guest's own first-message delivery reads the screen the same way (coding 39+).
|
|
31
31
|
*/
|
|
32
32
|
export declare function inputBoxReady(tail: string, glyph: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The first prompt typed by the VM's own guest (0.21.1+, with a guest that has it): `sf-agent conversation first-prompt` waits for
|
|
35
|
+
* the input box, pastes, waits for the paste to show and presses Enter inside the VM, with the same checks as
|
|
36
|
+
* `submitInitialPrompt`, in one request from here instead of an attachment and three round trips. Nothing was typed
|
|
37
|
+
* when the result is `unsupported` (an older guest without the command: this terminal types it) or `not_ready` (the
|
|
38
|
+
* harness never showed its input box within the guest's wait: the conversation's inbox takes it, where there is one).
|
|
39
|
+
*/
|
|
40
|
+
export declare function guestFirstPrompt(cell: Pick<CellClient, 'exec'>, ptyId: string, app: 'claude' | 'codex', prompt: string, workspaceKey: string, signal: AbortSignal): Promise<'submitted' | 'unsupported' | 'not_ready'>;
|
|
33
41
|
/**
|
|
34
42
|
* Wait for the native terminal to enable paste before submitting one first turn. No blind timing or input replay.
|
|
35
43
|
* When nothing was typed yet and the terminal never became ready, `queue` hands the prompt to the conversation's guest
|