@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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Your orchestrator (0.27.0+, SDK 0.29.0): `shard orchestrator say` (with --wait), `list`, `retry` and `dismiss`, and
|
|
3
|
+
* `restore` (0.28.0+, SDK 0.30.0), through @shardflux/sdk's `orchestrator` namespace. The command table is orchestrator-args.ts.
|
|
4
|
+
*
|
|
5
|
+
* The signed-in session reaches the person's orchestrator; SHARDFLUX_API_KEY in the environment (or a saved key without
|
|
6
|
+
* a session) reaches the orchestrator of the member who created that key (person-scope.ts). Text output is one line
|
|
7
|
+
* per input, tab-separated, id first; --json prints the API's objects.
|
|
8
|
+
*/
|
|
9
|
+
import { inputSettled } from '@shardflux/sdk';
|
|
10
|
+
import { parseDuration } from "./args.js";
|
|
11
|
+
import { out, str } from "./commands.js";
|
|
12
|
+
import { EXIT, UsageError } from "./errors.js";
|
|
13
|
+
import { humanBytes } from "./files-mirror.js";
|
|
14
|
+
import { inScope } from "./person-scope.js";
|
|
15
|
+
import { progress } from "./progress.js";
|
|
16
|
+
/** --wait's default: how long `say --wait` follows an input before it stops (the input goes on). */
|
|
17
|
+
export const SAY_WAIT_MS = 10 * 60_000;
|
|
18
|
+
const oneLine = (s, max) => {
|
|
19
|
+
const line = s.replace(/\s+/g, ' ').trim();
|
|
20
|
+
return line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line;
|
|
21
|
+
};
|
|
22
|
+
/** The selection flags and the API's id shape for each. */
|
|
23
|
+
const POINTERS = [
|
|
24
|
+
['task', 'task_id', /^task_[a-z2-7]{26}$/, 'task_…'],
|
|
25
|
+
['report', 'report_id', /^rep_[a-z2-7]{26}$/, 'rep_…'],
|
|
26
|
+
['needs-you', 'needs_you_id', /^[a-z]{2,6}_[A-Za-z0-9_-]{1,80}$/, 'perm_…, oq_…'],
|
|
27
|
+
['input', 'input_id', /^in_[a-z2-7]{26}$/, 'in_…'],
|
|
28
|
+
['conversation', 'conversation_id', /^conv_[a-z2-7]{26}$/, 'conv_…'],
|
|
29
|
+
];
|
|
30
|
+
/** The row the person points at, from --task, --report, --needs-you, --input and --conversation; undefined for none. */
|
|
31
|
+
export function selectionOf(values) {
|
|
32
|
+
const selection = {};
|
|
33
|
+
for (const [flag, field, pattern, example] of POINTERS) {
|
|
34
|
+
const v = str(values[flag]);
|
|
35
|
+
if (v === undefined)
|
|
36
|
+
continue;
|
|
37
|
+
if (!pattern.test(v))
|
|
38
|
+
throw new UsageError(`--${flag} must be an id (${example})`);
|
|
39
|
+
selection[field] = v;
|
|
40
|
+
}
|
|
41
|
+
return Object.keys(selection).length ? selection : undefined;
|
|
42
|
+
}
|
|
43
|
+
/** An input's state with what explains it: "queued (2 in line)", "waiting (Waiting for your computer: list ~/code)". */
|
|
44
|
+
export function stateLabel(i) {
|
|
45
|
+
if (i.state === 'queued')
|
|
46
|
+
return i.waiting ? `queued (${oneLine(i.waiting.message, 80)})` : i.queue_position ? `queued (${i.queue_position} in line)` : 'queued';
|
|
47
|
+
if (i.state === 'waiting' && i.waiting)
|
|
48
|
+
return `waiting (${oneLine(i.waiting.message, 80)})`;
|
|
49
|
+
return i.state;
|
|
50
|
+
}
|
|
51
|
+
/** What an input became, in a few words: the answer's headline, its tasks, the input it went to, or why it failed. */
|
|
52
|
+
export function becameText(i) {
|
|
53
|
+
if (i.state === 'merged' && i.merged_into)
|
|
54
|
+
return `added to ${i.merged_into}`;
|
|
55
|
+
if (i.state === 'failed' && i.error)
|
|
56
|
+
return oneLine(i.error.message, 100);
|
|
57
|
+
const parts = [];
|
|
58
|
+
if (i.answer)
|
|
59
|
+
parts.push(oneLine(i.answer.headline, 100));
|
|
60
|
+
if (i.task_ids.length)
|
|
61
|
+
parts.push(`${i.task_ids.length} task${i.task_ids.length === 1 ? '' : 's'}: ${i.task_ids.join(', ')}`);
|
|
62
|
+
return parts.join('; ');
|
|
63
|
+
}
|
|
64
|
+
/** One input as `shard orchestrator list` prints it: id, state, your words, what it became. */
|
|
65
|
+
export function inputLine(i) {
|
|
66
|
+
const became = becameText(i);
|
|
67
|
+
return `${i.id}\t${stateLabel(i)}\t${oneLine(i.text, 60)}${became ? `\t${became}` : ''}`;
|
|
68
|
+
}
|
|
69
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
70
|
+
const two = (n) => String(n).padStart(2, '0');
|
|
71
|
+
/**
|
|
72
|
+
* When an answer's point happened, short and in this computer's local time: "14:02" today, "Oct 16 14:02" this year,
|
|
73
|
+
* else "2025-10-16 14:02". A time without an offset is the person's local time and prints as written.
|
|
74
|
+
*/
|
|
75
|
+
export function shortTime(at, now = new Date()) {
|
|
76
|
+
const d = new Date(at);
|
|
77
|
+
if (!Number.isFinite(d.getTime()))
|
|
78
|
+
return at;
|
|
79
|
+
const clock = `${two(d.getHours())}:${two(d.getMinutes())}`;
|
|
80
|
+
if (d.getFullYear() !== now.getFullYear())
|
|
81
|
+
return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())} ${clock}`;
|
|
82
|
+
if (d.getMonth() !== now.getMonth() || d.getDate() !== now.getDate())
|
|
83
|
+
return `${MONTHS[d.getMonth()]} ${d.getDate()} ${clock}`;
|
|
84
|
+
return clock;
|
|
85
|
+
}
|
|
86
|
+
/** An answer's body: its points under their group headings when the agent wrote points (0.27.0+), else its facts. */
|
|
87
|
+
export function answerLines(answer, now = new Date()) {
|
|
88
|
+
if (!answer.points?.length)
|
|
89
|
+
return answer.facts.map((f) => ` - ${f}`);
|
|
90
|
+
const lines = [];
|
|
91
|
+
let group;
|
|
92
|
+
for (const p of answer.points) {
|
|
93
|
+
if (p.group !== group && p.group)
|
|
94
|
+
lines.push(` ${p.group}`);
|
|
95
|
+
group = p.group;
|
|
96
|
+
// A point the agent wrote without a lead is its words alone; every word prints, however long.
|
|
97
|
+
const words = p.lead ? `${p.lead}${p.detail ? `: ${p.detail}` : ''}` : p.detail ?? '';
|
|
98
|
+
lines.push(` - ${words}${p.at ? ` (${shortTime(p.at, now)})` : ''}`);
|
|
99
|
+
}
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
/** One file an answer shows (0.27.0+): ` media: loss-curve.png (image, 1200×800, 84.0 KB)`, the size in pixels once known. */
|
|
103
|
+
export function mediaLine(m) {
|
|
104
|
+
return ` media: ${m.name} (${[m.kind, ...(m.width && m.height ? [`${m.width}×${m.height}`] : []), humanBytes(m.size)].join(', ')})`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* What `say --wait` prints once an input is done, merged, failed or waiting: its curated result.
|
|
108
|
+
*
|
|
109
|
+
* Training is 73% done, on pace to finish at 16:40
|
|
110
|
+
* Moved
|
|
111
|
+
* - Loss is down to 1.84: from 2.31 at step 5,000 (14:02)
|
|
112
|
+
* - Checkpoint saved: at step 7,000
|
|
113
|
+
* Still open
|
|
114
|
+
* - Eval has not run: it starts after the last checkpoint
|
|
115
|
+
* media: loss-curve.png (image, 1200×800, 84.0 KB)
|
|
116
|
+
* Sources: train.log (~/code/lm-train/train.log), wandb.ai (https://wandb.ai/acme/lm)
|
|
117
|
+
* Follow-ups: Check again in an hour
|
|
118
|
+
* Tasks: task_… (shard task show <id>)
|
|
119
|
+
*
|
|
120
|
+
* An answer written as facts prints them as ` - <fact>` lines instead of the points. Each file the answer shows prints
|
|
121
|
+
* one media line after them (shard drive get <path> --sha256 <sha256> reads it; --json has both).
|
|
122
|
+
*/
|
|
123
|
+
export function resultText(i, now = new Date()) {
|
|
124
|
+
const lines = [];
|
|
125
|
+
if (i.answer) {
|
|
126
|
+
lines.push(i.answer.headline);
|
|
127
|
+
lines.push(...answerLines(i.answer, now));
|
|
128
|
+
for (const m of i.answer.media ?? [])
|
|
129
|
+
lines.push(mediaLine(m));
|
|
130
|
+
if (i.answer.sources.length)
|
|
131
|
+
lines.push(` Sources: ${i.answer.sources.map((s) => (s.label === s.ref ? s.label : `${s.label} (${s.ref})`)).join(', ')}`);
|
|
132
|
+
if (i.answer.actions.length)
|
|
133
|
+
lines.push(` Follow-ups: ${i.answer.actions.map((a) => a.label).join(' / ')}`);
|
|
134
|
+
}
|
|
135
|
+
if (i.task_ids.length)
|
|
136
|
+
lines.push(`Tasks: ${i.task_ids.join(', ')} (shard task show <id>)`);
|
|
137
|
+
if (i.state === 'merged')
|
|
138
|
+
lines.push(`Added to ${i.merged_into}: its thread has your words.`);
|
|
139
|
+
if (i.state === 'failed') {
|
|
140
|
+
lines.push(`Failed: ${i.error?.message ?? 'its thread stopped.'}`);
|
|
141
|
+
if (i.error?.retryable !== false)
|
|
142
|
+
lines.push(`Send it again: shard orchestrator retry ${i.id}`);
|
|
143
|
+
}
|
|
144
|
+
if (i.state === 'waiting' && i.waiting) {
|
|
145
|
+
lines.push(i.waiting.message);
|
|
146
|
+
const asked = i.question_ids.at(-1);
|
|
147
|
+
if (i.waiting.for === 'you' && asked)
|
|
148
|
+
lines.push(`Answer it: shard needs-you answer ${asked} --text <answer>`);
|
|
149
|
+
if (i.waiting.for === 'computer')
|
|
150
|
+
lines.push('It runs when your computer is back (the Shardflux app open).');
|
|
151
|
+
}
|
|
152
|
+
if (!lines.length)
|
|
153
|
+
lines.push(i.state === 'done' ? 'Done.' : stateLabel(i));
|
|
154
|
+
return `${lines.join('\n')}\n`;
|
|
155
|
+
}
|
|
156
|
+
/** Where `say --wait` stops following: nothing more happens to the input, or it waits on you or your computer. */
|
|
157
|
+
export const stopsWaiting = (i) => inputSettled(i) || i.state === 'waiting';
|
|
158
|
+
const orgOnly = (scope) => (scope.organizationId ? { organizationId: scope.organizationId } : {});
|
|
159
|
+
/**
|
|
160
|
+
* Follows input `id` until it stops waiting (stopsWaiting), `timeoutMs` passes, or Ctrl-C: a plain read, then the SDK's
|
|
161
|
+
* watch from its cursor, so no change between them is missed. Progress lines (its place in line, working) go to stderr.
|
|
162
|
+
*/
|
|
163
|
+
async function follow(ctx, client, scope, first, timeoutMs) {
|
|
164
|
+
let last = first, shown = '';
|
|
165
|
+
const show = (i) => {
|
|
166
|
+
const line = i.state === 'queued' ? `Queued${i.queue_position ? `: ${i.queue_position} in line` : ''}${i.waiting ? ` (${i.waiting.message})` : ''}` : i.state === 'working' ? 'Working' : '';
|
|
167
|
+
if (line && line !== shown)
|
|
168
|
+
progress(ctx, line);
|
|
169
|
+
shown = line;
|
|
170
|
+
};
|
|
171
|
+
const page = await client.orchestrator.inputs({ ...orgOnly(scope), signal: ctx.signal });
|
|
172
|
+
last = page.inputs?.find((i) => i.id === first.id) ?? last;
|
|
173
|
+
if (stopsWaiting(last))
|
|
174
|
+
return { input: last, timedOut: false };
|
|
175
|
+
show(last);
|
|
176
|
+
const abort = new AbortController();
|
|
177
|
+
const stop = () => abort.abort();
|
|
178
|
+
ctx.signal.addEventListener('abort', stop, { once: true });
|
|
179
|
+
let timedOut = false;
|
|
180
|
+
const timer = setTimeout(() => { timedOut = true; abort.abort(); }, timeoutMs);
|
|
181
|
+
try {
|
|
182
|
+
for await (const u of client.orchestrator.watch({ ...orgOnly(scope), cursor: page.cursor, signal: abort.signal })) {
|
|
183
|
+
last = u.inputs.find((i) => i.id === first.id) ?? last;
|
|
184
|
+
if (stopsWaiting(last))
|
|
185
|
+
return { input: last, timedOut: false };
|
|
186
|
+
show(last);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
ctx.signal.removeEventListener('abort', stop);
|
|
192
|
+
}
|
|
193
|
+
return { input: last, timedOut };
|
|
194
|
+
}
|
|
195
|
+
const idOf = (id) => {
|
|
196
|
+
if (!id || !/^in_[a-z2-7]{26}$/.test(id))
|
|
197
|
+
throw new UsageError('Name the input by its id (in_…): shard orchestrator list shows them.');
|
|
198
|
+
return id;
|
|
199
|
+
};
|
|
200
|
+
export const ORCHESTRATOR_HANDLERS = {
|
|
201
|
+
async 'orchestrator say'(ctx, values, words) {
|
|
202
|
+
const text = words.join(' ');
|
|
203
|
+
if (!text.trim())
|
|
204
|
+
throw new UsageError('missing <words...> for "shard orchestrator say"');
|
|
205
|
+
const selection = selectionOf(values);
|
|
206
|
+
const wait = values.wait === true;
|
|
207
|
+
const timeoutMs = str(values.timeout) === undefined ? SAY_WAIT_MS : parseDuration(str(values.timeout), 'timeout');
|
|
208
|
+
if (!wait && str(values.timeout) !== undefined)
|
|
209
|
+
throw new UsageError('--timeout goes with --wait.');
|
|
210
|
+
return inScope(ctx, values, async (client, scope) => {
|
|
211
|
+
const input = await client.orchestrator.say(text, { ...orgOnly(scope), ...(selection ? { selection } : {}), signal: ctx.signal });
|
|
212
|
+
if (!wait) {
|
|
213
|
+
out(ctx, `${input.id}\t${stateLabel(input)}\n`, { input });
|
|
214
|
+
return EXIT.ok;
|
|
215
|
+
}
|
|
216
|
+
progress(ctx, `Sent: ${input.id}`);
|
|
217
|
+
let followed;
|
|
218
|
+
try {
|
|
219
|
+
followed = await follow(ctx, client, scope, input, timeoutMs);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
if (ctx.signal.aborted)
|
|
223
|
+
return EXIT.interrupted;
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
if (ctx.signal.aborted && !followed.timedOut && !stopsWaiting(followed.input))
|
|
227
|
+
return EXIT.interrupted;
|
|
228
|
+
const end = followed.input;
|
|
229
|
+
if (followed.timedOut) {
|
|
230
|
+
out(ctx, `${end.id}\t${stateLabel(end)}\n`, { input: end });
|
|
231
|
+
ctx.io.stderr.write(`Still ${end.state} after ${Math.round(timeoutMs / 1000)} s; it goes on: shard orchestrator list shows it.\n`);
|
|
232
|
+
return EXIT.timeout;
|
|
233
|
+
}
|
|
234
|
+
out(ctx, resultText(end), { input: end });
|
|
235
|
+
return end.state === 'failed' ? EXIT.operationFailed : EXIT.ok;
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
async 'orchestrator list'(ctx, values) {
|
|
239
|
+
const page = await inScope(ctx, values, (client, scope) => client.orchestrator.inputs({ ...orgOnly(scope), signal: ctx.signal }));
|
|
240
|
+
const shown = (page.inputs ?? []).filter((i) => values.all === true || !i.dismissed_at);
|
|
241
|
+
const human = shown.length ? `${shown.map(inputLine).join('\n')}\n` : 'Nothing to show yet: shard orchestrator say <words> sends your orchestrator something.\n';
|
|
242
|
+
out(ctx, human, { inputs: page.inputs ?? [], cursor: page.cursor });
|
|
243
|
+
return EXIT.ok;
|
|
244
|
+
},
|
|
245
|
+
async 'orchestrator retry'(ctx, values, [id]) {
|
|
246
|
+
const input = await inScope(ctx, values, (client, scope) => client.orchestrator.retry(idOf(id), { ...orgOnly(scope), signal: ctx.signal }));
|
|
247
|
+
out(ctx, `${input.id}\t${stateLabel(input)}\n`, { input });
|
|
248
|
+
return EXIT.ok;
|
|
249
|
+
},
|
|
250
|
+
async 'orchestrator dismiss'(ctx, values, [id]) {
|
|
251
|
+
const input = await inScope(ctx, values, (client, scope) => client.orchestrator.dismiss(idOf(id), { ...orgOnly(scope), signal: ctx.signal }));
|
|
252
|
+
out(ctx, `Dismissed: ${oneLine(input.text, 80)}\n`, { input });
|
|
253
|
+
return EXIT.ok;
|
|
254
|
+
},
|
|
255
|
+
async 'orchestrator restore'(ctx, values, [id]) {
|
|
256
|
+
const input = await inScope(ctx, values, (client, scope) => client.orchestrator.restore(idOf(id), { ...orgOnly(scope), signal: ctx.signal }));
|
|
257
|
+
out(ctx, `Restored: ${oneLine(input.text, 80)}\n`, { input });
|
|
258
|
+
return EXIT.ok;
|
|
259
|
+
},
|
|
260
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Shardflux, ShardfluxAccount } from '@shardflux/sdk';
|
|
2
|
+
import type { Values } from './args.js';
|
|
3
|
+
import type { Ctx } from './commands.js';
|
|
4
|
+
export interface BoardClient {
|
|
5
|
+
client: Shardflux | ShardfluxAccount;
|
|
6
|
+
/** The signed-in session: the organization's whole board. */
|
|
7
|
+
session: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** SHARDFLUX_API_KEY in the environment wins; else the signed-in session; else the saved key (its error names both). */
|
|
10
|
+
export declare function boardClient(ctx: Ctx): BoardClient;
|
|
11
|
+
export interface Scope {
|
|
12
|
+
organizationId?: string;
|
|
13
|
+
projectId?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Runs `call` in the board's scope. With the session and no --org/--project: the saved default organization, else the
|
|
17
|
+
* only one; when the person is in several and none is saved (422), or the saved one is gone (404), the error lists
|
|
18
|
+
* theirs and names the fix (resolveOrg), and an organization that resolves is used.
|
|
19
|
+
*/
|
|
20
|
+
export declare function inScope<T>(ctx: Ctx, values: Values, call: (client: Shardflux | ShardfluxAccount, scope: Scope) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which client and scope a person's command uses (0.25.0+): the signed-in session reads every project of an
|
|
3
|
+
* organization; SHARDFLUX_API_KEY in the environment (or a saved key without a session) reads that key's project. Used
|
|
4
|
+
* by `shard orchestrator`, `shard transcribe`, `shard route` and `shard ask`.
|
|
5
|
+
*/
|
|
6
|
+
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
7
|
+
import { UUID } from "./args.js";
|
|
8
|
+
import { findOrg, resolveOrg, resolveProject } from "./account-scope.js";
|
|
9
|
+
import { str } from "./commands.js";
|
|
10
|
+
import { UsageError } from "./errors.js";
|
|
11
|
+
/** SHARDFLUX_API_KEY in the environment wins; else the signed-in session; else the saved key (its error names both). */
|
|
12
|
+
export function boardClient(ctx) {
|
|
13
|
+
if (!ctx.io.env.SHARDFLUX_API_KEY?.trim() && ctx.sessionSource() !== null)
|
|
14
|
+
return { client: ctx.account(), session: true };
|
|
15
|
+
return { client: ctx.cloud(), session: false };
|
|
16
|
+
}
|
|
17
|
+
/** --org / --project as ids (slugs resolved with the session); with neither, the saved default organization. */
|
|
18
|
+
async function scopeOf(ctx, values, b) {
|
|
19
|
+
const org = str(values.org), project = str(values.project);
|
|
20
|
+
if (!b.session) {
|
|
21
|
+
for (const [flag, v] of [['--org', org], ['--project', project]]) {
|
|
22
|
+
if (v !== undefined && !UUID.test(v))
|
|
23
|
+
throw new UsageError(`${flag} must be an id (UUID) with a project API key`, 'or sign in (shard auth login) to name it by slug');
|
|
24
|
+
}
|
|
25
|
+
return { scope: { ...(org === undefined ? {} : { organizationId: org }), ...(project === undefined ? {} : { projectId: project }) }, defaulted: false };
|
|
26
|
+
}
|
|
27
|
+
const account = b.client;
|
|
28
|
+
const scope = {};
|
|
29
|
+
if (org !== undefined)
|
|
30
|
+
scope.organizationId = UUID.test(org) ? org : (await findOrg(account, org)).id;
|
|
31
|
+
if (project !== undefined)
|
|
32
|
+
scope.projectId = UUID.test(project) ? project : (await resolveProject(ctx, account, values)).id;
|
|
33
|
+
if (org !== undefined || project !== undefined)
|
|
34
|
+
return { scope, defaulted: false };
|
|
35
|
+
const saved = ctx.store.profile(ctx.apiUrl).organization_id;
|
|
36
|
+
return { scope: saved ? { organizationId: saved } : {}, defaulted: true };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runs `call` in the board's scope. With the session and no --org/--project: the saved default organization, else the
|
|
40
|
+
* only one; when the person is in several and none is saved (422), or the saved one is gone (404), the error lists
|
|
41
|
+
* theirs and names the fix (resolveOrg), and an organization that resolves is used.
|
|
42
|
+
*/
|
|
43
|
+
export async function inScope(ctx, values, call) {
|
|
44
|
+
const b = boardClient(ctx);
|
|
45
|
+
const { scope, defaulted } = await scopeOf(ctx, values, b);
|
|
46
|
+
try {
|
|
47
|
+
return await call(b.client, scope);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (!b.session || !defaulted || !(err instanceof ShardfluxApiError))
|
|
51
|
+
throw err;
|
|
52
|
+
const several = err.status === 422 && scope.organizationId === undefined && /organization/i.test(err.message);
|
|
53
|
+
const gone = err.status === 404 && scope.organizationId !== undefined;
|
|
54
|
+
if (!several && !gone)
|
|
55
|
+
throw err;
|
|
56
|
+
const org = await resolveOrg(ctx, b.client, {});
|
|
57
|
+
if (org.id === scope.organizationId)
|
|
58
|
+
throw err;
|
|
59
|
+
return call(b.client, { organizationId: org.id });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Ctx } from './commands.js';
|
|
2
|
+
import type { TwinCell } from './worktree-twin.js';
|
|
3
|
+
import { projectHubCell } from './worktree-twin-engine.js';
|
|
4
|
+
/** Where work on a linked folder starts: this commit (in the project hub), on the folder's branch. */
|
|
5
|
+
export interface HubSeed {
|
|
6
|
+
commit: string;
|
|
7
|
+
branch: string | null;
|
|
8
|
+
}
|
|
9
|
+
/** Copies `commit` and its history from `folder` into the hub, kept under `ref`, unless the hub holds it already. */
|
|
10
|
+
export declare function sendToHub(cell: TwinCell, folder: string, commit: string, ref: string, opts?: {
|
|
11
|
+
hub?: string;
|
|
12
|
+
vmTmp?: string;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
/** The folder's start point in project `projectId`'s hub, through a computer of the project that reads the hub. */
|
|
16
|
+
export declare function seedProjectHub(ctx: Ctx, folder: string, projectId: string, opts?: {
|
|
17
|
+
open?: typeof projectHubCell;
|
|
18
|
+
hub?: string;
|
|
19
|
+
vmTmp?: string;
|
|
20
|
+
}): Promise<HubSeed>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A linked folder's start point in the project hub (0.28.0+). When an agent links a folder of the person's (a device's
|
|
3
|
+
* project_link), the folder as it is now (its commit, with uncommitted files as a snapshot commit on top, as a chat
|
|
4
|
+
* started from it begins) is copied into the project hub. Work on it can then start in the cloud with no conversation
|
|
5
|
+
* of that folder yet: a task names the repository, the branch and this commit, and its master checks the commit out
|
|
6
|
+
* of the hub. The hub keeps it under refs/shard/seed/link-<folder id>; nothing else in the hub moves.
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { rm } from 'node:fs/promises';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { UsageError } from "./errors.js";
|
|
13
|
+
import { writeSyncFileFrom } from "./project-sync-transfer.js";
|
|
14
|
+
import { HUB_PATH, git, inspectWorktree, twinSeed, worktreeId } from "./worktree-twin.js";
|
|
15
|
+
import { projectHubCell } from "./worktree-twin-engine.js";
|
|
16
|
+
/** Copies `commit` and its history from `folder` into the hub, kept under `ref`, unless the hub holds it already. */
|
|
17
|
+
export async function sendToHub(cell, folder, commit, ref, opts = {}) {
|
|
18
|
+
const hub = opts.hub ?? HUB_PATH;
|
|
19
|
+
const run = (...args) => cell.exec.run(['git', `--git-dir=${hub}`, '-c', 'gc.auto=0', ...args], { timeoutMs: 300_000, ...(opts.signal ? { signal: opts.signal } : {}) });
|
|
20
|
+
const holds = async () => (await run('cat-file', '-e', `${commit}^{commit}`)).exitCode === 0;
|
|
21
|
+
if (await holds()) {
|
|
22
|
+
const r = await run('update-ref', ref, commit);
|
|
23
|
+
if (r.exitCode !== 0)
|
|
24
|
+
throw new Error(`the project hub could not keep the folder's commit: ${r.stderr.trim().slice(-400)}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const tmpRef = `refs/shard/seed-tmp/${randomUUID()}`, local = join(tmpdir(), `shard-hub-seed-${randomUUID()}.bundle`), remote = `${opts.vmTmp ?? '/tmp'}/shard-hub-seed-${randomUUID()}.bundle`;
|
|
28
|
+
await git(folder, ['update-ref', tmpRef, commit]);
|
|
29
|
+
try {
|
|
30
|
+
await git(folder, ['bundle', 'create', '-q', local, tmpRef]);
|
|
31
|
+
await writeSyncFileFrom(cell, remote, local, opts.signal);
|
|
32
|
+
const r = await run('fetch', '-q', '--no-tags', remote, `+${tmpRef}:${ref}`);
|
|
33
|
+
if (r.exitCode !== 0)
|
|
34
|
+
throw new Error(`the project hub could not take the folder: ${r.stderr.trim().slice(-400)}`);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await git(folder, ['update-ref', '-d', tmpRef], { ok: [0, 1] });
|
|
38
|
+
await rm(local, { force: true });
|
|
39
|
+
await cell.files.remove(remote).catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
if (!await holds())
|
|
42
|
+
throw new Error('the project hub does not hold the folder after copying it');
|
|
43
|
+
}
|
|
44
|
+
/** The folder's start point in project `projectId`'s hub, through a computer of the project that reads the hub. */
|
|
45
|
+
export async function seedProjectHub(ctx, folder, projectId, opts = {}) {
|
|
46
|
+
const open = opts.open ?? projectHubCell;
|
|
47
|
+
const info = await inspectWorktree(folder);
|
|
48
|
+
if (!info.head)
|
|
49
|
+
throw new UsageError('Commit this folder once before linking it.');
|
|
50
|
+
const seed = await twinSeed(ctx.store.dir, folder, info.head, projectId);
|
|
51
|
+
const vm = await open(ctx, projectId, ctx.signal, 'shard-link');
|
|
52
|
+
try {
|
|
53
|
+
await sendToHub(vm.cell, folder, seed.commit, `refs/shard/seed/link-${worktreeId(folder)}`, { signal: ctx.signal, ...(opts.hub ? { hub: opts.hub } : {}), ...(opts.vmTmp ? { vmTmp: opts.vmTmp } : {}) });
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
vm.close();
|
|
57
|
+
}
|
|
58
|
+
return { commit: seed.commit, branch: info.branch?.replace(/^refs\/heads\//, '') || null };
|
|
59
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RegistryOptions } from '@shardflux/sdk';
|
|
2
2
|
export { NPM_REGISTRY, npmLatest, withRegistryLatest } from '@shardflux/sdk';
|
|
3
3
|
/** The npm packages whose versions `shard update` compares. */
|
|
4
|
-
export declare const NPM_CLIENTS: readonly [
|
|
4
|
+
export declare const NPM_CLIENTS: readonly string[];
|
|
5
5
|
/** The registry to ask: SHARDFLUX_NPM_REGISTRY, else npm's public registry. */
|
|
6
6
|
export declare function npmRegistry(env: Record<string, string | undefined>): string;
|
|
7
7
|
/** `latest` of every package in `packages`, asked in parallel; a package the registry did not answer is absent. */
|
package/dist/registry.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* `hold` (see withRegistryLatest).
|
|
5
5
|
*/
|
|
6
6
|
import { npmLatest, registryFromEnv } from '@shardflux/sdk';
|
|
7
|
+
import { macAppPackage } from "./mac-app.js";
|
|
7
8
|
export { NPM_REGISTRY, npmLatest, withRegistryLatest } from '@shardflux/sdk';
|
|
8
9
|
/** The npm packages whose versions `shard update` compares. */
|
|
9
|
-
export const NPM_CLIENTS = ['shardflux', '@shardflux/cli', '@shardflux/sdk', '@shardflux/mcp', '
|
|
10
|
+
export const NPM_CLIENTS = ['shardflux', '@shardflux/cli', '@shardflux/sdk', '@shardflux/mcp', ...new Set([macAppPackage('arm64'), macAppPackage('x64')])];
|
|
10
11
|
/** The registry to ask: SHARDFLUX_NPM_REGISTRY, else npm's public registry. */
|
|
11
12
|
export function npmRegistry(env) {
|
|
12
13
|
return registryFromEnv(env, 'npm');
|
|
@@ -1,25 +1,15 @@
|
|
|
1
|
-
import type { RouterResult, Shardflux, ShardfluxAccount } from '@shardflux/sdk';
|
|
2
|
-
import type { Values } from './args.js';
|
|
3
|
-
import type { Ctx, Handler } from './commands.js';
|
|
4
|
-
/** The API's bound on one input or question (characters). */
|
|
5
|
-
export declare const BOARD_TEXT_MAX = 4000;
|
|
6
|
-
interface BoardClient {
|
|
7
|
-
client: Shardflux | ShardfluxAccount;
|
|
8
|
-
/** The signed-in session: the organization's whole board. */
|
|
9
|
-
session: boolean;
|
|
10
|
-
}
|
|
11
|
-
/** SHARDFLUX_API_KEY in the environment wins; else the signed-in session; else the saved key (its error names both). */
|
|
12
|
-
export declare function boardClient(ctx: Ctx): BoardClient;
|
|
13
|
-
interface Scope {
|
|
14
|
-
organizationId?: string;
|
|
15
|
-
projectId?: string;
|
|
16
|
-
}
|
|
17
1
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
2
|
+
* `shard route` and `shard ask` (0.25.0+): the board's input bar from a terminal, through @shardflux/sdk's
|
|
3
|
+
* `router.route()` and `questions.stream()` / `questions.ask()`. The signed-in session reads every project of an
|
|
4
|
+
* organization; SHARDFLUX_API_KEY in the environment (or a saved key without a session) reads that key's project.
|
|
5
|
+
*
|
|
6
|
+
* Deprecated since 0.27.0: each prints a one-line notice on stderr naming
|
|
7
|
+
* `shard orchestrator say`, which sends the input to the person's orchestrator agent (orchestrator-commands.ts).
|
|
21
8
|
*/
|
|
22
|
-
|
|
9
|
+
import type { RouterResult } from '@shardflux/sdk';
|
|
10
|
+
import type { Handler } from './commands.js';
|
|
11
|
+
/** The API's bound on one input or question (characters). */
|
|
12
|
+
export declare const BOARD_TEXT_MAX = 4000;
|
|
23
13
|
/**
|
|
24
14
|
* The readable form of a RouterResult: the kind and where it goes, then each part (a split's parts numbered, with
|
|
25
15
|
* their titles).
|
|
@@ -29,5 +19,6 @@ export declare function inScope<T>(ctx: Ctx, values: Values, call: (client: Shar
|
|
|
29
19
|
* fix the hero image on the landing page
|
|
30
20
|
*/
|
|
31
21
|
export declare function routeText(r: RouterResult, names: ReadonlyMap<string, string>): string;
|
|
22
|
+
/** The one-line notice `shard route` and `shard ask` print on stderr (also with --json, whose output stays the same). */
|
|
23
|
+
export declare const deprecationNotice: (command: "route" | "ask") => string;
|
|
32
24
|
export declare const ROUTER_HANDLERS: Record<string, Handler>;
|
|
33
|
-
export {};
|
package/dist/router-commands.js
CHANGED
|
@@ -1,67 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
* `shard route` and `shard ask` (0.25.0+): the board's input bar from a terminal, through @shardflux/sdk's
|
|
3
|
-
* `router.route()` and `questions.stream()` / `questions.ask()`. The signed-in session reads every project of an
|
|
4
|
-
* organization; SHARDFLUX_API_KEY in the environment (or a saved key without a session) reads that key's project.
|
|
5
|
-
*/
|
|
6
|
-
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
7
|
-
import { UUID } from "./args.js";
|
|
8
|
-
import { findOrg, resolveOrg, resolveProject } from "./account-scope.js";
|
|
1
|
+
import { inScope } from "./person-scope.js";
|
|
9
2
|
import { out, str } from "./commands.js";
|
|
10
3
|
import { EXIT, UsageError } from "./errors.js";
|
|
11
4
|
/** The API's bound on one input or question (characters). */
|
|
12
5
|
export const BOARD_TEXT_MAX = 4000;
|
|
13
6
|
const TASK_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
14
|
-
/** SHARDFLUX_API_KEY in the environment wins; else the signed-in session; else the saved key (its error names both). */
|
|
15
|
-
export function boardClient(ctx) {
|
|
16
|
-
if (!ctx.io.env.SHARDFLUX_API_KEY?.trim() && ctx.sessionSource() !== null)
|
|
17
|
-
return { client: ctx.account(), session: true };
|
|
18
|
-
return { client: ctx.cloud(), session: false };
|
|
19
|
-
}
|
|
20
|
-
/** --org / --project as ids (slugs resolved with the session); with neither, the saved default organization. */
|
|
21
|
-
async function scopeOf(ctx, values, b) {
|
|
22
|
-
const org = str(values.org), project = str(values.project);
|
|
23
|
-
if (!b.session) {
|
|
24
|
-
for (const [flag, v] of [['--org', org], ['--project', project]]) {
|
|
25
|
-
if (v !== undefined && !UUID.test(v))
|
|
26
|
-
throw new UsageError(`${flag} must be an id (UUID) with a project API key`, 'or sign in (shard auth login) to name it by slug');
|
|
27
|
-
}
|
|
28
|
-
return { scope: { ...(org === undefined ? {} : { organizationId: org }), ...(project === undefined ? {} : { projectId: project }) }, defaulted: false };
|
|
29
|
-
}
|
|
30
|
-
const account = b.client;
|
|
31
|
-
const scope = {};
|
|
32
|
-
if (org !== undefined)
|
|
33
|
-
scope.organizationId = UUID.test(org) ? org : (await findOrg(account, org)).id;
|
|
34
|
-
if (project !== undefined)
|
|
35
|
-
scope.projectId = UUID.test(project) ? project : (await resolveProject(ctx, account, values)).id;
|
|
36
|
-
if (org !== undefined || project !== undefined)
|
|
37
|
-
return { scope, defaulted: false };
|
|
38
|
-
const saved = ctx.store.profile(ctx.apiUrl).organization_id;
|
|
39
|
-
return { scope: saved ? { organizationId: saved } : {}, defaulted: true };
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Runs `call` in the board's scope. With the session and no --org/--project: the saved default organization, else the
|
|
43
|
-
* only one; when the person is in several and none is saved (422), or the saved one is gone (404), the error lists
|
|
44
|
-
* theirs and names the fix (resolveOrg), and an organization that resolves is used.
|
|
45
|
-
*/
|
|
46
|
-
export async function inScope(ctx, values, call) {
|
|
47
|
-
const b = boardClient(ctx);
|
|
48
|
-
const { scope, defaulted } = await scopeOf(ctx, values, b);
|
|
49
|
-
try {
|
|
50
|
-
return await call(b.client, scope);
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
if (!b.session || !defaulted || !(err instanceof ShardfluxApiError))
|
|
54
|
-
throw err;
|
|
55
|
-
const several = err.status === 422 && scope.organizationId === undefined && /organization/i.test(err.message);
|
|
56
|
-
const gone = err.status === 404 && scope.organizationId !== undefined;
|
|
57
|
-
if (!several && !gone)
|
|
58
|
-
throw err;
|
|
59
|
-
const org = await resolveOrg(ctx, b.client, {});
|
|
60
|
-
if (org.id === scope.organizationId)
|
|
61
|
-
throw err;
|
|
62
|
-
return call(b.client, { organizationId: org.id });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
7
|
function textOf(words, what, command) {
|
|
66
8
|
const text = words.join(' ').trim();
|
|
67
9
|
if (!text)
|
|
@@ -118,6 +60,8 @@ export function routeText(r, names) {
|
|
|
118
60
|
}
|
|
119
61
|
return `${lines.join('\n')}\n`;
|
|
120
62
|
}
|
|
63
|
+
/** The one-line notice `shard route` and `shard ask` print on stderr (also with --json, whose output stays the same). */
|
|
64
|
+
export const deprecationNotice = (command) => `shard ${command} is deprecated; use shard orchestrator say <words> instead.\n`;
|
|
121
65
|
const interrupted = (ctx, err) => {
|
|
122
66
|
if (ctx.signal.aborted)
|
|
123
67
|
return EXIT.interrupted;
|
|
@@ -125,6 +69,7 @@ const interrupted = (ctx, err) => {
|
|
|
125
69
|
};
|
|
126
70
|
export const ROUTER_HANDLERS = {
|
|
127
71
|
async route(ctx, values, words) {
|
|
72
|
+
ctx.io.stderr.write(deprecationNotice('route'));
|
|
128
73
|
const text = textOf(words, 'text', 'route');
|
|
129
74
|
const selectedTaskId = taskOf(values);
|
|
130
75
|
try {
|
|
@@ -144,6 +89,7 @@ export const ROUTER_HANDLERS = {
|
|
|
144
89
|
}
|
|
145
90
|
},
|
|
146
91
|
async ask(ctx, values, words) {
|
|
92
|
+
ctx.io.stderr.write(deprecationNotice('ask'));
|
|
147
93
|
const text = textOf(words, 'question', 'ask');
|
|
148
94
|
const taskId = taskOf(values);
|
|
149
95
|
const params = { text, ...(taskId === undefined ? {} : { taskId }), signal: ctx.signal };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Ctx } from './commands.js';
|
|
2
|
+
/** Help/version starts also clean saved work, without requiring credentials when nothing is due. */
|
|
3
|
+
export declare function startupHousekeeping(ctx: Pick<Ctx, 'io' | 'store' | 'signal'>): Promise<void>;
|
|
4
|
+
/** Keep the command's already selected auth behavior, loading Git machinery only for tracked repositories. */
|
|
5
|
+
export declare function housekeepingIfTracked(ctx: Pick<Ctx, 'io' | 'apiUrl' | 'signal' | 'store' | 'cloud'>): Promise<void>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Shardflux } from '@shardflux/sdk';
|
|
2
|
+
import { AgentStateStore } from "./agent-state.js";
|
|
3
|
+
import { resolveApiUrl } from "./args.js";
|
|
4
|
+
import { UsageError } from "./errors.js";
|
|
5
|
+
/** Help/version starts also clean saved work, without requiring credentials when nothing is due. */
|
|
6
|
+
export async function startupHousekeeping(ctx) {
|
|
7
|
+
try {
|
|
8
|
+
const state = new AgentStateStore(ctx.store.dir).read();
|
|
9
|
+
if (!state.git_repositories || !Object.keys(state.git_repositories).length)
|
|
10
|
+
return;
|
|
11
|
+
const apiUrl = resolveApiUrl(undefined, ctx.io.env.SHARDFLUX_API_URL ?? ctx.io.env.SHARDFLUX_BASE_URL);
|
|
12
|
+
let cloud;
|
|
13
|
+
await (await import("./agent-git-commands.js")).housekeeping({ ...ctx, apiUrl, cloud: () => {
|
|
14
|
+
const key = ctx.io.env.SHARDFLUX_API_KEY?.trim() || ctx.store.profile(apiUrl).api_key?.token;
|
|
15
|
+
if (!key)
|
|
16
|
+
throw new UsageError('saved agents need a project key for housekeeping; run shard setup');
|
|
17
|
+
return cloud ??= new Shardflux({ apiKey: key, baseUrl: apiUrl, versionCheck: false });
|
|
18
|
+
} });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
ctx.io.stderr.write(`shard housekeeping: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Keep the command's already selected auth behavior, loading Git machinery only for tracked repositories. */
|
|
25
|
+
export async function housekeepingIfTracked(ctx) {
|
|
26
|
+
const repos = new AgentStateStore(ctx.store.dir).read().git_repositories;
|
|
27
|
+
if (!repos || !Object.keys(repos).length)
|
|
28
|
+
return;
|
|
29
|
+
await (await import("./agent-git-commands.js")).housekeeping(ctx);
|
|
30
|
+
}
|
package/dist/t3-launch.js
CHANGED
|
@@ -8,7 +8,7 @@ import { agentReachability } from "./agent-reachability.js";
|
|
|
8
8
|
import { conversationWorkspace, getConversation } from "./conversations.js";
|
|
9
9
|
import { resolveOn } from "./thread-on-computer.js";
|
|
10
10
|
import { ensureCloudClaude } from "./claude-cloud-login.js";
|
|
11
|
-
import { modelChoice } from "./agent-models.js";
|
|
11
|
+
import { liveModels, modelChoice } from "./agent-models.js";
|
|
12
12
|
/**
|
|
13
13
|
* A `t3_*` refusal raised once the workspace, conversation or thread exists names them (details.workspace_key,
|
|
14
14
|
* conversation_id, t3_thread_id), so a client opens what exists instead of starting it again.
|
|
@@ -32,7 +32,7 @@ export async function t3ThreadRequest(ctx, values, app) {
|
|
|
32
32
|
return undefined;
|
|
33
33
|
}
|
|
34
34
|
// The same choices as every other new thread (agent-models.ts); "default" leaves the agent's own.
|
|
35
|
-
const choice = modelChoice(values, app);
|
|
35
|
+
const choice = modelChoice(values, app, await liveModels(ctx, values, app));
|
|
36
36
|
return { app, prompt, ...(choice.model ? { model: choice.model } : {}), ...(choice.effort ? { effort: choice.effort } : {}), ...(images.length ? { images } : {}) };
|
|
37
37
|
}
|
|
38
38
|
/**
|