@shardflux/cli 0.25.0 → 0.28.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 +154 -1
- package/README.md +212 -36
- package/dist/account-commands.d.ts +2 -4
- package/dist/account-commands.js +2 -6
- 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-git-commands.d.ts +1 -2
- package/dist/agent-git-commands.js +2 -21
- package/dist/agent-models.d.ts +31 -1
- package/dist/agent-models.js +27 -4
- package/dist/agent-setup.d.ts +4 -2
- package/dist/agent-setup.js +6 -4
- package/dist/agent-shell.d.ts +2 -0
- package/dist/agent-shell.js +5 -0
- 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/cli-proxy.d.ts +1 -13
- package/dist/cli-proxy.js +2 -27
- 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 +17 -12
- 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 +24 -4
- package/dist/device-bridge.js +181 -8
- 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/format.js +3 -0
- package/dist/forwarder-refresh.d.ts +15 -0
- package/dist/forwarder-refresh.js +32 -0
- package/dist/handler-loader.d.ts +3 -0
- package/dist/handler-loader.js +43 -0
- package/dist/handlers.js +4 -2
- package/dist/land.d.ts +10 -5
- package/dist/land.js +56 -9
- package/dist/linux-helper.js +1 -2
- package/dist/mac-app.d.ts +11 -0
- package/dist/mac-app.js +11 -0
- package/dist/machine-setup-guard.d.ts +9 -0
- package/dist/machine-setup-guard.js +22 -0
- package/dist/machine-setup.d.ts +1 -8
- package/dist/machine-setup.js +2 -22
- package/dist/main.js +19 -16
- 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 +260 -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/startup-housekeeping.d.ts +5 -0
- package/dist/startup-housekeeping.js +30 -0
- package/dist/t3-launch.js +2 -2
- package/dist/t3-provider.js +4 -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.d.ts +1 -1
- package/dist/worktree-twin-engine.js +10 -7
- package/dist/worktree-twin.d.ts +6 -0
- package/dist/worktree-twin.js +8 -1
- package/package.json +12 -6
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
|
@@ -2,6 +2,7 @@ import type { CellClient, Workspace } from '@shardflux/sdk';
|
|
|
2
2
|
import type { Ctx } from './commands.js';
|
|
3
3
|
import type { Conversation } from './conversations.js';
|
|
4
4
|
import type { DirListing, LinkedProject } from './device-folders.js';
|
|
5
|
+
import type { HubSeed } from './project-hub-seed.js';
|
|
5
6
|
export type DeviceChoice = 'once' | 'always' | 'deny';
|
|
6
7
|
export interface DeviceRequest {
|
|
7
8
|
id: string;
|
|
@@ -62,8 +63,15 @@ export interface DeviceApproval {
|
|
|
62
63
|
brief?: string;
|
|
63
64
|
/** dir_list: how many levels below the folder are listed. */
|
|
64
65
|
depth?: number;
|
|
66
|
+
/**
|
|
67
|
+
* 0.27.0+: this person has not chosen how this computer answers their agents (device-mode.ts). A surface that knows
|
|
68
|
+
* it asks that first ("Allow everything" / "Ask each time", saved with `shard device approvals auto|ask`): once the
|
|
69
|
+
* mode lets this request run, the helper takes it by itself and this approval goes away; with "Ask each time" the
|
|
70
|
+
* request is then answered as usual. A surface that predates it shows the approval as before.
|
|
71
|
+
*/
|
|
72
|
+
choose_mode?: true;
|
|
65
73
|
}
|
|
66
|
-
type Result = {
|
|
74
|
+
export type Result = {
|
|
67
75
|
state: 'approved' | 'denied' | 'running' | 'done' | 'failed';
|
|
68
76
|
exit_code?: number;
|
|
69
77
|
stdout_b64?: string;
|
|
@@ -72,6 +80,12 @@ type Result = {
|
|
|
72
80
|
dir?: DirListing;
|
|
73
81
|
project?: LinkedProject;
|
|
74
82
|
};
|
|
83
|
+
/**
|
|
84
|
+
* Posts a request's answer. An API before a linked folder's start point (train 47) refuses a link's commit and branch
|
|
85
|
+
* (422 validation_failed): the link is done, so it is posted again without them (staging 2026-10-17: the answer was
|
|
86
|
+
* refused and the request read as failed).
|
|
87
|
+
*/
|
|
88
|
+
export declare function postResult(post: (answer: Result, again: boolean) => Promise<void>, result: Result): Promise<void>;
|
|
75
89
|
/** Resolve existing ancestors even before the conversation worktree is created. */
|
|
76
90
|
export declare function devicePath(root: string, requested?: string): {
|
|
77
91
|
path: string;
|
|
@@ -105,10 +119,17 @@ export declare function folderApproval(r: DeviceRequest, c: Conversation, target
|
|
|
105
119
|
export declare function tryLocallyAction(r: DeviceRequest): 'start' | 'stop' | null;
|
|
106
120
|
export declare function ensureConversationWorktree(ctx: Ctx, c: Conversation, folder: string, ws?: Workspace): Promise<string>;
|
|
107
121
|
export declare function deviceExec(argv: string[], cwd: string, timeoutSeconds?: number, signal?: AbortSignal): Promise<Result>;
|
|
122
|
+
export type DeviceAsk = (approval: DeviceApproval, signal?: AbortSignal) => Promise<DeviceChoice>;
|
|
108
123
|
export declare class DeviceExecutor {
|
|
109
124
|
#private;
|
|
110
|
-
constructor(ctx: Ctx, ask:
|
|
111
|
-
|
|
125
|
+
constructor(ctx: Ctx, ask: DeviceAsk, deviceId: string, options?: {
|
|
126
|
+
seedHub?: (ctx: Ctx, folder: string, projectId: string) => Promise<HubSeed>;
|
|
127
|
+
});
|
|
128
|
+
/** A new device id for a new sign-in (device-helper.ts): requests for the old one are no longer this device's. */
|
|
129
|
+
set deviceId(id: string);
|
|
130
|
+
get deviceId(): string;
|
|
131
|
+
/** `again`: a second answer of the same state with another body (its own idempotency key). */
|
|
132
|
+
result(r: DeviceRequest, result: Result, again?: string): Promise<void>;
|
|
112
133
|
execute(r: DeviceRequest): Promise<void>;
|
|
113
134
|
reap(): Promise<void>;
|
|
114
135
|
close(): Promise<void>;
|
|
@@ -131,4 +152,3 @@ export declare function askConnectedAcp(dir: string, approval: DeviceApproval, s
|
|
|
131
152
|
*/
|
|
132
153
|
export declare function approverSocket(dir: string): string;
|
|
133
154
|
export declare function askDeviceApprover(dir: string, approval: DeviceApproval, signal: AbortSignal): Promise<DeviceChoice | null>;
|
|
134
|
-
export {};
|
package/dist/device-bridge.js
CHANGED
|
@@ -16,6 +16,29 @@ import { withTargetLock } from "./target-lock.js";
|
|
|
16
16
|
import { checkConversationWorktree, rememberConversationWorktree } from "./conversation-worktree.js";
|
|
17
17
|
import { yourComputer } from "./this-computer.js";
|
|
18
18
|
import { canonicalDevicePath, deviceHome, displayPath, expandDevicePath, FOLDER_KINDS, isDevicePath, linkLocalProject, listDeviceDir } from "./device-folders.js";
|
|
19
|
+
import { ownConversation } from "./agent-config-return.js";
|
|
20
|
+
import { deviceApprovalMode, recordDeviceActivity, runsWithoutAsking } from "./device-mode.js";
|
|
21
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
22
|
+
/**
|
|
23
|
+
* Posts a request's answer. An API before a linked folder's start point (train 47) refuses a link's commit and branch
|
|
24
|
+
* (422 validation_failed): the link is done, so it is posted again without them (staging 2026-10-17: the answer was
|
|
25
|
+
* refused and the request read as failed).
|
|
26
|
+
*/
|
|
27
|
+
export async function postResult(post, result) {
|
|
28
|
+
try {
|
|
29
|
+
await post(result, false);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
const project = result.project, status = e.status, code = e.code;
|
|
33
|
+
if (!project?.commit || !(status === 400 || status === 422 || code === 'validation_failed'))
|
|
34
|
+
throw e;
|
|
35
|
+
const older = { ...project };
|
|
36
|
+
delete older.commit;
|
|
37
|
+
delete older.branch;
|
|
38
|
+
// Another body, so its own idempotency key.
|
|
39
|
+
await post({ ...result, project: older }, true);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
19
42
|
const LIMIT = 256 << 20, OUTPUT = (192 << 10) - 128;
|
|
20
43
|
const inside = (root, target) => target === root || target.startsWith(root + sep);
|
|
21
44
|
/**
|
|
@@ -203,9 +226,89 @@ export class DeviceExecutor {
|
|
|
203
226
|
#ask;
|
|
204
227
|
#deviceId;
|
|
205
228
|
#handles = new Map();
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
229
|
+
/** Requests that run without asking (device-mode.ts), until their outcome is recorded. */
|
|
230
|
+
#auto = new Map();
|
|
231
|
+
#identity = { at: 0 };
|
|
232
|
+
/** How an agent's folder link reaches the project hub (project-hub-seed.ts unless a test gives its own hub). */
|
|
233
|
+
#seedHub;
|
|
234
|
+
constructor(ctx, ask, deviceId, options = {}) {
|
|
235
|
+
this.#ctx = ctx;
|
|
236
|
+
this.#ask = ask;
|
|
237
|
+
this.#deviceId = deviceId;
|
|
238
|
+
this.#seedHub = options.seedHub;
|
|
239
|
+
}
|
|
240
|
+
/** A new device id for a new sign-in (device-helper.ts): requests for the old one are no longer this device's. */
|
|
241
|
+
set deviceId(id) { this.#deviceId = id; this.#identity = { at: 0 }; }
|
|
242
|
+
get deviceId() { return this.#deviceId; }
|
|
243
|
+
/** `again`: a second answer of the same state with another body (its own idempotency key). */
|
|
244
|
+
async result(r, result, again = '') {
|
|
245
|
+
// How a request that ran without asking ended is kept here first: it ran on this computer whether or not the
|
|
246
|
+
// answer reaches the API (a stopped helper, a network error).
|
|
247
|
+
const auto = this.#auto.get(r.id);
|
|
248
|
+
if (auto && result.state !== 'approved' && result.state !== 'running') {
|
|
249
|
+
this.#auto.delete(r.id);
|
|
250
|
+
recordDeviceActivity(this.#ctx.store.dir, { ...auto, at: new Date().toISOString(), state: result.state, ...(result.exit_code !== undefined ? { exit_code: result.exit_code } : {}) });
|
|
251
|
+
}
|
|
252
|
+
await conversationClient(this.#ctx).deviceRequests.result(safeId(r.id), result, { idempotencyKey: `${this.#deviceId}:${r.id}:${result.state}${again}`, signal: this.#ctx.signal });
|
|
253
|
+
}
|
|
254
|
+
/** This computer's person (the signed-in user, or who created its API key), checked every 10 minutes. */
|
|
255
|
+
async #me() {
|
|
256
|
+
if (Date.now() - this.#identity.at < 10 * 60_000)
|
|
257
|
+
return this.#identity.userId;
|
|
258
|
+
let userId = this.#ctx.store.profile(this.#ctx.apiUrl).session?.user?.id;
|
|
259
|
+
if (!userId) {
|
|
260
|
+
try {
|
|
261
|
+
const me = await conversationClient(this.#ctx).me();
|
|
262
|
+
userId = me.user?.id ?? me.api_key?.created_by_user_id ?? undefined;
|
|
263
|
+
}
|
|
264
|
+
catch { /* unknown: only conversations started here are this person's */ }
|
|
265
|
+
}
|
|
266
|
+
this.#identity = { at: Date.now(), ...(userId ? { userId } : {}) };
|
|
267
|
+
return userId;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Allow once by a grant, by this person's approval mode (device-mode.ts), or by asking. Only this person's own
|
|
271
|
+
* agents run without asking. Until they choose a mode, their first request asks for it (`choose_mode`), and a mode
|
|
272
|
+
* chosen while a request waits (the prompt, Settings, `shard device approvals`) takes the request by itself.
|
|
273
|
+
*/
|
|
274
|
+
async #decide(r, c, approval, granted) {
|
|
275
|
+
if (granted)
|
|
276
|
+
return 'once';
|
|
277
|
+
const dir = this.#ctx.store.dir, me = await this.#me();
|
|
278
|
+
const mine = ownConversation(c, localConversations(this.#ctx), me);
|
|
279
|
+
const mode = deviceApprovalMode(dir, me);
|
|
280
|
+
const auto = (m) => {
|
|
281
|
+
this.#auto.set(r.id, { at: new Date().toISOString(), request_id: r.id, conversation_id: c.id, kind: r.kind, summary: approval.summary, mode: m, state: 'running',
|
|
282
|
+
...(approval.conversation_title ? { conversation_title: approval.conversation_title } : {}), ...(approval.project_name ? { project_name: approval.project_name } : {}),
|
|
283
|
+
...(approval.argv ? { argv: approval.argv } : {}), ...(approval.local_path ? { local_path: approval.local_path } : {}) });
|
|
284
|
+
recordDeviceActivity(dir, this.#auto.get(r.id));
|
|
285
|
+
return 'once';
|
|
286
|
+
};
|
|
287
|
+
if (mine && mode && runsWithoutAsking(mode, r.kind))
|
|
288
|
+
return auto(mode);
|
|
289
|
+
if (!mine)
|
|
290
|
+
return this.#ask(approval);
|
|
291
|
+
const stop = new AbortController();
|
|
292
|
+
let chosen;
|
|
293
|
+
const watch = (async () => {
|
|
294
|
+
while (!stop.signal.aborted) {
|
|
295
|
+
await delay(400, undefined, { signal: stop.signal }).catch(() => undefined);
|
|
296
|
+
const now = deviceApprovalMode(dir, me);
|
|
297
|
+
if (!stop.signal.aborted && now && runsWithoutAsking(now, r.kind)) {
|
|
298
|
+
chosen = now;
|
|
299
|
+
stop.abort();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
})();
|
|
303
|
+
try {
|
|
304
|
+
const choice = await this.#ask(mode ? approval : { ...approval, choose_mode: true }, stop.signal).catch((e) => { if (chosen)
|
|
305
|
+
return 'once'; throw e; });
|
|
306
|
+
return chosen ? auto(chosen) : choice;
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
stop.abort();
|
|
310
|
+
await watch;
|
|
311
|
+
}
|
|
209
312
|
}
|
|
210
313
|
async execute(r) {
|
|
211
314
|
const ctx = this.#ctx;
|
|
@@ -218,8 +321,10 @@ export class DeviceExecutor {
|
|
|
218
321
|
const matching = Object.values(records).filter(record => record.project_id === r.project_id);
|
|
219
322
|
const folders = new Set(matching.map(record => record.folder));
|
|
220
323
|
const record = records[r.conversation_id] ?? (folders.size === 1 ? matching[0] : undefined);
|
|
324
|
+
// No folder of this conversation on this computer: a home conversation's command (an orchestrator thread asking the
|
|
325
|
+
// person's computer) runs in their home folder once they allow it; anything else is answered, never left waiting.
|
|
221
326
|
if (!record || record.project_id !== r.project_id)
|
|
222
|
-
return;
|
|
327
|
+
return this.#unplaced(r);
|
|
223
328
|
let c = await getConversation(ctx, r.conversation_id);
|
|
224
329
|
if (c.project_id !== r.project_id)
|
|
225
330
|
throw new Error('Device request project changed.');
|
|
@@ -227,7 +332,7 @@ export class DeviceExecutor {
|
|
|
227
332
|
const requested = r.exec?.cwd_relative ?? r.files?.local_path_relative ?? '.';
|
|
228
333
|
const target = devicePath(planned.path, requested), grants = new DeviceGrants(ctx.store.dir);
|
|
229
334
|
target.outside ||= planned.outside;
|
|
230
|
-
const choice = grants.allows(ctx.apiUrl, r.project_id, record.folder, r.kind, target.outside)
|
|
335
|
+
const choice = await this.#decide(r, c, deviceApproval(r, c, target, record.folder), grants.allows(ctx.apiUrl, r.project_id, record.folder, r.kind, target.outside));
|
|
231
336
|
if (choice === 'deny') {
|
|
232
337
|
await this.result(r, { state: 'denied' });
|
|
233
338
|
return;
|
|
@@ -331,6 +436,74 @@ export class DeviceExecutor {
|
|
|
331
436
|
ws?.cell().close();
|
|
332
437
|
}
|
|
333
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* A request whose conversation has no folder on this device (0.27.0+). A home conversation's exec runs in the home
|
|
441
|
+
* folder, or the ~/ or absolute directory it names, after the person allows this one request (never Always: commands
|
|
442
|
+
* never run under a grant). Everything else is refused with the reason, so its agent hears it instead of waiting forever.
|
|
443
|
+
*/
|
|
444
|
+
async #unplaced(r) {
|
|
445
|
+
const ctx = this.#ctx, home = deviceHome(ctx), say = (state, message) => this.result(r, { state, stderr_b64: Buffer.from(message).toString('base64') });
|
|
446
|
+
let c;
|
|
447
|
+
try {
|
|
448
|
+
c = await getConversation(ctx, r.conversation_id);
|
|
449
|
+
if (c.project_id !== r.project_id)
|
|
450
|
+
throw new Error('Device request project changed.');
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
if (ctx.signal.aborted)
|
|
454
|
+
return;
|
|
455
|
+
await say('denied', e instanceof Error ? e.message : String(e));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (archived(c)) {
|
|
459
|
+
await say('denied', 'The conversation has ended.');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (r.kind !== 'exec' || !r.exec || c.home !== true) {
|
|
463
|
+
await say('denied', `This conversation has no folder on ${yourComputer()}: commands and file moves need its folder here (open the conversation on this computer), or a ~/ path for a file.`);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
let target;
|
|
467
|
+
try {
|
|
468
|
+
const requested = r.exec.cwd_relative && r.exec.cwd_relative !== '.' ? r.exec.cwd_relative : '~';
|
|
469
|
+
target = canonicalDevicePath(expandDevicePath(isDevicePath(requested) ? requested : `~/${requested}`, home));
|
|
470
|
+
}
|
|
471
|
+
catch (e) {
|
|
472
|
+
await say('denied', e instanceof Error ? e.message : String(e));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const approval = { ...deviceApproval(r, c, { path: target, outside: true }, home),
|
|
476
|
+
summary: `Run on ${yourComputer()}: ${r.exec.argv.join(' ')} · in ${displayPath(target, home)}` };
|
|
477
|
+
let choice;
|
|
478
|
+
try {
|
|
479
|
+
choice = await this.#decide(r, c, approval, false);
|
|
480
|
+
}
|
|
481
|
+
catch (e) {
|
|
482
|
+
if (ctx.signal.aborted)
|
|
483
|
+
return;
|
|
484
|
+
await say('denied', e instanceof Error ? e.message : String(e));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (ctx.signal.aborted)
|
|
488
|
+
return;
|
|
489
|
+
if (choice !== 'once') {
|
|
490
|
+
await this.result(r, { state: 'denied' });
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const current = await getConversation(ctx, r.conversation_id).catch(() => undefined);
|
|
494
|
+
if (!current || current.project_id !== c.project_id || archived(current)) {
|
|
495
|
+
await say('denied', 'The conversation changed or ended during device approval.');
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
await this.result(r, { state: 'approved' });
|
|
499
|
+
await this.result(r, { state: 'running' });
|
|
500
|
+
try {
|
|
501
|
+
await this.result(r, await deviceExec(r.exec.argv, target, r.exec.timeout_seconds, ctx.signal));
|
|
502
|
+
}
|
|
503
|
+
catch (e) {
|
|
504
|
+
await say('failed', e instanceof Error ? e.message : String(e));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
334
507
|
/**
|
|
335
508
|
* A folder anywhere on this device (device-folders.ts). Every outcome is answered, so a request is never asked twice:
|
|
336
509
|
* denied before it was approved, failed after.
|
|
@@ -371,7 +544,7 @@ export class DeviceExecutor {
|
|
|
371
544
|
const grants = new DeviceGrants(ctx.store.dir);
|
|
372
545
|
let choice;
|
|
373
546
|
try {
|
|
374
|
-
choice =
|
|
547
|
+
choice = await this.#decide(r, c, folderApproval(r, c, target, folder, home, remote), grants.allowsFolder(ctx.apiUrl, target, r.kind));
|
|
375
548
|
}
|
|
376
549
|
catch (e) {
|
|
377
550
|
if (ctx.signal.aborted)
|
|
@@ -412,7 +585,7 @@ export class DeviceExecutor {
|
|
|
412
585
|
if (r.kind === 'dir_list')
|
|
413
586
|
result = { state: 'done', dir: await listDeviceDir(target, r.dir ?? {}, ctx.signal) };
|
|
414
587
|
else if (r.kind === 'project_link')
|
|
415
|
-
result = { state: 'done', project: await linkLocalProject(ctx, target, { ...(r.link?.brief ? { brief: r.link.brief } : {}), harness: r.link?.harness ?? current.harness, projectId: current.project_id }) };
|
|
588
|
+
result = { state: 'done', project: await linkLocalProject(ctx, target, { ...(r.link?.brief ? { brief: r.link.brief } : {}), harness: r.link?.harness ?? current.harness, projectId: current.project_id, seed: true }, undefined, this.#seedHub) };
|
|
416
589
|
else {
|
|
417
590
|
const f = r.files;
|
|
418
591
|
if (!f.remote_path.startsWith('/') || /[\0\r\n]/.test(f.remote_path))
|
|
@@ -445,7 +618,7 @@ export class DeviceExecutor {
|
|
|
445
618
|
}
|
|
446
619
|
result = { state: 'done', files_artifact: r.kind === 'files_pull' ? f.remote_path : target };
|
|
447
620
|
}
|
|
448
|
-
await this.result(r, result);
|
|
621
|
+
await postResult((answer, again) => this.result(r, answer, again ? ':older-api' : ''), result);
|
|
449
622
|
}
|
|
450
623
|
catch (e) {
|
|
451
624
|
await this.result(r, { state: 'failed', stderr_b64: Buffer.from(e instanceof Error ? e.message : String(e)).toString('base64') });
|
package/dist/device-folders.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Ctx, Handler } from './commands.js';
|
|
2
|
+
import type { HubSeed } from './project-hub-seed.js';
|
|
2
3
|
export declare const FOLDER_KINDS: Set<string>;
|
|
3
4
|
/** An absolute or `~/…` path: one the device resolves on its own, outside any conversation's folder. */
|
|
4
5
|
export declare function isDevicePath(path: string): boolean;
|
|
@@ -25,12 +26,15 @@ export declare function listDeviceDir(root: string, options?: {
|
|
|
25
26
|
max_entries?: number;
|
|
26
27
|
include_hidden?: boolean;
|
|
27
28
|
}, signal?: AbortSignal): Promise<DirListing>;
|
|
29
|
+
/** `commit` and `branch`: where work on it starts, in the project hub (an agent's link, 0.28.0+). */
|
|
28
30
|
export interface LinkedProject {
|
|
29
31
|
project_id: string;
|
|
30
32
|
repo_remote: string;
|
|
31
33
|
folder: string;
|
|
32
34
|
name: string;
|
|
33
35
|
conversation_id?: string;
|
|
36
|
+
commit?: string;
|
|
37
|
+
branch?: string;
|
|
34
38
|
}
|
|
35
39
|
type Starter = (ctx: Ctx, folder: string, harness: 'claude' | 'codex', brief: string) => Promise<{
|
|
36
40
|
id: string;
|
|
@@ -60,7 +64,8 @@ export declare function linkLocalProject(ctx: Ctx, path: string, options?: {
|
|
|
60
64
|
brief?: string;
|
|
61
65
|
harness?: 'claude' | 'codex';
|
|
62
66
|
projectId?: string;
|
|
63
|
-
|
|
67
|
+
seed?: boolean;
|
|
68
|
+
}, start?: Starter, seedHub?: (ctx: Ctx, folder: string, projectId: string) => Promise<HubSeed>): Promise<LinkedProject>;
|
|
64
69
|
/** `shard projects link <path>` (0.25.0+): what the desktop app's "Add a folder…" runs, the same as a device's project_link. */
|
|
65
70
|
export declare const PROJECT_LINK_HANDLERS: Record<string, Handler>;
|
|
66
71
|
export {};
|
package/dist/device-folders.js
CHANGED
|
@@ -18,6 +18,7 @@ import { UsageError } from "./errors.js";
|
|
|
18
18
|
import { ensureProjectGit } from "./project-git.js";
|
|
19
19
|
import { normalizeRemote, projectIdentity } from "./project-identity.js";
|
|
20
20
|
import { selectProject } from "./project-preparation.js";
|
|
21
|
+
import { seedProjectHub } from "./project-hub-seed.js";
|
|
21
22
|
export const FOLDER_KINDS = new Set(['dir_list', 'project_link']);
|
|
22
23
|
/** An absolute or `~/…` path: one the device resolves on its own, outside any conversation's folder. */
|
|
23
24
|
export function isDevicePath(path) { return isAbsolute(path) || path === '~' || path.startsWith('~/'); }
|
|
@@ -200,7 +201,7 @@ function lastLine(stderr) {
|
|
|
200
201
|
* and kept as this device's live copy: what conversations started from it finish lands back in it. With `brief`, a
|
|
201
202
|
* conversation starts there at once. A folder without Git gets a repository, as a new chat from it does.
|
|
202
203
|
*/
|
|
203
|
-
export async function linkLocalProject(ctx, path, options = {}, start = startFromFolder) {
|
|
204
|
+
export async function linkLocalProject(ctx, path, options = {}, start = startFromFolder, seedHub = seedProjectHub) {
|
|
204
205
|
const { folder } = projectIdentity(path);
|
|
205
206
|
await ensureProjectGit(ctx, folder);
|
|
206
207
|
const identity = projectIdentity(folder);
|
|
@@ -218,7 +219,10 @@ export async function linkLocalProject(ctx, path, options = {}, start = startFro
|
|
|
218
219
|
if (!projectId)
|
|
219
220
|
throw new UsageError('Choose your default project first: shard projects use <id|slug>.');
|
|
220
221
|
const name = [...(identity.remote ? identity.slug : basename(folder))].slice(0, 200).join('') || identity.slug;
|
|
221
|
-
|
|
222
|
+
// An agent's link: the folder as it is now goes to the project hub, so its work can start in the cloud at once.
|
|
223
|
+
const seed = options.seed ? await seedHub(ctx, folder, projectId) : null;
|
|
224
|
+
return { project_id: projectId, repo_remote: identity.remote ?? `file://${folder}`, folder, name, ...(conversationId ? { conversation_id: conversationId } : {}),
|
|
225
|
+
...(seed ? { commit: seed.commit, ...(seed.branch ? { branch: seed.branch } : {}) } : {}) };
|
|
222
226
|
}
|
|
223
227
|
/** `shard projects link <path>` (0.25.0+): what the desktop app's "Add a folder…" runs, the same as a device's project_link. */
|
|
224
228
|
export const PROJECT_LINK_HANDLERS = {
|
package/dist/device-helper.d.ts
CHANGED
|
@@ -9,3 +9,11 @@ export declare function runDeviceHelper(ctx: Ctx, options?: {
|
|
|
9
9
|
* A home conversation, an Orchestrator master or a chat the desktop app started asks from these.
|
|
10
10
|
*/
|
|
11
11
|
export declare function deviceProjects(ctx: Ctx): Promise<string[]>;
|
|
12
|
+
/** 409 `device_id_taken`: this device id is registered in a project by another identity (POST /v1/device-requests/heartbeat). */
|
|
13
|
+
export declare function deviceIdTaken(e: unknown): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* A new device id for this computer, replacing `old` (0.27.0+). Local state stays: grants are kept by API and folder
|
|
16
|
+
* or project (device-bridge.ts DeviceGrants), the approval mode by person (device-mode.ts), and the twin's refs by the
|
|
17
|
+
* id that made them. The old id is kept in `device-id-history.json` (the last 20) for support.
|
|
18
|
+
*/
|
|
19
|
+
export declare function renewDeviceId(dir: string, old: string, projectId: string): string;
|
package/dist/device-helper.js
CHANGED
|
@@ -42,22 +42,24 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
})().finally(() => abort.abort());
|
|
44
44
|
void input.catch(() => abort.abort());
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// `until` ends one question early: the person chose a mode that lets the request run by itself (device-mode.ts).
|
|
46
|
+
const ask = async (approval, until) => {
|
|
47
|
+
const asking = until ? AbortSignal.any([signal, until]) : signal;
|
|
48
|
+
const acp = await askConnectedAcp(ctx.store.dir, approval, asking);
|
|
47
49
|
if (acp)
|
|
48
50
|
return acp;
|
|
49
51
|
// The desktop app, while it runs, shows it as a Needs-you item (0.25.0+); else the menu bar or the service files.
|
|
50
|
-
const app = await askDeviceApprover(ctx.store.dir, approval,
|
|
52
|
+
const app = await askDeviceApprover(ctx.store.dir, approval, asking);
|
|
51
53
|
if (app)
|
|
52
54
|
return app;
|
|
53
55
|
if (options.service)
|
|
54
|
-
return askLocalDeviceApproval(ctx, approval);
|
|
56
|
+
return askLocalDeviceApproval({ ...ctx, signal: asking }, approval);
|
|
55
57
|
return new Promise(resolve => {
|
|
56
58
|
const denied = () => { pending.delete(approval.request_id); resolve('deny'); };
|
|
57
|
-
|
|
58
|
-
pending.set(approval.request_id, choice => {
|
|
59
|
+
asking.addEventListener('abort', denied, { once: true });
|
|
60
|
+
pending.set(approval.request_id, choice => { asking.removeEventListener('abort', denied); resolve(choice); });
|
|
59
61
|
ctx.io.stdout.write(JSON.stringify(approval) + '\n');
|
|
60
|
-
if (
|
|
62
|
+
if (asking.aborted)
|
|
61
63
|
denied();
|
|
62
64
|
}).finally(() => { if (!signal.aborted)
|
|
63
65
|
ctx.io.stdout.write(JSON.stringify({ type: 'approval_resolved', request_id: approval.request_id }) + '\n'); });
|
|
@@ -74,6 +76,10 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
74
76
|
writePrivateFile(ctx.store.dir, deviceFile, deviceId + '\n');
|
|
75
77
|
}
|
|
76
78
|
const executor = new DeviceExecutor(ctx, ask, deviceId);
|
|
79
|
+
// A device id another identity registered (an earlier API key `shard setup` replaced, another account) can never
|
|
80
|
+
// register for this one: a refusal gives this computer a new id (0.27.0+), at most once every 10 minutes, so a second
|
|
81
|
+
// sign-in change later renews again and a refusal right after a renewal stops (never a loop).
|
|
82
|
+
let renewedAt = 0;
|
|
77
83
|
const report = (e) => { ctx.io.stderr.write(`Device bridge: ${e instanceof Error ? e.message : String(e)}\n`); };
|
|
78
84
|
let mirroring, twin, returning, preparing, projects;
|
|
79
85
|
try {
|
|
@@ -150,6 +156,7 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
150
156
|
// Registered for every project an agent may ask from (deviceProjects); one poll covers them all where the API
|
|
151
157
|
// lists by device (0.25.0+), else one per project.
|
|
152
158
|
const registered = new Map();
|
|
159
|
+
const registrationFailures = new Map();
|
|
153
160
|
let all, deviceWide = true, links = true;
|
|
154
161
|
// This device's local conversations of a project (started here or opened here): their requests come here first.
|
|
155
162
|
const linked = (projectId) => Object.values(localConversations(ctx)).filter(r => !r.handed_off && (!projectId || r.project_id === projectId))
|
|
@@ -176,13 +183,42 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
176
183
|
all = { ids: await deviceProjects(ctx).catch(() => all?.ids ?? []), at: Date.now() };
|
|
177
184
|
const projects = [...new Set([...Object.values(localConversations(ctx)).map(r => r.project_id), ...all.ids])];
|
|
178
185
|
for (const projectId of projects) {
|
|
186
|
+
const failed = registrationFailures.get(projectId);
|
|
187
|
+
if (failed && (failed.next === null || Date.now() < failed.next))
|
|
188
|
+
continue;
|
|
179
189
|
if (Date.now() - (registered.get(projectId) ?? 0) < 10_000)
|
|
180
190
|
continue;
|
|
181
|
-
await beat(projectId).then(() =>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
await beat(projectId).then(() => {
|
|
192
|
+
registered.set(projectId, Date.now());
|
|
193
|
+
registrationFailures.delete(projectId);
|
|
194
|
+
}, (e) => {
|
|
195
|
+
if (deviceIdTaken(e) && Date.now() - renewedAt > 600_000) {
|
|
196
|
+
renewedAt = Date.now();
|
|
197
|
+
const old = deviceId;
|
|
198
|
+
deviceId = renewDeviceId(ctx.store.dir, old, projectId);
|
|
199
|
+
executor.deviceId = deviceId;
|
|
200
|
+
// Every project registers again under the new id, at once.
|
|
201
|
+
registered.clear();
|
|
202
|
+
registrationFailures.clear();
|
|
203
|
+
report(new Error(`this computer's device id ${old} belongs to another sign-in (an earlier API key or account); it registers as ${deviceId} for this one.`));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// A refused registration belongs to this project alone. Repeating a
|
|
207
|
+
// device identity conflict cannot repair it or hold every later project.
|
|
208
|
+
const attempts = (failed?.attempts ?? 0) + 1;
|
|
209
|
+
const permanent = e instanceof ShardfluxApiError && !e.retryable;
|
|
210
|
+
const stopped = permanent || attempts >= 8;
|
|
211
|
+
const wait = Math.min(300_000, 5000 * 2 ** Math.min(attempts - 1, 6));
|
|
212
|
+
registrationFailures.set(projectId, { attempts, next: stopped ? null : Date.now() + wait });
|
|
213
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
214
|
+
if (stopped) {
|
|
215
|
+
const action = e instanceof ShardfluxApiError && e.details?.reason === 'device_id_taken'
|
|
216
|
+
? 'Its device id was renewed moments ago and is refused again; restart the device helper.'
|
|
217
|
+
: 'Check connectivity or sign in again, then restart the device helper to retry.';
|
|
218
|
+
report(new Error(`Device helper needs attention for project ${projectId || 'default'} after ${attempts} failed registrations: ${message} ${action}`));
|
|
219
|
+
}
|
|
220
|
+
else
|
|
221
|
+
report(new Error(`Device registration for project ${projectId || 'default'} failed (attempt ${attempts}/8): ${message} Retrying in ${wait / 1000}s.`));
|
|
186
222
|
});
|
|
187
223
|
}
|
|
188
224
|
let polled = false;
|
|
@@ -269,3 +305,26 @@ export async function deviceProjects(ctx) {
|
|
|
269
305
|
}
|
|
270
306
|
return [...ids];
|
|
271
307
|
}
|
|
308
|
+
/** 409 `device_id_taken`: this device id is registered in a project by another identity (POST /v1/device-requests/heartbeat). */
|
|
309
|
+
export function deviceIdTaken(e) {
|
|
310
|
+
return e instanceof ShardfluxApiError && e.status === 409 && e.details?.reason === 'device_id_taken';
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* A new device id for this computer, replacing `old` (0.27.0+). Local state stays: grants are kept by API and folder
|
|
314
|
+
* or project (device-bridge.ts DeviceGrants), the approval mode by person (device-mode.ts), and the twin's refs by the
|
|
315
|
+
* id that made them. The old id is kept in `device-id-history.json` (the last 20) for support.
|
|
316
|
+
*/
|
|
317
|
+
export function renewDeviceId(dir, old, projectId) {
|
|
318
|
+
const id = randomUUID(), historyFile = join(dir, 'device-id-history.json');
|
|
319
|
+
let history = [];
|
|
320
|
+
try {
|
|
321
|
+
const value = JSON.parse(readFileSync(historyFile, 'utf8'));
|
|
322
|
+
if (Array.isArray(value))
|
|
323
|
+
history = value;
|
|
324
|
+
}
|
|
325
|
+
catch { /* first one */ }
|
|
326
|
+
history = [...history, { id: old, replaced_at: new Date().toISOString(), replaced_by: id, project_id: projectId || null }].slice(-20);
|
|
327
|
+
writePrivateFile(dir, historyFile, JSON.stringify(history, null, 2) + '\n');
|
|
328
|
+
writePrivateFile(dir, join(dir, 'device-id'), id + '\n');
|
|
329
|
+
return id;
|
|
330
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type DeviceApprovalMode = 'auto' | 'read' | 'ask';
|
|
2
|
+
export declare const DEVICE_APPROVAL_MODES: readonly DeviceApprovalMode[];
|
|
3
|
+
/**
|
|
4
|
+
* The mode before the person chooses one. `undefined`: nothing is decided for them; the first request asks which they
|
|
5
|
+
* want (the app's prompt, or one line from `shard`), and the app's onboarding asks too. 'auto' or 'ask' here chooses
|
|
6
|
+
* for everyone who has not.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_DEVICE_APPROVALS: DeviceApprovalMode | undefined;
|
|
9
|
+
/** The kinds that only read this computer: a folder listing, and a file copied to the conversation. */
|
|
10
|
+
export declare const READ_KINDS: ReadonlySet<string>;
|
|
11
|
+
export declare const MODE_LABELS: Record<DeviceApprovalMode, string>;
|
|
12
|
+
/** The one line a person without the app reads when a request needs the choice ("this Mac" on macOS). */
|
|
13
|
+
export declare const chooseLine: (platform?: NodeJS.Platform) => string;
|
|
14
|
+
export interface DeviceSettings {
|
|
15
|
+
approvals?: DeviceApprovalMode;
|
|
16
|
+
chosen_at?: string;
|
|
17
|
+
/** Who chose it, when known: another person signed in on this computer chooses for themselves. */
|
|
18
|
+
user_id?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function readDeviceSettings(dir: string): DeviceSettings;
|
|
21
|
+
/** This person's mode on this computer; undefined until they choose one (and DEFAULT_DEVICE_APPROVALS is unset). */
|
|
22
|
+
export declare function deviceApprovalMode(dir: string, userId?: string): DeviceApprovalMode | undefined;
|
|
23
|
+
export declare function setDeviceApprovalMode(dir: string, mode: string, userId?: string): DeviceSettings;
|
|
24
|
+
/** Whether a request of this kind runs without asking under this mode (for the person's own agents). */
|
|
25
|
+
export declare function runsWithoutAsking(mode: DeviceApprovalMode | undefined, kind: string): boolean;
|
|
26
|
+
/** One line of what ran without asking: the start (`running`) and how it ended, folded by `request_id`. */
|
|
27
|
+
export interface DeviceActivity {
|
|
28
|
+
at: string;
|
|
29
|
+
request_id: string;
|
|
30
|
+
conversation_id: string;
|
|
31
|
+
kind: string;
|
|
32
|
+
summary: string;
|
|
33
|
+
mode: DeviceApprovalMode;
|
|
34
|
+
state: 'running' | 'done' | 'failed' | 'denied';
|
|
35
|
+
conversation_title?: string;
|
|
36
|
+
project_name?: string;
|
|
37
|
+
argv?: string[];
|
|
38
|
+
local_path?: string;
|
|
39
|
+
exit_code?: number;
|
|
40
|
+
finished_at?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function recordDeviceActivity(dir: string, entry: DeviceActivity): void;
|
|
43
|
+
/** What ran without asking, newest first: one entry per request with its latest state. */
|
|
44
|
+
export declare function deviceActivity(dir: string, limit?: number): DeviceActivity[];
|