@shardflux/cli 0.14.5 → 0.15.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 +69 -0
- package/README.md +76 -12
- package/dist/account-args.js +31 -10
- package/dist/account-commands.d.ts +2 -0
- package/dist/account-commands.js +158 -14
- package/dist/acp-catalog.js +2 -2
- package/dist/acp-login.d.ts +20 -0
- package/dist/acp-login.js +122 -0
- package/dist/acp.js +36 -13
- package/dist/agent-commands.js +71 -6
- package/dist/agent-git-commands.js +2 -1
- package/dist/agent-reachability.js +1 -1
- package/dist/agent-target-commands.d.ts +14 -0
- package/dist/agent-target-commands.js +79 -13
- package/dist/agent-targets.d.ts +7 -0
- package/dist/agent-targets.js +44 -32
- package/dist/args.d.ts +3 -1
- package/dist/args.js +21 -9
- package/dist/browser-login.d.ts +35 -0
- package/dist/browser-login.js +145 -0
- package/dist/codex-cloud.d.ts +49 -0
- package/dist/codex-cloud.js +157 -0
- package/dist/command-center-actions.js +7 -0
- package/dist/command-center-renderer.d.ts +3 -0
- package/dist/command-center-renderer.js +42 -38
- package/dist/command-center.js +65 -11
- package/dist/conversation-terminal.js +19 -0
- package/dist/conversations.d.ts +1 -1
- package/dist/conversations.js +21 -8
- package/dist/credential-prompts.d.ts +4 -2
- package/dist/credential-prompts.js +38 -16
- package/dist/device-bridge.d.ts +13 -0
- package/dist/device-bridge.js +14 -4
- package/dist/device-helper.js +2 -1
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +15 -2
- package/dist/format.js +3 -1
- package/dist/handlers.js +2 -1
- package/dist/initial-prompt.d.ts +26 -0
- package/dist/initial-prompt.js +196 -0
- package/dist/legal.d.ts +36 -0
- package/dist/legal.js +44 -0
- package/dist/main.js +6 -2
- package/dist/menubar.d.ts +8 -0
- package/dist/menubar.js +9 -1
- package/dist/onboarding-app/AGENTS.md +19 -0
- package/dist/onboarding-app/CLAUDE.md +19 -0
- package/dist/onboarding-app/README.md +12 -0
- package/dist/onboarding-app/dev.mjs +18 -0
- package/dist/onboarding-app/index.html +25 -0
- package/dist/onboarding-app/package.json +12 -0
- package/dist/onboarding-app/server.mjs +41 -0
- package/dist/onboarding-sample.d.ts +3 -0
- package/dist/onboarding-sample.js +29 -0
- package/dist/onboarding.d.ts +258 -0
- package/dist/onboarding.js +371 -0
- package/dist/terminal-brand.d.ts +5 -0
- package/dist/terminal-brand.js +18 -0
- package/package.json +3 -3
package/dist/command-center.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ensureMenubar } from "./menubar.js";
|
|
|
12
12
|
import { MenubarManager } from "./menubar.js";
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import { retrySetupStep } from "./assisted-setup.js";
|
|
15
|
+
import { localConversations } from "./conversations.js";
|
|
15
16
|
export async function showCenter(ctx, scope, options = {}) {
|
|
16
17
|
const input = ctx.io.stdin;
|
|
17
18
|
if (!input.isTTY || !input.setRawMode || !input.on || !input.off)
|
|
@@ -30,6 +31,7 @@ export async function showCenter(ctx, scope, options = {}) {
|
|
|
30
31
|
let timer, escapeTimer;
|
|
31
32
|
let refreshing;
|
|
32
33
|
let edit;
|
|
34
|
+
let showDone = false, draftPrompt = '';
|
|
33
35
|
let menuActions = [];
|
|
34
36
|
let finish;
|
|
35
37
|
const done = new Promise(resolve => { finish = resolve; });
|
|
@@ -41,7 +43,10 @@ export async function showCenter(ctx, scope, options = {}) {
|
|
|
41
43
|
ctx.io.stdout.write(renderer.draw(renderCenter(view, size.columns, size.rows, ctx.io.env.NO_COLOR === undefined && ctx.io.env.TERM !== 'dumb')));
|
|
42
44
|
};
|
|
43
45
|
const updateRows = () => {
|
|
44
|
-
|
|
46
|
+
const rows = centerRows(data, scope, view.filter);
|
|
47
|
+
view.doneCount = rows.filter(row => row.badge === 'done').length;
|
|
48
|
+
view.showDone = showDone;
|
|
49
|
+
view.rows = showDone || view.filter ? rows : rows.filter(row => row.badge !== 'done');
|
|
45
50
|
if (!view.rows.some(r => r.id === view.selected))
|
|
46
51
|
view.selected = view.rows[0]?.id;
|
|
47
52
|
};
|
|
@@ -157,6 +162,43 @@ export async function showCenter(ctx, scope, options = {}) {
|
|
|
157
162
|
return;
|
|
158
163
|
}
|
|
159
164
|
if (edit) {
|
|
165
|
+
if ((edit.kind === 'task' || edit.kind === 'project') && key === 'enter') {
|
|
166
|
+
if (!edit.text.trim()) {
|
|
167
|
+
view.message = edit.kind === 'task' ? 'Describe what the agent should do.' : 'Choose a project folder.';
|
|
168
|
+
draw();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (edit.kind === 'task') {
|
|
172
|
+
draftPrompt = edit.text;
|
|
173
|
+
const row = view.rows.find(r => r.id === view.selected);
|
|
174
|
+
const records = Object.values(localConversations(ctx));
|
|
175
|
+
const folder = scope?.folder ?? records.find(r => r.conversation.workspace_id === row?.workspace?.id)?.folder ?? records[0]?.folder ?? process.cwd();
|
|
176
|
+
edit = { kind: 'project', text: folder };
|
|
177
|
+
view.message = '';
|
|
178
|
+
view.prompt = `Project folder: ${folder} (Enter accepts, Esc cancels)`;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
let project;
|
|
182
|
+
try {
|
|
183
|
+
project = projectIdentity(edit.text);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
view.message = 'Choose an accessible project folder.';
|
|
187
|
+
draw();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
edit = undefined;
|
|
191
|
+
view.prompt = undefined;
|
|
192
|
+
view.message = '';
|
|
193
|
+
const preferred = options.app ?? new AgentStateStore(ctx.store.dir).app(project.remote ?? project.folder);
|
|
194
|
+
menu('Start conversation', [preferred, preferred === 'claude' ? 'codex' : 'claude'].map(app => ({
|
|
195
|
+
label: `${app === 'claude' ? 'Claude' : 'Codex'} · ${project.slug}`,
|
|
196
|
+
run: () => dispatch('new', { path: project.folder, app, prompt: draftPrompt }, [], true),
|
|
197
|
+
})));
|
|
198
|
+
}
|
|
199
|
+
draw();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
160
202
|
if (key === 'escape') {
|
|
161
203
|
if (edit.kind === 'filter') {
|
|
162
204
|
view.filter = edit.previous ?? '';
|
|
@@ -164,6 +206,7 @@ export async function showCenter(ctx, scope, options = {}) {
|
|
|
164
206
|
}
|
|
165
207
|
edit = undefined;
|
|
166
208
|
view.prompt = undefined;
|
|
209
|
+
view.message = '';
|
|
167
210
|
}
|
|
168
211
|
else if (key === 'enter') {
|
|
169
212
|
const current = edit;
|
|
@@ -193,14 +236,14 @@ export async function showCenter(ctx, scope, options = {}) {
|
|
|
193
236
|
}
|
|
194
237
|
else if (key === 'backspace')
|
|
195
238
|
edit.text = [...edit.text].slice(0, -1).join('');
|
|
196
|
-
else if ([...key].length === 1 && key >= ' ' && edit.text.length < (edit.kind === 'filter' ? 256 : 64))
|
|
239
|
+
else if ([...key].length === 1 && key >= ' ' && edit.text.length < (edit.kind === 'task' ? 4096 : edit.kind === 'project' ? 2048 : edit.kind === 'filter' ? 256 : 64))
|
|
197
240
|
edit.text += key;
|
|
198
241
|
if (edit) {
|
|
199
242
|
if (edit.kind === 'filter') {
|
|
200
243
|
view.filter = edit.text;
|
|
201
244
|
updateRows();
|
|
202
245
|
}
|
|
203
|
-
view.prompt = `${edit.kind === 'filter' ? '
|
|
246
|
+
view.prompt = `${edit.kind === 'filter' ? 'Search' : edit.kind === 'task' ? 'What should the agent do?' : edit.kind === 'project' ? 'Project folder' : 'New agent name'}: ${edit.text} (Enter accepts, Esc cancels)`;
|
|
204
247
|
}
|
|
205
248
|
draw();
|
|
206
249
|
return;
|
|
@@ -230,10 +273,28 @@ export async function showCenter(ctx, scope, options = {}) {
|
|
|
230
273
|
}
|
|
231
274
|
if (key === '/') {
|
|
232
275
|
edit = { kind: 'filter', text: view.filter, previous: view.filter };
|
|
233
|
-
view.prompt = `
|
|
276
|
+
view.prompt = `Search: ${view.filter}`;
|
|
234
277
|
draw();
|
|
235
278
|
return;
|
|
236
279
|
}
|
|
280
|
+
if (key === 'D') {
|
|
281
|
+
showDone = !showDone;
|
|
282
|
+
updateRows();
|
|
283
|
+
draw();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (key === 'n' || key === 'enter' && view.rows.find(r => r.id === view.selected)?.start) {
|
|
287
|
+
edit = { kind: 'task', text: '' };
|
|
288
|
+
view.message = '';
|
|
289
|
+
view.prompt = 'What should the agent do? ';
|
|
290
|
+
draw();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (key === '?') {
|
|
294
|
+
const choices = [['n', 'New conversation'], ['o', 'Open in…'], ['N', 'New agent workspace'], ['f', 'Fork workspace'], ['p', 'Park'], ['l', 'View logs'], ...(view.rows.find(row => row.id === view.selected)?.setup ? [['r', 'Retry setup']] : []), ...extra, ['D', showDone ? 'Hide done conversations' : 'Show done conversations']];
|
|
295
|
+
menu('Conversation actions', choices.map(([key, label]) => ({ label: `${key} ${label}`, run: () => handle(key) })));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
237
298
|
const row = view.rows.find(r => r.id === view.selected);
|
|
238
299
|
if (!row)
|
|
239
300
|
return;
|
|
@@ -257,10 +318,6 @@ export async function showCenter(ctx, scope, options = {}) {
|
|
|
257
318
|
}
|
|
258
319
|
const project = scope ? scope.remote ?? scope.folder : row.workspace?.labels.repo_remote || row.project;
|
|
259
320
|
const app = row.agent?.app || options.app || new AgentStateStore(ctx.store.dir).app(project, row.workspace?.labels.app === 'codex' ? 'codex' : 'claude');
|
|
260
|
-
if (row.start && ['enter', 'n', 'N'].includes(key)) {
|
|
261
|
-
menu('Start here', [app, app === 'claude' ? 'codex' : 'claude'].map(app => ({ label: `Start ${app === 'claude' ? 'Claude' : 'Codex'} here`, run: () => dispatch('new', { path: scope.folder, app }, [], true) })));
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
321
|
if (key === 'enter') {
|
|
265
322
|
const target = new AgentStateStore(ctx.store.dir).target(project, ['terminal', ...installedTargets(ctx.io.env).targets]);
|
|
266
323
|
await dispatch('open', { app, project, in: target }, [row.key], true);
|
|
@@ -272,9 +329,6 @@ export async function showCenter(ctx, scope, options = {}) {
|
|
|
272
329
|
await dispatch('open', { app, project, in: target }, [row.key], true);
|
|
273
330
|
} })));
|
|
274
331
|
}
|
|
275
|
-
else if (key === 'n') {
|
|
276
|
-
menu('New session', ['claude', 'codex'].map(app => ({ label: `${app === 'claude' ? 'Claude' : 'Codex'} (reattach if running)`, run: () => dispatch('new', { app, project }, [row.key], true) })));
|
|
277
|
-
}
|
|
278
332
|
else if (key === 'N' || key === 'f') {
|
|
279
333
|
edit = { kind: key === 'N' ? 'new' : 'fork', text: '' };
|
|
280
334
|
view.prompt = 'New agent name: ';
|
|
@@ -13,13 +13,22 @@ import { ensureMenubar, MenubarManager } from "./menubar.js";
|
|
|
13
13
|
import { UsageError } from "./errors.js";
|
|
14
14
|
import { localResumeTerminal } from "./conversation-local-resume.js";
|
|
15
15
|
import { agentReachability } from "./agent-reachability.js";
|
|
16
|
+
import { promptWithImages, readInitialImages, readInitialPrompt, uploadInitialImages } from "./initial-prompt.js";
|
|
17
|
+
import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
|
|
16
18
|
/** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
|
|
17
19
|
export async function conversationTerminal(ctx, ws, c, project, values) {
|
|
20
|
+
if (c.route === 'codex-cloud')
|
|
21
|
+
return openCodexConversation(ctx, c);
|
|
18
22
|
await restoreConversationBinding(ctx, c, ws);
|
|
19
23
|
return terminal(ctx, ws, c.harness, conversationName(c), project, values, undefined, c.worktree_path, { id: c.id, session: c.harness_session_id });
|
|
20
24
|
}
|
|
21
25
|
/** Select a durable conversation, then use the harness's native terminal for all interaction. */
|
|
22
26
|
export const launchConversationProject = async (ctx, values, [path = './', explicit]) => {
|
|
27
|
+
const text = await readInitialPrompt(ctx, values);
|
|
28
|
+
// Images are read and validated before allocation, held in memory, and written only into the new VM.
|
|
29
|
+
const images = await readInitialImages(ctx, values, text);
|
|
30
|
+
const initialPrompt = text === undefined ? undefined : promptWithImages(text, images);
|
|
31
|
+
values = { ...values, ...(initialPrompt === undefined ? {} : { initialPrompt }) };
|
|
23
32
|
const project = projectIdentity(path), store = new AgentStateStore(ctx.store.dir);
|
|
24
33
|
const app = explicit === 'claude' || explicit === 'codex' ? explicit : store.app(project.remote ?? project.folder);
|
|
25
34
|
if (explicit && explicit !== app)
|
|
@@ -39,9 +48,13 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
|
|
|
39
48
|
return localResumeTerminal(ctx, c, project.folder);
|
|
40
49
|
const next = await resumeConversation(ctx, c);
|
|
41
50
|
rememberConversation(ctx, next, project.folder);
|
|
51
|
+
if (next.route === 'codex-cloud')
|
|
52
|
+
return openCodexConversation(ctx, next);
|
|
42
53
|
const ws = await conversationWorkspace(ctx, next);
|
|
43
54
|
return conversationTerminal(ctx, ws, next, project.remote ?? project.folder, values);
|
|
44
55
|
}
|
|
56
|
+
if (app === 'codex')
|
|
57
|
+
return launchCodexCloud(ctx, values, path, images);
|
|
45
58
|
await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
|
|
46
59
|
const signedIn = app === 'claude' ? await cloudClaudeLogin(ctx) : true;
|
|
47
60
|
if (!signedIn)
|
|
@@ -59,7 +72,10 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
|
|
|
59
72
|
const ws = await ctx.cloud().workspaces.open({ ...agentReachability(values), key: `${project.slug}/${name}`, template: prepared.template,
|
|
60
73
|
labels: { ...projectLabels(project, name, app), ...prepared.labels, repo_path: projectWorkspacePath(project) },
|
|
61
74
|
wait: { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal } });
|
|
75
|
+
if (values.computer === true)
|
|
76
|
+
await ws.setComputerUse(true);
|
|
62
77
|
await seedAgentRepository(ctx, ws, project.folder, { populate: true });
|
|
78
|
+
await uploadInitialImages(ws.cell(), images, ctx.signal);
|
|
63
79
|
if (!signedIn)
|
|
64
80
|
await loginClaudeInWorkspace(ctx, ws);
|
|
65
81
|
if (!(await savedMachine(ctx))?.template)
|
|
@@ -67,6 +83,9 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
|
|
|
67
83
|
return terminal(ctx, ws, app, name, project.remote ?? project.folder, values, prepared.setupHash);
|
|
68
84
|
}
|
|
69
85
|
const { conversation: c, workspace: ws } = created;
|
|
86
|
+
if (values.computer === true)
|
|
87
|
+
await ws.setComputerUse(true);
|
|
88
|
+
await uploadInitialImages(ws.cell(), images, ctx.signal);
|
|
70
89
|
if (values.reachable !== undefined)
|
|
71
90
|
await ctx.cloud().workspaces.open({ key: ws.key, ...agentReachability(values), wait: false });
|
|
72
91
|
if (!signedIn)
|
package/dist/conversations.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface Conversation {
|
|
|
9
9
|
summary: string;
|
|
10
10
|
harness: Harness;
|
|
11
11
|
harness_session_id: string | null;
|
|
12
|
-
route: 'terminal' | 'acp' | 'codex-app' | 'claude-desktop' | 't3-ssh';
|
|
12
|
+
route: 'terminal' | 'acp' | 'codex-app' | 'codex-cloud' | 'claude-desktop' | 't3-ssh';
|
|
13
13
|
workspace_id: string;
|
|
14
14
|
worktree_path: string;
|
|
15
15
|
branch: string;
|
package/dist/conversations.js
CHANGED
|
@@ -104,14 +104,27 @@ export async function createConversationRecord(ctx, folder, harness, route, name
|
|
|
104
104
|
export async function createConversation(ctx, folder, harness, route, name) {
|
|
105
105
|
const project = projectIdentity(folder);
|
|
106
106
|
const c = await createConversationRecord(ctx, project.folder, harness, route, name);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
try {
|
|
108
|
+
const ws = await conversationWorkspace(ctx, c);
|
|
109
|
+
// This is a one-time seed. A conversation never watches or writes the owner's checkout.
|
|
110
|
+
await seedAgentRepository(ctx, ws, project.folder, { root: c.worktree_path, populate: true });
|
|
111
|
+
const cell = ws.cell();
|
|
112
|
+
const result = await cell.exec.run(['git', '-C', c.worktree_path, 'checkout', '-B', c.branch, c.base_commit], { timeoutMs: 30_000, signal: ctx.signal });
|
|
113
|
+
if (result.exitCode !== 0)
|
|
114
|
+
throw new Error(`Conversation branch could not be prepared: ${result.stderr.trim()}`);
|
|
115
|
+
return { conversation: c, workspace: ws };
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (route === 'codex-cloud') {
|
|
119
|
+
try {
|
|
120
|
+
await ctx.cloud().workspaces.delete(c.workspace_id);
|
|
121
|
+
}
|
|
122
|
+
catch (cleanup) {
|
|
123
|
+
throw new AggregateError([error, cleanup], `Cloud setup failed; remove owned workspace ${c.workspace_id} before retrying.`, { cause: cleanup });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
115
128
|
}
|
|
116
129
|
export async function resumeConversation(ctx, c) {
|
|
117
130
|
if (c.state !== 'done' && c.state !== 'archived')
|
|
@@ -5,6 +5,8 @@ type Selection = {
|
|
|
5
5
|
vault: Vault;
|
|
6
6
|
item: string;
|
|
7
7
|
always: boolean;
|
|
8
|
+
scope?: 'ask' | 'project' | 'machine';
|
|
9
|
+
remember?: boolean;
|
|
8
10
|
fields: CredentialRequest['fields'];
|
|
9
11
|
};
|
|
10
12
|
export declare class SiteVaultStore {
|
|
@@ -12,8 +14,8 @@ export declare class SiteVaultStore {
|
|
|
12
14
|
constructor(directory: string);
|
|
13
15
|
key(api: string, project: string, site: string): string;
|
|
14
16
|
read(): Record<string, Selection>;
|
|
15
|
-
get(api: string, project: string, site: string): Selection | undefined;
|
|
16
|
-
save(api: string, project: string, site: string, selection: Selection): void;
|
|
17
|
+
get(api: string, project: string, site: string, profile?: string): Selection | undefined;
|
|
18
|
+
save(api: string, project: string, site: string, selection: Selection, profile?: string): void;
|
|
17
19
|
}
|
|
18
20
|
export declare function approveCredential(ctx: Ctx, request: CredentialRequest, selection: Selection, remember?: boolean): Promise<void>;
|
|
19
21
|
export declare function configureCredentialFill(ctx: Ctx, ws: Workspace): Promise<boolean>;
|
|
@@ -21,12 +21,20 @@ export class SiteVaultStore {
|
|
|
21
21
|
throw new UsageError('Could not read site vault choices.');
|
|
22
22
|
return raw;
|
|
23
23
|
}
|
|
24
|
-
get(api, project, site) {
|
|
25
|
-
|
|
24
|
+
get(api, project, site, profile) {
|
|
25
|
+
const all = this.read();
|
|
26
|
+
return all[this.key(api, project, site)] ?? (profile ? all[this.key(api, `machine:${profile}`, site)] : undefined);
|
|
26
27
|
}
|
|
27
|
-
save(api, project, site, selection) {
|
|
28
|
+
save(api, project, site, selection, profile) {
|
|
28
29
|
const all = this.read();
|
|
29
|
-
|
|
30
|
+
if (selection.scope === 'machine') {
|
|
31
|
+
if (!profile)
|
|
32
|
+
throw new UsageError('Sign in or save a project profile before choosing machine scope.');
|
|
33
|
+
delete all[this.key(api, project, site)];
|
|
34
|
+
all[this.key(api, `machine:${profile}`, site)] = selection;
|
|
35
|
+
}
|
|
36
|
+
else
|
|
37
|
+
all[this.key(api, project, site)] = selection;
|
|
30
38
|
mkdirSync(join(this.file, '..'), { recursive: true, mode: 0o700 });
|
|
31
39
|
const temp = `${this.file}.${process.pid}.tmp`;
|
|
32
40
|
writeFileSync(temp, JSON.stringify(all), { mode: 0o600, flag: 'wx' });
|
|
@@ -34,9 +42,20 @@ export class SiteVaultStore {
|
|
|
34
42
|
chmodSync(this.file, 0o600);
|
|
35
43
|
}
|
|
36
44
|
}
|
|
45
|
+
function vaultProfile(ctx) {
|
|
46
|
+
const profile = ctx.store.profile(ctx.apiUrl);
|
|
47
|
+
return profile.organization_id ? `org:${profile.organization_id}${profile.session?.user?.id ? `|user:${profile.session.user.id}` : ""}` : profile.session?.user?.id ? `user:${profile.session.user.id}` : profile.api_key?.key_id ? `key:${profile.api_key.key_id}` : undefined;
|
|
48
|
+
}
|
|
49
|
+
function approvalScope(value) {
|
|
50
|
+
if (value !== undefined && !['ask', 'project', 'machine'].includes(value))
|
|
51
|
+
throw new UsageError('--scope must be ask, project or machine');
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
37
54
|
export async function approveCredential(ctx, request, selection, remember = false) {
|
|
38
55
|
if (request.state !== 'pending' || Date.parse(request.expires_at) <= Date.now())
|
|
39
56
|
throw new UsageError(`Credential request ${request.id} has expired or ended. ${VAULT_SECURITY_NOTE}`);
|
|
57
|
+
if (selection.scope === 'machine' && !vaultProfile(ctx))
|
|
58
|
+
throw new UsageError('Sign in or save a project profile before choosing machine scope.');
|
|
40
59
|
const credential = await vaultCredential(selection.vault, selection.item, request.site, { signal: ctx.signal });
|
|
41
60
|
for (const value of Object.values(credential))
|
|
42
61
|
if (typeof value === 'string' && value)
|
|
@@ -50,8 +69,8 @@ export async function approveCredential(ctx, request, selection, remember = fals
|
|
|
50
69
|
if (answered.state !== 'fulfilled')
|
|
51
70
|
throw new UsageError(`Credential request is ${answered.state}. ${VAULT_SECURITY_NOTE}`);
|
|
52
71
|
if (remember)
|
|
53
|
-
await ctx.cloud().credentialRequests.remember(request.workspace_id, request.site,
|
|
54
|
-
new SiteVaultStore(ctx.store.dir).save(ctx.apiUrl, request.project_id, request.site, selection);
|
|
72
|
+
await ctx.cloud().credentialRequests.remember(request.workspace_id, request.site, subset, selection.scope ? { scope: selection.scope === 'ask' ? 'conversation' : selection.scope } : {});
|
|
73
|
+
new SiteVaultStore(ctx.store.dir).save(ctx.apiUrl, request.project_id, request.site, { ...selection, always: selection.scope ? selection.scope !== 'ask' : selection.always, remember }, vaultProfile(ctx));
|
|
55
74
|
ctx.io.stderr.write(`Approved ${request.site}. ${remember ? REMEMBERED_SECURITY_NOTE : VAULT_SECURITY_NOTE}\n`);
|
|
56
75
|
}
|
|
57
76
|
export async function configureCredentialFill(ctx, ws) {
|
|
@@ -88,23 +107,23 @@ export async function watchCredentialPrompts(ctx, ws, ask, signal) {
|
|
|
88
107
|
continue;
|
|
89
108
|
seen.add(r.id);
|
|
90
109
|
try {
|
|
91
|
-
const saved = store.get(ctx.apiUrl, r.project_id, r.site);
|
|
92
|
-
if (saved
|
|
93
|
-
await approveCredential(ctx, r, saved);
|
|
110
|
+
const saved = store.get(ctx.apiUrl, r.project_id, r.site, vaultProfile(ctx));
|
|
111
|
+
if (saved && (saved.scope ? saved.scope !== 'ask' : saved.always) && r.fields.every((f) => saved.fields.includes(f))) {
|
|
112
|
+
await approveCredential(ctx, r, saved, saved.remember);
|
|
94
113
|
continue;
|
|
95
114
|
}
|
|
96
|
-
const choice = (await ask(`${r.site} requests ${r.fields.join(', ')}.
|
|
115
|
+
const choice = (await ask(`${r.site} requests ${r.fields.join(', ')}. Ask every time / Allow for this project / Allow on my machine / Deny [once/project/machine/deny]. ${VAULT_SECURITY_NOTE}\n`)).toLowerCase();
|
|
97
116
|
if (choice === 'deny') {
|
|
98
117
|
await ctx.cloud().credentialRequests.deny(r.id);
|
|
99
118
|
ctx.io.stderr.write(`Denied ${r.site}. ${VAULT_SECURITY_NOTE}\n`);
|
|
100
119
|
continue;
|
|
101
120
|
}
|
|
102
|
-
if (
|
|
103
|
-
throw new UsageError(`Choose once,
|
|
121
|
+
if (!['once', 'always', 'project', 'machine'].includes(choice))
|
|
122
|
+
throw new UsageError(`Choose once, project, machine or deny. ${VAULT_SECURITY_NOTE}`);
|
|
104
123
|
const vault = vaultType(saved?.vault ?? (await ask(`Choose hsec, 1password, bitwarden or keychain. ${VAULT_SECURITY_NOTE}\n`)));
|
|
105
124
|
const item = saved?.item ?? (await ask(`Vault item name for ${r.site}. ${VAULT_SECURITY_NOTE}\n`));
|
|
106
125
|
const remember = (await ask(`Remember for unattended use? [yes/no]. ${REMEMBERED_SECURITY_NOTE}\n`)).toLowerCase() === 'yes';
|
|
107
|
-
await approveCredential(ctx, r, { vault, item, always: choice === '
|
|
126
|
+
await approveCredential(ctx, r, { vault, item, always: choice !== 'once', scope: choice === 'once' ? 'ask' : choice === 'machine' ? 'machine' : 'project', fields: r.fields }, remember);
|
|
108
127
|
}
|
|
109
128
|
catch (e) {
|
|
110
129
|
if (signal.aborted)
|
|
@@ -162,7 +181,7 @@ export const CREDENTIAL_HANDLERS = {
|
|
|
162
181
|
const item = str(values.item);
|
|
163
182
|
if (!item)
|
|
164
183
|
throw new UsageError('--item names a vault login');
|
|
165
|
-
await approveCredential(ctx, request, { vault, item, always: values.always === true, fields: request.fields }, values.remember === true);
|
|
184
|
+
await approveCredential(ctx, request, { vault, item, always: values.always === true || ['project', 'machine'].includes(str(values.scope) ?? ''), scope: approvalScope(str(values.scope)), fields: request.fields }, values.remember === true);
|
|
166
185
|
if (ctx.json)
|
|
167
186
|
ctx.io.stdout.write(JSON.stringify({
|
|
168
187
|
approved: true,
|
|
@@ -178,9 +197,12 @@ export const CREDENTIAL_HANDLERS = {
|
|
|
178
197
|
: `Credential request ${r.state}. ${VAULT_SECURITY_NOTE}\n`);
|
|
179
198
|
return 0;
|
|
180
199
|
},
|
|
181
|
-
async 'credentials forget'(ctx,
|
|
200
|
+
async 'credentials forget'(ctx, values, [ref, site]) {
|
|
182
201
|
const ws = await resolveWorkspace(ctx, ref);
|
|
183
|
-
|
|
202
|
+
const scope = str(values.scope);
|
|
203
|
+
if (scope && !['conversation', 'project', 'machine'].includes(scope))
|
|
204
|
+
throw new UsageError('--scope must be conversation, project or machine');
|
|
205
|
+
await ctx.cloud().credentialRequests.forget(ws.id, site, { scope: scope });
|
|
184
206
|
ctx.io.stdout.write(ctx.json
|
|
185
207
|
? JSON.stringify({
|
|
186
208
|
forgotten: true,
|
package/dist/device-bridge.d.ts
CHANGED
|
@@ -31,6 +31,14 @@ export interface DeviceApproval {
|
|
|
31
31
|
conversation_id: string;
|
|
32
32
|
summary: string;
|
|
33
33
|
outside: boolean;
|
|
34
|
+
kind?: DeviceRequest['kind'];
|
|
35
|
+
argv?: string[];
|
|
36
|
+
local_path?: string;
|
|
37
|
+
remote_path?: string;
|
|
38
|
+
project_name?: string;
|
|
39
|
+
conversation_title?: string;
|
|
40
|
+
local_port?: number;
|
|
41
|
+
remote_port?: number;
|
|
34
42
|
}
|
|
35
43
|
type Result = {
|
|
36
44
|
state: 'approved' | 'denied' | 'running' | 'done' | 'failed';
|
|
@@ -56,6 +64,11 @@ export declare class DeviceGrants {
|
|
|
56
64
|
save(api: string, project: string, root: string, kind: string): Promise<void>;
|
|
57
65
|
}
|
|
58
66
|
export declare function deviceSummary(r: DeviceRequest): string;
|
|
67
|
+
/** Structured local approval details; the summary remains for older menu bar and ACP clients. */
|
|
68
|
+
export declare function deviceApproval(r: DeviceRequest, c: Conversation, target: {
|
|
69
|
+
path: string;
|
|
70
|
+
outside: boolean;
|
|
71
|
+
}, folder: string): DeviceApproval;
|
|
59
72
|
export declare function tryLocallyAction(r: DeviceRequest): 'start' | 'stop' | null;
|
|
60
73
|
export declare function ensureConversationWorktree(ctx: Ctx, c: Conversation, folder: string, ws?: Workspace): Promise<string>;
|
|
61
74
|
export declare function deviceExec(argv: string[], cwd: string, timeoutSeconds?: number, signal?: AbortSignal): Promise<Result>;
|
package/dist/device-bridge.js
CHANGED
|
@@ -3,7 +3,7 @@ import { spawn } from 'node:child_process';
|
|
|
3
3
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs';
|
|
4
4
|
import { readFile, writeFile, mkdir, realpath, stat } from 'node:fs/promises';
|
|
5
5
|
import { createConnection, createServer } from 'node:net';
|
|
6
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
7
7
|
import { WorkspaceTunnels } from '@shardflux/sdk';
|
|
8
8
|
import { writePrivateFile } from "./credentials.js";
|
|
9
9
|
import { conversationName, conversationWorkspace, getConversation, localConversations } from "./conversations.js";
|
|
@@ -73,8 +73,18 @@ export function deviceSummary(r) {
|
|
|
73
73
|
if (r.kind === 'exec')
|
|
74
74
|
return `Run on your Mac: ${JSON.stringify(r.exec?.argv)} · directory ${r.exec?.cwd_relative ?? '.'}`;
|
|
75
75
|
if (r.files)
|
|
76
|
-
return `${r.kind === 'files_pull' ? '
|
|
77
|
-
return `${r.kind === 'port_forward' ? 'Open
|
|
76
|
+
return `${r.kind === 'files_pull' ? 'Share with conversation' : 'Save on your Mac'}: ${r.files.local_path_relative} ↔ ${r.files.remote_path}`;
|
|
77
|
+
return `${r.kind === 'port_forward' ? 'Open conversation preview on your Mac' : 'Connect conversation to your Mac'}: ${r.port?.remote_port} ↔ localhost:${r.port?.local_port}`;
|
|
78
|
+
}
|
|
79
|
+
/** Structured local approval details; the summary remains for older menu bar and ACP clients. */
|
|
80
|
+
export function deviceApproval(r, c, target, folder) {
|
|
81
|
+
return {
|
|
82
|
+
type: 'approval', request_id: r.id, project_id: r.project_id, conversation_id: c.id,
|
|
83
|
+
summary: deviceSummary(r), outside: target.outside, kind: r.kind, local_path: target.path,
|
|
84
|
+
project_name: basename(folder), conversation_title: c.title || conversationName(c),
|
|
85
|
+
...(r.exec ? { argv: r.exec.argv } : {}), ...(r.files ? { remote_path: r.files.remote_path } : {}),
|
|
86
|
+
...(r.port ? { local_port: r.port.local_port, remote_port: r.port.remote_port } : {}),
|
|
87
|
+
};
|
|
78
88
|
}
|
|
79
89
|
export function tryLocallyAction(r) {
|
|
80
90
|
if (r.kind === 'try_locally')
|
|
@@ -172,7 +182,7 @@ export class DeviceExecutor {
|
|
|
172
182
|
const requested = r.exec?.cwd_relative ?? r.files?.local_path_relative ?? '.';
|
|
173
183
|
const target = devicePath(planned.path, requested), grants = new DeviceGrants(ctx.store.dir);
|
|
174
184
|
target.outside ||= planned.outside;
|
|
175
|
-
const choice = grants.allows(ctx.apiUrl, r.project_id, record.folder, r.kind, target.outside) ? 'once' : await this.#ask(
|
|
185
|
+
const choice = grants.allows(ctx.apiUrl, r.project_id, record.folder, r.kind, target.outside) ? 'once' : await this.#ask(deviceApproval(r, c, target, record.folder));
|
|
176
186
|
if (choice === 'deny') {
|
|
177
187
|
await this.result(r, { state: 'denied' });
|
|
178
188
|
return;
|
package/dist/device-helper.js
CHANGED
|
@@ -46,7 +46,8 @@ export async function runDeviceHelper(ctx) {
|
|
|
46
46
|
ctx.io.stdout.write(JSON.stringify(approval) + '\n');
|
|
47
47
|
if (signal.aborted)
|
|
48
48
|
denied();
|
|
49
|
-
})
|
|
49
|
+
}).finally(() => { if (!signal.aborted)
|
|
50
|
+
ctx.io.stdout.write(JSON.stringify({ type: 'approval_resolved', request_id: approval.request_id }) + '\n'); });
|
|
50
51
|
};
|
|
51
52
|
const working = new Map();
|
|
52
53
|
const mirrored = new ConversationMirrorState();
|
package/dist/errors.d.ts
CHANGED
|
@@ -45,6 +45,15 @@ export declare class CodedError extends HintedError {
|
|
|
45
45
|
readonly details: Record<string, unknown> | undefined;
|
|
46
46
|
constructor(code: string, message: string, hint?: string, details?: Record<string, unknown>);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* A command that can create an account ran without the agreement to the Terms of Service (0.15.0+): no
|
|
50
|
+
* --accept-terms / SHARDFLUX_ACCEPT_TERMS=1 and no terminal to ask on, or the person answered no. Nothing was sent.
|
|
51
|
+
* Code `terms_not_accepted`, exit 2 (usage); `details` has both links and the flag.
|
|
52
|
+
*/
|
|
53
|
+
export declare class TermsNotAcceptedError extends CodedError {
|
|
54
|
+
name: string;
|
|
55
|
+
constructor(declined?: boolean);
|
|
56
|
+
}
|
|
48
57
|
/** No usable credential: SHARDFLUX_API_KEY / the saved key, or the session, missing or malformed (never carries it). */
|
|
49
58
|
export declare class AuthConfigError extends HintedError {
|
|
50
59
|
name: string;
|
package/dist/errors.js
CHANGED
|
@@ -66,6 +66,19 @@ export class CodedError extends HintedError {
|
|
|
66
66
|
this.details = details;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* A command that can create an account ran without the agreement to the Terms of Service (0.15.0+): no
|
|
71
|
+
* --accept-terms / SHARDFLUX_ACCEPT_TERMS=1 and no terminal to ask on, or the person answered no. Nothing was sent.
|
|
72
|
+
* Code `terms_not_accepted`, exit 2 (usage); `details` has both links and the flag.
|
|
73
|
+
*/
|
|
74
|
+
export class TermsNotAcceptedError extends CodedError {
|
|
75
|
+
name = 'TermsNotAcceptedError';
|
|
76
|
+
constructor(declined = false) {
|
|
77
|
+
super('terms_not_accepted', declined
|
|
78
|
+
? 'Not signed up: creating an account needs your agreement to the Terms of Service and Privacy Policy.'
|
|
79
|
+
: 'Re-run with --accept-terms to agree to the Terms of Service (https://shardflux.dev/terms) and Privacy Policy (https://shardflux.dev/privacy); creating an account needs that agreement.', 'read https://shardflux.dev/terms and https://shardflux.dev/privacy, then re-run with --accept-terms (or set SHARDFLUX_ACCEPT_TERMS=1)', { terms_url: 'https://shardflux.dev/terms', privacy_url: 'https://shardflux.dev/privacy', flag: '--accept-terms' });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
69
82
|
/** No usable credential: SHARDFLUX_API_KEY / the saved key, or the session, missing or malformed (never carries it). */
|
|
70
83
|
export class AuthConfigError extends HintedError {
|
|
71
84
|
name = 'AuthConfigError';
|
|
@@ -139,7 +152,7 @@ export class ExecSessionRunningError extends HintedError {
|
|
|
139
152
|
this.session = session;
|
|
140
153
|
}
|
|
141
154
|
}
|
|
142
|
-
const AUTH_CODES = new Set(['unauthenticated', 'invalid_credentials', 'token_invalid', 'forbidden', 'mfa_required', 'email_unverified', 'step_up_required', 'csrf_failed']);
|
|
155
|
+
const AUTH_CODES = new Set(['unauthenticated', 'invalid_credentials', 'token_invalid', 'forbidden', 'mfa_required', 'email_unverified', 'step_up_required', 'csrf_failed', 'access_denied', 'expired_token']);
|
|
143
156
|
const KEY_PATTERN = /sfk_[A-Za-z0-9]{8,}_[A-Za-z0-9]+/g;
|
|
144
157
|
/** User session tokens (`sfu_` + 43 base64url characters), also cut short. */
|
|
145
158
|
const SESSION_PATTERN = /sfu_[A-Za-z0-9_-]{16,}/g;
|
|
@@ -261,7 +274,7 @@ export function describeError(err) {
|
|
|
261
274
|
export function exitCodeFor(err, interrupted = false) {
|
|
262
275
|
if (interrupted || err instanceof InterruptedError)
|
|
263
276
|
return EXIT.interrupted;
|
|
264
|
-
if (err instanceof UsageError || err instanceof TemplateFileError)
|
|
277
|
+
if (err instanceof UsageError || err instanceof TemplateFileError || err instanceof TermsNotAcceptedError)
|
|
265
278
|
return EXIT.usage;
|
|
266
279
|
if (err instanceof AuthConfigError || err instanceof MfaPendingError)
|
|
267
280
|
return EXIT.auth;
|
package/dist/format.js
CHANGED
|
@@ -813,10 +813,12 @@ export function keyStatus(k, now = Date.now()) {
|
|
|
813
813
|
return 'expired';
|
|
814
814
|
return 'active';
|
|
815
815
|
}
|
|
816
|
+
/** Every workspace tool a key can carry (`computer` since the API's computer use). */
|
|
817
|
+
const ALL_TOOLS = ['exec', 'files', 'pty', 'process', 'git', 'browser', 'computer'];
|
|
816
818
|
export function toolsText(tools) {
|
|
817
819
|
if (tools.length === 0)
|
|
818
820
|
return '(none: read-only)';
|
|
819
|
-
return
|
|
821
|
+
return ALL_TOOLS.every((t) => tools.includes(t)) ? 'all' : tools.join(',');
|
|
820
822
|
}
|
|
821
823
|
export function apiKeyTable(rows, savedId) {
|
|
822
824
|
return table(['', 'ID', 'KEY ID', 'NAME', 'TOOLS', 'STATUS', 'CREATED', 'EXPIRES', 'LAST USED'], rows.map((k) => [mark(k.id === savedId), k.id, k.key_id, k.name, toolsText(k.tool_permissions), keyStatus(k), time(k.created_at), time(k.expires_at), time(k.last_used_at)]));
|
package/dist/handlers.js
CHANGED
|
@@ -15,4 +15,5 @@ import { MENUBAR_HANDLERS } from "./menubar.js";
|
|
|
15
15
|
import { UPDATE_HANDLERS } from "./update.js";
|
|
16
16
|
import { FILESYSTEM_HANDLERS } from "./filesystem-commands.js";
|
|
17
17
|
import { CONVERSATION_HANDLERS as LOCAL_CONVERSATION_HANDLERS } from "./conversation-local.js";
|
|
18
|
-
|
|
18
|
+
import { CODEX_CLOUD_HANDLERS } from "./codex-cloud.js";
|
|
19
|
+
export const HANDLERS = { ...CODEX_CLOUD_HANDLERS, ...CONVERSATION_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...UPDATE_HANDLERS };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Ctx } from './commands.js';
|
|
2
|
+
import type { Values } from './args.js';
|
|
3
|
+
import type { TerminalConnection } from './agent-terminal.js';
|
|
4
|
+
import type { CellClient } from '@shardflux/sdk';
|
|
5
|
+
/** The user chose this repository. Trust exactly that new VM worktree before launching Codex's first turn. */
|
|
6
|
+
export declare function prepareInitialCodexProject(cell: CellClient, cwd: string, ptyId: string): Promise<void>;
|
|
7
|
+
export declare function readInitialPrompt(ctx: Ctx, values: Values): Promise<string | undefined>;
|
|
8
|
+
/** Images for a new conversation's first message. They are held in memory and written only into the conversation VM. */
|
|
9
|
+
export interface InitialImage {
|
|
10
|
+
path: string;
|
|
11
|
+
data: Uint8Array;
|
|
12
|
+
type: 'png' | 'jpeg' | 'gif' | 'webp';
|
|
13
|
+
}
|
|
14
|
+
export declare const ATTACHMENT_DIR = "/home/user/.shardflux/attachments";
|
|
15
|
+
export declare const MAX_INITIAL_IMAGES = 5;
|
|
16
|
+
export declare const MAX_INITIAL_IMAGE_BYTES: number;
|
|
17
|
+
/** Standard input may carry several PNG files back to back (the menu bar's in-memory captures). */
|
|
18
|
+
export declare function splitImages(data: Uint8Array): Uint8Array[];
|
|
19
|
+
/** Reads and validates `--image <file|->` before any VM is allocated. */
|
|
20
|
+
export declare function readInitialImages(ctx: Ctx, values: Values, prompt: string | undefined): Promise<InitialImage[]>;
|
|
21
|
+
/** One VM path per line: Claude Code attaches each as an image when the prompt is pasted; Codex opens it with view_image. */
|
|
22
|
+
export declare function promptWithImages(prompt: string, images: readonly InitialImage[]): string;
|
|
23
|
+
/** Writes the images into the conversation VM before the first message refers to them. */
|
|
24
|
+
export declare function uploadInitialImages(cell: Pick<CellClient, 'files'>, images: readonly InitialImage[], signal?: AbortSignal): Promise<void>;
|
|
25
|
+
/** Wait for the native terminal to enable paste before submitting one first turn. No blind timing or input replay. */
|
|
26
|
+
export declare function submitInitialPrompt(connect: (signal: AbortSignal) => Promise<TerminalConnection>, prompt: string, workspaceKey: string, signal: AbortSignal, app?: 'claude' | 'codex', write?: (bytes: Uint8Array) => Promise<unknown>): Promise<void>;
|