@shardflux/cli 0.22.0 → 0.23.1
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 +102 -0
- package/README.md +47 -14
- package/dist/acp-journal.js +58 -6
- package/dist/acp-login.d.ts +10 -3
- package/dist/acp-login.js +31 -4
- package/dist/acp-models.d.ts +6 -1
- package/dist/acp-models.js +21 -16
- package/dist/acp.js +60 -7
- package/dist/agent-commands.js +29 -15
- package/dist/agent-target-commands.d.ts +20 -1
- package/dist/agent-target-commands.js +49 -18
- package/dist/agent-targets.js +6 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +14 -14
- package/dist/cli-proxy-forward.d.ts +0 -2
- package/dist/cli-proxy-forward.js +2 -9
- package/dist/cli-proxy-hub.d.ts +22 -30
- package/dist/cli-proxy-hub.js +65 -94
- package/dist/cli-proxy.d.ts +12 -38
- package/dist/cli-proxy.js +51 -168
- package/dist/codex-cloud.d.ts +9 -4
- package/dist/codex-cloud.js +47 -14
- package/dist/codex-linux.d.ts +14 -0
- package/dist/codex-linux.js +69 -0
- package/dist/command-center-actions.js +13 -4
- 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 +2 -2
- package/dist/conversation-commands.js +16 -2
- package/dist/conversation-terminal.js +3 -2
- package/dist/conversations.d.ts +14 -1
- package/dist/conversations.js +61 -7
- package/dist/desktop-account.d.ts +16 -0
- package/dist/desktop-account.js +29 -0
- package/dist/desktop-linux.js +16 -3
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +5 -2
- package/dist/filesystem-commands.js +25 -3
- package/dist/menubar.d.ts +6 -5
- package/dist/menubar.js +40 -28
- package/dist/move-worktrees.d.ts +13 -7
- package/dist/move-worktrees.js +68 -17
- package/dist/move.js +20 -25
- package/dist/onboarding.js +4 -2
- package/dist/prepared-repository.d.ts +2 -0
- package/dist/prepared-repository.js +1 -1
- package/dist/prepared-repository.py +3 -2
- package/dist/project-preparation.d.ts +14 -0
- package/dist/project-preparation.js +16 -4
- package/dist/project-sync.d.ts +3 -0
- package/dist/project-sync.js +4 -1
- package/dist/t3-launch.d.ts +2 -1
- package/dist/t3-launch.js +11 -2
- package/dist/t3-proxy.d.ts +81 -0
- package/dist/t3-proxy.js +325 -0
- package/dist/terminal-apps.d.ts +11 -0
- package/dist/terminal-apps.js +18 -5
- package/dist/update.d.ts +2 -0
- package/dist/update.js +32 -6
- package/package.json +5 -5
|
@@ -60,8 +60,14 @@ export const CENTER_HANDLERS = {
|
|
|
60
60
|
}
|
|
61
61
|
if (values.review === true)
|
|
62
62
|
throw new UsageError('--review needs --on <conversation>.');
|
|
63
|
-
if (
|
|
64
|
-
|
|
63
|
+
if (values.prompt !== undefined || values['prompt-file'] !== undefined || values.image !== undefined) {
|
|
64
|
+
// --in terminal is where a new conversation opens anyway: it starts one with its first message (0.23.0+). Codex
|
|
65
|
+
// opens in Codex Cloud, which says so.
|
|
66
|
+
if (fresh && values.in === 'terminal')
|
|
67
|
+
return launchConversationProject(ctx, values, [str(values.path) ?? './', ...(str(values.app) ? [str(values.app)] : [])]);
|
|
68
|
+
const other = ref ? `the agent argument ${ref}` : values.name !== undefined ? '--name' : values.like !== undefined ? '--like' : values.in !== undefined ? `--in ${str(values.in)}` : undefined;
|
|
69
|
+
if (other)
|
|
70
|
+
throw new UsageError(`--prompt, --prompt-file and --image are for a new conversation and cannot be combined with ${other}: omit it.`, 'start one with shard agents new --path <folder> --app claude|codex --prompt "<task>"; --in auto opens it where agents targets --open-in says');
|
|
65
71
|
}
|
|
66
72
|
if (!ref && values.name === undefined && values.like === undefined && values.in === undefined) {
|
|
67
73
|
return launchConversationProject(ctx, values, [str(values.path) ?? './', ...(str(values.app) ? [str(values.app)] : [])]);
|
|
@@ -145,8 +151,11 @@ export const CENTER_HANDLERS = {
|
|
|
145
151
|
if (ws.labels.shard_conversation && CONVERSATION_ID.test(ws.labels.shard_conversation))
|
|
146
152
|
conversation = await getConversation(ctx, ws.labels.shard_conversation);
|
|
147
153
|
}
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
// Codex Cloud holds the conversation's output in its Codex thread; no terminal in the VM shows it (0.23.0+: the
|
|
155
|
+
// refusal names the commands that do).
|
|
156
|
+
if (conversation?.route === 'codex-cloud') {
|
|
157
|
+
throw new UsageError(`Codex Cloud runs this conversation, so its output is in its Codex thread, not a terminal: open the thread with shard agents open ${conversation.id}; shard c wait ${conversation.id} waits for its reply and shard c get ${conversation.id} shows it.`);
|
|
158
|
+
}
|
|
150
159
|
if (conversation?.route === 'acp')
|
|
151
160
|
throw new UsageError('This conversation runs through its ACP client, which shows its output; it has no terminal to follow.');
|
|
152
161
|
if (conversation && values.app !== undefined && values.app !== conversation.harness)
|
|
@@ -24,6 +24,8 @@ export interface CenterRow {
|
|
|
24
24
|
/** Older K5 captures used the workspace key as the agent label. K1 needs its leaf name. */
|
|
25
25
|
export declare const agentName: (key: string, labels: Record<string, string>) => string;
|
|
26
26
|
export declare const projectName: (key: string, labels: Record<string, string>) => string;
|
|
27
|
+
/** What `shard agents open` gets for a row: a Cloud thread's conversation id, else the VM's key. */
|
|
28
|
+
export declare const openReference: (row: Pick<CenterRow, "key" | "agent">) => string;
|
|
27
29
|
export declare function loadCenter(ctx: Ctx): Promise<CenterData>;
|
|
28
30
|
export declare function scopedProject(data: CenterData, scope: ProjectScope): string;
|
|
29
31
|
export declare function centerRows(data: CenterData, scope?: ProjectScope, filter?: string): CenterRow[];
|
|
@@ -6,6 +6,11 @@ import { readSetupProgress } from "./assisted-setup.js";
|
|
|
6
6
|
export const agentName = (key, labels) => (labels.agent || key).split('/').at(-1);
|
|
7
7
|
export const projectName = (key, labels) => labels.shard_project || key.split('/')[0];
|
|
8
8
|
const parked = (workspace) => ['suspended', 'hibernated', 'parked'].includes(workspace?.observed_state ?? '');
|
|
9
|
+
/** A Codex Cloud thread (API rows labeled shard_route=codex-cloud, 0.23.0+): it runs in Codex, so its VM being parked
|
|
10
|
+
* says nothing about it, and it opens by its conversation id, in Codex, without waking that VM. */
|
|
11
|
+
const cloudThread = (agent) => agent?.labels.shard_route === 'codex-cloud' && !!agent.conversation_id;
|
|
12
|
+
/** What `shard agents open` gets for a row: a Cloud thread's conversation id, else the VM's key. */
|
|
13
|
+
export const openReference = (row) => cloudThread(row.agent) ? row.agent.conversation_id : row.key;
|
|
9
14
|
export async function loadCenter(ctx) {
|
|
10
15
|
const collect = async (path) => {
|
|
11
16
|
const rows = [], cursors = new Set();
|
|
@@ -62,7 +67,7 @@ export function centerRows(data, scope, filter = '') {
|
|
|
62
67
|
const workspace = workspaces.get(agent.workspace_id);
|
|
63
68
|
if (workspace?.deleted_at)
|
|
64
69
|
continue;
|
|
65
|
-
const badge = agent.app === 'claude' && agent.credential_status === 'needs_sign_in' ? 'Claude sign-in needed' : agent.state === 'exited' ? 'done' : parked(workspace) ? 'parked' : agent.state === 'waiting' ? 'needs you' : 'working';
|
|
70
|
+
const badge = agent.app === 'claude' && agent.credential_status === 'needs_sign_in' ? 'Claude sign-in needed' : agent.state === 'exited' ? 'done' : !cloudThread(agent) && parked(workspace) ? 'parked' : agent.state === 'waiting' ? 'needs you' : 'working';
|
|
66
71
|
rows.push({ id: agent.id, key: agent.workspace_key, project: projectName(agent.workspace_key, agent.labels), agent, workspace, badge,
|
|
67
72
|
attention: badge === 'Claude sign-in needed' ? -1 : ['needs you', 'working', 'parked', 'done'].indexOf(badge) });
|
|
68
73
|
withSessions.add(agent.workspace_id);
|
package/dist/command-center.js
CHANGED
|
@@ -5,7 +5,7 @@ import { AgentStateStore } from "./agent-state.js";
|
|
|
5
5
|
import { installedTargets } from "./agent-targets.js";
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { agentAction, availableTargets } from "./command-center-actions.js";
|
|
8
|
-
import { centerRows, loadCenter } from "./command-center-model.js";
|
|
8
|
+
import { centerRows, loadCenter, openReference } from "./command-center-model.js";
|
|
9
9
|
import { AnsiRenderer, renderCenter } from "./command-center-renderer.js";
|
|
10
10
|
import { CenterKeys } from "./command-center-keys.js";
|
|
11
11
|
import { ensureMenubar } from "./menubar.js";
|
|
@@ -320,13 +320,13 @@ export async function showCenter(ctx, scope, options = {}) {
|
|
|
320
320
|
const app = row.agent?.app || options.app || new AgentStateStore(ctx.store.dir).app(project, row.workspace?.labels.app === 'codex' ? 'codex' : 'claude');
|
|
321
321
|
if (key === 'enter') {
|
|
322
322
|
const target = new AgentStateStore(ctx.store.dir).target(project, ['terminal', ...installedTargets(ctx.io.env).targets]);
|
|
323
|
-
await dispatch('open', { app, project, in: target }, [row
|
|
323
|
+
await dispatch('open', { app, project, in: target }, [openReference(row)], true);
|
|
324
324
|
}
|
|
325
325
|
else if (key === 'o') {
|
|
326
326
|
const targets = await availableTargets(ctx);
|
|
327
327
|
menu('Open in…', targets.map(target => ({ label: target, run: async () => {
|
|
328
328
|
new AgentStateStore(ctx.store.dir).saveTarget(project, target);
|
|
329
|
-
await dispatch('open', { app, project, in: target }, [row
|
|
329
|
+
await dispatch('open', { app, project, in: target }, [openReference(row)], true);
|
|
330
330
|
} })));
|
|
331
331
|
}
|
|
332
332
|
else if (key === 'N' || key === 'f') {
|
package/dist/commands.js
CHANGED
|
@@ -1104,7 +1104,7 @@ export const KEY_HANDLERS = {
|
|
|
1104
1104
|
})));
|
|
1105
1105
|
}
|
|
1106
1106
|
if (s.state === 'starting' || s.state === 'running') {
|
|
1107
|
-
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`);
|
|
1108
1108
|
}
|
|
1109
1109
|
const result = { exitCode: s.exit_code ?? null, termSignal: s.term_signal ?? null, timedOut: s.timed_out ?? false, canceled: s.canceled ?? false };
|
|
1110
1110
|
if (!ctx.json && result.exitCode === null && !result.timedOut && !(result.termSignal !== null && result.termSignal > 0)) {
|
|
@@ -1121,7 +1121,7 @@ export const KEY_HANDLERS = {
|
|
|
1121
1121
|
const s = await cellOf(ctx, ws).exec.cancel(sessionId, graceMs);
|
|
1122
1122
|
const ended = s.state !== 'starting' && s.state !== 'running';
|
|
1123
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})` : ''}`;
|
|
1124
|
-
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);
|
|
1125
1125
|
return 0;
|
|
1126
1126
|
},
|
|
1127
1127
|
async 'workspaces exec-input'(ctx, values, [ref, sessionId, data]) {
|
|
@@ -19,9 +19,23 @@ export const CONVERSATION_HANDLERS = {
|
|
|
19
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; },
|
|
20
20
|
async 'conversations list'(ctx, values) {
|
|
21
21
|
const state = str(values.state);
|
|
22
|
-
const
|
|
22
|
+
const data = [], seen = new Set();
|
|
23
|
+
let cursor = str(values.cursor), next;
|
|
24
|
+
if (cursor)
|
|
25
|
+
seen.add(cursor);
|
|
26
|
+
do {
|
|
27
|
+
const page = await conversationClient(ctx).conversations.list({ projectId: str(values.project), state, parentId: str(values.parent), cursor, limit: values.limit ? Number(values.limit) : values.all ? 200 : undefined });
|
|
28
|
+
data.push(...page.data);
|
|
29
|
+
next = page.nextCursor;
|
|
30
|
+
cursor = next ?? undefined;
|
|
31
|
+
if (values.all && cursor) {
|
|
32
|
+
if (seen.has(cursor) || seen.size >= 100)
|
|
33
|
+
throw new UsageError('The conversation list repeated its cursor. Try again.');
|
|
34
|
+
seen.add(cursor);
|
|
35
|
+
}
|
|
36
|
+
} while (values.all && cursor);
|
|
23
37
|
// A conversation that needs you adds a fifth column: why (0.18.0+).
|
|
24
|
-
out(ctx,
|
|
38
|
+
out(ctx, 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', { data, nextCursor: next });
|
|
25
39
|
return 0;
|
|
26
40
|
},
|
|
27
41
|
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; },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { linuxTerminalSession } from "./linux-terminal.js";
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import { attachable, openShardInTerminal, reportTerminalOpen } from "./terminal-apps.js";
|
|
3
|
+
import { attachable, handedToTerminal, openShardInTerminal, reportTerminalOpen } from "./terminal-apps.js";
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { AgentStateStore } from "./agent-state.js";
|
|
6
6
|
import { projectIdentity, projectLabels } from "./project-identity.js";
|
|
@@ -71,7 +71,8 @@ export async function reopenConversation(ctx, c, values) {
|
|
|
71
71
|
*/
|
|
72
72
|
export const launchProjectCommand = async (ctx, values, [path = './', explicit]) => {
|
|
73
73
|
const named = explicit ?? (values.claude === true ? 'claude' : values.codex === true ? 'codex' : undefined);
|
|
74
|
-
|
|
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);
|
|
75
76
|
if (values.resume !== true && values.review !== true) {
|
|
76
77
|
const project = projectIdentity(path);
|
|
77
78
|
const app = named === 'claude' || named === 'codex' ? named : new AgentStateStore(ctx.store.dir).app(project.remote ?? project.folder);
|
package/dist/conversations.d.ts
CHANGED
|
@@ -111,6 +111,9 @@ export declare function rememberConversation(ctx: Pick<Ctx, 'store' | 'apiUrl'>,
|
|
|
111
111
|
export declare function conversationsUnavailable(e: unknown): boolean;
|
|
112
112
|
export declare function getConversation(ctx: Ctx, id: string): Promise<Conversation>;
|
|
113
113
|
export declare function listConversations(ctx: Ctx, projectId?: string): Promise<Conversation[]>;
|
|
114
|
+
/** Resolve an open reference without using the local cwd or waking a VM. A conversation key survives a VM move;
|
|
115
|
+
* a computer key names its bound conversation. Legacy computers without that binding keep the workspace path. */
|
|
116
|
+
export declare function conversationForOpen(ctx: Ctx, ref: string): Promise<Conversation | undefined>;
|
|
114
117
|
export declare function resolveConversation(ctx: Ctx, ref: string, folder?: string): Promise<Conversation>;
|
|
115
118
|
/** Re-resolve the binding on every attach: a move changes the VM, never the conversation identity. */
|
|
116
119
|
export declare function conversationWorkspace(ctx: Ctx, c: Conversation): Promise<Workspace>;
|
|
@@ -131,6 +134,11 @@ export interface MovedInSession {
|
|
|
131
134
|
* they pass. The conversation API's availability is read meanwhile too.
|
|
132
135
|
*/
|
|
133
136
|
export declare function createConversationRecord(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, allocated?: (workspace: WorkspaceView) => void, idempotencyKey?: string, choice?: ModelChoice, movedIn?: MovedInSession, ready?: Promise<unknown>): Promise<Conversation>;
|
|
137
|
+
/**
|
|
138
|
+
* Files of a conversation's prepared start still on their way (large media that follow the first prompt): an editor's
|
|
139
|
+
* file sync leaves them alone until they land, instead of sending them a second time.
|
|
140
|
+
*/
|
|
141
|
+
export declare function preparedInFlight(ctx: Pick<Ctx, 'store' | 'apiUrl'>, id: string): string[];
|
|
134
142
|
/**
|
|
135
143
|
* Brings the conversation's accepted revision into its VM (0.22.0+): only what the project cache lacks is sent, and
|
|
136
144
|
* with `defer` large incompressible files follow the first prompt. Resumable: a later call (the device helper, or
|
|
@@ -142,6 +150,7 @@ export declare function prepareAcceptedConversation(ctx: Ctx, c: Conversation, w
|
|
|
142
150
|
setup?: Promise<{
|
|
143
151
|
depsHash: string;
|
|
144
152
|
} | undefined>;
|
|
153
|
+
landNow?: boolean;
|
|
145
154
|
}): Promise<boolean>;
|
|
146
155
|
/**
|
|
147
156
|
* Sends a conversation's deferred large files and lands them in its worktree. Runs while its VM is awake (it never
|
|
@@ -157,7 +166,11 @@ export declare function seedConversation(ctx: Ctx, c: Conversation, ws: Workspac
|
|
|
157
166
|
seedRepository?(ws: Workspace, c: Conversation): Promise<void>;
|
|
158
167
|
}, changes?: Promise<LocalChanges>, setup?: Promise<{
|
|
159
168
|
depsHash: string;
|
|
160
|
-
} | undefined
|
|
169
|
+
} | undefined>, editor?: {
|
|
170
|
+
initialFiles?: readonly string[];
|
|
171
|
+
onInitialFiles?: () => void;
|
|
172
|
+
landNow?: boolean;
|
|
173
|
+
}): Promise<void>;
|
|
161
174
|
export declare function createConversation(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, choice?: ModelChoice, movedIn?: MovedInSession & {
|
|
162
175
|
beforeSeed(ws: Workspace, c: Conversation): Promise<void>;
|
|
163
176
|
seedRepository?(ws: Workspace, c: Conversation): Promise<void>;
|
package/dist/conversations.js
CHANGED
|
@@ -87,15 +87,55 @@ export async function getConversation(ctx, id) {
|
|
|
87
87
|
return validateConversation(c);
|
|
88
88
|
}
|
|
89
89
|
export async function listConversations(ctx, projectId) {
|
|
90
|
-
const all = [];
|
|
90
|
+
const all = [], seen = new Set();
|
|
91
91
|
let cursor;
|
|
92
92
|
do {
|
|
93
93
|
const page = await conversationClient(ctx).conversations.list({ ...(projectId ? { projectId } : {}), limit: 200, ...(cursor ? { cursor } : {}) });
|
|
94
94
|
all.push(...page.data.map(validateConversation));
|
|
95
95
|
cursor = page.nextCursor ?? undefined;
|
|
96
|
+
if (cursor) {
|
|
97
|
+
if (seen.has(cursor) || seen.size >= 100)
|
|
98
|
+
throw new UsageError('The conversation list repeated its cursor. Try again.');
|
|
99
|
+
seen.add(cursor);
|
|
100
|
+
}
|
|
96
101
|
} while (cursor);
|
|
97
102
|
return all;
|
|
98
103
|
}
|
|
104
|
+
/** Resolve an open reference without using the local cwd or waking a VM. A conversation key survives a VM move;
|
|
105
|
+
* a computer key names its bound conversation. Legacy computers without that binding keep the workspace path. */
|
|
106
|
+
export async function conversationForOpen(ctx, ref) {
|
|
107
|
+
if (/^conv_[a-z0-9]{26}$/.test(ref))
|
|
108
|
+
return getConversation(ctx, ref);
|
|
109
|
+
if (!ref.includes('/'))
|
|
110
|
+
return undefined;
|
|
111
|
+
const ws = await ctx.cloud().workspaces.findByKey(ref, { signal: ctx.signal });
|
|
112
|
+
if (ws) {
|
|
113
|
+
const id = ws.labels.shard_conversation;
|
|
114
|
+
if (!id)
|
|
115
|
+
return undefined;
|
|
116
|
+
try {
|
|
117
|
+
const c = await getConversation(ctx, id);
|
|
118
|
+
return c.workspace_id === ws.data.id ? c : undefined;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (conversationsUnavailable(error))
|
|
122
|
+
return undefined;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let matches;
|
|
127
|
+
try {
|
|
128
|
+
matches = (await listConversations(ctx)).filter(c => c.key === ref);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (conversationsUnavailable(error))
|
|
132
|
+
return undefined;
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
if (matches.length > 1)
|
|
136
|
+
throw new UsageError('Use the conversation id.');
|
|
137
|
+
return matches[0] ? getConversation(ctx, matches[0].id) : undefined;
|
|
138
|
+
}
|
|
99
139
|
export async function resolveConversation(ctx, ref, folder) {
|
|
100
140
|
if (ref.startsWith('conv_'))
|
|
101
141
|
return getConversation(ctx, ref);
|
|
@@ -190,8 +230,8 @@ export async function createConversationRecord(ctx, folder, harness, route, name
|
|
|
190
230
|
const commit = seed.commit;
|
|
191
231
|
// The prepared revision of that state (0.22.0+): its media and what Git does not hold (.env files, filter output),
|
|
192
232
|
// captured while the VM is requested and boots. A capture that cannot complete falls back to the copy seed, never fails
|
|
193
|
-
// the start.
|
|
194
|
-
const capture = movedIn
|
|
233
|
+
// the start. Every route but a moved-in session (its own worktrees) starts this way, editors' (ACP) too (0.22.0+).
|
|
234
|
+
const capture = movedIn ? Promise.resolve(undefined) : capturePreparedSnapshot(ctx.store.dir, project.folder, seed, project.remote, ctx.signal).catch((error) => {
|
|
195
235
|
if (ctx.signal.aborted)
|
|
196
236
|
throw error;
|
|
197
237
|
ctx.io.stderr.write(`Project snapshot: ${error instanceof Error ? error.message : String(error)}; copying the project instead.\n`);
|
|
@@ -211,6 +251,7 @@ export async function createConversationRecord(ctx, folder, harness, route, name
|
|
|
211
251
|
// The seed awaits the capture (prepareAcceptedConversation), so it runs on while the VM boots, with the Git settings
|
|
212
252
|
// the VM repository copies from this one.
|
|
213
253
|
pendingCaptures.set(c.id, capture.then(async (prepared) => {
|
|
254
|
+
seedPhase('capture_done');
|
|
214
255
|
if (!prepared)
|
|
215
256
|
return undefined;
|
|
216
257
|
new AgentStateStore(ctx.store.dir).update(doc => {
|
|
@@ -223,6 +264,14 @@ export async function createConversationRecord(ctx, folder, harness, route, name
|
|
|
223
264
|
acceptedProgress(c.id, c.key);
|
|
224
265
|
return c;
|
|
225
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Files of a conversation's prepared start still on their way (large media that follow the first prompt): an editor's
|
|
269
|
+
* file sync leaves them alone until they land, instead of sending them a second time.
|
|
270
|
+
*/
|
|
271
|
+
export function preparedInFlight(ctx, id) {
|
|
272
|
+
const preparation = localConversations(ctx)[id]?.preparation;
|
|
273
|
+
return preparation?.state === 'deferred' ? preparation.pending ?? [] : [];
|
|
274
|
+
}
|
|
226
275
|
/** Captures still running for conversations this process created (createConversationRecord). */
|
|
227
276
|
const pendingCaptures = new Map();
|
|
228
277
|
/** A start the copy seed took over: its prepared revision is no longer this conversation's (recorded as the reason). */
|
|
@@ -280,7 +329,7 @@ export async function prepareAcceptedConversation(ctx, c, ws, options = {}) {
|
|
|
280
329
|
const target = { root: c.worktree_path, branch: c.branch, options: { signal: ctx.signal, adopt: true,
|
|
281
330
|
git: fresh?.git ?? await preparedGitConfig(local.folder, snapshot.manifest, ctx.signal), ...(outside ? { dependencySource: templateCheckout(c) } : {}),
|
|
282
331
|
after: { script: SEED_SNAPSHOT, args: [c.worktree_path, c.branch.replace(/^shard\//, ''), HUB_PATH, 'background'] },
|
|
283
|
-
...(intent ? { setupIntent: { conversation: c.id, depsHash: intent.depsHash } } : {}), workspace: ws.id } };
|
|
332
|
+
...(intent ? { setupIntent: { conversation: c.id, depsHash: intent.depsHash } } : {}), workspace: ws.id, ...(options.landNow ? { landNow: true } : {}) } };
|
|
284
333
|
let transfer, apply;
|
|
285
334
|
try {
|
|
286
335
|
// A warm start: the probe checks the revision out in its own exec.
|
|
@@ -363,7 +412,7 @@ export async function completeDeferredPreparation(ctx, c) {
|
|
|
363
412
|
* its prepared revision (0.22.0+), or the copy seed when the start has none or the VM cannot take one. Repeatable: a
|
|
364
413
|
* start resumed after a deadline continues here.
|
|
365
414
|
*/
|
|
366
|
-
export async function seedConversation(ctx, c, ws, folder, movedIn, changes, setup) {
|
|
415
|
+
export async function seedConversation(ctx, c, ws, folder, movedIn, changes, setup, editor = {}) {
|
|
367
416
|
// Requests the copy seed starts and nothing waits for (the history backfill's start), settled before this returns.
|
|
368
417
|
const background = [];
|
|
369
418
|
const cell = ws.cell();
|
|
@@ -383,9 +432,14 @@ export async function seedConversation(ctx, c, ws, folder, movedIn, changes, set
|
|
|
383
432
|
void recordIntent();
|
|
384
433
|
await movedIn.seedRepository(ws, c);
|
|
385
434
|
}
|
|
386
|
-
else if (
|
|
435
|
+
else if (await prepareAcceptedConversation(ctx, c, ws, { fallback: true, ...(setup ? { setup } : {}), ...(editor.landNow ? { landNow: true } : {}) })) {
|
|
436
|
+
// A prepared worktree appears whole: an editor's agent settings are there with everything else.
|
|
437
|
+
editor.onInitialFiles?.();
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
387
440
|
await seedAgentRepository(ctx, ws, folder, { root: c.worktree_path, populate: true, changes: changes ?? localChanges(folder, ctx.signal),
|
|
388
|
-
background: started => background.push(started), beforeHistory: recordIntent()
|
|
441
|
+
background: started => background.push(started), beforeHistory: recordIntent(),
|
|
442
|
+
...(editor.initialFiles ? { initialFiles: editor.initialFiles } : {}), ...(editor.onInitialFiles ? { onInitialFiles: editor.onInitialFiles } : {}) });
|
|
389
443
|
}
|
|
390
444
|
await recorded;
|
|
391
445
|
seedPhase('seed_done');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Ctx } from './commands.js';
|
|
2
|
+
/** The desktop Settings snapshot uses the selected project key's organization, as AccountClient on macOS does.
|
|
3
|
+
* The optional user identity comes from the session; saved credentials never imply connector liveness. */
|
|
4
|
+
export declare function desktopAccount(ctx: Ctx): Promise<{
|
|
5
|
+
identity: {
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
email_verified: boolean;
|
|
9
|
+
display_name: string | null;
|
|
10
|
+
mfa_enabled: boolean;
|
|
11
|
+
} | null;
|
|
12
|
+
organization: Record<string, unknown>;
|
|
13
|
+
usage: Record<string, unknown> | null;
|
|
14
|
+
spend: Record<string, unknown> | null;
|
|
15
|
+
credentials: import("@shardflux/sdk").AgentCredentialMetadata[] | null;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { UsageError } from "./errors.js";
|
|
2
|
+
/** The desktop Settings snapshot uses the selected project key's organization, as AccountClient on macOS does.
|
|
3
|
+
* The optional user identity comes from the session; saved credentials never imply connector liveness. */
|
|
4
|
+
export async function desktopAccount(ctx) {
|
|
5
|
+
const token = ctx.apiKey().token;
|
|
6
|
+
const request = async (path) => {
|
|
7
|
+
const response = await (ctx.accountOptions().fetch ?? fetch)(new URL(`v1/${path}`, `${ctx.apiUrl.replace(/\/+$/, '')}/`), {
|
|
8
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, redirect: 'error',
|
|
9
|
+
signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(20_000)]),
|
|
10
|
+
});
|
|
11
|
+
if (!response.ok)
|
|
12
|
+
throw new UsageError(`Couldn’t refresh your account (${response.status}). Try again.`);
|
|
13
|
+
return await response.json();
|
|
14
|
+
};
|
|
15
|
+
const me = await ctx.cloud().me();
|
|
16
|
+
const org = me.api_key?.organization_id;
|
|
17
|
+
if (!org)
|
|
18
|
+
throw new UsageError('The project key did not identify its organization. Sign in to Shardflux again.');
|
|
19
|
+
const [organization, usage, spend, credentials, identity] = await Promise.all([
|
|
20
|
+
request(`organizations/${encodeURIComponent(org)}`),
|
|
21
|
+
request(`organizations/${encodeURIComponent(org)}/usage/summary`).catch(() => null),
|
|
22
|
+
request(`organizations/${encodeURIComponent(org)}/spend`).catch(() => null),
|
|
23
|
+
ctx.cloud().agentCredentials.list().catch(() => null),
|
|
24
|
+
ctx.sessionSource() ? ctx.account().me().then(value => value.user).catch(() => null) : Promise.resolve(null),
|
|
25
|
+
]);
|
|
26
|
+
if (organization.id !== org || typeof organization.name !== 'string' || !organization.name)
|
|
27
|
+
throw new UsageError('The organization response is unreadable. Refresh your account and try again.');
|
|
28
|
+
return { identity, organization, usage, spend, credentials };
|
|
29
|
+
}
|
package/dist/desktop-linux.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, execFile } from 'node:child_process';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, chmodSync, mkdtempSync } from 'node:fs';
|
|
3
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, chmodSync, mkdtempSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
@@ -19,6 +19,15 @@ export function linuxDesktopPackage(arch = process.arch) {
|
|
|
19
19
|
}
|
|
20
20
|
export function graphicalSession(env) { return !!(env.DISPLAY || env.WAYLAND_DISPLAY); }
|
|
21
21
|
const desktopString = (text) => `"${text.replace(/(["`$\\])/g, '\\$1')}"`;
|
|
22
|
+
// fnm's multishell aliases belong to the launching shell; the desktop and its autostart entry outlive it.
|
|
23
|
+
function stableDesktopRuntime(runtime) {
|
|
24
|
+
try {
|
|
25
|
+
return { node: realpathSync(runtime.node), script: realpathSync(runtime.script) };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new UsageError('Open shard from its installed CLI to start the Shardflux window.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
22
31
|
export class LinuxDesktopManager {
|
|
23
32
|
home;
|
|
24
33
|
app;
|
|
@@ -117,6 +126,7 @@ export class LinuxDesktopManager {
|
|
|
117
126
|
throw new UsageError('Install the window with npm i -g shardflux --include=optional, then run shard.');
|
|
118
127
|
if (!graphicalSession(this.#env))
|
|
119
128
|
throw new UsageError('Open shard from your desktop session to show the window.');
|
|
129
|
+
const stable = stableDesktopRuntime(runtime);
|
|
120
130
|
const current = await this.status(dir);
|
|
121
131
|
if (!current.version || (compareVersions(current.version, version) ?? -1) < 0) {
|
|
122
132
|
if (existsSync(this.app) && (lstatSync(this.app).isSymbolicLink() || !current.version))
|
|
@@ -148,11 +158,14 @@ export class LinuxDesktopManager {
|
|
|
148
158
|
}
|
|
149
159
|
}
|
|
150
160
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
151
|
-
const env = { ...this.#env, SHARDFLUX_CONFIG_DIR: dir, SHARDFLUX_API_URL: api, SHARDFLUX_NODE:
|
|
161
|
+
const env = { ...this.#env, SHARDFLUX_CONFIG_DIR: dir, SHARDFLUX_API_URL: api, SHARDFLUX_NODE: stable.node, SHARDFLUX_CLI: stable.script };
|
|
152
162
|
const desktop = join(this.home, '.local/share/applications/dev.shardflux.Desktop.desktop');
|
|
153
163
|
mkdirSync(dirname(desktop), { recursive: true });
|
|
154
|
-
const args = ['/usr/bin/env', `SHARDFLUX_CONFIG_DIR=${dir}`, `SHARDFLUX_API_URL=${api}`, `SHARDFLUX_NODE=${
|
|
164
|
+
const args = ['/usr/bin/env', `SHARDFLUX_CONFIG_DIR=${dir}`, `SHARDFLUX_API_URL=${api}`, `SHARDFLUX_NODE=${stable.node}`, `SHARDFLUX_CLI=${stable.script}`, join(this.app, 'shardflux-desktop')];
|
|
155
165
|
writeFileSync(desktop, `[Desktop Entry]\nType=Application\nName=Shardflux\nComment=Your agents, a shortcut away\nExec=${args.map(desktopString).join(' ')}\nIcon=${join(this.app, 'ui/assets/shardflux-icon.png')}\nTerminal=false\nCategories=Development;\nStartupWMClass=dev.shardflux.Desktop\n`, { mode: 0o644 });
|
|
166
|
+
const autostart = join(this.home, '.config/autostart/shardflux.desktop');
|
|
167
|
+
if (existsSync(autostart))
|
|
168
|
+
cpSync(desktop, autostart);
|
|
156
169
|
if ((await this.status(dir)).running)
|
|
157
170
|
await this.#exec(join(this.app, 'shardflux-desktop'), ['--control', view], { env, timeout: 5_000 });
|
|
158
171
|
else {
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckoutStatus, ExecSession, ExecutionResult } from '@shardflux/sdk';
|
|
1
|
+
import type { RateLimitInfo, CheckoutStatus, ExecSession, ExecutionResult } from '@shardflux/sdk';
|
|
2
2
|
export declare const EXIT: {
|
|
3
3
|
readonly ok: 0;
|
|
4
4
|
/** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
|
|
@@ -145,6 +145,7 @@ export interface ErrorInfo {
|
|
|
145
145
|
execution_id?: string;
|
|
146
146
|
/** The refusal's Retry-After in seconds (0.9.0+), e.g. 429 quota_exceeded from the cell gateway. */
|
|
147
147
|
retry_after_seconds?: number;
|
|
148
|
+
rate_limit?: RateLimitInfo;
|
|
148
149
|
}
|
|
149
150
|
/**
|
|
150
151
|
* Removes anything shaped like a Shardflux API key or user session token from text, and each of the given secrets
|
package/dist/errors.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* stderr with the API's error code, message and request id; the process exit
|
|
4
4
|
* code tells scripts what kind of failure it was (README "Exit codes").
|
|
5
5
|
*/
|
|
6
|
-
import { CheckoutTimeoutError, DurabilityLostError, ExecStartError, FilesystemSaveFailedError, FilesystemSaveTimeoutError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
|
|
6
|
+
import { budgetHint, CheckoutTimeoutError, DurabilityLostError, ExecStartError, FilesystemSaveFailedError, FilesystemSaveTimeoutError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
|
|
7
7
|
export const EXIT = {
|
|
8
8
|
ok: 0,
|
|
9
9
|
/** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
|
|
@@ -212,7 +212,9 @@ export function planDiskLimit(failure) {
|
|
|
212
212
|
/** Bytes as the plan names a disk: `2 GiB`, `2.5 GiB`. */
|
|
213
213
|
export const gib = (bytes) => `${Number((bytes / 2 ** 30).toFixed(2))} GiB`;
|
|
214
214
|
export function ownHint(err) {
|
|
215
|
-
|
|
215
|
+
if (err instanceof ShardfluxApiError && err.reason === 'codex_chatgpt_login_required')
|
|
216
|
+
return 'Sign in to Codex with ChatGPT using codex login, then copy your Codex login with shard setup again.';
|
|
217
|
+
return budgetHint(err) ?? (err instanceof HintedError ? err.hint : undefined);
|
|
216
218
|
}
|
|
217
219
|
function isNamed(err, name) {
|
|
218
220
|
return typeof err === 'object' && err !== null && err.name === name;
|
|
@@ -238,6 +240,7 @@ export function describeError(err) {
|
|
|
238
240
|
...(err.operationId ? { operation_id: err.operationId } : {}),
|
|
239
241
|
...(err.details ? { details: err.details } : {}),
|
|
240
242
|
source: err.source,
|
|
243
|
+
...(err.rateLimit ? { rate_limit: err.rateLimit } : {}),
|
|
241
244
|
...(err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {}),
|
|
242
245
|
};
|
|
243
246
|
}
|
|
@@ -8,10 +8,32 @@ import { AGENT_HANDLERS } from "./agent-commands.js";
|
|
|
8
8
|
import { readSetupProgress } from "./assisted-setup.js";
|
|
9
9
|
import { syncAgentConfig } from "./agent-config-sync.js";
|
|
10
10
|
import { progress } from "./progress.js";
|
|
11
|
+
import { getConversation, localConversations } from "./conversations.js";
|
|
11
12
|
function remote() {
|
|
12
13
|
const project = projectIdentity('./');
|
|
13
14
|
return { project, repoRemote: project.remote ?? normalizeRemote(`file://${project.folder}`) };
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The VM this computer saves a project's template from (0.23.0+): the newest conversation this checkout started whose
|
|
18
|
+
* VM is there now, else the project's workspace (`<project>/main`, setups from before conversations). Each
|
|
19
|
+
* conversation's VM is read from the API: a restore by any client moves it to a new VM. In a VM, `shard template save`
|
|
20
|
+
* saves that VM itself.
|
|
21
|
+
*/
|
|
22
|
+
async function templateSource(ctx, project) {
|
|
23
|
+
const gone = (error) => { if (error instanceof ShardfluxApiError && error.status === 404)
|
|
24
|
+
return undefined; throw error; };
|
|
25
|
+
const started = Object.values(localConversations(ctx)).filter(r => r.folder === project.folder && r.api_url === ctx.apiUrl)
|
|
26
|
+
.sort((a, b) => Date.parse(b.conversation.created_at) - Date.parse(a.conversation.created_at));
|
|
27
|
+
for (const r of started) {
|
|
28
|
+
const c = await getConversation(ctx, r.conversation.id).catch(gone);
|
|
29
|
+
if (!c || c.ended_at || !c.workspace_id)
|
|
30
|
+
continue;
|
|
31
|
+
const ws = await ctx.cloud().workspaces.get(c.workspace_id, { signal: ctx.signal }).catch(gone);
|
|
32
|
+
if (ws && !ws.data.deleted_at)
|
|
33
|
+
return ws;
|
|
34
|
+
}
|
|
35
|
+
return await ctx.cloud().workspaces.findByKey(`${project.slug}/main`, { signal: ctx.signal }) ?? undefined;
|
|
36
|
+
}
|
|
15
37
|
/**
|
|
16
38
|
* One line per status: what is in use and what is saving or failed, its platform base and setup cache. A project
|
|
17
39
|
* template unused for 30 days (rebase_deferred, 0.20.1+) follows the machine when the next conversation starts.
|
|
@@ -202,7 +224,7 @@ export const FILESYSTEM_HANDLERS = {
|
|
|
202
224
|
async template(ctx) {
|
|
203
225
|
const { project, repoRemote } = remote();
|
|
204
226
|
const state = await ctx.cloud().templates.getProject(repoRemote, { signal: ctx.signal });
|
|
205
|
-
const ws = await
|
|
227
|
+
const ws = await templateSource(ctx, project);
|
|
206
228
|
const progress = ws ? await readSetupProgress(ctx, ws.data) : undefined;
|
|
207
229
|
const attachments = ws ? await ctx.cloud().volumes.listAttached(ws.id) : undefined;
|
|
208
230
|
const mounted = attachments?.data.some((a) => a.mount_path === '/mnt/shardflux-cache' && a.mounted && a.volume.name === 'shardflux-setup-cache' && a.volume.org_shared);
|
|
@@ -210,9 +232,9 @@ export const FILESYSTEM_HANDLERS = {
|
|
|
210
232
|
},
|
|
211
233
|
async 'template save'(ctx, values) {
|
|
212
234
|
const { project, repoRemote } = remote();
|
|
213
|
-
const ws = await
|
|
235
|
+
const ws = await templateSource(ctx, project);
|
|
214
236
|
if (!ws)
|
|
215
|
-
throw new UsageError('
|
|
237
|
+
throw new UsageError('Start a conversation in this project with shard ./ before saving its template', 'or run shard template save in a conversation\'s VM');
|
|
216
238
|
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');
|
|
217
239
|
},
|
|
218
240
|
async 'template rollback'(ctx, values) {
|
package/dist/menubar.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { Handler } from './commands.js';
|
|
2
|
-
|
|
2
|
+
/** `env` adds variables to this process's environment for the tool (the app's --control reads its config directory). */
|
|
3
|
+
export type NativeRunner = (tool: string, args: string[], env?: Record<string, string>) => Promise<string>;
|
|
3
4
|
/**
|
|
4
5
|
* Every native tool a default MenubarManager runs (plutil, codesign, xattr, open, the app's --control) goes through
|
|
5
6
|
* `nativeTools.run`. The CLI test support replaces it, so a test run never starts, stops or inspects a real app,
|
|
6
7
|
* whether or not a built Shardflux.app is in the package.
|
|
7
8
|
*/
|
|
8
9
|
export declare const nativeTools: {
|
|
9
|
-
run: (tool: string, args: string[], signal?: AbortSignal) => Promise<string>;
|
|
10
|
+
run: (tool: string, args: string[], signal?: AbortSignal, env?: Record<string, string>) => Promise<string>;
|
|
10
11
|
};
|
|
11
12
|
export interface MenubarStatus {
|
|
12
13
|
installed: boolean;
|
|
@@ -57,9 +58,9 @@ export declare class MenubarManager {
|
|
|
57
58
|
packagedVersion(): Promise<string>;
|
|
58
59
|
/** Whether the app shows any window (see windowsOpen); null when that could not be read. */
|
|
59
60
|
windowsOpen(pids: number[]): Promise<boolean | null>;
|
|
60
|
-
status(): Promise<MenubarStatus>;
|
|
61
|
-
stop(): Promise<MenubarStatus>;
|
|
62
|
-
login(value: 'on' | 'off'): Promise<MenubarStatus>;
|
|
61
|
+
status(configDir?: string): Promise<MenubarStatus>;
|
|
62
|
+
stop(configDir?: string): Promise<MenubarStatus>;
|
|
63
|
+
login(value: 'on' | 'off', configDir?: string): Promise<MenubarStatus>;
|
|
63
64
|
start(configDir: string, apiUrl: string): Promise<MenubarStatus>;
|
|
64
65
|
/**
|
|
65
66
|
* Brings an installed app up to the one this CLI's npm package carries, without disturbing the person: nothing when
|