@shardflux/cli 0.24.0 → 0.25.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 +148 -0
- package/README.md +263 -1
- package/dist/account-args.js +5 -0
- package/dist/account-pool.js +5 -1
- package/dist/acp-journal.js +23 -1
- package/dist/acp-permission-elsewhere.d.ts +38 -0
- package/dist/acp-permission-elsewhere.js +85 -0
- package/dist/acp-rpc.d.ts +2 -1
- package/dist/acp-rpc.js +11 -2
- package/dist/acp.js +95 -22
- package/dist/agent-credentials.d.ts +3 -0
- package/dist/agent-credentials.js +7 -0
- package/dist/args.d.ts +1 -1
- package/dist/args.js +172 -9
- package/dist/board-args.d.ts +6 -0
- package/dist/board-args.js +172 -0
- package/dist/board-commands.d.ts +62 -0
- package/dist/board-commands.js +412 -0
- package/dist/codex-cloud.d.ts +30 -3
- package/dist/codex-cloud.js +86 -12
- package/dist/conversation-commands.d.ts +30 -0
- package/dist/conversation-commands.js +328 -9
- package/dist/conversation-transcript.d.ts +44 -0
- package/dist/conversation-transcript.js +206 -0
- package/dist/conversations.d.ts +7 -3
- package/dist/conversations.js +20 -10
- package/dist/device-approvals.js +4 -3
- package/dist/device-bridge.d.ts +42 -1
- package/dist/device-bridge.js +223 -12
- package/dist/device-folders.d.ts +66 -0
- package/dist/device-folders.js +236 -0
- package/dist/device-helper.d.ts +6 -0
- package/dist/device-helper.js +98 -14
- package/dist/drive-commands.d.ts +25 -0
- package/dist/drive-commands.js +201 -0
- package/dist/errors.js +2 -1
- package/dist/format.js +6 -2
- package/dist/handlers.js +7 -1
- package/dist/land.d.ts +116 -0
- package/dist/land.js +238 -0
- package/dist/main.js +10 -1
- package/dist/move-files.d.ts +39 -0
- package/dist/move-files.js +167 -0
- package/dist/move.js +12 -11
- package/dist/onboarding.d.ts +10 -6
- package/dist/onboarding.js +24 -6
- package/dist/project-git.d.ts +7 -1
- package/dist/project-git.js +16 -3
- package/dist/project-preparation.d.ts +7 -0
- package/dist/project-preparation.js +14 -0
- package/dist/project-template-setup.js +32 -11
- package/dist/router-commands.d.ts +33 -0
- package/dist/router-commands.js +179 -0
- package/dist/task-land-hub.py +150 -0
- package/dist/transcribe-commands.d.ts +7 -0
- package/dist/transcribe-commands.js +80 -0
- package/dist/worktree-twin-engine.d.ts +8 -0
- package/dist/worktree-twin-engine.js +28 -22
- package/package.json +2 -2
package/dist/device-helper.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
-
import { DeviceExecutor, askConnectedAcp } from "./device-bridge.js";
|
|
2
|
+
import { DeviceExecutor, askConnectedAcp, askDeviceApprover } from "./device-bridge.js";
|
|
3
|
+
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
4
|
+
import { allOrgs, allProjects } from "./account-scope.js";
|
|
3
5
|
import { ConversationMirrorState, mirrorProjectConversations } from "./conversation-stash.js";
|
|
4
6
|
import { FilesMirrorState, mirrorFilesOnce } from "./files-mirror.js";
|
|
5
7
|
import { withTargetLock } from "./target-lock.js";
|
|
@@ -44,6 +46,10 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
44
46
|
const acp = await askConnectedAcp(ctx.store.dir, approval, signal);
|
|
45
47
|
if (acp)
|
|
46
48
|
return acp;
|
|
49
|
+
// 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, signal);
|
|
51
|
+
if (app)
|
|
52
|
+
return app;
|
|
47
53
|
if (options.service)
|
|
48
54
|
return askLocalDeviceApproval(ctx, approval);
|
|
49
55
|
return new Promise(resolve => {
|
|
@@ -141,22 +147,71 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
141
147
|
await delay(wait, undefined, { signal }).catch(() => undefined);
|
|
142
148
|
}
|
|
143
149
|
})();
|
|
150
|
+
// Registered for every project an agent may ask from (deviceProjects); one poll covers them all where the API
|
|
151
|
+
// lists by device (0.25.0+), else one per project.
|
|
152
|
+
const registered = new Map();
|
|
153
|
+
let all, deviceWide = true, links = true;
|
|
154
|
+
// This device's local conversations of a project (started here or opened here): their requests come here first.
|
|
155
|
+
const linked = (projectId) => Object.values(localConversations(ctx)).filter(r => !r.handed_off && (!projectId || r.project_id === projectId))
|
|
156
|
+
.sort((a, b) => (b.conversation.created_at ?? '').localeCompare(a.conversation.created_at ?? '')).slice(0, 500).map(r => r.conversation.id);
|
|
157
|
+
const beat = (projectId) => conversationClient(ctx).deviceRequests.heartbeat({ ...(projectId ? { projectId } : {}), deviceId, leaseSeconds: 30, ...(links ? { conversationIds: linked(projectId) } : {}) })
|
|
158
|
+
.catch((e) => {
|
|
159
|
+
// An API before device links (0.25.0+ clients) refuses the list: register without it from then on.
|
|
160
|
+
if (!links || !(e instanceof ShardfluxApiError && [400, 422].includes(e.status)))
|
|
161
|
+
throw e;
|
|
162
|
+
links = false;
|
|
163
|
+
return conversationClient(ctx).deviceRequests.heartbeat({ ...(projectId ? { projectId } : {}), deviceId, leaseSeconds: 30 });
|
|
164
|
+
});
|
|
165
|
+
const take = (rows) => {
|
|
166
|
+
for (const r of rows) {
|
|
167
|
+
if (r.device_id !== deviceId || working.has(r.id) || working.size >= 8)
|
|
168
|
+
continue;
|
|
169
|
+
const work = executor.execute(r).catch(report).finally(() => working.delete(r.id));
|
|
170
|
+
working.set(r.id, work);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
144
173
|
while (!signal.aborted) {
|
|
145
174
|
try {
|
|
146
|
-
|
|
147
|
-
await
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
175
|
+
if (!all || Date.now() - all.at > 300_000)
|
|
176
|
+
all = { ids: await deviceProjects(ctx).catch(() => all?.ids ?? []), at: Date.now() };
|
|
177
|
+
const projects = [...new Set([...Object.values(localConversations(ctx)).map(r => r.project_id), ...all.ids])];
|
|
178
|
+
for (const projectId of projects) {
|
|
179
|
+
if (Date.now() - (registered.get(projectId) ?? 0) < 10_000)
|
|
180
|
+
continue;
|
|
181
|
+
await beat(projectId).then(() => registered.set(projectId, Date.now()), (e) => {
|
|
182
|
+
// A project this sign-in no longer reaches is skipped until the next listing, never the others.
|
|
183
|
+
if (!(e instanceof ShardfluxApiError && [403, 404].includes(e.status)))
|
|
184
|
+
throw e;
|
|
185
|
+
registered.set(projectId, Date.now() + 290_000);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
let polled = false;
|
|
189
|
+
if (deviceWide && registered.size) {
|
|
190
|
+
try {
|
|
191
|
+
let cursor;
|
|
192
|
+
do {
|
|
193
|
+
const page = await conversationClient(ctx).deviceRequests.list({ deviceId, state: 'pending', limit: 200, ...(cursor ? { cursor } : {}) });
|
|
194
|
+
take(page.data);
|
|
195
|
+
cursor = page.nextCursor ?? undefined;
|
|
196
|
+
} while (cursor && !signal.aborted);
|
|
197
|
+
polled = true;
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
if (e instanceof ShardfluxApiError && [400, 422].includes(e.status))
|
|
201
|
+
deviceWide = false;
|
|
202
|
+
else
|
|
203
|
+
throw e;
|
|
204
|
+
}
|
|
159
205
|
}
|
|
206
|
+
if (!polled)
|
|
207
|
+
for (const projectId of projects) {
|
|
208
|
+
let cursor;
|
|
209
|
+
do {
|
|
210
|
+
const page = await conversationClient(ctx).deviceRequests.list({ ...(projectId ? { projectId } : {}), state: 'pending', limit: 200, ...(cursor ? { cursor } : {}) });
|
|
211
|
+
take(page.data);
|
|
212
|
+
cursor = page.nextCursor ?? undefined;
|
|
213
|
+
} while (cursor && !signal.aborted);
|
|
214
|
+
}
|
|
160
215
|
await executor.reap();
|
|
161
216
|
}
|
|
162
217
|
catch (e) {
|
|
@@ -185,3 +240,32 @@ export async function runDeviceHelper(ctx, options = {}) {
|
|
|
185
240
|
await input.catch(() => undefined);
|
|
186
241
|
}
|
|
187
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* The projects this device answers for besides its local conversations' (0.25.0+): the signed-in project (the API
|
|
245
|
+
* key's, or the default one), and with a person's own sign-in and no key, every project they can reach (at most 100).
|
|
246
|
+
* A home conversation, an Orchestrator master or a chat the desktop app started asks from these.
|
|
247
|
+
*/
|
|
248
|
+
export async function deviceProjects(ctx) {
|
|
249
|
+
const profile = ctx.store.profile(ctx.apiUrl), ids = new Set();
|
|
250
|
+
if (profile.project_id)
|
|
251
|
+
ids.add(profile.project_id);
|
|
252
|
+
if (profile.api_key?.project_id)
|
|
253
|
+
ids.add(profile.api_key.project_id);
|
|
254
|
+
const key = Boolean(ctx.io.env.SHARDFLUX_API_KEY?.trim()) || profile.api_key !== undefined;
|
|
255
|
+
const session = Boolean(ctx.io.env.SHARDFLUX_SESSION_TOKEN?.trim()) || profile.session !== undefined;
|
|
256
|
+
if (key && !ids.size)
|
|
257
|
+
ids.add(''); // the key's own project: the API takes it from the key
|
|
258
|
+
if (!key && session) {
|
|
259
|
+
const account = ctx.account();
|
|
260
|
+
for (const org of await allOrgs(account)) {
|
|
261
|
+
for (const p of await allProjects(account, org.id)) {
|
|
262
|
+
ids.add(p.id);
|
|
263
|
+
if (ids.size >= 100)
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
if (ids.size >= 100)
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return [...ids];
|
|
271
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DriveFile, ModelTemplate, Shardflux, ShardfluxAccount } from '@shardflux/sdk';
|
|
2
|
+
import type { Values } from './args.js';
|
|
3
|
+
import type { Ctx, Handler } from './commands.js';
|
|
4
|
+
/** A project API key is in use: SHARDFLUX_API_KEY, else the key `shard setup` saved for this API URL. */
|
|
5
|
+
export declare const hasApiKey: (ctx: Ctx) => boolean;
|
|
6
|
+
export interface ProjectScope {
|
|
7
|
+
client: Shardflux | ShardfluxAccount;
|
|
8
|
+
/** Undefined only when `needId` was false and the API key's own project is meant. */
|
|
9
|
+
projectId: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Who acts and in which project. `--project` acts as the signed-in person when there is a sign-in (their projects,
|
|
13
|
+
* by id or slug), else with the API key. Without it: the API key's own project, else the person's default project.
|
|
14
|
+
* `needId` false leaves the key's project unnamed (the API defaults to it), saving a lookup.
|
|
15
|
+
*/
|
|
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;
|
|
19
|
+
/**
|
|
20
|
+
* The model template as people read it: the primary provider and why, the connected accounts, then one row per role
|
|
21
|
+
* with what it runs and, for a role whose provider is out of usage, until when it waits and what the other provider
|
|
22
|
+
* would run instead.
|
|
23
|
+
*/
|
|
24
|
+
export declare function modelTemplateText(t: ModelTemplate): string;
|
|
25
|
+
export declare const DRIVE_HANDLERS: Record<string, Handler>;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project drive and the model template (0.25.0+): `shard drive ls|get|put|rm` and `shard models`,
|
|
3
|
+
* `shard models primary`. Both act in one project: the API key's own, or `--project`; a person signed in without a
|
|
4
|
+
* key acts in their default project (`shard projects use`), or the only one there is.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
7
|
+
import { basename } from 'node:path';
|
|
8
|
+
import { encodeDrivePath } from '@shardflux/sdk';
|
|
9
|
+
import { UUID } from "./args.js";
|
|
10
|
+
import { resolveOrg, resolveProject } from "./account-scope.js";
|
|
11
|
+
import { out, principal, readInput, str } from "./commands.js";
|
|
12
|
+
import { AuthConfigError, UsageError } from "./errors.js";
|
|
13
|
+
import { humanBytes } from "./files-mirror.js";
|
|
14
|
+
import { json, table, time } from "./format.js";
|
|
15
|
+
/** A drive file of up to 5 GiB is read into memory to send it with its SHA-256. */
|
|
16
|
+
const MAX_PUT_BYTES = 5 * 1024 ** 3;
|
|
17
|
+
const SHA256 = /^[0-9a-f]{64}$/i;
|
|
18
|
+
/** A project API key is in use: SHARDFLUX_API_KEY, else the key `shard setup` saved for this API URL. */
|
|
19
|
+
export const hasApiKey = (ctx) => Boolean(ctx.io.env.SHARDFLUX_API_KEY?.trim()) || ctx.store.profile(ctx.apiUrl).api_key !== undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Who acts and in which project. `--project` acts as the signed-in person when there is a sign-in (their projects,
|
|
22
|
+
* by id or slug), else with the API key. Without it: the API key's own project, else the person's default project.
|
|
23
|
+
* `needId` false leaves the key's project unnamed (the API defaults to it), saving a lookup.
|
|
24
|
+
*/
|
|
25
|
+
export async function projectScope(ctx, values, needId = true) {
|
|
26
|
+
const given = str(values.project);
|
|
27
|
+
const signedIn = ctx.sessionSource() !== null;
|
|
28
|
+
if (signedIn && (given !== undefined || !hasApiKey(ctx))) {
|
|
29
|
+
const account = ctx.account();
|
|
30
|
+
return { client: account, projectId: (await resolveProject(ctx, account, values)).id };
|
|
31
|
+
}
|
|
32
|
+
const cloud = ctx.cloud();
|
|
33
|
+
if (given !== undefined) {
|
|
34
|
+
if (!UUID.test(given))
|
|
35
|
+
throw new UsageError(`--project with an API key takes the project id (got "${given}").`, 'or sign in to name projects by slug: shard auth login');
|
|
36
|
+
return { client: cloud, projectId: given };
|
|
37
|
+
}
|
|
38
|
+
if (!needId)
|
|
39
|
+
return { client: cloud, projectId: undefined };
|
|
40
|
+
return { client: cloud, projectId: ctx.apiKey().saved?.project_id ?? (await principal(ctx)).project_id };
|
|
41
|
+
}
|
|
42
|
+
/** A drive path checked as the SDK sends it: relative, no empty, `.` or `..` segments (exit 2 before any request). */
|
|
43
|
+
function drivePath(path, what = 'drive path') {
|
|
44
|
+
try {
|
|
45
|
+
encodeDrivePath(path);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (err instanceof TypeError)
|
|
49
|
+
throw new UsageError(`${what}: ${err.message}`);
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
/** `drive ls` rows: size, saved time, type and path (the path last, as `files ls` puts the name). */
|
|
55
|
+
export function driveTable(rows) {
|
|
56
|
+
return table(['SIZE', 'SAVED', 'TYPE', 'PATH'], rows.map((f) => [humanBytes(f.size), time(f.created_at), f.content_type, f.path]));
|
|
57
|
+
}
|
|
58
|
+
const SOURCES = {
|
|
59
|
+
marked: 'the organization’s choice',
|
|
60
|
+
project: 'this project’s choice',
|
|
61
|
+
first_connected: 'connected first',
|
|
62
|
+
};
|
|
63
|
+
const ROLE_ORDER = ['master', 'builder', 'hard', 'light', 'reviewer'];
|
|
64
|
+
const providerName = (p) => (p === 'claude' ? 'Claude' : 'Codex');
|
|
65
|
+
/**
|
|
66
|
+
* The model template as people read it: the primary provider and why, the connected accounts, then one row per role
|
|
67
|
+
* with what it runs and, for a role whose provider is out of usage, until when it waits and what the other provider
|
|
68
|
+
* would run instead.
|
|
69
|
+
*/
|
|
70
|
+
export function modelTemplateText(t) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
if (t.primary === null)
|
|
73
|
+
lines.push('Connect Claude or Codex to run tasks: shard agents accounts add claude (or codex).');
|
|
74
|
+
else
|
|
75
|
+
lines.push(`Primary: ${t.primary} (${t.primary_source ? SOURCES[t.primary_source] : 'connected first'})${t.secondary ? `, secondary: ${t.secondary}` : ''}`);
|
|
76
|
+
if (t.connected.length)
|
|
77
|
+
lines.push(`Connected: ${t.connected.map((c) => `${c.provider}${c.plan_type ? ` (${c.plan_type})` : ''}`).join(', ')}`);
|
|
78
|
+
if (t.task_harness !== null || t.attempt > 0)
|
|
79
|
+
lines.push(`Task: ${[t.task_harness ? `on ${t.task_harness}` : '', t.attempt > 0 ? `retry ${t.attempt}` : ''].filter(Boolean).join(', ')}`);
|
|
80
|
+
const rows = ROLE_ORDER.map((role) => {
|
|
81
|
+
const r = t.roles[role];
|
|
82
|
+
const effort = `${r.effort ?? 'default'}${r.planning_effort ? ` (plans ${r.planning_effort})` : ''}`;
|
|
83
|
+
const wait = r.wait ? `${time(r.wait.until)}${r.wait.alternative ? `, or now: ${r.wait.alternative.label}` : ''}` : '-';
|
|
84
|
+
return [role, r.harness, r.model, effort, wait];
|
|
85
|
+
});
|
|
86
|
+
return `${lines.join('\n')}\n\n${table(['ROLE', 'HARNESS', 'MODEL', 'EFFORT', 'WAITS UNTIL'], rows)}`;
|
|
87
|
+
}
|
|
88
|
+
function harnessOf(values) {
|
|
89
|
+
const h = str(values.harness);
|
|
90
|
+
if (h !== undefined && h !== 'claude' && h !== 'codex')
|
|
91
|
+
throw new UsageError(`--harness must be claude or codex (got "${h}").`);
|
|
92
|
+
return h;
|
|
93
|
+
}
|
|
94
|
+
function attemptOf(values) {
|
|
95
|
+
const a = str(values.attempt);
|
|
96
|
+
if (a === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (!/^\d+$/.test(a) || Number(a) > 10)
|
|
99
|
+
throw new UsageError(`--attempt must be 0 for a first run, or 1 to 10 for a retry (got "${a}").`);
|
|
100
|
+
return Number(a);
|
|
101
|
+
}
|
|
102
|
+
export const DRIVE_HANDLERS = {
|
|
103
|
+
async 'drive ls'(ctx, values, [prefix]) {
|
|
104
|
+
const { client, projectId } = await projectScope(ctx, values);
|
|
105
|
+
const limitText = str(values.limit);
|
|
106
|
+
const limit = limitText === undefined ? undefined : Number(limitText);
|
|
107
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 1000))
|
|
108
|
+
throw new UsageError('--limit must be 1 to 1000.');
|
|
109
|
+
const rows = [];
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
let cursor = str(values.cursor), next;
|
|
112
|
+
do {
|
|
113
|
+
const page = await client.drive.list(projectId, { ...(prefix ? { prefix } : {}), ...(limit ? { limit } : values.all === true ? { limit: 1000 } : {}), ...(cursor ? { cursor } : {}), signal: ctx.signal });
|
|
114
|
+
rows.push(...page.data);
|
|
115
|
+
next = page.nextCursor;
|
|
116
|
+
cursor = next ?? undefined;
|
|
117
|
+
if (values.all === true && cursor) {
|
|
118
|
+
if (seen.has(cursor))
|
|
119
|
+
throw new UsageError('The drive listing repeated its cursor. Try again.');
|
|
120
|
+
seen.add(cursor);
|
|
121
|
+
}
|
|
122
|
+
} while (values.all === true && cursor);
|
|
123
|
+
out(ctx, rows.length ? driveTable(rows) : 'No files.\n', { data: rows, next_cursor: next });
|
|
124
|
+
if (!ctx.json && next)
|
|
125
|
+
ctx.io.stderr.write(`more: shard drive ls${prefix ? ` ${prefix}` : ''} --cursor ${next}\n`);
|
|
126
|
+
return 0;
|
|
127
|
+
},
|
|
128
|
+
async 'drive get'(ctx, values, [path]) {
|
|
129
|
+
drivePath(path);
|
|
130
|
+
const sha256 = str(values.sha256)?.toLowerCase();
|
|
131
|
+
if (sha256 !== undefined && !SHA256.test(sha256))
|
|
132
|
+
throw new UsageError('--sha256 takes the 64 hex characters of a version’s SHA-256 (shard drive ls --json shows them).');
|
|
133
|
+
const { client, projectId } = await projectScope(ctx, values);
|
|
134
|
+
const file = await client.drive.read(projectId, path, { ...(sha256 ? { sha256 } : {}), signal: ctx.signal });
|
|
135
|
+
const dest = str(values.out);
|
|
136
|
+
const meta = { path, sha256: file.sha256, size: file.size, content_type: file.contentType };
|
|
137
|
+
if (dest !== undefined) {
|
|
138
|
+
await writeFile(dest, file.bytes);
|
|
139
|
+
out(ctx, `Wrote ${path} to ${dest} (${humanBytes(file.size)}, sha256 ${file.sha256})\n`, { ...meta, out: dest });
|
|
140
|
+
}
|
|
141
|
+
else if (ctx.json) {
|
|
142
|
+
ctx.io.stdout.write(json({ ...meta, content_base64: Buffer.from(file.bytes).toString('base64') }));
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
ctx.io.stdout.write(file.bytes);
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
},
|
|
149
|
+
async 'drive put'(ctx, values, [local, target]) {
|
|
150
|
+
if (local === '-' && target === undefined)
|
|
151
|
+
throw new UsageError('Name the drive path when the file comes from standard input: shard drive put - <drive-path>');
|
|
152
|
+
const path = drivePath(target ?? basename(local), target === undefined ? `the file name "${basename(local)}"` : 'drive path');
|
|
153
|
+
const bytes = local === '-' ? await readInput(ctx, '-', MAX_PUT_BYTES, 'standard input') : await readFile(local).catch((e) => {
|
|
154
|
+
throw new UsageError(`cannot read "${local}" (${e.code === 'EISDIR' ? 'a folder; put its files one by one' : e.code ?? e.message})`);
|
|
155
|
+
});
|
|
156
|
+
const contentType = str(values['content-type']);
|
|
157
|
+
const { client, projectId } = await projectScope(ctx, values);
|
|
158
|
+
const file = await client.drive.save(projectId, path, bytes, { ...(contentType ? { contentType } : {}), signal: ctx.signal });
|
|
159
|
+
out(ctx, `Saved ${file.path} (${humanBytes(file.size)}, ${file.content_type})\nsha256 ${file.sha256}\n`, { file });
|
|
160
|
+
return 0;
|
|
161
|
+
},
|
|
162
|
+
async 'drive rm'(ctx, values, [path]) {
|
|
163
|
+
drivePath(path);
|
|
164
|
+
const { client, projectId } = await projectScope(ctx, values);
|
|
165
|
+
await client.drive.remove(projectId, path, { signal: ctx.signal });
|
|
166
|
+
out(ctx, `Removed ${path}\n`, { path, removed: true });
|
|
167
|
+
return 0;
|
|
168
|
+
},
|
|
169
|
+
async models(ctx, values) {
|
|
170
|
+
const harness = harnessOf(values), attempt = attemptOf(values);
|
|
171
|
+
const { client, projectId } = await projectScope(ctx, values, false);
|
|
172
|
+
const t = await client.modelTemplate.get({ ...(projectId ? { projectId } : {}), ...(harness ? { harness } : {}), ...(attempt !== undefined ? { attempt } : {}), signal: ctx.signal });
|
|
173
|
+
out(ctx, modelTemplateText(t), t);
|
|
174
|
+
return 0;
|
|
175
|
+
},
|
|
176
|
+
async 'models primary'(ctx, values, [choice]) {
|
|
177
|
+
const word = choice.toLowerCase();
|
|
178
|
+
if (!['claude', 'codex', 'clear'].includes(word))
|
|
179
|
+
throw new UsageError(`Choose claude, codex or clear (got "${choice}").`);
|
|
180
|
+
const provider = word === 'clear' ? null : word;
|
|
181
|
+
if (str(values.org) !== undefined && str(values.project) !== undefined)
|
|
182
|
+
throw new UsageError('Choose --org (the organization’s default) or --project (one project).');
|
|
183
|
+
let t, whose;
|
|
184
|
+
if (str(values.org) !== undefined) {
|
|
185
|
+
if (ctx.sessionSource() === null)
|
|
186
|
+
throw new AuthConfigError('The organization’s default is set by its owners and admins, signed in.', 'sign in: shard auth login (an API key sets its own project’s: shard models primary <claude|codex> without --org)');
|
|
187
|
+
const account = ctx.account();
|
|
188
|
+
const org = await resolveOrg(ctx, account, values);
|
|
189
|
+
t = await account.modelTemplate.setPrimary({ provider, organizationId: org.id, signal: ctx.signal });
|
|
190
|
+
whose = `${org.name}’s`;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
const { client, projectId } = await projectScope(ctx, values);
|
|
194
|
+
t = await client.modelTemplate.setPrimary({ provider, projectId: projectId, signal: ctx.signal });
|
|
195
|
+
whose = 'this project’s';
|
|
196
|
+
}
|
|
197
|
+
// The template that follows shows the primary now in effect and where it comes from.
|
|
198
|
+
out(ctx, `${provider ? `${providerName(provider)} is ${whose} primary.` : `Cleared ${whose} primary.`}\n${modelTemplateText(t)}`, t);
|
|
199
|
+
return 0;
|
|
200
|
+
},
|
|
201
|
+
};
|
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
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { DEVICE_HANDLERS } from "./device-approvals.js";
|
|
2
|
+
import { PROJECT_LINK_HANDLERS } from "./device-folders.js";
|
|
2
3
|
import { CREDENTIAL_HANDLERS } from "./credential-prompts.js";
|
|
3
4
|
import { CONVERSATION_HANDLERS } from "./conversation-commands.js";
|
|
5
|
+
import { BOARD_HANDLERS } from "./board-commands.js";
|
|
6
|
+
import { TRANSCRIPT_HANDLERS } from "./conversation-transcript.js";
|
|
7
|
+
import { ROUTER_HANDLERS } from "./router-commands.js";
|
|
8
|
+
import { TRANSCRIBE_HANDLERS } from "./transcribe-commands.js";
|
|
4
9
|
/**
|
|
5
10
|
* Every command's handler, keyed by its path ("workspaces open", "auth login"): the project-key commands of
|
|
6
11
|
* commands.ts and the account plane of account-commands.ts (0.5.0). args.test.ts checks that it matches COMMANDS.
|
|
@@ -26,4 +31,5 @@ import { AGENT_CONFIG_HANDLERS } from "./agent-config-sync.js";
|
|
|
26
31
|
import { MACHINE_SETUP_HANDLERS } from "./machine-setup.js";
|
|
27
32
|
import { SETTINGS_HANDLERS } from "./client-settings.js";
|
|
28
33
|
import { PROJECT_PREPARATION_HANDLERS } from "./project-preparation.js";
|
|
29
|
-
|
|
34
|
+
import { DRIVE_HANDLERS } from "./drive-commands.js";
|
|
35
|
+
export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...PROJECT_LINK_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...BOARD_HANDLERS, ...TRANSCRIPT_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_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 };
|
package/dist/land.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
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' | '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 person's folder for a project on this computer: a linked folder (shard projects link) or where it was started. */
|
|
65
|
+
export declare function linkedFolder(ctx: Pick<Ctx, 'store' | 'apiUrl'>, projectId: string, conversationId?: string): string | null;
|
|
66
|
+
export declare const displayPath: (path: string) => string;
|
|
67
|
+
export interface LandOutcome {
|
|
68
|
+
landed: true;
|
|
69
|
+
task_id?: string;
|
|
70
|
+
conversation_id?: string;
|
|
71
|
+
title: string;
|
|
72
|
+
result: {
|
|
73
|
+
branch: string;
|
|
74
|
+
sha: string;
|
|
75
|
+
};
|
|
76
|
+
hub: {
|
|
77
|
+
into: string;
|
|
78
|
+
sha: string;
|
|
79
|
+
method: string;
|
|
80
|
+
};
|
|
81
|
+
folder: (FolderLanding & {
|
|
82
|
+
display: string;
|
|
83
|
+
}) | null;
|
|
84
|
+
recorded: boolean;
|
|
85
|
+
record_error?: string;
|
|
86
|
+
/** One line for people: "Landed in ~/code/best-todo · 3f2a1c9". */
|
|
87
|
+
message: string;
|
|
88
|
+
}
|
|
89
|
+
/** What a landing needs: the API, a VM that reads the project hub, the person's folder for a project, this device. */
|
|
90
|
+
export interface LandDeps {
|
|
91
|
+
client: {
|
|
92
|
+
tasks: Pick<ReturnType<typeof conversationClient>['tasks'], 'get' | 'recordLanding' | 'steer'>;
|
|
93
|
+
conversations: Pick<ReturnType<typeof conversationClient>['conversations'], 'get' | 'land'>;
|
|
94
|
+
};
|
|
95
|
+
hubCell(projectId: string): Promise<{
|
|
96
|
+
cell: TwinCell;
|
|
97
|
+
close(): void;
|
|
98
|
+
}>;
|
|
99
|
+
folder(projectId: string, conversationId?: string): string | null;
|
|
100
|
+
device: string;
|
|
101
|
+
stateDir: string;
|
|
102
|
+
hub?: string;
|
|
103
|
+
quietMs?: number;
|
|
104
|
+
signal?: AbortSignal;
|
|
105
|
+
}
|
|
106
|
+
export declare function landDeps(ctx: Ctx): LandDeps;
|
|
107
|
+
/** What a landing did, for people: the line, then what was not recorded. */
|
|
108
|
+
export declare function landedText(o: LandOutcome): string;
|
|
109
|
+
/** `shard task land <task>`: the task's result into the project's default branch and the linked folder, then recorded. */
|
|
110
|
+
export declare function landTask(d: LandDeps, taskId: string, opts?: {
|
|
111
|
+
path?: string;
|
|
112
|
+
}): Promise<LandOutcome>;
|
|
113
|
+
/** `shard conversations land <id> merge`: the conversation's branch into the default branch and its folder, then recorded. */
|
|
114
|
+
export declare function landConversation(d: LandDeps, id: string, opts?: {
|
|
115
|
+
path?: string;
|
|
116
|
+
}): Promise<LandOutcome>;
|