@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
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const ORG = {
|
|
2
|
+
org: { type: 'string', value: '<id|slug>', description: 'Organization whose orchestrator it is (default: the saved one, shard orgs use, else your only organization). Slugs need your signed-in session.' },
|
|
3
|
+
};
|
|
4
|
+
const WHO = 'Your orchestrator is yours: one agent on its own Shardflux computer, the same on every device. Uses your signed-in session, or SHARDFLUX_API_KEY when it is set (a project key you created: a new orchestrator is filed under its project).';
|
|
5
|
+
export const ORCHESTRATOR_COMMANDS = [
|
|
6
|
+
{
|
|
7
|
+
path: ['orchestrator', 'say'],
|
|
8
|
+
summary: 'Say something to your orchestrator; one of its threads hears each input at once (0.27.0+)',
|
|
9
|
+
positionals: [{ name: 'words', variadic: true, words: true, required: true }],
|
|
10
|
+
options: {
|
|
11
|
+
task: { type: 'string', value: '<task-id>', description: 'Point at a task (task_…).' },
|
|
12
|
+
report: { type: 'string', value: '<report-id>', description: 'Point at a report (rep_…).' },
|
|
13
|
+
'needs-you': { type: 'string', value: '<item-id>', description: 'Point at something that waits on you (perm_…, oq_…).' },
|
|
14
|
+
input: { type: 'string', value: '<input-id>', description: 'Point at an earlier input (in_…).' },
|
|
15
|
+
conversation: { type: 'string', value: '<conversation-id>', description: 'Point at a conversation (conv_…).' },
|
|
16
|
+
wait: { type: 'boolean', description: 'Follow it until it is done, merged, failed or waiting, then print what it became.' },
|
|
17
|
+
timeout: { type: 'string', value: '<duration>', description: 'With --wait: stop following after this long (default 10m). The input goes on.' },
|
|
18
|
+
...ORG,
|
|
19
|
+
},
|
|
20
|
+
description: [
|
|
21
|
+
'Your exact words go to your orchestrator, with the row you point at. It answers at once: the input id and its state (queued, with its place in line; every input waits its turn in order). One of your orchestrator’s threads hears each input at once, all in parallel: a follow-up reaches the thread of the request it is about (the input reads merged), the row you point at reaches the thread of that row’s input, and anything new gets a thread of its own, which answers it, puts tasks on the board (shard board) or asks you something (shard needs-you, oq_…).',
|
|
22
|
+
'--wait follows it and prints its curated result: the answer’s headline and its points under their group headings (with when each happened) or its facts, a media line per file it shows (an image with its size in pixels), with their sources and follow-ups, the tasks it created, the line it waits on (a question names the shard needs-you answer command), or the input it was added to. Exit 6 when it failed (shard orchestrator retry sends it again), 5 when --timeout passed first.',
|
|
23
|
+
WHO,
|
|
24
|
+
'With --json: {input}, as it was stored, or with --wait as it ended.',
|
|
25
|
+
].join('\n\n'),
|
|
26
|
+
examples: ['shard orchestrator say Lets check on our language model training progress --wait', 'shard orchestrator say --task task_h7g4d7tqyv2pz3a2npyo4rpgzq use postgres instead', 'shard orchestrator say "what is blocking pricing?" --wait --json'],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: ['orchestrator', 'list'],
|
|
30
|
+
summary: 'List what you said to your orchestrator and what each became, newest first (0.27.0+)',
|
|
31
|
+
positionals: [],
|
|
32
|
+
options: { all: { type: 'boolean', description: 'Also list the inputs you dismissed.' }, ...ORG },
|
|
33
|
+
description: [
|
|
34
|
+
'One line per input: its id, its state (queued with its place in line, working, waiting with what it waits on, done, merged, failed), your words, and what it became (the answer’s headline, its tasks, the input it was added to, or why it failed). Inputs still in flight are always listed.',
|
|
35
|
+
WHO,
|
|
36
|
+
'With --json: {inputs, cursor}, every input as the API answered it.',
|
|
37
|
+
].join('\n\n'),
|
|
38
|
+
examples: ['shard orchestrator list', 'shard orchestrator list --json | jq -r \'.inputs[] | select(.state=="done") | .answer.headline\''],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: ['orchestrator', 'retry'],
|
|
42
|
+
summary: 'Send a failed input to your orchestrator again (0.27.0+)',
|
|
43
|
+
positionals: [{ name: 'input-id', required: true }],
|
|
44
|
+
options: ORG,
|
|
45
|
+
description: 'It goes back in line and a new thread takes it. Prints its id and state. Only a failed input is sent again.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: ['orchestrator', 'dismiss'],
|
|
49
|
+
summary: 'Take an input out of your orchestrator’s list on every device (0.27.0+)',
|
|
50
|
+
positionals: [{ name: 'input-id', required: true }],
|
|
51
|
+
options: ORG,
|
|
52
|
+
description: 'What it created stays on the board. shard orchestrator list --all still shows it; shard orchestrator restore brings it back.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: ['orchestrator', 'restore'],
|
|
56
|
+
summary: 'Bring a dismissed input back into your orchestrator’s list on every device (0.28.0+)',
|
|
57
|
+
positionals: [{ name: 'input-id', required: true }],
|
|
58
|
+
options: ORG,
|
|
59
|
+
description: [
|
|
60
|
+
'Undoes shard orchestrator dismiss: the input is listed again with what it became. Restoring an input that is not dismissed changes nothing. Questions the dismiss closed stay closed.',
|
|
61
|
+
'With --json: {input}.',
|
|
62
|
+
].join('\n\n'),
|
|
63
|
+
},
|
|
64
|
+
];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { OrchestratorInput, OrchestratorSelection } from '@shardflux/sdk';
|
|
2
|
+
import type { Values } from './args.js';
|
|
3
|
+
import type { Handler } from './commands.js';
|
|
4
|
+
/** --wait's default: how long `say --wait` follows an input before it stops (the input goes on). */
|
|
5
|
+
export declare const SAY_WAIT_MS: number;
|
|
6
|
+
/** The row the person points at, from --task, --report, --needs-you, --input and --conversation; undefined for none. */
|
|
7
|
+
export declare function selectionOf(values: Values): OrchestratorSelection | undefined;
|
|
8
|
+
/** An input's state with what explains it: "queued (2 in line)", "waiting (Waiting for your computer: list ~/code)". */
|
|
9
|
+
export declare function stateLabel(i: OrchestratorInput): string;
|
|
10
|
+
/** What an input became, in a few words: the answer's headline, its tasks, the input it went to, or why it failed. */
|
|
11
|
+
export declare function becameText(i: OrchestratorInput): string;
|
|
12
|
+
/** One input as `shard orchestrator list` prints it: id, state, your words, what it became. */
|
|
13
|
+
export declare function inputLine(i: OrchestratorInput): string;
|
|
14
|
+
/**
|
|
15
|
+
* When an answer's point happened, short and in this computer's local time: "14:02" today, "Oct 16 14:02" this year,
|
|
16
|
+
* else "2025-10-16 14:02". A time without an offset is the person's local time and prints as written.
|
|
17
|
+
*/
|
|
18
|
+
export declare function shortTime(at: string, now?: Date): string;
|
|
19
|
+
/** An answer's body: its points under their group headings when the agent wrote points (0.27.0+), else its facts. */
|
|
20
|
+
export declare function answerLines(answer: NonNullable<OrchestratorInput['answer']>, now?: Date): string[];
|
|
21
|
+
/** One file an answer shows (0.27.0+): ` media: loss-curve.png (image, 1200×800, 84.0 KB)`, the size in pixels once known. */
|
|
22
|
+
export declare function mediaLine(m: NonNullable<NonNullable<OrchestratorInput['answer']>['media']>[number]): string;
|
|
23
|
+
/**
|
|
24
|
+
* What `say --wait` prints once an input is done, merged, failed or waiting: its curated result.
|
|
25
|
+
*
|
|
26
|
+
* Training is 73% done, on pace to finish at 16:40
|
|
27
|
+
* Moved
|
|
28
|
+
* - Loss is down to 1.84: from 2.31 at step 5,000 (14:02)
|
|
29
|
+
* - Checkpoint saved: at step 7,000
|
|
30
|
+
* Still open
|
|
31
|
+
* - Eval has not run: it starts after the last checkpoint
|
|
32
|
+
* media: loss-curve.png (image, 1200×800, 84.0 KB)
|
|
33
|
+
* Sources: train.log (~/code/lm-train/train.log), wandb.ai (https://wandb.ai/acme/lm)
|
|
34
|
+
* Follow-ups: Check again in an hour
|
|
35
|
+
* Tasks: task_… (shard task show <id>)
|
|
36
|
+
*
|
|
37
|
+
* An answer written as facts prints them as ` - <fact>` lines instead of the points. Each file the answer shows prints
|
|
38
|
+
* one media line after them (shard drive get <path> --sha256 <sha256> reads it; --json has both).
|
|
39
|
+
*/
|
|
40
|
+
export declare function resultText(i: OrchestratorInput, now?: Date): string;
|
|
41
|
+
/** Where `say --wait` stops following: nothing more happens to the input, or it waits on you or your computer. */
|
|
42
|
+
export declare const stopsWaiting: (i: OrchestratorInput) => boolean;
|
|
43
|
+
export declare const ORCHESTRATOR_HANDLERS: Record<string, Handler>;
|
|
@@ -0,0 +1,262 @@
|
|
|
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, ORCHESTRATOR_TEXT_MAX } 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
|
+
if (text.length > ORCHESTRATOR_TEXT_MAX)
|
|
206
|
+
throw new UsageError(`your words are ${text.length} characters; one input takes up to ${ORCHESTRATOR_TEXT_MAX}`);
|
|
207
|
+
const selection = selectionOf(values);
|
|
208
|
+
const wait = values.wait === true;
|
|
209
|
+
const timeoutMs = str(values.timeout) === undefined ? SAY_WAIT_MS : parseDuration(str(values.timeout), 'timeout');
|
|
210
|
+
if (!wait && str(values.timeout) !== undefined)
|
|
211
|
+
throw new UsageError('--timeout goes with --wait.');
|
|
212
|
+
return inScope(ctx, values, async (client, scope) => {
|
|
213
|
+
const input = await client.orchestrator.say(text, { ...orgOnly(scope), ...(selection ? { selection } : {}), signal: ctx.signal });
|
|
214
|
+
if (!wait) {
|
|
215
|
+
out(ctx, `${input.id}\t${stateLabel(input)}\n`, { input });
|
|
216
|
+
return EXIT.ok;
|
|
217
|
+
}
|
|
218
|
+
progress(ctx, `Sent: ${input.id}`);
|
|
219
|
+
let followed;
|
|
220
|
+
try {
|
|
221
|
+
followed = await follow(ctx, client, scope, input, timeoutMs);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
if (ctx.signal.aborted)
|
|
225
|
+
return EXIT.interrupted;
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
if (ctx.signal.aborted && !followed.timedOut && !stopsWaiting(followed.input))
|
|
229
|
+
return EXIT.interrupted;
|
|
230
|
+
const end = followed.input;
|
|
231
|
+
if (followed.timedOut) {
|
|
232
|
+
out(ctx, `${end.id}\t${stateLabel(end)}\n`, { input: end });
|
|
233
|
+
ctx.io.stderr.write(`Still ${end.state} after ${Math.round(timeoutMs / 1000)} s; it goes on: shard orchestrator list shows it.\n`);
|
|
234
|
+
return EXIT.timeout;
|
|
235
|
+
}
|
|
236
|
+
out(ctx, resultText(end), { input: end });
|
|
237
|
+
return end.state === 'failed' ? EXIT.operationFailed : EXIT.ok;
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
async 'orchestrator list'(ctx, values) {
|
|
241
|
+
const page = await inScope(ctx, values, (client, scope) => client.orchestrator.inputs({ ...orgOnly(scope), signal: ctx.signal }));
|
|
242
|
+
const shown = (page.inputs ?? []).filter((i) => values.all === true || !i.dismissed_at);
|
|
243
|
+
const human = shown.length ? `${shown.map(inputLine).join('\n')}\n` : 'Nothing to show yet: shard orchestrator say <words> sends your orchestrator something.\n';
|
|
244
|
+
out(ctx, human, { inputs: page.inputs ?? [], cursor: page.cursor });
|
|
245
|
+
return EXIT.ok;
|
|
246
|
+
},
|
|
247
|
+
async 'orchestrator retry'(ctx, values, [id]) {
|
|
248
|
+
const input = await inScope(ctx, values, (client, scope) => client.orchestrator.retry(idOf(id), { ...orgOnly(scope), signal: ctx.signal }));
|
|
249
|
+
out(ctx, `${input.id}\t${stateLabel(input)}\n`, { input });
|
|
250
|
+
return EXIT.ok;
|
|
251
|
+
},
|
|
252
|
+
async 'orchestrator dismiss'(ctx, values, [id]) {
|
|
253
|
+
const input = await inScope(ctx, values, (client, scope) => client.orchestrator.dismiss(idOf(id), { ...orgOnly(scope), signal: ctx.signal }));
|
|
254
|
+
out(ctx, `Dismissed: ${oneLine(input.text, 80)}\n`, { input });
|
|
255
|
+
return EXIT.ok;
|
|
256
|
+
},
|
|
257
|
+
async 'orchestrator restore'(ctx, values, [id]) {
|
|
258
|
+
const input = await inScope(ctx, values, (client, scope) => client.orchestrator.restore(idOf(id), { ...orgOnly(scope), signal: ctx.signal }));
|
|
259
|
+
out(ctx, `Restored: ${oneLine(input.text, 80)}\n`, { input });
|
|
260
|
+
return EXIT.ok;
|
|
261
|
+
},
|
|
262
|
+
};
|
|
@@ -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,14 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
+
* 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).
|
|
8
|
+
*/
|
|
9
|
+
import type { RouterResult } from '@shardflux/sdk';
|
|
10
|
+
import type { Handler } from './commands.js';
|
|
3
11
|
/** The API's bound on one input or question (characters). */
|
|
4
12
|
export declare const BOARD_TEXT_MAX = 4000;
|
|
5
|
-
interface BoardClient {
|
|
6
|
-
client: Shardflux | ShardfluxAccount;
|
|
7
|
-
/** The signed-in session: the organization's whole board. */
|
|
8
|
-
session: boolean;
|
|
9
|
-
}
|
|
10
|
-
/** SHARDFLUX_API_KEY in the environment wins; else the signed-in session; else the saved key (its error names both). */
|
|
11
|
-
export declare function boardClient(ctx: Ctx): BoardClient;
|
|
12
13
|
/**
|
|
13
14
|
* The readable form of a RouterResult: the kind and where it goes, then each part (a split's parts numbered, with
|
|
14
15
|
* their titles).
|
|
@@ -18,5 +19,6 @@ export declare function boardClient(ctx: Ctx): BoardClient;
|
|
|
18
19
|
* fix the hero image on the landing page
|
|
19
20
|
*/
|
|
20
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;
|
|
21
24
|
export declare const ROUTER_HANDLERS: Record<string, Handler>;
|
|
22
|
-
export {};
|