@shardflux/cli 0.24.1 → 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 +159 -1
- package/README.md +182 -35
- package/dist/account-pool.d.ts +11 -1
- package/dist/account-pool.js +48 -0
- package/dist/acp-journal.js +23 -1
- 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 +97 -14
- package/dist/board-args.js +26 -4
- package/dist/board-commands.d.ts +9 -5
- package/dist/board-commands.js +43 -16
- 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 +76 -8
- package/dist/conversation-terminal.js +2 -2
- package/dist/conversations.d.ts +2 -1
- 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 +14 -1
- package/dist/device-folders.js +20 -8
- package/dist/device-helper.d.ts +8 -0
- package/dist/device-helper.js +83 -13
- 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/errors.js +2 -1
- package/dist/format.js +6 -2
- package/dist/handlers.js +5 -2
- package/dist/land.d.ts +121 -0
- package/dist/land.js +284 -0
- package/dist/mac-app.d.ts +11 -0
- package/dist/mac-app.js +11 -0
- package/dist/main.js +9 -1
- package/dist/menubar.d.ts +18 -13
- package/dist/menubar.js +60 -50
- package/dist/onboarding.d.ts +4 -4
- 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 -10
- package/dist/router-commands.js +5 -59
- package/dist/t3-launch.js +2 -2
- package/dist/task-land-hub.py +165 -0
- 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.d.ts +7 -0
- package/dist/transcribe-commands.js +84 -0
- 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 +8 -0
- package/dist/worktree-twin-engine.js +29 -23
- package/dist/worktree-twin.d.ts +6 -0
- package/dist/worktree-twin.js +8 -1
- package/package.json +6 -5
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,7 +156,19 @@ 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();
|
|
153
|
-
|
|
159
|
+
const registrationFailures = new Map();
|
|
160
|
+
let all, deviceWide = true, links = true;
|
|
161
|
+
// This device's local conversations of a project (started here or opened here): their requests come here first.
|
|
162
|
+
const linked = (projectId) => Object.values(localConversations(ctx)).filter(r => !r.handed_off && (!projectId || r.project_id === projectId))
|
|
163
|
+
.sort((a, b) => (b.conversation.created_at ?? '').localeCompare(a.conversation.created_at ?? '')).slice(0, 500).map(r => r.conversation.id);
|
|
164
|
+
const beat = (projectId) => conversationClient(ctx).deviceRequests.heartbeat({ ...(projectId ? { projectId } : {}), deviceId, leaseSeconds: 30, ...(links ? { conversationIds: linked(projectId) } : {}) })
|
|
165
|
+
.catch((e) => {
|
|
166
|
+
// An API before device links (0.25.0+ clients) refuses the list: register without it from then on.
|
|
167
|
+
if (!links || !(e instanceof ShardfluxApiError && [400, 422].includes(e.status)))
|
|
168
|
+
throw e;
|
|
169
|
+
links = false;
|
|
170
|
+
return conversationClient(ctx).deviceRequests.heartbeat({ ...(projectId ? { projectId } : {}), deviceId, leaseSeconds: 30 });
|
|
171
|
+
});
|
|
154
172
|
const take = (rows) => {
|
|
155
173
|
for (const r of rows) {
|
|
156
174
|
if (r.device_id !== deviceId || working.has(r.id) || working.size >= 8)
|
|
@@ -165,13 +183,42 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
165
183
|
all = { ids: await deviceProjects(ctx).catch(() => all?.ids ?? []), at: Date.now() };
|
|
166
184
|
const projects = [...new Set([...Object.values(localConversations(ctx)).map(r => r.project_id), ...all.ids])];
|
|
167
185
|
for (const projectId of projects) {
|
|
186
|
+
const failed = registrationFailures.get(projectId);
|
|
187
|
+
if (failed && (failed.next === null || Date.now() < failed.next))
|
|
188
|
+
continue;
|
|
168
189
|
if (Date.now() - (registered.get(projectId) ?? 0) < 10_000)
|
|
169
190
|
continue;
|
|
170
|
-
await
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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.`));
|
|
175
222
|
});
|
|
176
223
|
}
|
|
177
224
|
let polled = false;
|
|
@@ -258,3 +305,26 @@ export async function deviceProjects(ctx) {
|
|
|
258
305
|
}
|
|
259
306
|
return [...ids];
|
|
260
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[];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How this computer answers its own agents (0.27.0+): the person chooses once, per device, whether their agents run
|
|
3
|
+
* things here without asking each time. Kept in `<config>/device-settings.json`; changed in the app's Settings or with
|
|
4
|
+
* `shard device approvals auto|read|ask`. Requests that ran without asking are kept in `<config>/device-activity.jsonl`
|
|
5
|
+
* (what ran, for which conversation, and how it ended), shown by the app and `shard device approvals`.
|
|
6
|
+
*
|
|
7
|
+
* - `ask`: every request asks (Allow once, or Always where a grant applies).
|
|
8
|
+
* - `read`: listing a folder and copying a file to a conversation run without asking; anything that changes this
|
|
9
|
+
* computer (commands, files saved here, ports, linking a folder) asks. Commands are never classified by their text.
|
|
10
|
+
* - `auto`: every request from this person's own agents runs without asking.
|
|
11
|
+
*
|
|
12
|
+
* Only this person's own conversations run without asking (`ownConversation`): a teammate's request always asks.
|
|
13
|
+
*/
|
|
14
|
+
import { appendFileSync, lstatSync, readFileSync, statSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { writePrivateFile, ensurePrivateDir } from "./credentials.js";
|
|
17
|
+
import { UsageError } from "./errors.js";
|
|
18
|
+
import { thisComputer } from "./this-computer.js";
|
|
19
|
+
export const DEVICE_APPROVAL_MODES = ['auto', 'read', 'ask'];
|
|
20
|
+
/**
|
|
21
|
+
* The mode before the person chooses one. `undefined`: nothing is decided for them; the first request asks which they
|
|
22
|
+
* want (the app's prompt, or one line from `shard`), and the app's onboarding asks too. 'auto' or 'ask' here chooses
|
|
23
|
+
* for everyone who has not.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_DEVICE_APPROVALS = undefined;
|
|
26
|
+
/** The kinds that only read this computer: a folder listing, and a file copied to the conversation. */
|
|
27
|
+
export const READ_KINDS = new Set(['dir_list', 'files_pull']);
|
|
28
|
+
export const MODE_LABELS = {
|
|
29
|
+
auto: 'Allow everything from my agents',
|
|
30
|
+
read: 'Allow reading, ask before changes',
|
|
31
|
+
ask: 'Ask every time',
|
|
32
|
+
};
|
|
33
|
+
/** The one line a person without the app reads when a request needs the choice ("this Mac" on macOS). */
|
|
34
|
+
export const chooseLine = (platform = process.platform) => `Let your agents run things on ${thisComputer(platform)} without asking each time? Allow everything: shard device approvals auto · Ask each time: shard device approvals ask`;
|
|
35
|
+
const SETTINGS = 'device-settings.json', ACTIVITY = 'device-activity.jsonl';
|
|
36
|
+
export function readDeviceSettings(dir) {
|
|
37
|
+
const file = join(dir, SETTINGS);
|
|
38
|
+
try {
|
|
39
|
+
if (lstatSync(file).isSymbolicLink() || (statSync(file).mode & 0o077))
|
|
40
|
+
return {};
|
|
41
|
+
const value = JSON.parse(readFileSync(file, 'utf8'));
|
|
42
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
43
|
+
return {};
|
|
44
|
+
return DEVICE_APPROVAL_MODES.includes(value.approvals) ? value : { ...value, approvals: undefined };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** This person's mode on this computer; undefined until they choose one (and DEFAULT_DEVICE_APPROVALS is unset). */
|
|
51
|
+
export function deviceApprovalMode(dir, userId) {
|
|
52
|
+
const s = readDeviceSettings(dir);
|
|
53
|
+
if (s.approvals && !(s.user_id && userId && s.user_id !== userId))
|
|
54
|
+
return s.approvals;
|
|
55
|
+
return DEFAULT_DEVICE_APPROVALS;
|
|
56
|
+
}
|
|
57
|
+
export function setDeviceApprovalMode(dir, mode, userId) {
|
|
58
|
+
if (!DEVICE_APPROVAL_MODES.includes(mode))
|
|
59
|
+
throw new UsageError(`Unknown approval mode "${mode}": auto, read or ask.`);
|
|
60
|
+
const next = { ...readDeviceSettings(dir), approvals: mode, chosen_at: new Date().toISOString() };
|
|
61
|
+
if (userId)
|
|
62
|
+
next.user_id = userId;
|
|
63
|
+
else
|
|
64
|
+
delete next.user_id;
|
|
65
|
+
writePrivateFile(dir, join(dir, SETTINGS), JSON.stringify(next, null, 2) + '\n');
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
/** Whether a request of this kind runs without asking under this mode (for the person's own agents). */
|
|
69
|
+
export function runsWithoutAsking(mode, kind) {
|
|
70
|
+
return mode === 'auto' || (mode === 'read' && READ_KINDS.has(kind));
|
|
71
|
+
}
|
|
72
|
+
const ACTIVITY_MAX_BYTES = 512 << 10, ACTIVITY_KEEP = 400;
|
|
73
|
+
export function recordDeviceActivity(dir, entry) {
|
|
74
|
+
const file = join(dir, ACTIVITY);
|
|
75
|
+
try {
|
|
76
|
+
ensurePrivateDir(dir);
|
|
77
|
+
let size = 0;
|
|
78
|
+
try {
|
|
79
|
+
size = statSync(file).size;
|
|
80
|
+
}
|
|
81
|
+
catch { /* first entry */ }
|
|
82
|
+
if (size > ACTIVITY_MAX_BYTES) {
|
|
83
|
+
const keep = readFileSync(file, 'utf8').split('\n').filter(Boolean).slice(-ACTIVITY_KEEP);
|
|
84
|
+
writePrivateFile(dir, file, keep.join('\n') + '\n');
|
|
85
|
+
}
|
|
86
|
+
appendFileSync(file, JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
87
|
+
}
|
|
88
|
+
catch { /* the request's own result still reaches its conversation */ }
|
|
89
|
+
}
|
|
90
|
+
/** What ran without asking, newest first: one entry per request with its latest state. */
|
|
91
|
+
export function deviceActivity(dir, limit = 20) {
|
|
92
|
+
let text;
|
|
93
|
+
try {
|
|
94
|
+
if (lstatSync(join(dir, ACTIVITY)).isSymbolicLink())
|
|
95
|
+
return [];
|
|
96
|
+
text = readFileSync(join(dir, ACTIVITY), 'utf8');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const byId = new Map();
|
|
102
|
+
for (const line of text.split('\n')) {
|
|
103
|
+
if (!line)
|
|
104
|
+
continue;
|
|
105
|
+
let e;
|
|
106
|
+
try {
|
|
107
|
+
e = JSON.parse(line);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!e || typeof e.request_id !== 'string')
|
|
113
|
+
continue;
|
|
114
|
+
const had = byId.get(e.request_id);
|
|
115
|
+
byId.delete(e.request_id);
|
|
116
|
+
byId.set(e.request_id, had ? { ...had, ...e, at: had.at, ...(e.state !== 'running' ? { finished_at: e.at } : {}) } : e);
|
|
117
|
+
}
|
|
118
|
+
return [...byId.values()].reverse().slice(0, limit);
|
|
119
|
+
}
|
package/dist/drive-commands.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export interface ProjectScope {
|
|
|
14
14
|
* `needId` false leaves the key's project unnamed (the API defaults to it), saving a lookup.
|
|
15
15
|
*/
|
|
16
16
|
export declare function projectScope(ctx: Ctx, values: Values, needId?: boolean): Promise<ProjectScope>;
|
|
17
|
-
/** `drive ls` rows: size, saved time, type and path (the path last, as `files ls` puts the name). */
|
|
18
|
-
export declare function driveTable(rows: readonly DriveFile[]): string;
|
|
17
|
+
/** `drive ls` and `brain ls` rows: size, saved time, type and path (the path last, as `files ls` puts the name). */
|
|
18
|
+
export declare function driveTable(rows: readonly Pick<DriveFile, 'size' | 'created_at' | 'content_type' | 'path'>[]): string;
|
|
19
19
|
/**
|
|
20
20
|
* The model template as people read it: the primary provider and why, the connected accounts, then one row per role
|
|
21
21
|
* with what it runs and, for a role whose provider is out of usage, until when it waits and what the other provider
|
package/dist/drive-commands.js
CHANGED
|
@@ -51,7 +51,7 @@ function drivePath(path, what = 'drive path') {
|
|
|
51
51
|
}
|
|
52
52
|
return path;
|
|
53
53
|
}
|
|
54
|
-
/** `drive ls` rows: size, saved time, type and path (the path last, as `files ls` puts the name). */
|
|
54
|
+
/** `drive ls` and `brain ls` rows: size, saved time, type and path (the path last, as `files ls` puts the name). */
|
|
55
55
|
export function driveTable(rows) {
|
|
56
56
|
return table(['SIZE', 'SAVED', 'TYPE', 'PATH'], rows.map((f) => [humanBytes(f.size), time(f.created_at), f.content_type, f.path]));
|
|
57
57
|
}
|
package/dist/errors.js
CHANGED
|
@@ -266,7 +266,8 @@ export function describeError(err) {
|
|
|
266
266
|
// details.reason: the operation error's reason (e.g. the scheduler's no_ready_hosts), printed after the message.
|
|
267
267
|
const opDetails = err.operation.error?.details;
|
|
268
268
|
const reason = typeof opDetails === 'object' && opDetails !== null ? opDetails.reason : undefined;
|
|
269
|
-
const
|
|
269
|
+
const retryAfter = typeof opDetails === 'object' && opDetails !== null ? opDetails.retry_after_seconds : undefined;
|
|
270
|
+
const details = { ...(err.errorCode ? { error_code: err.errorCode } : {}), ...(typeof reason === 'string' ? { reason } : {}), ...(typeof retryAfter === 'number' && Number.isFinite(retryAfter) && retryAfter > 0 ? { retry_after_seconds: retryAfter } : {}) };
|
|
270
271
|
// A suspend-when-idle canceled workspace_active (0.7.0+): the workspace was in use and keeps running.
|
|
271
272
|
const message = err.workspaceActive ? `${err.message}: the workspace was in use, so the suspend-when-idle was canceled; it keeps running` : err.message;
|
|
272
273
|
return { code: 'operation_failed', message, operation_id: err.operationId, retryable: err.retryable, ...(Object.keys(details).length ? { details } : {}), operation: err.operation };
|
package/dist/format.js
CHANGED
|
@@ -448,7 +448,7 @@ const hoursText = (h) => String(Math.round(h * 100) / 100);
|
|
|
448
448
|
* while the GPU is stopped (cap_state gpu_blocked), why and what brings it back. Workspaces keep running meanwhile.
|
|
449
449
|
* Nothing without `gpu` (plans without GPU-hours).
|
|
450
450
|
*/
|
|
451
|
-
function gpuLines(gpu, resetsAt) {
|
|
451
|
+
function gpuLines(gpu, resetsAt, overageAvailable) {
|
|
452
452
|
if (!isObject(gpu) || typeof gpu.used_hours !== 'number')
|
|
453
453
|
return [];
|
|
454
454
|
const included = typeof gpu.included_hours === 'number' ? gpu.included_hours : null;
|
|
@@ -470,6 +470,10 @@ function gpuLines(gpu, resetsAt) {
|
|
|
470
470
|
rows.push(['GPU', `stopped (spend_cap_reached): workspaces keep running; raise the spend cap (shard billing overage cap <dollars>), or wait for the reset${resets}`]);
|
|
471
471
|
break;
|
|
472
472
|
default:
|
|
473
|
+
if (!overageAvailable) {
|
|
474
|
+
rows.push(['GPU', 'stopped (allowance_used): workspaces keep running; choose a paid plan (shard billing plans); overage is unavailable on the current plan']);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
473
477
|
rows.push(['GPU', `stopped (allowance_used): workspaces keep running; turn on overage (shard billing overage on --cap <dollars>), upgrade (shard billing plans), or wait for the reset${resets}`]);
|
|
474
478
|
}
|
|
475
479
|
return rows;
|
|
@@ -507,7 +511,7 @@ export function usageText(summary) {
|
|
|
507
511
|
head.push(['retained state', 'used up: new workspaces and forks are refused (delete workspaces you no longer need, or upgrade)']);
|
|
508
512
|
}
|
|
509
513
|
const resetsAt = isObject(s.period) ? [s.period.resets_at, s.period.end].find((t) => typeof t === 'string') : undefined;
|
|
510
|
-
head.push(...gpuLines(s.gpu, resetsAt));
|
|
514
|
+
head.push(...gpuLines(s.gpu, resetsAt, !(isObject(s.spend_cap) && s.spend_cap.available === false)));
|
|
511
515
|
for (const [k, v] of Object.entries(s))
|
|
512
516
|
if (!USAGE_KNOWN.has(k) && (typeof v !== 'object' || v === null))
|
|
513
517
|
head.push([k.replace(/_/g, ' '), scalar(v)]);
|
package/dist/handlers.js
CHANGED
|
@@ -5,6 +5,8 @@ import { CONVERSATION_HANDLERS } from "./conversation-commands.js";
|
|
|
5
5
|
import { BOARD_HANDLERS } from "./board-commands.js";
|
|
6
6
|
import { TRANSCRIPT_HANDLERS } from "./conversation-transcript.js";
|
|
7
7
|
import { ROUTER_HANDLERS } from "./router-commands.js";
|
|
8
|
+
import { ORCHESTRATOR_HANDLERS } from "./orchestrator-commands.js";
|
|
9
|
+
import { TRANSCRIBE_HANDLERS } from "./transcribe-commands.js";
|
|
8
10
|
/**
|
|
9
11
|
* Every command's handler, keyed by its path ("workspaces open", "auth login"): the project-key commands of
|
|
10
12
|
* commands.ts and the account plane of account-commands.ts (0.5.0). args.test.ts checks that it matches COMMANDS.
|
|
@@ -24,11 +26,12 @@ import { TWIN_HANDLERS } from "./worktree-twin-engine.js";
|
|
|
24
26
|
import { CODEX_CLOUD_HANDLERS } from "./codex-cloud.js";
|
|
25
27
|
import { THREAD_HANDLERS } from "./thread-on-computer.js";
|
|
26
28
|
import { CLI_PROXY_HANDLERS } from "./cli-proxy.js";
|
|
27
|
-
import { ACCOUNT_POOL_HANDLERS } from "./account-pool.js";
|
|
29
|
+
import { ACCOUNT_POOL_HANDLERS, AGENT_MODELS_HANDLERS } from "./account-pool.js";
|
|
28
30
|
import { MOVE_HANDLERS } from "./move.js";
|
|
29
31
|
import { AGENT_CONFIG_HANDLERS } from "./agent-config-sync.js";
|
|
30
32
|
import { MACHINE_SETUP_HANDLERS } from "./machine-setup.js";
|
|
31
33
|
import { SETTINGS_HANDLERS } from "./client-settings.js";
|
|
32
34
|
import { PROJECT_PREPARATION_HANDLERS } from "./project-preparation.js";
|
|
33
35
|
import { DRIVE_HANDLERS } from "./drive-commands.js";
|
|
34
|
-
|
|
36
|
+
import { BRAIN_HANDLERS } from "./brain-commands.js";
|
|
37
|
+
export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...PROJECT_LINK_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...ORCHESTRATOR_HANDLERS, ...BOARD_HANDLERS, ...TRANSCRIPT_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...AGENT_MODELS_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...MACHINE_SETUP_HANDLERS, ...SETTINGS_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS, ...ROUTER_HANDLERS, ...TRANSCRIBE_HANDLERS, ...DRIVE_HANDLERS, ...BRAIN_HANDLERS };
|
package/dist/land.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Ctx } from './commands.js';
|
|
2
|
+
import { conversationClient } from './conversation-commands.js';
|
|
3
|
+
import type { TwinCell } from './worktree-twin.js';
|
|
4
|
+
/** The hub side of a landing, shipped next to this module (src in the monorepo, dist when published). */
|
|
5
|
+
export declare function hubScript(): string;
|
|
6
|
+
export interface HubLanding {
|
|
7
|
+
state: 'landed' | 'conflict' | 'unrelated' | 'missing' | 'raced' | 'error';
|
|
8
|
+
into: string;
|
|
9
|
+
before?: string | null;
|
|
10
|
+
after?: string;
|
|
11
|
+
result?: string;
|
|
12
|
+
method?: 'ff' | 'merge' | 'already' | 'created';
|
|
13
|
+
created_result?: boolean;
|
|
14
|
+
contained?: string[];
|
|
15
|
+
merge_base?: string | null;
|
|
16
|
+
stage?: 'integrate' | 'default';
|
|
17
|
+
conflicts?: string[];
|
|
18
|
+
source?: string;
|
|
19
|
+
message?: string;
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Merges `result` (made from `sources` when missing) into the hub's default branch, in a VM that reads the hub. */
|
|
23
|
+
export declare function landOnHub(cell: TwinCell, input: {
|
|
24
|
+
result: string;
|
|
25
|
+
sources: string[];
|
|
26
|
+
title: string;
|
|
27
|
+
into?: string;
|
|
28
|
+
hub?: string;
|
|
29
|
+
}, signal?: AbortSignal): Promise<HubLanding>;
|
|
30
|
+
export interface FolderLanding {
|
|
31
|
+
path: string;
|
|
32
|
+
into: string;
|
|
33
|
+
state: 'landed' | 'already' | 'conflict' | 'waiting' | 'skipped';
|
|
34
|
+
sha?: string;
|
|
35
|
+
method?: string;
|
|
36
|
+
files?: number;
|
|
37
|
+
kept?: string[];
|
|
38
|
+
conflicts?: string[];
|
|
39
|
+
backup?: string | null;
|
|
40
|
+
reason?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface FolderInput {
|
|
43
|
+
folder: string;
|
|
44
|
+
into: string;
|
|
45
|
+
name: string;
|
|
46
|
+
title: string;
|
|
47
|
+
branch: string;
|
|
48
|
+
base: string;
|
|
49
|
+
device: string;
|
|
50
|
+
stateDir: string;
|
|
51
|
+
hub?: string;
|
|
52
|
+
quietMs?: number;
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
/** How long a busy folder (a file just saved, a Git operation) is waited for before the answer is "waiting". */
|
|
55
|
+
patienceMs?: number;
|
|
56
|
+
}
|
|
57
|
+
/** The branch to land in: the one checked out in the folder, else the hub's default branch when the folder has it. */
|
|
58
|
+
export declare function folderBranch(folder: string, fallback: string): Promise<string | null>;
|
|
59
|
+
/**
|
|
60
|
+
* The result into the folder through the twin's landing (from the hub): the branch moves, the person's uncommitted
|
|
61
|
+
* edits stay, and the landed commit is checked to be on the branch.
|
|
62
|
+
*/
|
|
63
|
+
export declare function landInFolder(cell: TwinCell, f: FolderInput): Promise<FolderLanding>;
|
|
64
|
+
/** The repository a folder holds, as conversations started from it name it (its origin, else the folder itself). */
|
|
65
|
+
export declare function folderRepository(folder: string): string | null;
|
|
66
|
+
/**
|
|
67
|
+
* The person's folder for a project on this computer: a linked folder (shard projects link) or where it was started.
|
|
68
|
+
* With `repository` (a project holds every folder its people linked), only a folder of that repository.
|
|
69
|
+
*/
|
|
70
|
+
export declare function linkedFolder(ctx: Pick<Ctx, 'store' | 'apiUrl'>, projectId: string, conversationId?: string, repository?: string): string | null;
|
|
71
|
+
export declare const displayPath: (path: string) => string;
|
|
72
|
+
export interface LandOutcome {
|
|
73
|
+
landed: true;
|
|
74
|
+
task_id?: string;
|
|
75
|
+
conversation_id?: string;
|
|
76
|
+
title: string;
|
|
77
|
+
result: {
|
|
78
|
+
branch: string;
|
|
79
|
+
sha: string;
|
|
80
|
+
};
|
|
81
|
+
hub: {
|
|
82
|
+
into: string;
|
|
83
|
+
sha: string;
|
|
84
|
+
method: string;
|
|
85
|
+
};
|
|
86
|
+
folder: (FolderLanding & {
|
|
87
|
+
display: string;
|
|
88
|
+
}) | null;
|
|
89
|
+
recorded: boolean;
|
|
90
|
+
record_error?: string;
|
|
91
|
+
/** One line for people: "Landed in ~/code/best-todo · 3f2a1c9". */
|
|
92
|
+
message: string;
|
|
93
|
+
}
|
|
94
|
+
/** What a landing needs: the API, a VM that reads the project hub, the person's folder for a project, this device. */
|
|
95
|
+
export interface LandDeps {
|
|
96
|
+
client: {
|
|
97
|
+
tasks: Pick<ReturnType<typeof conversationClient>['tasks'], 'get' | 'recordLanding' | 'steer'>;
|
|
98
|
+
conversations: Pick<ReturnType<typeof conversationClient>['conversations'], 'get' | 'land'>;
|
|
99
|
+
};
|
|
100
|
+
hubCell(projectId: string): Promise<{
|
|
101
|
+
cell: TwinCell;
|
|
102
|
+
close(): void;
|
|
103
|
+
}>;
|
|
104
|
+
folder(projectId: string, conversationId?: string, repository?: string): string | null;
|
|
105
|
+
device: string;
|
|
106
|
+
stateDir: string;
|
|
107
|
+
hub?: string;
|
|
108
|
+
quietMs?: number;
|
|
109
|
+
signal?: AbortSignal;
|
|
110
|
+
}
|
|
111
|
+
export declare function landDeps(ctx: Ctx): LandDeps;
|
|
112
|
+
/** What a landing did, for people: the line, then what was not recorded. */
|
|
113
|
+
export declare function landedText(o: LandOutcome): string;
|
|
114
|
+
/** `shard task land <task>`: the task's result into the project's default branch and the linked folder, then recorded. */
|
|
115
|
+
export declare function landTask(d: LandDeps, taskId: string, opts?: {
|
|
116
|
+
path?: string;
|
|
117
|
+
}): Promise<LandOutcome>;
|
|
118
|
+
/** `shard conversations land <id> merge`: the conversation's branch into the default branch and its folder, then recorded. */
|
|
119
|
+
export declare function landConversation(d: LandDeps, id: string, opts?: {
|
|
120
|
+
path?: string;
|
|
121
|
+
}): Promise<LandOutcome>;
|