@hanzo/build 0.2.9 → 0.2.11
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/README.md +126 -23
- package/lib/api/agents.d.ts +80 -0
- package/lib/api/agents.js +148 -0
- package/lib/api/billing.d.ts +205 -0
- package/lib/api/billing.js +300 -0
- package/lib/api/call.d.ts +6 -0
- package/lib/api/call.js +11 -0
- package/lib/api/capabilities.d.ts +25 -0
- package/lib/api/capabilities.js +40 -0
- package/lib/api/changes.d.ts +56 -0
- package/lib/api/changes.js +63 -0
- package/lib/api/coding.d.ts +42 -4
- package/lib/api/coding.js +62 -3
- package/lib/api/connectors.d.ts +90 -0
- package/lib/api/connectors.js +109 -0
- package/lib/api/consent.d.ts +22 -0
- package/lib/api/consent.js +28 -0
- package/lib/api/environment.d.ts +57 -0
- package/lib/api/environment.js +85 -0
- package/lib/api/git.d.ts +6 -2
- package/lib/api/git.js +13 -5
- package/lib/api/github.d.ts +13 -0
- package/lib/api/github.js +9 -0
- package/lib/api/harness.d.ts +44 -0
- package/lib/api/harness.js +348 -0
- package/lib/api/keys.d.ts +41 -0
- package/lib/api/keys.js +55 -0
- package/lib/api/machines.d.ts +56 -0
- package/lib/api/machines.js +84 -0
- package/lib/api/members.d.ts +47 -0
- package/lib/api/members.js +88 -0
- package/lib/api/memory.d.ts +25 -0
- package/lib/api/memory.js +34 -0
- package/lib/api/places.d.ts +2 -2
- package/lib/api/places.js +3 -26
- package/lib/api/plugins.d.ts +55 -0
- package/lib/api/plugins.js +62 -0
- package/lib/api/pref.d.ts +49 -0
- package/lib/api/pref.js +68 -0
- package/lib/api/profile.d.ts +21 -0
- package/lib/api/profile.js +40 -0
- package/lib/api/projects.d.ts +19 -2
- package/lib/api/projects.js +29 -4
- package/lib/api/provider.d.ts +42 -0
- package/lib/api/provider.js +58 -0
- package/lib/api/sandbox.d.ts +30 -0
- package/lib/api/sandbox.js +49 -0
- package/lib/api/sessions.d.ts +39 -4
- package/lib/api/sessions.js +58 -6
- package/lib/api/skills.d.ts +70 -0
- package/lib/api/skills.js +90 -0
- package/lib/api/tools.d.ts +34 -0
- package/lib/api/tools.js +37 -0
- package/lib/api/turn.d.ts +84 -16
- package/lib/api/turn.js +224 -4
- package/lib/api/webhooks.d.ts +60 -0
- package/lib/api/webhooks.js +81 -0
- package/lib/ask.d.ts +18 -0
- package/lib/ask.js +69 -0
- package/lib/builder.js +39 -19
- package/lib/customize/agents.d.ts +2 -0
- package/lib/customize/agents.js +125 -0
- package/lib/customize/connectors.d.ts +2 -0
- package/lib/customize/connectors.js +197 -0
- package/lib/customize/index.d.ts +6 -0
- package/lib/customize/index.js +50 -0
- package/lib/customize/plugins.d.ts +2 -0
- package/lib/customize/plugins.js +93 -0
- package/lib/customize/skills.d.ts +2 -0
- package/lib/customize/skills.js +125 -0
- package/lib/customize/ui.d.ts +110 -0
- package/lib/customize/ui.js +99 -0
- package/lib/desk.d.ts +8 -1
- package/lib/desk.js +29 -35
- package/lib/door.d.ts +7 -0
- package/lib/door.js +89 -0
- package/lib/environment.d.ts +15 -0
- package/lib/environment.js +132 -0
- package/lib/files.d.ts +14 -0
- package/lib/files.js +111 -0
- package/lib/find.d.ts +7 -0
- package/lib/find.js +77 -0
- package/lib/foot.d.ts +16 -0
- package/lib/foot.js +36 -0
- package/lib/forge.js +8 -4
- package/lib/git.d.ts +9 -0
- package/lib/git.js +74 -0
- package/lib/host.d.ts +5 -0
- package/lib/landing.d.ts +10 -0
- package/lib/landing.js +47 -14
- package/lib/markdown.d.ts +49 -0
- package/lib/markdown.js +123 -0
- package/lib/plans.d.ts +1 -0
- package/lib/plans.js +113 -0
- package/lib/prefs.d.ts +30 -0
- package/lib/prefs.js +83 -0
- package/lib/prose.d.ts +4 -0
- package/lib/prose.js +53 -0
- package/lib/route.d.ts +16 -1
- package/lib/route.js +32 -1
- package/lib/run.js +207 -28
- package/lib/section.js +2 -2
- package/lib/settings/account.d.ts +5 -0
- package/lib/settings/account.js +111 -0
- package/lib/settings/billing.d.ts +1 -0
- package/lib/settings/billing.js +104 -0
- package/lib/settings/capabilities.d.ts +1 -0
- package/lib/settings/capabilities.js +55 -0
- package/lib/settings/card.d.ts +31 -0
- package/lib/settings/card.js +105 -0
- package/lib/settings/code.d.ts +1 -0
- package/lib/settings/code.js +51 -0
- package/lib/settings/environments.d.ts +1 -0
- package/lib/settings/environments.js +53 -0
- package/lib/settings/general.d.ts +1 -0
- package/lib/settings/general.js +44 -0
- package/lib/settings/index.d.ts +4 -0
- package/lib/settings/index.js +22 -0
- package/lib/settings/integrations.d.ts +1 -0
- package/lib/settings/integrations.js +81 -0
- package/lib/settings/keys.d.ts +1 -0
- package/lib/settings/keys.js +102 -0
- package/lib/settings/machines.d.ts +1 -0
- package/lib/settings/machines.js +118 -0
- package/lib/settings/members.d.ts +1 -0
- package/lib/settings/members.js +67 -0
- package/lib/settings/memory.d.ts +1 -0
- package/lib/settings/memory.js +56 -0
- package/lib/settings/notifications.d.ts +1 -0
- package/lib/settings/notifications.js +97 -0
- package/lib/settings/privacy.d.ts +1 -0
- package/lib/settings/privacy.js +63 -0
- package/lib/settings/sections.d.ts +16 -0
- package/lib/settings/sections.js +32 -0
- package/lib/settings/ui.d.ts +49 -0
- package/lib/settings/ui.js +47 -0
- package/lib/settings/usage.d.ts +1 -0
- package/lib/settings/usage.js +114 -0
- package/lib/shelf.d.ts +6 -0
- package/lib/shelf.js +86 -16
- package/lib/switch.js +1 -2
- package/lib/transcript.d.ts +12 -0
- package/lib/transcript.js +56 -0
- package/lib/voice.d.ts +3 -1
- package/lib/voice.js +10 -4
- package/package.json +1 -1
- package/lib/account.d.ts +0 -14
- package/lib/account.js +0 -38
- package/lib/e2e/site.spec.d.ts +0 -1
- package/lib/e2e/site.spec.js +0 -125
- package/lib/mcp.d.ts +0 -1
- package/lib/mcp.js +0 -28
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/** Colour codes a terminal would have drawn; a sandbox is not a terminal, but a harness may still print them. */
|
|
2
|
+
const ANSI = /\u001b\[[0-9;?]*[A-Za-z]/g;
|
|
3
|
+
/**
|
|
4
|
+
* A shell command's words, as `sh` splits them: quotes grouped, `'"'"'`
|
|
5
|
+
* joined, a backslash escaping the next character outside single quotes.
|
|
6
|
+
*/
|
|
7
|
+
export function words(line) {
|
|
8
|
+
const out = [];
|
|
9
|
+
let cur = '';
|
|
10
|
+
let open = false;
|
|
11
|
+
let q = '';
|
|
12
|
+
for (let i = 0; i < line.length; i++) {
|
|
13
|
+
const c = line[i];
|
|
14
|
+
if (q === "'") {
|
|
15
|
+
if (c === "'")
|
|
16
|
+
q = '';
|
|
17
|
+
else
|
|
18
|
+
cur += c;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (q === '"') {
|
|
22
|
+
if (c === '"')
|
|
23
|
+
q = '';
|
|
24
|
+
else if (c === '\\' && i + 1 < line.length)
|
|
25
|
+
cur += line[++i];
|
|
26
|
+
else
|
|
27
|
+
cur += c;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (c === "'" || c === '"') {
|
|
31
|
+
q = c;
|
|
32
|
+
open = true;
|
|
33
|
+
}
|
|
34
|
+
else if (c === '\\' && i + 1 < line.length) {
|
|
35
|
+
cur += line[++i];
|
|
36
|
+
open = true;
|
|
37
|
+
}
|
|
38
|
+
else if (/\s/.test(c)) {
|
|
39
|
+
if (open)
|
|
40
|
+
out.push(cur);
|
|
41
|
+
cur = '';
|
|
42
|
+
open = false;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
cur += c;
|
|
46
|
+
open = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (open)
|
|
50
|
+
out.push(cur);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** The command a harness ran, out of the `bash -lc '…'` it wraps each one in. */
|
|
54
|
+
export function unwrap(command) {
|
|
55
|
+
const w = words(command);
|
|
56
|
+
if (w.length === 3 && /(^|\/)(ba|z)?sh$/.test(w[0]) && (w[1] === '-lc' || w[1] === '-c'))
|
|
57
|
+
return w[2];
|
|
58
|
+
return command;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The file a command only reads, or ''. `cat f`, `nl -ba f`, `sed -n '1,80p' f`,
|
|
62
|
+
* `head -n 40 f`, `tail f` — one file, and nothing piped, chained or redirected.
|
|
63
|
+
*/
|
|
64
|
+
export function reads(command) {
|
|
65
|
+
const inner = unwrap(command);
|
|
66
|
+
if (/[|;&<>`$]/.test(inner))
|
|
67
|
+
return '';
|
|
68
|
+
const [cmd, ...args] = words(inner);
|
|
69
|
+
const one = (xs) => {
|
|
70
|
+
const files = xs.filter((a) => !a.startsWith('-'));
|
|
71
|
+
return files.length === 1 ? files[0] : '';
|
|
72
|
+
};
|
|
73
|
+
switch (cmd) {
|
|
74
|
+
case 'cat':
|
|
75
|
+
case 'nl':
|
|
76
|
+
return one(args);
|
|
77
|
+
case 'sed':
|
|
78
|
+
return args.length === 3 && args[0] === '-n' && /^\d+(,\d+)?p$/.test(args[1]) ? args[2] : '';
|
|
79
|
+
case 'head':
|
|
80
|
+
case 'tail':
|
|
81
|
+
return one(args[0] === '-n' ? args.slice(2) : args);
|
|
82
|
+
}
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
/** A path to show: a relative one as it is, an absolute one by its name — never the sandbox's layout. */
|
|
86
|
+
export const shown = (path) => {
|
|
87
|
+
if (!path.startsWith('/'))
|
|
88
|
+
return path;
|
|
89
|
+
const cut = path.replace(/\/+$/, '');
|
|
90
|
+
return cut.slice(cut.lastIndexOf('/') + 1);
|
|
91
|
+
};
|
|
92
|
+
/** A unified diff's sections, by the path each changes, from its first hunk on. */
|
|
93
|
+
export function sections(diff) {
|
|
94
|
+
const out = new Map();
|
|
95
|
+
let at = '';
|
|
96
|
+
let lines = [];
|
|
97
|
+
const close = () => {
|
|
98
|
+
if (at)
|
|
99
|
+
out.set(at, lines.join('\n'));
|
|
100
|
+
};
|
|
101
|
+
for (const line of diff.split('\n')) {
|
|
102
|
+
const m = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
|
|
103
|
+
if (m) {
|
|
104
|
+
close();
|
|
105
|
+
at = m[2];
|
|
106
|
+
lines = [];
|
|
107
|
+
}
|
|
108
|
+
else if (at && (lines.length || line.startsWith('@@')))
|
|
109
|
+
lines.push(line);
|
|
110
|
+
}
|
|
111
|
+
close();
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
const HEAD = /^(exec|codex|thinking|user|apply patch|tokens used|turn interrupted|context compacted)$/;
|
|
115
|
+
const LEAD = /^(patch|mcp|web search|warning|ERROR|deprecated|model rerouted|hook): /;
|
|
116
|
+
const RESULT = /^\s*(succeeded|exited (-?\d+)|declined|in progress)( in [\d.]+m?s)?:$/;
|
|
117
|
+
/** A reader that appends the cards it finds to `out`, keyed by `mint`. */
|
|
118
|
+
export function harness(out, mint) {
|
|
119
|
+
let rest = '';
|
|
120
|
+
let mode = 'prose';
|
|
121
|
+
// The banner is a rule, the settings, and a rule.
|
|
122
|
+
let rules = 0;
|
|
123
|
+
let card = null;
|
|
124
|
+
let final = [];
|
|
125
|
+
const open = [];
|
|
126
|
+
const edits = [];
|
|
127
|
+
let edit = null;
|
|
128
|
+
let diff = null;
|
|
129
|
+
const said = (text) => {
|
|
130
|
+
const c = { kind: 'said', key: mint(), who: 'agent', text };
|
|
131
|
+
out.push(c);
|
|
132
|
+
return c;
|
|
133
|
+
};
|
|
134
|
+
const patch = () => {
|
|
135
|
+
const c = { kind: 'edit', key: mint(), files: [], patch: '', ran: 'running' };
|
|
136
|
+
out.push(c);
|
|
137
|
+
edit = { card: c, paths: [] };
|
|
138
|
+
edits.push(edit);
|
|
139
|
+
return edit;
|
|
140
|
+
};
|
|
141
|
+
// The diff printed after a patch is the turn's so far; the edit keeps the latest.
|
|
142
|
+
const settle = () => {
|
|
143
|
+
if (diff && edit)
|
|
144
|
+
edit.card.patch = diff.join('\n');
|
|
145
|
+
diff = null;
|
|
146
|
+
};
|
|
147
|
+
const step = (name, detail, ran) => {
|
|
148
|
+
const c = { kind: 'step', key: mint(), name, detail, output: '', ran };
|
|
149
|
+
out.push(c);
|
|
150
|
+
return c;
|
|
151
|
+
};
|
|
152
|
+
const line = (raw) => {
|
|
153
|
+
const l = raw.replace(ANSI, '').replace(/\r$/, '');
|
|
154
|
+
if (mode === 'config') {
|
|
155
|
+
if (l === '--------' && ++rules === 2)
|
|
156
|
+
mode = 'prose';
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (mode === 'command') {
|
|
160
|
+
const cut = l.lastIndexOf(' in ');
|
|
161
|
+
const command = cut > 0 ? l.slice(0, cut) : l;
|
|
162
|
+
const file = reads(command);
|
|
163
|
+
const c = file
|
|
164
|
+
? { kind: 'read', key: mint(), file: shown(file), ran: 'running' }
|
|
165
|
+
: { kind: 'shell', key: mint(), command: unwrap(command), output: '', ran: 'running' };
|
|
166
|
+
out.push(c);
|
|
167
|
+
open.push(c);
|
|
168
|
+
card = null;
|
|
169
|
+
mode = 'prose';
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (mode === 'tokens') {
|
|
173
|
+
mode = 'final';
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (mode === 'diff' && !HEAD.test(l) && !LEAD.test(l)) {
|
|
177
|
+
diff.push(l);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const result = RESULT.exec(l);
|
|
181
|
+
if (result && open.length) {
|
|
182
|
+
settle();
|
|
183
|
+
const c = open.pop();
|
|
184
|
+
c.ran = result[1] === 'succeeded' ? 'done' : result[1] === 'declined' ? 'cancelled' : result[1] === 'in progress' ? 'running' : 'error';
|
|
185
|
+
card = c;
|
|
186
|
+
mode = 'output';
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (HEAD.test(l)) {
|
|
190
|
+
settle();
|
|
191
|
+
card = null;
|
|
192
|
+
switch (l) {
|
|
193
|
+
case 'exec':
|
|
194
|
+
mode = 'command';
|
|
195
|
+
return;
|
|
196
|
+
case 'codex':
|
|
197
|
+
case 'thinking':
|
|
198
|
+
card = said('');
|
|
199
|
+
mode = 'agent';
|
|
200
|
+
return;
|
|
201
|
+
case 'user':
|
|
202
|
+
mode = 'user';
|
|
203
|
+
return;
|
|
204
|
+
case 'apply patch':
|
|
205
|
+
patch();
|
|
206
|
+
mode = 'prose';
|
|
207
|
+
return;
|
|
208
|
+
case 'tokens used':
|
|
209
|
+
mode = 'tokens';
|
|
210
|
+
return;
|
|
211
|
+
default:
|
|
212
|
+
out.push({ kind: 'note', key: mint(), text: l === 'turn interrupted' ? 'The agent was interrupted' : 'The agent compacted its context' });
|
|
213
|
+
mode = 'prose';
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (/^Hanzo Dev v\S+$/.test(l)) {
|
|
218
|
+
settle();
|
|
219
|
+
mode = 'config';
|
|
220
|
+
rules = 0;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const patched = /^patch: (completed|failed|declined|in_progress)$/.exec(l);
|
|
224
|
+
if (patched) {
|
|
225
|
+
settle();
|
|
226
|
+
const e = edit && edit.card.ran === 'running' ? edit : patch();
|
|
227
|
+
e.card.ran = patched[1] === 'completed' ? 'done' : patched[1] === 'in_progress' ? 'running' : 'error';
|
|
228
|
+
card = null;
|
|
229
|
+
mode = 'patch';
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (mode === 'patch' && l.startsWith('diff --git ')) {
|
|
233
|
+
diff = [l];
|
|
234
|
+
mode = 'diff';
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const mcp = /^mcp: (\S+) (started|\((completed|failed|in_progress)\))$/.exec(l);
|
|
238
|
+
if (mcp) {
|
|
239
|
+
settle();
|
|
240
|
+
const ran = mcp[3] === 'completed' ? 'done' : mcp[3] === 'failed' ? 'error' : 'running';
|
|
241
|
+
const started = [...out].reverse().find((c) => c.kind === 'step' && c.name === mcp[1] && c.ran === 'running');
|
|
242
|
+
if (started && started.kind === 'step' && mcp[2] !== 'started')
|
|
243
|
+
started.ran = ran;
|
|
244
|
+
else
|
|
245
|
+
step(mcp[1], 'tool', ran);
|
|
246
|
+
card = null;
|
|
247
|
+
mode = 'prose';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const web = /^web search: (.*)$/.exec(l);
|
|
251
|
+
if (web) {
|
|
252
|
+
settle();
|
|
253
|
+
const prior = out[out.length - 1];
|
|
254
|
+
// Printed when the search starts and again when it lands: one card.
|
|
255
|
+
if (!(prior && prior.kind === 'step' && prior.name === 'Web search' && prior.detail === web[1]))
|
|
256
|
+
step('Web search', web[1], 'done');
|
|
257
|
+
card = null;
|
|
258
|
+
mode = 'prose';
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const warn = /^(warning|ERROR|deprecated|model rerouted): ?(.*)$/.exec(l);
|
|
262
|
+
if (warn) {
|
|
263
|
+
settle();
|
|
264
|
+
out.push({ kind: 'note', key: mint(), text: warn[1] === 'ERROR' ? warn[2] : `${warn[1]}: ${warn[2]}` });
|
|
265
|
+
card = null;
|
|
266
|
+
mode = 'prose';
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (l.startsWith('hook: '))
|
|
270
|
+
return;
|
|
271
|
+
const todo = /^ {2}(✓|→|•) (.*)$/.exec(l);
|
|
272
|
+
if (todo && mode !== 'output' && mode !== 'agent') {
|
|
273
|
+
const row = `${todo[1] === '✓' ? '✓' : todo[1] === '→' ? '●' : '○'} ${todo[2]}`;
|
|
274
|
+
if (card && card.kind === 'step' && card.name === 'Plan')
|
|
275
|
+
card.output += `\n${row}`;
|
|
276
|
+
else {
|
|
277
|
+
card = step('Plan', '', 'done');
|
|
278
|
+
if (card.kind === 'step')
|
|
279
|
+
card.output = row;
|
|
280
|
+
}
|
|
281
|
+
mode = 'prose';
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
switch (mode) {
|
|
285
|
+
case 'user':
|
|
286
|
+
return;
|
|
287
|
+
case 'patch':
|
|
288
|
+
if (l.trim() && edit)
|
|
289
|
+
edit.paths.push(l.trim());
|
|
290
|
+
return;
|
|
291
|
+
case 'final':
|
|
292
|
+
final.push(l);
|
|
293
|
+
return;
|
|
294
|
+
case 'output':
|
|
295
|
+
if (card && card.kind === 'shell')
|
|
296
|
+
card.output += card.output ? `\n${l}` : l;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// Prose: what the agent said under its header, or lines no header owns.
|
|
300
|
+
if (card && card.kind === 'said') {
|
|
301
|
+
card.text += card.text ? `\n${l}` : l;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!l.trim())
|
|
305
|
+
return;
|
|
306
|
+
card = said(l);
|
|
307
|
+
mode = 'agent';
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
feed(chunk) {
|
|
311
|
+
const text = rest + chunk;
|
|
312
|
+
const lines = text.split('\n');
|
|
313
|
+
rest = lines.pop() ?? '';
|
|
314
|
+
for (const l of lines)
|
|
315
|
+
line(l);
|
|
316
|
+
},
|
|
317
|
+
/** The last line even without its newline, and what only the whole output can decide. */
|
|
318
|
+
end() {
|
|
319
|
+
if (rest)
|
|
320
|
+
line(rest);
|
|
321
|
+
rest = '';
|
|
322
|
+
settle();
|
|
323
|
+
// The final message is printed twice: under its header, and bare at the end.
|
|
324
|
+
const tail = final.join('\n').trim();
|
|
325
|
+
const spoken = out.filter((c) => c.kind === 'said' && c.who === 'agent').map((c) => c.text.trim());
|
|
326
|
+
if (tail && !spoken.includes(tail))
|
|
327
|
+
said(tail);
|
|
328
|
+
final = [];
|
|
329
|
+
// A patch names the sandbox's paths; the diff names them as the repository
|
|
330
|
+
// does, and is the whole turn's, so an edit keeps the sections of its own files.
|
|
331
|
+
for (const e of edits) {
|
|
332
|
+
const parts = sections(e.card.patch);
|
|
333
|
+
const known = [...parts.keys()];
|
|
334
|
+
e.card.files = e.paths.map((p) => known.find((k) => p === k || p.endsWith(`/${k}`)) ?? shown(p));
|
|
335
|
+
if (!e.card.files.length)
|
|
336
|
+
e.card.files = known;
|
|
337
|
+
const mine = e.card.files.filter((f) => parts.has(f));
|
|
338
|
+
e.card.patch = mine.length === 1 ? parts.get(mine[0]) : mine.map((f) => `${f}\n${parts.get(f)}`).join('\n');
|
|
339
|
+
}
|
|
340
|
+
edits.length = 0;
|
|
341
|
+
for (let i = out.length - 1; i >= 0; i--) {
|
|
342
|
+
const c = out[i];
|
|
343
|
+
if (c.kind === 'said' && !c.text.trim())
|
|
344
|
+
out.splice(i, 1);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A person's own API keys: one secret key (sk-, it belongs on a server) and one
|
|
3
|
+
* publishable key (pk-, safe in a page's source), at most one of each.
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/account/keys {keys: [{type, prefix, key?, limit?, createdAt}]}
|
|
6
|
+
* POST /v1/account/keys {type, limit?} → {key, type, limit}, answered once
|
|
7
|
+
* DELETE /v1/account/keys?type=… revoke that one
|
|
8
|
+
*
|
|
9
|
+
* Creating is rotating: a new key of a type ends the one before it. A secret key
|
|
10
|
+
* is answered once and by its prefix after; a publishable key is public by
|
|
11
|
+
* construction and always comes back whole. A limit only narrows what a key may
|
|
12
|
+
* reach — `model:zen5`, `project:acme`, `product:commerce`, `model:*`.
|
|
13
|
+
*/
|
|
14
|
+
import { type Target } from './call.ts';
|
|
15
|
+
export declare const KINDS: readonly ['secret', 'publishable'];
|
|
16
|
+
export type Kind = (typeof KINDS)[number];
|
|
17
|
+
export interface Key {
|
|
18
|
+
type: Kind;
|
|
19
|
+
/** The recognizable head, never enough to use the key. */
|
|
20
|
+
prefix: string;
|
|
21
|
+
/** The whole key: a publishable key's only. */
|
|
22
|
+
key: string;
|
|
23
|
+
/** `kind:name` entries; none reaches whatever its holder does. */
|
|
24
|
+
limit: string[];
|
|
25
|
+
/** When it last changed, as IAM records it. */
|
|
26
|
+
created: string;
|
|
27
|
+
}
|
|
28
|
+
export interface Minted {
|
|
29
|
+
type: Kind;
|
|
30
|
+
/** The key itself. A secret one is never readable again. */
|
|
31
|
+
key: string;
|
|
32
|
+
limit: string[];
|
|
33
|
+
}
|
|
34
|
+
export declare function keys(t: Target): Promise<Key[]>;
|
|
35
|
+
/** A `kind:name` limit: a lowercase kind, a colon, and a name or `*`. */
|
|
36
|
+
export declare const LIMIT: RegExp;
|
|
37
|
+
/** The limits a person typed, split on commas and spaces; throws on the first that is not one. */
|
|
38
|
+
export declare function limits(raw: string): string[];
|
|
39
|
+
/** Create a key of this type, ending the one before it. The answer is the only time a secret key is shown. */
|
|
40
|
+
export declare function mint(t: Target, type: Kind, limit?: string[]): Promise<Minted>;
|
|
41
|
+
export declare function revoke(t: Target, type: Kind): Promise<void>;
|
package/lib/api/keys.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A person's own API keys: one secret key (sk-, it belongs on a server) and one
|
|
3
|
+
* publishable key (pk-, safe in a page's source), at most one of each.
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/account/keys {keys: [{type, prefix, key?, limit?, createdAt}]}
|
|
6
|
+
* POST /v1/account/keys {type, limit?} → {key, type, limit}, answered once
|
|
7
|
+
* DELETE /v1/account/keys?type=… revoke that one
|
|
8
|
+
*
|
|
9
|
+
* Creating is rotating: a new key of a type ends the one before it. A secret key
|
|
10
|
+
* is answered once and by its prefix after; a publishable key is public by
|
|
11
|
+
* construction and always comes back whole. A limit only narrows what a key may
|
|
12
|
+
* reach — `model:zen5`, `project:acme`, `product:commerce`, `model:*`.
|
|
13
|
+
*/
|
|
14
|
+
import { call, query } from './call.js';
|
|
15
|
+
export const KINDS = ['secret', 'publishable'];
|
|
16
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
17
|
+
const list = (v) => (Array.isArray(v) ? v.filter((s) => typeof s === 'string' && s !== '') : []);
|
|
18
|
+
const kind = (v) => (KINDS.includes(v) ? v : null);
|
|
19
|
+
export async function keys(t) {
|
|
20
|
+
const r = await call(t, 'GET', '/v1/account/keys');
|
|
21
|
+
const rows = Array.isArray(r?.keys) ? r.keys : [];
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const raw of rows) {
|
|
24
|
+
const o = (raw && typeof raw === 'object' ? raw : {});
|
|
25
|
+
const type = kind(o.type);
|
|
26
|
+
if (!type)
|
|
27
|
+
continue;
|
|
28
|
+
out.push({ type, prefix: str(o.prefix), key: str(o.key), limit: list(o.limit), created: str(o.createdAt) });
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/** A `kind:name` limit: a lowercase kind, a colon, and a name or `*`. */
|
|
33
|
+
export const LIMIT = /^[a-z][a-z0-9-]*:[^\s,:]+$/;
|
|
34
|
+
/** The limits a person typed, split on commas and spaces; throws on the first that is not one. */
|
|
35
|
+
export function limits(raw) {
|
|
36
|
+
const out = raw
|
|
37
|
+
.split(/[\s,]+/)
|
|
38
|
+
.map((s) => s.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
const bad = out.find((s) => !LIMIT.test(s));
|
|
41
|
+
if (bad)
|
|
42
|
+
throw new Error(`${bad} is not a limit — write it as kind:name, like model:zen5`);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/** Create a key of this type, ending the one before it. The answer is the only time a secret key is shown. */
|
|
46
|
+
export async function mint(t, type, limit = []) {
|
|
47
|
+
const r = await call(t, 'POST', '/v1/account/keys', limit.length ? { type, limit } : { type });
|
|
48
|
+
const key = str(r?.key) || str(r?.accessKey);
|
|
49
|
+
if (!key)
|
|
50
|
+
throw new Error('The platform answered no key');
|
|
51
|
+
return { type: kind(r?.type) ?? type, key, limit: list(r?.limit) };
|
|
52
|
+
}
|
|
53
|
+
export async function revoke(t, type) {
|
|
54
|
+
await call(t, 'DELETE', `/v1/account/keys${query({ type })}`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The org's machines: its own computers a run can be sent to, beside the sandbox.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/agent/targets {targets: [...]}, newest first
|
|
5
|
+
* POST /v1/agent/targets register one by hand {label, kind, host}
|
|
6
|
+
* PATCH /v1/agent/targets/{id} rename it, drain it, bring it back
|
|
7
|
+
* DELETE /v1/agent/targets/{id} deregister it
|
|
8
|
+
* POST /v1/agent/targets/{id}/key mint or rotate its claim key, answered once
|
|
9
|
+
*
|
|
10
|
+
* A machine belongs to the member who registered it. Only that member or an org
|
|
11
|
+
* admin changes, removes or keys it, and the platform answers anyone else the
|
|
12
|
+
* same 404 an unknown id gets — the record names no owner — so a 404 here is
|
|
13
|
+
* said as that rule. `hanzo link`, run on the machine, registers it under its
|
|
14
|
+
* hostname and keeps it online while it runs.
|
|
15
|
+
*/
|
|
16
|
+
import { type Target } from './call.ts';
|
|
17
|
+
export declare const KINDS: readonly ['laptop', 'gpu', 'cloud', 'cluster', 'machine'];
|
|
18
|
+
export type Kind = (typeof KINDS)[number];
|
|
19
|
+
export interface Machine {
|
|
20
|
+
/** `tgt_…` */
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
/** laptop | gpu | cloud | cluster | machine */
|
|
24
|
+
kind: string;
|
|
25
|
+
/** online | offline | draining, as the platform judges it from the last heartbeat. */
|
|
26
|
+
status: string;
|
|
27
|
+
/** "10 vCPU / 122G / 1× GB10", when the machine reported one. */
|
|
28
|
+
capacity: string;
|
|
29
|
+
/** The hostname its runs report, or '' for one addressed by id only. */
|
|
30
|
+
host: string;
|
|
31
|
+
/** The org's runs mapped to it, and how many of those are running now. */
|
|
32
|
+
sessions: number;
|
|
33
|
+
running: number;
|
|
34
|
+
/** When it last beat, RFC 3339, or '' when it never has. */
|
|
35
|
+
seen: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function machine(raw: unknown): Machine;
|
|
38
|
+
export declare function machines(t: Target): Promise<Machine[]>;
|
|
39
|
+
/** Register a machine by hand. `hanzo link` on a machine with this hostname takes the row over. */
|
|
40
|
+
export declare function add(t: Target, what: {
|
|
41
|
+
label: string;
|
|
42
|
+
kind: Kind;
|
|
43
|
+
host?: string;
|
|
44
|
+
}): Promise<Machine>;
|
|
45
|
+
/** Rename a machine, or drain it (`draining`) and bring it back (`online`). */
|
|
46
|
+
export declare function change(t: Target, id: string, what: {
|
|
47
|
+
label?: string;
|
|
48
|
+
status?: 'online' | 'draining';
|
|
49
|
+
}): Promise<Machine>;
|
|
50
|
+
export declare function remove(t: Target, id: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Mint the machine's claim key, answered this once: only its hash is kept, and
|
|
53
|
+
* minting again ends the key before it. A runner on the machine sends it as
|
|
54
|
+
* `X-Target-Key` when it claims the runs sent there.
|
|
55
|
+
*/
|
|
56
|
+
export declare function key(t: Target, id: string): Promise<string>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The org's machines: its own computers a run can be sent to, beside the sandbox.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/agent/targets {targets: [...]}, newest first
|
|
5
|
+
* POST /v1/agent/targets register one by hand {label, kind, host}
|
|
6
|
+
* PATCH /v1/agent/targets/{id} rename it, drain it, bring it back
|
|
7
|
+
* DELETE /v1/agent/targets/{id} deregister it
|
|
8
|
+
* POST /v1/agent/targets/{id}/key mint or rotate its claim key, answered once
|
|
9
|
+
*
|
|
10
|
+
* A machine belongs to the member who registered it. Only that member or an org
|
|
11
|
+
* admin changes, removes or keys it, and the platform answers anyone else the
|
|
12
|
+
* same 404 an unknown id gets — the record names no owner — so a 404 here is
|
|
13
|
+
* said as that rule. `hanzo link`, run on the machine, registers it under its
|
|
14
|
+
* hostname and keeps it online while it runs.
|
|
15
|
+
*/
|
|
16
|
+
import { call, Refusal, seg } from './call.js';
|
|
17
|
+
export const KINDS = ['laptop', 'gpu', 'cloud', 'cluster', 'machine'];
|
|
18
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
19
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
20
|
+
export function machine(raw) {
|
|
21
|
+
const o = (raw && typeof raw === 'object' ? raw : {});
|
|
22
|
+
return {
|
|
23
|
+
id: str(o.id),
|
|
24
|
+
label: str(o.label) || str(o.host) || str(o.id),
|
|
25
|
+
kind: str(o.kind),
|
|
26
|
+
status: str(o.status),
|
|
27
|
+
capacity: str(o.capacity),
|
|
28
|
+
host: str(o.host),
|
|
29
|
+
sessions: num(o.sessions),
|
|
30
|
+
running: num(o.running),
|
|
31
|
+
seen: str(o.metricsAt),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function machines(t) {
|
|
35
|
+
const raw = (await call(t, 'GET', '/v1/agent/targets')) ?? {};
|
|
36
|
+
return (Array.isArray(raw.targets) ? raw.targets : []).map(machine).filter((m) => m.id);
|
|
37
|
+
}
|
|
38
|
+
/** The platform's 404 for a machine that is someone else's, said as the rule it is. */
|
|
39
|
+
async function owned(p) {
|
|
40
|
+
try {
|
|
41
|
+
return await p;
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (e instanceof Refusal && e.status === 404)
|
|
45
|
+
throw new Refusal(404, 'This machine is gone, or not yours to change: only the member who linked it, or an org admin, can.');
|
|
46
|
+
throw e;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Register a machine by hand. `hanzo link` on a machine with this hostname takes the row over. */
|
|
50
|
+
export async function add(t, what) {
|
|
51
|
+
const label = what.label.trim();
|
|
52
|
+
if (!label)
|
|
53
|
+
throw new Error('A machine needs a name');
|
|
54
|
+
const host = (what.host ?? '').trim();
|
|
55
|
+
return machine(await call(t, 'POST', '/v1/agent/targets', { label, kind: what.kind, ...(host ? { host } : {}) }));
|
|
56
|
+
}
|
|
57
|
+
/** Rename a machine, or drain it (`draining`) and bring it back (`online`). */
|
|
58
|
+
export async function change(t, id, what) {
|
|
59
|
+
const body = {};
|
|
60
|
+
if (what.label !== undefined) {
|
|
61
|
+
const label = what.label.trim();
|
|
62
|
+
if (!label)
|
|
63
|
+
throw new Error('A machine needs a name');
|
|
64
|
+
body.label = label;
|
|
65
|
+
}
|
|
66
|
+
if (what.status !== undefined)
|
|
67
|
+
body.status = what.status;
|
|
68
|
+
return machine(await owned(call(t, 'PATCH', `/v1/agent/targets/${seg(id)}`, body)));
|
|
69
|
+
}
|
|
70
|
+
export async function remove(t, id) {
|
|
71
|
+
await owned(call(t, 'DELETE', `/v1/agent/targets/${seg(id)}`));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Mint the machine's claim key, answered this once: only its hash is kept, and
|
|
75
|
+
* minting again ends the key before it. A runner on the machine sends it as
|
|
76
|
+
* `X-Target-Key` when it claims the runs sent there.
|
|
77
|
+
*/
|
|
78
|
+
export async function key(t, id) {
|
|
79
|
+
const r = await owned(call(t, 'POST', `/v1/agent/targets/${seg(id)}/key`));
|
|
80
|
+
const k = str(r?.claimKey);
|
|
81
|
+
if (!k)
|
|
82
|
+
throw new Error('The platform answered no key');
|
|
83
|
+
return k;
|
|
84
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is in the organization, and who has been asked to join it, as Hanzo IAM
|
|
3
|
+
* holds them.
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/iam/memberships?org= the roster: {status, data: [membership], data2: count}
|
|
6
|
+
* GET /v1/iam/invitations?owner= {invitations, total} (org admin)
|
|
7
|
+
* POST /v1/iam/invitations issue one (org admin)
|
|
8
|
+
* DELETE /v1/iam/invitations/{owner}/{name} withdraw one (org admin)
|
|
9
|
+
*
|
|
10
|
+
* IAM decides the scope from the credential: a member reads the roster of an org
|
|
11
|
+
* they belong to, and only an admin of the org reads or writes its invitations.
|
|
12
|
+
* An invitation is a code IAM redeems when someone signs up with it; IAM sends
|
|
13
|
+
* no email of its own.
|
|
14
|
+
*/
|
|
15
|
+
import { type Target } from './call.ts';
|
|
16
|
+
export type Role = 'owner' | 'admin' | 'member';
|
|
17
|
+
export interface Member {
|
|
18
|
+
/** `<homeOrg>/<username>`, IAM's id for the person. */
|
|
19
|
+
user: string;
|
|
20
|
+
/** The username alone. */
|
|
21
|
+
name: string;
|
|
22
|
+
role: Role;
|
|
23
|
+
since: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function member(raw: unknown): Member;
|
|
26
|
+
/** The org's people, owners first. Grants to a team or narrowed to one project are not the org's roster. */
|
|
27
|
+
export declare function roster(t: Target, org: string): Promise<Member[]>;
|
|
28
|
+
export interface Invitation {
|
|
29
|
+
owner: string;
|
|
30
|
+
name: string;
|
|
31
|
+
email: string;
|
|
32
|
+
/** What the invitee enters when they sign up. */
|
|
33
|
+
code: string;
|
|
34
|
+
/** How many may join with it, and how many have. */
|
|
35
|
+
seats: number;
|
|
36
|
+
used: number;
|
|
37
|
+
/** `Active` is redeemable; anything else is not. */
|
|
38
|
+
state: string;
|
|
39
|
+
created: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function invitation(raw: unknown): Invitation;
|
|
42
|
+
export declare function invitations(t: Target, org: string): Promise<Invitation[]>;
|
|
43
|
+
/** An address that can receive an invitation. */
|
|
44
|
+
export declare const EMAIL: RegExp;
|
|
45
|
+
/** Invite one address into the org: one seat, redeemable now, pinned to that address. */
|
|
46
|
+
export declare function invite(t: Target, org: string, email: string): Promise<Invitation>;
|
|
47
|
+
export declare function revoke(t: Target, i: Pick<Invitation, 'owner' | 'name'>): Promise<void>;
|