@shardflux/cli 0.14.5 → 0.15.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 +61 -0
- package/README.md +72 -10
- package/dist/account-args.js +28 -7
- package/dist/account-commands.js +140 -11
- 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 +55 -4
- package/dist/agent-git-commands.js +2 -1
- package/dist/agent-reachability.js +1 -1
- package/dist/agent-target-commands.js +26 -1
- 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/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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { browserSessionNote, workspaceBrowserSessionNote } from "./browser-profile-copy.js";
|
|
2
1
|
import { agentName } from "./command-center-model.js";
|
|
3
2
|
import { setupLine } from "./assisted-setup.js";
|
|
3
|
+
import { terminalBrand } from "./terminal-brand.js";
|
|
4
4
|
/** API titles and local input never get to send terminal controls. */
|
|
5
5
|
export function plain(value) { return [...value].map(ch => { const code = ch.codePointAt(0); return code < 32 || code >= 127 && code <= 159 ? ' ' : ch; }).join(''); }
|
|
6
6
|
function width(ch) {
|
|
@@ -23,63 +23,67 @@ export function clip(value, columns) {
|
|
|
23
23
|
export function renderCenter(view, columns, height, color = true) {
|
|
24
24
|
columns = Math.max(1, Math.min(1000, columns));
|
|
25
25
|
height = Math.max(1, Math.min(1000, height));
|
|
26
|
-
const
|
|
26
|
+
const prompt = view.prompt?.replace(/ {2}\(Enter accepts, Esc cancels\)$/, '');
|
|
27
|
+
const lines = [prompt ? ` ${prompt}` : ' + New conversation n', view.filter && !prompt ? ` Search: ${view.filter}` : ''];
|
|
28
|
+
const selectedRow = view.rows.find(row => row.id === view.selected);
|
|
27
29
|
const body = [];
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const age = (since) => {
|
|
31
|
+
if (!since || !Number.isFinite(Date.parse(since)))
|
|
32
|
+
return '';
|
|
33
|
+
const minutes = Math.max(0, Math.floor(((view.now ?? Date.now()) - Date.parse(since)) / 60000));
|
|
34
|
+
return minutes < 1 ? 'now' : minutes < 60 ? `${minutes}m` : minutes < 1440 ? `${Math.floor(minutes / 60)}h` : `${Math.floor(minutes / 1440)}d`;
|
|
35
|
+
};
|
|
34
36
|
for (const row of view.rows) {
|
|
35
|
-
if (row.project !== project) {
|
|
36
|
-
project = row.project;
|
|
37
|
-
key = '';
|
|
38
|
-
body.push({ text: project });
|
|
39
|
-
const setup = view.rows.find(candidate => candidate.project === project && candidate.setup)?.setup;
|
|
40
|
-
const progress = setup && setupLine(project, setup);
|
|
41
|
-
if (progress)
|
|
42
|
-
body.push({ text: ` ${progress}` });
|
|
43
|
-
}
|
|
44
37
|
if (row.start) {
|
|
45
|
-
body.push({ text:
|
|
38
|
+
body.push({ text: ` Start a conversation in ${row.project}`, id: row.id });
|
|
46
39
|
continue;
|
|
47
40
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
const session = row.agent?.id.slice(row.agent.workspace_id.length + 1);
|
|
56
|
-
body.push({ text: ` [${row.badge}] ${row.agent ? `${row.agent.app} ${clip(session, 12)} ${row.agent.title || 'Untitled session'}` : 'Open a session'}`, id: row.id, badge: row.badge });
|
|
41
|
+
const title = row.agent?.title || (row.agent ? 'Untitled conversation' : 'Open a conversation');
|
|
42
|
+
const project = `${row.project}/${agentName(row.key, row.agent?.labels ?? row.workspace?.labels ?? {})}`;
|
|
43
|
+
const suffix = ` ${clip(project, Math.max(8, Math.floor(columns * 0.29)))} ${age(row.agent?.since).padStart(3)}`;
|
|
44
|
+
const titleWidth = Math.max(1, columns - suffix.length - 7);
|
|
45
|
+
const name = clip(title, titleWidth);
|
|
46
|
+
body.push({ text: ` ${row.badge === 'done' ? '○' : '●'} ${name}${' '.repeat(Math.max(0, titleWidth - [...name].reduce((n, c) => n + width(c), 0)))}${suffix}`, id: row.id, badge: row.badge });
|
|
57
47
|
}
|
|
58
48
|
if (!body.length)
|
|
59
|
-
body.push({ text: view.
|
|
49
|
+
body.push({ text: view.message ? '' : view.filter ? ' No conversations match this search.' : ' Your conversations will appear here.' });
|
|
60
50
|
const available = Math.max(0, height - 7);
|
|
61
51
|
const selected = body.findIndex(r => r.id === view.selected);
|
|
62
52
|
const start = Math.max(0, selected - available + 1);
|
|
63
53
|
for (const row of body.slice(start, start + available)) {
|
|
64
|
-
let line = clip(
|
|
54
|
+
let line = clip(row.text, columns);
|
|
55
|
+
if (row.id && row.id === view.selected)
|
|
56
|
+
line = '›' + line.slice(1);
|
|
65
57
|
if (color) {
|
|
66
58
|
if (row.badge === 'working')
|
|
67
|
-
line = `\x1b[38;
|
|
59
|
+
line = line.replace('●', `\x1b[38;2;${terminalBrand['sf-molten'].join(';')}m●\x1b[39m`);
|
|
68
60
|
if (row.badge === 'done')
|
|
69
|
-
line = `\x1b[2m${line}\x1b[
|
|
70
|
-
if (row.id === view.selected)
|
|
71
|
-
line = `\x1b[
|
|
61
|
+
line = `\x1b[2m${line}\x1b[22m`;
|
|
62
|
+
if (row.id && row.id === view.selected)
|
|
63
|
+
line = `\x1b[48;2;${terminalBrand['sf-overlay'].join(';')}m\x1b[38;2;${terminalBrand['sf-ink'].join(';')}m${line}\x1b[0m`;
|
|
72
64
|
}
|
|
73
65
|
lines.push(line);
|
|
74
66
|
}
|
|
75
|
-
while (lines.length < height -
|
|
67
|
+
while (lines.length < height - 5)
|
|
76
68
|
lines.push('');
|
|
77
|
-
|
|
69
|
+
const profiles = selectedRow?.browserProfiles ?? [];
|
|
70
|
+
const sites = (rows) => [...new Set(rows.flatMap(p => p.sites.map(s => s.site)))].join(', ');
|
|
71
|
+
const sharedSites = sites(profiles.filter(p => p.scope === 'project'));
|
|
72
|
+
const ownSites = sites(profiles.filter(p => p.scope !== 'project'));
|
|
73
|
+
const browserContext = [sharedSites && `project · ${sharedSites}`, ownSites && `conversation · ${ownSites}`].filter(Boolean).join(' | ');
|
|
74
|
+
const detail = selectedRow && !selectedRow.start ? `${selectedRow.agent?.app === 'codex' ? 'Codex' : 'Claude'} · ${selectedRow.badge}${selectedRow.agent?.last_message ? ` · ${selectedRow.agent.last_message}` : ''}` : '';
|
|
75
|
+
lines.push(view.message || (selectedRow?.setup && setupLine(selectedRow.project, selectedRow.setup)) || detail);
|
|
76
|
+
lines.push(browserContext ? ` Browser sign-ins: ${browserContext}` : '');
|
|
77
|
+
lines.push(view.doneCount ? ` ${view.doneCount} done · D ${view.showDone ? 'hide' : 'show'}` : '');
|
|
78
|
+
lines.push(view.prompt ? ' Enter continue Esc cancel' : ' Enter open n new / search ? actions q quit');
|
|
79
|
+
lines.push(view.menubarHint ?? '');
|
|
78
80
|
if (view.menu) {
|
|
79
|
-
const menu = [view.menu.title, ...view.menu.items.map((item, i) => `${i === view.menu.selected ? '
|
|
80
|
-
const offset = Math.min(
|
|
81
|
+
const menu = [` ${view.menu.title}`, '', ...view.menu.items.map((item, i) => `${i === view.menu.selected ? '›' : ' '} ${item}`), '', ' Enter select Esc back'];
|
|
82
|
+
const offset = Math.min(2, Math.max(0, height - menu.length));
|
|
81
83
|
for (let i = 0; i < menu.length && offset + i < height; i++)
|
|
82
|
-
lines[offset + i] = menu[i];
|
|
84
|
+
lines[offset + i] = clip(menu[i], columns);
|
|
85
|
+
for (let i = offset + menu.length; i < height - 1; i++)
|
|
86
|
+
lines[i] = '';
|
|
83
87
|
}
|
|
84
88
|
return lines.slice(0, height).map(line => line.includes('\x1b') ? line : clip(line, columns));
|
|
85
89
|
}
|
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;
|