@shardflux/cli 0.25.0 → 0.28.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 +130 -1
- package/README.md +145 -34
- package/dist/account-pool.d.ts +11 -1
- package/dist/account-pool.js +48 -0
- package/dist/agent-commands.js +4 -3
- package/dist/agent-models.d.ts +31 -1
- package/dist/agent-models.js +27 -4
- package/dist/args.d.ts +1 -1
- package/dist/args.js +67 -12
- package/dist/board-args.js +8 -3
- package/dist/board-commands.d.ts +7 -3
- package/dist/board-commands.js +22 -12
- package/dist/brain-commands.d.ts +2 -0
- package/dist/brain-commands.js +78 -0
- package/dist/client-settings.d.ts +2 -1
- package/dist/client-settings.js +14 -2
- package/dist/codex-cloud.d.ts +2 -0
- package/dist/codex-cloud.js +14 -11
- package/dist/commands.js +8 -0
- package/dist/conversation-commands.d.ts +5 -1
- package/dist/conversation-commands.js +58 -6
- package/dist/conversation-terminal.js +2 -2
- package/dist/desktop-linux.d.ts +12 -1
- package/dist/desktop-linux.js +47 -18
- package/dist/device-approvals.js +24 -2
- package/dist/device-bridge.d.ts +21 -4
- package/dist/device-bridge.js +173 -7
- package/dist/device-folders.d.ts +6 -1
- package/dist/device-folders.js +6 -2
- package/dist/device-helper.d.ts +8 -0
- package/dist/device-helper.js +71 -12
- package/dist/device-mode.d.ts +44 -0
- package/dist/device-mode.js +119 -0
- package/dist/drive-commands.d.ts +2 -2
- package/dist/drive-commands.js +1 -1
- package/dist/handlers.js +4 -2
- package/dist/land.d.ts +9 -4
- package/dist/land.js +52 -6
- package/dist/mac-app.d.ts +11 -0
- package/dist/mac-app.js +11 -0
- package/dist/main.js +2 -0
- package/dist/menubar.d.ts +18 -13
- package/dist/menubar.js +60 -50
- package/dist/orchestrator-args.d.ts +7 -0
- package/dist/orchestrator-args.js +64 -0
- package/dist/orchestrator-commands.d.ts +43 -0
- package/dist/orchestrator-commands.js +262 -0
- package/dist/person-scope.d.ts +20 -0
- package/dist/person-scope.js +61 -0
- package/dist/project-hub-seed.d.ts +20 -0
- package/dist/project-hub-seed.js +59 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +2 -1
- package/dist/router-commands.d.ts +12 -21
- package/dist/router-commands.js +5 -59
- package/dist/t3-launch.js +2 -2
- package/dist/task-land-hub.py +16 -1
- package/dist/this-computer.d.ts +4 -2
- package/dist/this-computer.js +20 -2
- package/dist/thread-on-computer.js +2 -2
- package/dist/timing.js +3 -1
- package/dist/transcribe-commands.js +6 -2
- package/dist/update.js +4 -2
- package/dist/wait-status.d.ts +1 -1
- package/dist/wait-status.js +2 -0
- package/dist/worktree-twin-engine.js +1 -1
- package/dist/worktree-twin.d.ts +6 -0
- package/dist/worktree-twin.js +8 -1
- package/package.json +6 -5
package/dist/client-settings.js
CHANGED
|
@@ -4,6 +4,7 @@ import { UsageError } from "./errors.js";
|
|
|
4
4
|
import { objectValue } from "./acp-rpc.js";
|
|
5
5
|
import { openInTargets } from "./t3-mode.js";
|
|
6
6
|
import { storedTerminal, terminalName } from "./terminal-apps.js";
|
|
7
|
+
import { TwinLedger } from "./worktree-twin.js";
|
|
7
8
|
/** The platform's marker (the guest's "☁ " without its space). */
|
|
8
9
|
export const DEFAULT_TITLE_PREFIX = '☁';
|
|
9
10
|
/** At most this many characters on one line, as the API takes it (POST /v1/conversations title_prefix). */
|
|
@@ -43,7 +44,7 @@ export function saveTitlePrefix(dir, choice) {
|
|
|
43
44
|
doc.title_prefix = choice; });
|
|
44
45
|
}
|
|
45
46
|
/** `shard settings --json`. title_prefix: null is the default, "" none. open_in and terminal_app are the saved
|
|
46
|
-
* choices (null: automatic). */
|
|
47
|
+
* choices (null: automatic). auto_land (0.27.0+): finished cloud work lands in your folders on its own (default on). */
|
|
47
48
|
export function clientSettings(dir) {
|
|
48
49
|
const doc = new AgentStateStore(dir).read();
|
|
49
50
|
const saved = objectValue(doc.open_in);
|
|
@@ -58,6 +59,7 @@ export function clientSettings(dir) {
|
|
|
58
59
|
title_prefix_default: DEFAULT_TITLE_PREFIX,
|
|
59
60
|
open_in: { claude: openIn('claude'), codex: openIn('codex') },
|
|
60
61
|
terminal_app: terminal ? { ...terminal, name: terminalName(terminal.id) } : null,
|
|
62
|
+
auto_land: new TwinLedger(dir).autoLand(),
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
const sampleTitle = (prefix) => `${prefix === null ? DEFAULT_TITLE_PREFIX + ' ' : prefix ? prefix + ' ' : ''}${SAMPLE}`;
|
|
@@ -68,11 +70,21 @@ export function settingsText(s) {
|
|
|
68
70
|
`Title marker ${markerLine(s.title_prefix)} "${sampleTitle(s.title_prefix)}"`,
|
|
69
71
|
`Open in Claude: ${target(s.open_in.claude)} · Codex: ${target(s.open_in.codex)}`,
|
|
70
72
|
`Terminal app ${s.terminal_app ? `${s.terminal_app.name}, ${s.terminal_app.mode === 'tab' ? 'new tab' : 'new window'}` : 'automatic'}`,
|
|
73
|
+
`Auto land ${s.auto_land ? 'on: finished cloud work lands in your folder' : 'off'}`,
|
|
71
74
|
'',
|
|
72
|
-
'Change the marker with shard settings --title-prefix <text|none|default
|
|
75
|
+
'Change the marker with shard settings --title-prefix <text|none|default>, auto land with --auto-land on|off; where conversations open with shard agents targets --open-in and --terminal.',
|
|
73
76
|
].join('\n') + '\n';
|
|
74
77
|
}
|
|
75
78
|
function settings(ctx, values) {
|
|
79
|
+
const land = values['auto-land'];
|
|
80
|
+
if (land !== undefined) {
|
|
81
|
+
if (land !== 'on' && land !== 'off')
|
|
82
|
+
throw new UsageError('--auto-land takes on or off.');
|
|
83
|
+
new TwinLedger(ctx.store.dir).setAutoLand(land === 'on');
|
|
84
|
+
const s = clientSettings(ctx.store.dir);
|
|
85
|
+
out(ctx, s.auto_land ? 'Finished cloud work lands in your folder automatically.\n' : 'Cloud work stays in the cloud until you land it (shard task land, shard conversations land).\n', s);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
76
88
|
const spec = values['title-prefix'];
|
|
77
89
|
if (spec !== undefined && typeof spec !== 'string')
|
|
78
90
|
throw new UsageError('--title-prefix takes text, none or default.');
|
package/dist/codex-cloud.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type { InitialImage } from './initial-prompt.js';
|
|
|
8
8
|
/** Cloud threads open in the browser, preserving the durable provider destination on every OS. */
|
|
9
9
|
export declare function openCodexThread(task: CodexCloudTask, env: Ctx['io']['env'], platform?: NodeJS.Platform): Promise<void>;
|
|
10
10
|
export declare function submitCloudInitialPrompt(ctx: Ctx, c: Conversation, prompt: string, workspaceKey: string): Promise<void>;
|
|
11
|
+
/** Admit the already seeded executor and hand its first prompt to the durable inbox. */
|
|
12
|
+
export declare function startCloudTask(ctx: Ctx, c: Conversation, workspaceKey: string, prompt?: string): Promise<CodexCloudTask>;
|
|
11
13
|
/** Prints the Cloud thread (and its link) and opens it in your browser; --json prints it instead. */
|
|
12
14
|
export declare function printCloudThread(ctx: Ctx, c: Conversation, task: CodexCloudTask, submitted?: boolean, choice?: ModelChoice): Promise<void>;
|
|
13
15
|
/**
|
package/dist/codex-cloud.js
CHANGED
|
@@ -5,7 +5,7 @@ import { CodedError, UsageError } from "./errors.js";
|
|
|
5
5
|
import { json } from "./format.js";
|
|
6
6
|
import { syncAgentCredentials } from "./agent-credentials.js";
|
|
7
7
|
import { createConversation, conversationWorkspace, getConversation, localConversations, recordedChoice, resumeConversation, seedConversation } from "./conversations.js";
|
|
8
|
-
import { CHOICE_PENDING, choiceReport, hasChoice, modelChoice } from "./agent-models.js";
|
|
8
|
+
import { CHOICE_PENDING, choiceReport, hasChoice, liveModels, modelChoice } from "./agent-models.js";
|
|
9
9
|
import { AgentStateStore } from "./agent-state.js";
|
|
10
10
|
import { installedTargets } from "./agent-targets.js";
|
|
11
11
|
import { ensureProjectGit } from "./project-git.js";
|
|
@@ -39,6 +39,15 @@ export async function submitCloudInitialPrompt(ctx, c, prompt, workspaceKey) {
|
|
|
39
39
|
throw new CodedError('initial_prompt_failed', `Created ${workspaceKey}, but the initial prompt could not be confirmed. Open that conversation before starting another.`, undefined, { workspace_key: workspaceKey, conversation_id: c.id, submitted: null });
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/** Admit the already seeded executor and hand its first prompt to the durable inbox. */
|
|
43
|
+
export async function startCloudTask(ctx, c, workspaceKey, prompt) {
|
|
44
|
+
await ctx.cloud().codex.admit(c.id, { idempotencyKey: `codex-${c.id}`, signal: ctx.signal });
|
|
45
|
+
// The durable inbox holds this input until the executor is active. Setup
|
|
46
|
+
// continues while the prompt is queued; a lost send retains the stable key.
|
|
47
|
+
if (prompt !== undefined)
|
|
48
|
+
await submitCloudInitialPrompt(ctx, c, prompt, workspaceKey);
|
|
49
|
+
return ctx.cloud().codex.waitTask(c.id, { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal });
|
|
50
|
+
}
|
|
42
51
|
/** Prints the Cloud thread (and its link) and opens it in your browser; --json prints it instead. */
|
|
43
52
|
export async function printCloudThread(ctx, c, task, submitted = false, choice) { return output(ctx, c, task, submitted, choice); }
|
|
44
53
|
/** Whether this computer has the Codex app, which opens a Cloud thread's codex:// link (on Linux 0.23.0+). */
|
|
@@ -215,7 +224,7 @@ export async function launchCodexCloud(ctx, values, path, images = []) {
|
|
|
215
224
|
throw new UsageError('New Codex conversations open in Codex Cloud. Omit --in or use --in codex-app.');
|
|
216
225
|
if (values.like !== undefined)
|
|
217
226
|
throw new UsageError('Choose the project directory with --path; new Codex conversations use its latest machine template.');
|
|
218
|
-
const choice = modelChoice(values, 'codex');
|
|
227
|
+
const choice = modelChoice(values, 'codex', await liveModels(ctx, values, 'codex'));
|
|
219
228
|
const project = projectIdentity(path);
|
|
220
229
|
await ensureProjectGit(ctx, project.folder);
|
|
221
230
|
await gitRunner(project.folder, { ...process.env, ...ctx.io.env }, ctx.signal)(['rev-parse', 'HEAD']);
|
|
@@ -247,12 +256,9 @@ export async function launchCodexCloud(ctx, values, path, images = []) {
|
|
|
247
256
|
// --computer: the conversation VM's desktop is on before Codex starts, so the thread gets the computer tools.
|
|
248
257
|
// The thread's tools run in this VM, so its first message can name the images by their VM paths.
|
|
249
258
|
// Idempotency follows the durable conversation, including a lost response.
|
|
250
|
-
await ctx.cloud().codex.admit(c.id, { idempotencyKey: `codex-${c.id}`, signal: ctx.signal });
|
|
251
|
-
const task = await ctx.cloud().codex.waitTask(c.id, { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal });
|
|
252
|
-
new AgentStateStore(ctx.store.dir).save(project.remote ?? project.folder, 'codex');
|
|
253
259
|
const submitted = typeof values.initialPrompt === 'string';
|
|
254
|
-
|
|
255
|
-
|
|
260
|
+
const task = await startCloudTask(ctx, c, created.workspace.key, submitted ? values.initialPrompt : undefined);
|
|
261
|
+
new AgentStateStore(ctx.store.dir).save(project.remote ?? project.folder, 'codex');
|
|
256
262
|
patch(c.id, { state: 'active', submitted, prompt: undefined });
|
|
257
263
|
await output(ctx, c, task, submitted, choice);
|
|
258
264
|
return 0;
|
|
@@ -325,10 +331,7 @@ export async function resumeCloudStartup(ctx, c) {
|
|
|
325
331
|
const ws = await conversationWorkspace(ctx, c);
|
|
326
332
|
// The same seed the start began: its own captured revision (never a newer checkout), or the copy seed.
|
|
327
333
|
await seedConversation(ctx, c, ws, local.folder);
|
|
328
|
-
await ctx
|
|
329
|
-
const task = await ctx.cloud().codex.waitTask(c.id, { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal });
|
|
330
|
-
if (start.prompt !== undefined && !start.submitted)
|
|
331
|
-
await submitCloudInitialPrompt(ctx, c, start.prompt, ws.key);
|
|
334
|
+
const task = await startCloudTask(ctx, c, ws.key, start.submitted ? undefined : start.prompt);
|
|
332
335
|
new AgentStateStore(ctx.store.dir).update(doc => {
|
|
333
336
|
const records = doc.conversations;
|
|
334
337
|
if (records[c.id]?.cloud_start)
|
package/dist/commands.js
CHANGED
|
@@ -1425,6 +1425,14 @@ export const KEY_HANDLERS = {
|
|
|
1425
1425
|
out(ctx, `wrote ${r.bytes_written} bytes to ${r.path}${r.sha256 ? ` (sha256 ${r.sha256})` : ''}${tree !== null ? `, tree revision ${tree}` : ''}\n`, tree !== null ? { ...r, tree_revision: tree } : r);
|
|
1426
1426
|
return 0;
|
|
1427
1427
|
},
|
|
1428
|
+
async 'files delete'(ctx, values, [ref, path]) {
|
|
1429
|
+
const ws = await resolveWorkspace(ctx, ref);
|
|
1430
|
+
const ifRev = ifRevision(values);
|
|
1431
|
+
fileFirstOnly(ws, ifRev !== undefined ? ['--if-revision'] : []);
|
|
1432
|
+
await cellOf(ctx, ws).files.remove(path, { recursive: values.recursive === true, ...(ifRev !== undefined ? { ifTreeRevision: ifRev } : {}) });
|
|
1433
|
+
out(ctx, `deleted ${path}\n`, { path, removed: true });
|
|
1434
|
+
return 0;
|
|
1435
|
+
},
|
|
1428
1436
|
async 'files ls'(ctx, values, [ref, path]) {
|
|
1429
1437
|
const limitText = str(values.limit);
|
|
1430
1438
|
const ws = await resolveWorkspace(ctx, ref);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Ctx, Handler } from './commands.js';
|
|
2
|
-
import type { CellClient, ConversationPermission, ListeningPort, MessageAttachment, PermissionDecision } from '@shardflux/sdk';
|
|
2
|
+
import type { CellClient, ConversationPermission, ListeningPort, MessageAttachment, MessageDelivery, PermissionDecision } from '@shardflux/sdk';
|
|
3
3
|
export declare function conversationClient(ctx: Ctx): import("@shardflux/sdk").Shardflux | import("@shardflux/sdk").ShardfluxAccount;
|
|
4
4
|
/** Why a conversation needs you, as one line of at most `max` characters; undefined in other states (0.18.0+). */
|
|
5
5
|
export declare function questionLine(c: {
|
|
@@ -17,6 +17,10 @@ export declare function displayTitle(c: {
|
|
|
17
17
|
* keeps its file name in the conversation's computer.
|
|
18
18
|
*/
|
|
19
19
|
export declare function readAttachments(paths: readonly string[]): Promise<MessageAttachment[]>;
|
|
20
|
+
/** How long `conversations send --wait` waits for the agent to have the message (0.27.0+). */
|
|
21
|
+
export declare const SEND_WAIT_MS = 60000;
|
|
22
|
+
/** What happened to a sent message, as `conversations send --wait` prints it (0.27.0+). */
|
|
23
|
+
export declare function deliveryLine(d: MessageDelivery): string;
|
|
20
24
|
/** A permission request's command, path or URL as one line of at most `max` characters: its first line (0.25.0+). */
|
|
21
25
|
export declare function detailLine(detail: string, max?: number): string;
|
|
22
26
|
/** One permission request as a list row (0.25.0+): id, state, title, detail, and the decision once answered. */
|
|
@@ -2,10 +2,10 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { open } from 'node:fs/promises';
|
|
3
3
|
import { basename } from 'node:path';
|
|
4
4
|
import { out, str, strs } from "./commands.js";
|
|
5
|
-
import { CodedError, UsageError } from "./errors.js";
|
|
6
|
-
import { MAX_ATTACHMENT_BYTES, MAX_ATTACHMENTS, PtyConnection, planAttachments } from '@shardflux/sdk';
|
|
5
|
+
import { CodedError, EXIT, UsageError } from "./errors.js";
|
|
6
|
+
import { MAX_ATTACHMENT_BYTES, MAX_ATTACHMENTS, MAX_MARK_IDS, PtyConnection, planAttachments } from '@shardflux/sdk';
|
|
7
7
|
import { FilesMirror, humanBytes, listMirrorConversations, workspaceHub } from "./files-mirror.js";
|
|
8
|
-
import { modelChoice } from "./agent-models.js";
|
|
8
|
+
import { liveModels, modelChoice } from "./agent-models.js";
|
|
9
9
|
import { storedTitlePrefix } from "./client-settings.js";
|
|
10
10
|
import { PtyOutputCursor, passiveUnavailable, sleepingAttach } from "./agent-terminal.js";
|
|
11
11
|
import { progress } from "./progress.js";
|
|
@@ -83,6 +83,16 @@ function checkMessage(text, files) {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
const sentLines = (r) => `${r.restored ? 'Restored\n' : ''}${(r.files ?? []).map(f => `Attached ${f.path}\n`).join('')}Message sent (${r.seq}).\n`;
|
|
86
|
+
/** How long `conversations send --wait` waits for the agent to have the message (0.27.0+). */
|
|
87
|
+
export const SEND_WAIT_MS = 60_000;
|
|
88
|
+
/** What happened to a sent message, as `conversations send --wait` prints it (0.27.0+). */
|
|
89
|
+
export function deliveryLine(d) {
|
|
90
|
+
if (d.state === 'pending')
|
|
91
|
+
return 'Not delivered yet.';
|
|
92
|
+
if (d.state === 'uncertain')
|
|
93
|
+
return 'Delivery not confirmed yet: check the conversation before sending it again.';
|
|
94
|
+
return d.steered === true ? 'Steered into the running turn.' : d.steered === false ? 'Started the next turn.' : 'Delivered to the agent.';
|
|
95
|
+
}
|
|
86
96
|
/** A permission request's command, path or URL as one line of at most `max` characters: its first line (0.25.0+). */
|
|
87
97
|
export function detailLine(detail, max = 80) {
|
|
88
98
|
const line = detail.split('\n').find(l => l.trim())?.replace(/\s+/g, ' ').trim() ?? '';
|
|
@@ -191,6 +201,29 @@ export async function watchTerminal(ctx, cell, ptyId) {
|
|
|
191
201
|
ctx.io.stdout.write('\x1b[0m\x1b[?25h\r\n');
|
|
192
202
|
}
|
|
193
203
|
}
|
|
204
|
+
/** A conversation id as the marks commands take it: conv_ and 26 letters a-z and digits 2-7 (0.28.0+). */
|
|
205
|
+
const CONVERSATION_ID = /^conv_[a-z2-7]{26}$/;
|
|
206
|
+
/** What `conversations archive`, `unarchive` and `seen` print before each conversation they marked (0.28.0+). */
|
|
207
|
+
const MARKED = { archive: 'Archived', unarchive: 'Unarchived', seen: 'Marked seen' };
|
|
208
|
+
/**
|
|
209
|
+
* Your own marks (0.28.0+, SDK 0.30.0), the same on every device: every id in one request, each once, checked before
|
|
210
|
+
* it is sent. Prints one line per conversation marked and, on stderr, "Not found: <id>" for each one skipped; --json
|
|
211
|
+
* prints the API's answer, {data, skipped}. Exit 3 when none was marked and some were skipped (not found), else 0.
|
|
212
|
+
*/
|
|
213
|
+
async function markConversations(ctx, kind, ids) {
|
|
214
|
+
const unique = [...new Set(ids)], invalid = unique.filter(id => !CONVERSATION_ID.test(id));
|
|
215
|
+
if (invalid.length)
|
|
216
|
+
throw new UsageError(`Not a conversation id: ${invalid.join(', ')}`, 'conversation ids look like conv_k3m5q7s2u4w6y3b5d7f2h4j6ln (shard conversations list shows them)');
|
|
217
|
+
if (unique.length > MAX_MARK_IDS)
|
|
218
|
+
throw new UsageError(`${unique.length} conversations named: one command takes up to ${MAX_MARK_IDS}.`, `run it again for the rest, ${MAX_MARK_IDS} at a time`);
|
|
219
|
+
const client = conversationClient(ctx), o = { signal: ctx.signal };
|
|
220
|
+
const r = kind === 'archive' ? await client.conversations.archive(unique, o) : kind === 'unarchive' ? await client.conversations.unarchive(unique, o) : await client.conversations.markSeen(unique, o);
|
|
221
|
+
out(ctx, r.data.map(c => `${MARKED[kind]} ${c.id}\t${displayTitle(c)}\n`).join(''), r);
|
|
222
|
+
if (!ctx.json)
|
|
223
|
+
for (const id of r.skipped)
|
|
224
|
+
ctx.io.stderr.write(`Not found: ${id}\n`);
|
|
225
|
+
return !r.data.length && r.skipped.length ? EXIT.notFound : EXIT.ok;
|
|
226
|
+
}
|
|
194
227
|
export const CONVERSATION_HANDLERS = {
|
|
195
228
|
async 'conversations resume'(ctx, _values, [id]) { const r = await conversationClient(ctx).conversations.resume(id, { signal: ctx.signal }); out(ctx, `${displayTitle(r.conversation)}: resumed.\n`, r); return 0; },
|
|
196
229
|
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; },
|
|
@@ -311,8 +344,23 @@ export const CONVERSATION_HANDLERS = {
|
|
|
311
344
|
throw new UsageError('missing <text> for "shard conversations send"');
|
|
312
345
|
if (files.length)
|
|
313
346
|
checkMessage(text ?? '', files);
|
|
314
|
-
const
|
|
315
|
-
|
|
347
|
+
const client = conversationClient(ctx), r = await client.conversations.send(id, text ?? '', { signal: ctx.signal, ...(files.length ? { files } : {}) });
|
|
348
|
+
if (values.wait !== true) {
|
|
349
|
+
out(ctx, sentLines(r), r);
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
// --wait (0.27.0+): a working agent takes it into its running turn, an idle one starts its next turn with it. The
|
|
353
|
+
// message is sent either way, so a delivery that cannot be read is said on stderr and the command still succeeds.
|
|
354
|
+
let delivery = null;
|
|
355
|
+
try {
|
|
356
|
+
delivery = await client.conversations.waitForDelivery(id, r.seq, { timeoutMs: SEND_WAIT_MS, signal: ctx.signal });
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
if (ctx.signal.aborted)
|
|
360
|
+
throw e;
|
|
361
|
+
progress(ctx, `Where the message stands could not be read: ${e instanceof Error ? e.message : String(e)}`);
|
|
362
|
+
}
|
|
363
|
+
out(ctx, `${sentLines(r)}${delivery ? `${deliveryLine(delivery)}\n` : ''}`, { ...r, delivery });
|
|
316
364
|
return 0;
|
|
317
365
|
},
|
|
318
366
|
/**
|
|
@@ -326,7 +374,7 @@ export const CONVERSATION_HANDLERS = {
|
|
|
326
374
|
const app = str(values.app) ?? 'claude';
|
|
327
375
|
if (app !== 'claude' && app !== 'codex')
|
|
328
376
|
throw new UsageError('Choose --app claude or codex.');
|
|
329
|
-
const choice = modelChoice(values, app);
|
|
377
|
+
const choice = modelChoice(values, app, await liveModels(ctx, values, app));
|
|
330
378
|
if (prompt !== undefined && !prompt.trim())
|
|
331
379
|
throw new UsageError('--prompt needs text.');
|
|
332
380
|
const files = await readAttachments(strs(values.file));
|
|
@@ -378,6 +426,10 @@ export const CONVERSATION_HANDLERS = {
|
|
|
378
426
|
return 0;
|
|
379
427
|
},
|
|
380
428
|
async 'conversations move'(ctx, _values, [id, to]) { const r = await conversationClient(ctx).conversations.move(id, to, { signal: ctx.signal }); out(ctx, `Move scheduled for the end of the turn.\n`, r); return 0; },
|
|
429
|
+
// Your inbox, the same on every device (0.28.0+): archive, unarchive and seen, all ids in one request.
|
|
430
|
+
async 'conversations archive'(ctx, _values, ids) { return markConversations(ctx, 'archive', ids); },
|
|
431
|
+
async 'conversations unarchive'(ctx, _values, ids) { return markConversations(ctx, 'unarchive', ids); },
|
|
432
|
+
async 'conversations seen'(ctx, _values, ids) { return markConversations(ctx, 'seen', ids); },
|
|
381
433
|
// Everywhere at once, its computer included (0.25.0+); refused without --yes, as every deletion is. --json: the API's answer.
|
|
382
434
|
async 'conversations delete'(ctx, values, [id]) {
|
|
383
435
|
if (values.yes !== true)
|
|
@@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { AgentStateStore } from "./agent-state.js";
|
|
6
6
|
import { projectIdentity, projectLabels } from "./project-identity.js";
|
|
7
7
|
import { createConversation, conversationName, conversationWorkspace, conversationsUnavailable, listConversations, localConversations, recordedChoice, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
|
|
8
|
-
import { modelChoice } from "./agent-models.js";
|
|
8
|
+
import { liveModels, modelChoice } from "./agent-models.js";
|
|
9
9
|
import { terminal } from "./agent-commands.js";
|
|
10
10
|
import { cloudClaudeLogin, loginClaudeInWorkspace, requireClaudeTerminal } from "./claude-cloud-login.js";
|
|
11
11
|
import { syncAgentCredentials } from "./agent-credentials.js";
|
|
@@ -138,7 +138,7 @@ async function startConversation(ctx, values, [path = './', explicit], openIn) {
|
|
|
138
138
|
if (explicit && explicit !== app)
|
|
139
139
|
throw new UsageError('Choose claude or codex.');
|
|
140
140
|
// Checked before a VM is allocated; the new conversation's record carries it to its guest (0.17.0+).
|
|
141
|
-
const choice = modelChoice(values, app);
|
|
141
|
+
const choice = modelChoice(values, app, await liveModels(ctx, values, app));
|
|
142
142
|
if (values.resume === true) {
|
|
143
143
|
// The native pickers are VM-local. C-K6's project picker must select the binding first.
|
|
144
144
|
const records = localConversations(ctx);
|
package/dist/desktop-linux.d.ts
CHANGED
|
@@ -10,6 +10,17 @@ export interface DesktopStatus {
|
|
|
10
10
|
}
|
|
11
11
|
export declare function linuxDesktopPackage(arch?: NodeJS.Architecture): string | null;
|
|
12
12
|
export declare function graphicalSession(env: NodeJS.ProcessEnv): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Whether `pid` runs the app installed at `app`: the GTK tray app (shardflux-desktop) or the one Shardflux app
|
|
15
|
+
* (shardflux-desktop-bin behind its launcher). By /proc/<pid>/exe, because Chromium rewrites an Electron process's
|
|
16
|
+
* cmdline into one space-joined title; a recycled PID runs something else, and a zombie has exited.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isAppProcess(pid: number, app: string, proc?: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The libraries ldd could not find, named as the loader names them: the app in the package decides which it needs (the
|
|
21
|
+
* GTK tray app's WebKit and app indicator, the one app's GTK, NSS and ALSA).
|
|
22
|
+
*/
|
|
23
|
+
export declare function missingLibrariesMessage(ldd: string): string;
|
|
13
24
|
export declare class LinuxDesktopManager {
|
|
14
25
|
#private;
|
|
15
26
|
readonly home: string;
|
|
@@ -20,7 +31,7 @@ export declare class LinuxDesktopManager {
|
|
|
20
31
|
env?: NodeJS.ProcessEnv;
|
|
21
32
|
exec?: typeof exec;
|
|
22
33
|
spawn?: typeof spawn;
|
|
23
|
-
isProcess?: (pid: number,
|
|
34
|
+
isProcess?: (pid: number, app: string) => boolean;
|
|
24
35
|
});
|
|
25
36
|
status(dir: string): Promise<DesktopStatus>;
|
|
26
37
|
packagedVersion(): string | null;
|
package/dist/desktop-linux.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, execFile } from 'node:child_process';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, chmodSync, mkdtempSync } from 'node:fs';
|
|
4
|
-
import { dirname, join } from 'node:path';
|
|
3
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, writeFileSync, chmodSync, mkdtempSync } from 'node:fs';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { UsageError } from "./errors.js";
|
|
@@ -18,6 +18,41 @@ export function linuxDesktopPackage(arch = process.arch) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
export function graphicalSession(env) { return !!(env.DISPLAY || env.WAYLAND_DISPLAY); }
|
|
21
|
+
/**
|
|
22
|
+
* Whether `pid` runs the app installed at `app`: the GTK tray app (shardflux-desktop) or the one Shardflux app
|
|
23
|
+
* (shardflux-desktop-bin behind its launcher). By /proc/<pid>/exe, because Chromium rewrites an Electron process's
|
|
24
|
+
* cmdline into one space-joined title; a recycled PID runs something else, and a zombie has exited.
|
|
25
|
+
*/
|
|
26
|
+
export function isAppProcess(pid, app, proc = '/proc') {
|
|
27
|
+
try {
|
|
28
|
+
const exe = readlinkSync(`${proc}/${pid}/exe`).replace(/ \(deleted\)$/, '');
|
|
29
|
+
let real = app;
|
|
30
|
+
try {
|
|
31
|
+
real = realpathSync(app);
|
|
32
|
+
}
|
|
33
|
+
catch { /* not installed */ }
|
|
34
|
+
if (![app, real].includes(dirname(exe)) || !/^shardflux-desktop(-bin)?$/.test(basename(exe)))
|
|
35
|
+
return false;
|
|
36
|
+
const stat = readFileSync(`${proc}/${pid}/stat`, 'utf8');
|
|
37
|
+
if (stat.slice(stat.lastIndexOf(')') + 2).startsWith('Z'))
|
|
38
|
+
return false;
|
|
39
|
+
process.kill(pid, 0);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The libraries ldd could not find, named as the loader names them: the app in the package decides which it needs (the
|
|
48
|
+
* GTK tray app's WebKit and app indicator, the one app's GTK, NSS and ALSA).
|
|
49
|
+
*/
|
|
50
|
+
export function missingLibrariesMessage(ldd) {
|
|
51
|
+
const missing = [...new Set([...ldd.matchAll(/^\s*(\S+)\s+=>\s+not found/gm)].map(m => m[1]))];
|
|
52
|
+
return `Shardflux needs desktop libraries this computer does not have${missing.length ? ` (${missing.join(', ')})` : ''}. Install them with your package manager, then run shard.`;
|
|
53
|
+
}
|
|
54
|
+
/** The Electron executable of a one-app directory (behind its launcher), else the GTK app's executable. */
|
|
55
|
+
const appBinary = (dir) => existsSync(join(dir, 'shardflux-desktop-bin')) ? join(dir, 'shardflux-desktop-bin') : join(dir, 'shardflux-desktop');
|
|
21
56
|
const desktopString = (text) => `"${text.replace(/(["`$\\])/g, '\\$1')}"`;
|
|
22
57
|
// fnm's multishell aliases belong to the launching shell; the desktop and its autostart entry outlive it.
|
|
23
58
|
function stableDesktopRuntime(runtime) {
|
|
@@ -43,15 +78,7 @@ export class LinuxDesktopManager {
|
|
|
43
78
|
this.#env = { ...process.env, ...options.env, HOME: this.home };
|
|
44
79
|
this.#exec = options.exec ?? exec;
|
|
45
80
|
this.#spawn = options.spawn ?? spawn;
|
|
46
|
-
this.#isProcess = options.isProcess ?? ((pid,
|
|
47
|
-
if (readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0')[0] !== executable)
|
|
48
|
-
return false;
|
|
49
|
-
process.kill(pid, 0);
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return false;
|
|
54
|
-
} });
|
|
81
|
+
this.#isProcess = options.isProcess ?? ((pid, app) => isAppProcess(pid, app));
|
|
55
82
|
}
|
|
56
83
|
#version(path) {
|
|
57
84
|
try {
|
|
@@ -67,7 +94,7 @@ export class LinuxDesktopManager {
|
|
|
67
94
|
try {
|
|
68
95
|
const pid = Number(readFileSync(join(dir, 'desktop-linux.pid'), 'utf8'));
|
|
69
96
|
// Recycled PIDs must never make us stop an unrelated process.
|
|
70
|
-
if (Number.isInteger(pid) && pid > 0 && this.#isProcess(pid,
|
|
97
|
+
if (Number.isInteger(pid) && pid > 0 && this.#isProcess(pid, this.app))
|
|
71
98
|
pids.push(pid);
|
|
72
99
|
}
|
|
73
100
|
catch { /* no running app */ }
|
|
@@ -84,7 +111,8 @@ export class LinuxDesktopManager {
|
|
|
84
111
|
const entry = join(this.home, '.local/share/applications/dev.shardflux.Desktop.desktop');
|
|
85
112
|
if (!existsSync(entry))
|
|
86
113
|
return Promise.reject(new UsageError('Run shard menubar start first.'));
|
|
87
|
-
|
|
114
|
+
// A login start shows only the tray icon (the one app reads --background; 0.27.0+).
|
|
115
|
+
writeFileSync(file, readFileSync(entry, 'utf8').replace(/^Exec=(.*)$/m, (_m, cmd) => `Exec=${cmd.includes('--background') ? cmd : `${cmd} --background`}`), { mode: 0o644 });
|
|
88
116
|
return Promise.resolve();
|
|
89
117
|
}
|
|
90
118
|
async stop(dir) {
|
|
@@ -109,7 +137,7 @@ export class LinuxDesktopManager {
|
|
|
109
137
|
return { result: 'current', from: before.version, to: version };
|
|
110
138
|
if (before.running) {
|
|
111
139
|
try {
|
|
112
|
-
const { stdout } = await this.#exec(join(this.app, 'shardflux-desktop'), ['--control', 'idle'], { env: this.#env, timeout: 5000 });
|
|
140
|
+
const { stdout } = await this.#exec(join(this.app, 'shardflux-desktop'), ['--control', 'idle'], { env: { ...this.#env, SHARDFLUX_CONFIG_DIR: dir }, timeout: 5000 });
|
|
113
141
|
if (!JSON.parse(stdout).idle)
|
|
114
142
|
return { result: 'busy', from: before.version, to: version };
|
|
115
143
|
}
|
|
@@ -132,14 +160,15 @@ export class LinuxDesktopManager {
|
|
|
132
160
|
if (existsSync(this.app) && (lstatSync(this.app).isSymbolicLink() || !current.version))
|
|
133
161
|
throw new UsageError(`Refusing to replace an unrelated directory at ${this.app}.`);
|
|
134
162
|
// The loader probes every shared library before any installed copy is stopped or replaced.
|
|
135
|
-
await this.#exec('/usr/bin/ldd', [
|
|
136
|
-
throw new UsageError(
|
|
163
|
+
await this.#exec('/usr/bin/ldd', [appBinary(source)], { timeout: 5_000 }).then(({ stdout }) => { if (stdout.includes('not found'))
|
|
164
|
+
throw new UsageError(missingLibrariesMessage(stdout)); });
|
|
137
165
|
mkdirSync(dirname(this.app), { recursive: true });
|
|
138
166
|
const staging = mkdtempSync(join(dirname(this.app), '.desktop-'));
|
|
139
167
|
const staged = join(staging, 'app'), backup = join(staging, 'previous');
|
|
140
168
|
try {
|
|
141
|
-
cpSync(source, staged, { recursive: true });
|
|
169
|
+
cpSync(source, staged, { recursive: true, verbatimSymlinks: true });
|
|
142
170
|
chmodSync(join(staged, 'shardflux-desktop'), 0o755);
|
|
171
|
+
chmodSync(appBinary(staged), 0o755);
|
|
143
172
|
if (current.running)
|
|
144
173
|
await this.stop(dir);
|
|
145
174
|
if (existsSync(this.app))
|
|
@@ -165,7 +194,7 @@ export class LinuxDesktopManager {
|
|
|
165
194
|
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
195
|
const autostart = join(this.home, '.config/autostart/shardflux.desktop');
|
|
167
196
|
if (existsSync(autostart))
|
|
168
|
-
|
|
197
|
+
await this.login(true);
|
|
169
198
|
if ((await this.status(dir)).running)
|
|
170
199
|
await this.#exec(join(this.app, 'shardflux-desktop'), ['--control', view], { env, timeout: 5_000 });
|
|
171
200
|
else {
|
package/dist/device-approvals.js
CHANGED
|
@@ -6,6 +6,8 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
6
6
|
import { writePrivateFile } from "./credentials.js";
|
|
7
7
|
import { UsageError } from "./errors.js";
|
|
8
8
|
import { json } from "./format.js";
|
|
9
|
+
import { thisComputer } from "./this-computer.js";
|
|
10
|
+
import { chooseLine, MODE_LABELS, deviceActivity, readDeviceSettings, setDeviceApprovalMode } from "./device-mode.js";
|
|
9
11
|
const paths = (dir, id) => {
|
|
10
12
|
if (!/^[a-zA-Z0-9_-]{1,96}$/.test(id))
|
|
11
13
|
throw new UsageError('Invalid device request id.');
|
|
@@ -39,12 +41,19 @@ export function answerDeviceApproval(dir, id, choice) {
|
|
|
39
41
|
throw new UsageError('Access outside the conversation folder must be approved once.');
|
|
40
42
|
writePrivateFile(join(dir, 'device-answers'), paths(dir, id).answer, JSON.stringify({ nonce: pending.nonce, choice }));
|
|
41
43
|
}
|
|
44
|
+
/** Config dirs whose helper printed the choose line (once per helper, not per request). */
|
|
45
|
+
const toldChoice = new Set();
|
|
42
46
|
/** Local user-owned files carry approvals; cloud data can never answer its own request. */
|
|
43
47
|
export async function askLocalDeviceApproval(ctx, approval) {
|
|
44
48
|
const file = paths(ctx.store.dir, approval.request_id), nonce = randomUUID();
|
|
45
49
|
rmSync(file.answer, { force: true });
|
|
46
50
|
writePrivateFile(join(ctx.store.dir, 'device-approvals'), file.pending, JSON.stringify({ ...approval, nonce, pid: process.pid }));
|
|
47
|
-
|
|
51
|
+
// Not chosen yet (device-mode.ts): one line with the exact commands; choosing one answers this request too.
|
|
52
|
+
if (approval.choose_mode && !toldChoice.has(ctx.store.dir)) {
|
|
53
|
+
toldChoice.add(ctx.store.dir);
|
|
54
|
+
ctx.io.stderr.write(`Device bridge: ${chooseLine()}\n`);
|
|
55
|
+
}
|
|
56
|
+
spawnSync('notify-send', ['--', 'Shardflux approval', `${approval.summary}\n${approval.choose_mode ? chooseLine() : 'Review: shard device requests'}`], { env: ctx.io.env, stdio: 'ignore', timeout: 2000 });
|
|
48
57
|
try {
|
|
49
58
|
while (!ctx.signal.aborted) {
|
|
50
59
|
try {
|
|
@@ -62,10 +71,23 @@ export async function askLocalDeviceApproval(ctx, approval) {
|
|
|
62
71
|
rmSync(file.answer, { force: true });
|
|
63
72
|
}
|
|
64
73
|
}
|
|
74
|
+
const activityLine = (a) => `${a.at.slice(0, 16).replace('T', ' ')} ${a.state}${a.exit_code !== undefined ? ` (exit ${a.exit_code})` : ''} ${a.summary}${a.conversation_title ? ` · ${a.conversation_title}` : ''}\n`;
|
|
65
75
|
export const DEVICE_HANDLERS = {
|
|
76
|
+
/** `shard device approvals [auto|read|ask]` (0.27.0+): how this computer answers your agents, and what ran without asking. */
|
|
77
|
+
'device approvals'(ctx, _values, [mode]) {
|
|
78
|
+
const settings = mode ? setDeviceApprovalMode(ctx.store.dir, mode, ctx.store.profile(ctx.apiUrl).session?.user?.id) : readDeviceSettings(ctx.store.dir);
|
|
79
|
+
const recent = deviceActivity(ctx.store.dir, 10);
|
|
80
|
+
if (ctx.json) {
|
|
81
|
+
ctx.io.stdout.write(json({ approvals: settings.approvals ?? null, chosen_at: settings.chosen_at ?? null, recent }));
|
|
82
|
+
return Promise.resolve(0);
|
|
83
|
+
}
|
|
84
|
+
const now = settings.approvals ? `Your agents on ${thisComputer()}: ${MODE_LABELS[settings.approvals]}.\n` : `Your agents on ${thisComputer()}: not chosen yet; each request asks.\n${chooseLine()}\n`;
|
|
85
|
+
ctx.io.stdout.write(now + (recent.length ? `Ran without asking:\n${recent.map(activityLine).join('')}` : ''));
|
|
86
|
+
return Promise.resolve(0);
|
|
87
|
+
},
|
|
66
88
|
'device requests'(ctx) {
|
|
67
89
|
const requests = pendingDeviceApprovals(ctx.store.dir);
|
|
68
|
-
ctx.io.stdout.write(ctx.json ? json({ requests }) : requests.length ? requests.map(r => `${r.request_id}: ${r.summary}\n Project: ${r.project_name ?? r.project_id}; conversation: ${r.conversation_title ?? r.conversation_id}\n Local: ${r.local_path ?? '(none)'}; remote: ${r.remote_path ?? '(none)'}${r.argv ? `\n Command: ${JSON.stringify(r.argv)}` : ''}${r.local_port ? `\n Ports: ${r.local_port} → ${r.remote_port}` : ''}${r.repo_remote ? `\n Repository: ${r.repo_remote}` : ''}${r.brief ? `\n First message: ${r.brief.split('\n')[0].slice(0, 200)}` : ''}\n shard device approve ${r.request_id}${r.scope === 'folder' ? ` [--always: for ${r.folder}]` : ''} | shard device deny ${r.request_id}\n`).join('') : 'No device requests waiting for approval.\n');
|
|
90
|
+
ctx.io.stdout.write(ctx.json ? json({ requests }) : requests.length ? requests.map(r => `${r.request_id}: ${r.summary}\n Project: ${r.project_name ?? r.project_id}; conversation: ${r.conversation_title ?? r.conversation_id}\n Local: ${r.local_path ?? '(none)'}; remote: ${r.remote_path ?? '(none)'}${r.argv ? `\n Command: ${JSON.stringify(r.argv)}` : ''}${r.local_port ? `\n Ports: ${r.local_port} → ${r.remote_port}` : ''}${r.repo_remote ? `\n Repository: ${r.repo_remote}` : ''}${r.brief ? `\n First message: ${r.brief.split('\n')[0].slice(0, 200)}` : ''}\n shard device approve ${r.request_id}${r.scope === 'folder' ? ` [--always: for ${r.folder}]` : ''} | shard device deny ${r.request_id}\n`).join('') + (requests.some(r => r.choose_mode) ? `${chooseLine()}\n` : '') : 'No device requests waiting for approval.\n');
|
|
69
91
|
return Promise.resolve(0);
|
|
70
92
|
},
|
|
71
93
|
...Object.fromEntries(['approve', 'deny'].map(action => [`device ${action}`, (ctx, values, [id]) => {
|
package/dist/device-bridge.d.ts
CHANGED
|
@@ -62,8 +62,15 @@ export interface DeviceApproval {
|
|
|
62
62
|
brief?: string;
|
|
63
63
|
/** dir_list: how many levels below the folder are listed. */
|
|
64
64
|
depth?: number;
|
|
65
|
+
/**
|
|
66
|
+
* 0.27.0+: this person has not chosen how this computer answers their agents (device-mode.ts). A surface that knows
|
|
67
|
+
* it asks that first ("Allow everything" / "Ask each time", saved with `shard device approvals auto|ask`): once the
|
|
68
|
+
* mode lets this request run, the helper takes it by itself and this approval goes away; with "Ask each time" the
|
|
69
|
+
* request is then answered as usual. A surface that predates it shows the approval as before.
|
|
70
|
+
*/
|
|
71
|
+
choose_mode?: true;
|
|
65
72
|
}
|
|
66
|
-
type Result = {
|
|
73
|
+
export type Result = {
|
|
67
74
|
state: 'approved' | 'denied' | 'running' | 'done' | 'failed';
|
|
68
75
|
exit_code?: number;
|
|
69
76
|
stdout_b64?: string;
|
|
@@ -72,6 +79,12 @@ type Result = {
|
|
|
72
79
|
dir?: DirListing;
|
|
73
80
|
project?: LinkedProject;
|
|
74
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* Posts a request's answer. An API before a linked folder's start point (train 47) refuses a link's commit and branch
|
|
84
|
+
* (422 validation_failed): the link is done, so it is posted again without them (staging 2026-10-17: the answer was
|
|
85
|
+
* refused and the request read as failed).
|
|
86
|
+
*/
|
|
87
|
+
export declare function postResult(post: (answer: Result, again: boolean) => Promise<void>, result: Result): Promise<void>;
|
|
75
88
|
/** Resolve existing ancestors even before the conversation worktree is created. */
|
|
76
89
|
export declare function devicePath(root: string, requested?: string): {
|
|
77
90
|
path: string;
|
|
@@ -105,10 +118,15 @@ export declare function folderApproval(r: DeviceRequest, c: Conversation, target
|
|
|
105
118
|
export declare function tryLocallyAction(r: DeviceRequest): 'start' | 'stop' | null;
|
|
106
119
|
export declare function ensureConversationWorktree(ctx: Ctx, c: Conversation, folder: string, ws?: Workspace): Promise<string>;
|
|
107
120
|
export declare function deviceExec(argv: string[], cwd: string, timeoutSeconds?: number, signal?: AbortSignal): Promise<Result>;
|
|
121
|
+
export type DeviceAsk = (approval: DeviceApproval, signal?: AbortSignal) => Promise<DeviceChoice>;
|
|
108
122
|
export declare class DeviceExecutor {
|
|
109
123
|
#private;
|
|
110
|
-
constructor(ctx: Ctx, ask:
|
|
111
|
-
|
|
124
|
+
constructor(ctx: Ctx, ask: DeviceAsk, deviceId: string);
|
|
125
|
+
/** A new device id for a new sign-in (device-helper.ts): requests for the old one are no longer this device's. */
|
|
126
|
+
set deviceId(id: string);
|
|
127
|
+
get deviceId(): string;
|
|
128
|
+
/** `again`: a second answer of the same state with another body (its own idempotency key). */
|
|
129
|
+
result(r: DeviceRequest, result: Result, again?: string): Promise<void>;
|
|
112
130
|
execute(r: DeviceRequest): Promise<void>;
|
|
113
131
|
reap(): Promise<void>;
|
|
114
132
|
close(): Promise<void>;
|
|
@@ -131,4 +149,3 @@ export declare function askConnectedAcp(dir: string, approval: DeviceApproval, s
|
|
|
131
149
|
*/
|
|
132
150
|
export declare function approverSocket(dir: string): string;
|
|
133
151
|
export declare function askDeviceApprover(dir: string, approval: DeviceApproval, signal: AbortSignal): Promise<DeviceChoice | null>;
|
|
134
|
-
export {};
|