@pablopunk/tau 0.0.0-stage → 0.1.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.
Files changed (64) hide show
  1. package/dist/auth-storage-Dk3CUY3q.js +1814 -0
  2. package/dist/bin.js +23 -0
  3. package/dist/cli-entry-DcDpQ30L.js +13 -0
  4. package/dist/client-C91-b2CL.js +1 -0
  5. package/dist/client-y2WkNuZj.js +7977 -0
  6. package/dist/dist-BFP19NOO.js +432 -0
  7. package/dist/dist-Bh1bQHZA.js +38890 -0
  8. package/dist/dist-Cq73_r65.js +3 -0
  9. package/dist/esm-CMGh_WhC.js +9446 -0
  10. package/dist/execute-BeGXhU1O.js +1757 -0
  11. package/dist/fake-claude-connectors-DSP9UC8d.js +1 -0
  12. package/dist/fake-claude-connectors-sDLeP10T.js +107 -0
  13. package/dist/fake-claude-sdk-CvqW_1iQ.js +571 -0
  14. package/dist/fake-claude-thread-script-2iHHwUZM.js +48 -0
  15. package/dist/last-exit-write-ksHeHGrC.js +9 -0
  16. package/dist/lib-DIX7q_HJ.js +1460 -0
  17. package/dist/main-CI9q691u.js +1490 -0
  18. package/dist/names-cTrEXB6w.js +35 -0
  19. package/dist/node-BpwblPFP.js +56269 -0
  20. package/dist/pdfjs-nTcHjDPT.js +69856 -0
  21. package/dist/photon_rs-DBr7W_rK.js +4252 -0
  22. package/dist/promise.try-CAAOYvMP.js +1426 -0
  23. package/dist/readability-DsDNO63a.js +1670 -0
  24. package/dist/rolldown-runtime-1p2KPIdQ.js +44 -0
  25. package/dist/runtime-i-oEtgAl.js +2721 -0
  26. package/dist/sdk-missing-CHbVbY1G.js +10 -0
  27. package/dist/sdk-port-BGw9ORUR.js +215 -0
  28. package/dist/serve-92lMbcls.js +2958 -0
  29. package/dist/serve-BcVZj5e2.js +1 -0
  30. package/dist/service-command-BVr33vlP.js +1 -0
  31. package/dist/service-command-C7WDeeR2.js +621 -0
  32. package/dist/shell-CQa4e7rx.js +643 -0
  33. package/dist/skills/builtin/tau/SKILL.md +51 -0
  34. package/dist/streamableHttp-BBaRYV9h.js +1 -0
  35. package/dist/streamableHttp-DPAoMlJd.js +1931 -0
  36. package/dist/tool-DBZK2lci.js +14335 -0
  37. package/dist/tool-server-BjP37EJg.js +40 -0
  38. package/dist/turndown.cjs-Bo1xLg2S.js +15620 -0
  39. package/dist/turndown.cjs-jpUF50AR.js +3 -0
  40. package/dist/types-C4M3CRp2.js +7592 -0
  41. package/dist/undici-8cI97lnP.js +27110 -0
  42. package/dist/wait-for-lock-BMk6v3J5.js +1 -0
  43. package/dist/wait-for-lock-D2V3vuye.js +43951 -0
  44. package/dist/watchdog.js +47 -0
  45. package/dist/web/dist/assets/addon-fit-DIOBYJe3.js +1 -0
  46. package/dist/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  47. package/dist/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  48. package/dist/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  49. package/dist/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  50. package/dist/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  51. package/dist/web/dist/assets/index--f7AYPm0.js +108 -0
  52. package/dist/web/dist/assets/index-7m0crRNF.css +2 -0
  53. package/dist/web/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  54. package/dist/web/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  55. package/dist/web/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  56. package/dist/web/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  57. package/dist/web/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  58. package/dist/web/dist/assets/wght-DMLdVLkb.css +1 -0
  59. package/dist/web/dist/assets/xterm-BrP-ENHg.css +1 -0
  60. package/dist/web/dist/assets/xterm-CerOhKYV.js +16 -0
  61. package/dist/web/dist/favicon.svg +14 -0
  62. package/dist/web/dist/index.html +25 -0
  63. package/package.json +53 -4
  64. package/README.md +0 -3
@@ -0,0 +1,1490 @@
1
+ import { $ as routes, B as COMMAND_NOUNS, G as isModelRefNode, H as commandOf, J as LIVE_PATH, K as routeFields, L as isErrorBody, M as decodeServerFrame, N as encodeFrame, U as commands, V as FLOW_COMMANDS, W as flagNameOf, Z as fillPath, a as readRunningInstance, c as lastAssistantText, it as RedeemPairingResponseSchema, l as viewToText, nt as nextViewState, z as AUTO_FIELDS } from "./wait-for-lock-D2V3vuye.js";
2
+ import { a as lifecycleFlagsOf, c as createHubFileStore, i as commandWordOf, l as alreadyLinkedRefusal, n as serverLogOf, o as cliVersion, r as writeStoredPort, s as readOrCreateToken, t as serveInForeground, u as connectRefusal } from "./serve-92lMbcls.js";
3
+ import { a as TemporaryEntryError, i as realContext, r as createServiceManager, t as runServiceCommand } from "./service-command-C7WDeeR2.js";
4
+ import { t as cliEntry } from "./cli-entry-DcDpQ30L.js";
5
+ import { createReadStream, existsSync, readFileSync, statSync, watchFile } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { homedir, hostname } from "node:os";
8
+ import { fileURLToPath } from "node:url";
9
+ import { promisify } from "node:util";
10
+ import { randomUUID } from "node:crypto";
11
+ import { readFile, readdir } from "node:fs/promises";
12
+ import { execFile } from "node:child_process";
13
+ import { Value } from "typebox/value";
14
+ import { createInterface } from "node:readline/promises";
15
+ import QRCode from "qrcode";
16
+ //#region ../protocol/src/live-info.ts
17
+ function docOf(view, kind) {
18
+ return view.docs[kind];
19
+ }
20
+ function isQueuedMessage(item) {
21
+ return item.mode === "steer" || item.mode === "followUp";
22
+ }
23
+ function queuedBlocks(item) {
24
+ const content = item.content;
25
+ if (typeof content === "string") return content;
26
+ const blocks = content?.content;
27
+ if (typeof blocks === "string") return blocks;
28
+ return Array.isArray(blocks) ? blocks : [];
29
+ }
30
+ function queuedText(item) {
31
+ const blocks = queuedBlocks(item);
32
+ if (typeof blocks === "string") return blocks;
33
+ return blocks.map((block) => block.type === "text" ? block.text : "").join("");
34
+ }
35
+ function queuedImages(item) {
36
+ const blocks = queuedBlocks(item);
37
+ if (typeof blocks === "string") return [];
38
+ return blocks.filter((block) => block.type === "image");
39
+ }
40
+ function queuedOf(item) {
41
+ return {
42
+ id: Number(item.id),
43
+ mode: item.mode,
44
+ text: queuedText(item),
45
+ images: queuedImages(item)
46
+ };
47
+ }
48
+ function retryOf(live) {
49
+ const generation = live.generation;
50
+ if (!generation?.retry) return void 0;
51
+ return {
52
+ attempt: generation.attempt,
53
+ ...generation.retry
54
+ };
55
+ }
56
+ function readLive(view) {
57
+ const live = docOf(view, "pi.live") ?? {};
58
+ const inbox = docOf(view, "pi.inbox");
59
+ return {
60
+ busy: live.run !== void 0,
61
+ streaming: live.generation?.message,
62
+ retry: retryOf(live),
63
+ slots: live.tools ?? [],
64
+ compacting: (live.compactions?.length ?? 0) > 0,
65
+ queued: (inbox?.items ?? []).filter(isQueuedMessage).map(queuedOf)
66
+ };
67
+ }
68
+ /** A failure that ends the command with a message on stderr and an exit code. */
69
+ var CliExit = class extends Error {
70
+ exitCode;
71
+ constructor(exitCode, message) {
72
+ super(message);
73
+ this.name = "CliExit";
74
+ this.exitCode = exitCode;
75
+ }
76
+ };
77
+ const usageError = (message) => new CliExit(2, message);
78
+ const failure = (message) => new CliExit(1, message);
79
+ const NOT_RUNNING_MESSAGE = "tau is not running. Run `tau` to start it.";
80
+ //#endregion
81
+ //#region src/machines/connect-args.ts
82
+ const USAGE = "Usage: tau connect <link | code --hub <url>> [--name <label>] [--yes]";
83
+ function parseConnectArgs(argv) {
84
+ let target;
85
+ let hub;
86
+ let name;
87
+ let yes = false;
88
+ for (let index = 0; index < argv.length; index++) {
89
+ const token = argv[index];
90
+ const next = argv[index + 1];
91
+ if (token === "--hub" && next !== void 0) hub = argv[++index];
92
+ else if (token.startsWith("--hub=")) hub = token.slice(6);
93
+ else if (token === "--name" && next !== void 0) name = argv[++index];
94
+ else if (token.startsWith("--name=")) name = token.slice(7);
95
+ else if (token === "--yes" || token === "-y") yes = true;
96
+ else if (!token.startsWith("-") && target === void 0) target = token;
97
+ else throw usageError(`Unknown argument "${token}". ${USAGE}`);
98
+ }
99
+ if (target === void 0) throw usageError(USAGE);
100
+ return {
101
+ target,
102
+ ...hub !== void 0 && { hub },
103
+ name: name?.trim() || hostname(),
104
+ yes
105
+ };
106
+ }
107
+ //#endregion
108
+ //#region src/machines/connect-io.ts
109
+ async function ask(question) {
110
+ if (!process.stdin.isTTY) throw usageError("This needs a terminal. Add --yes to skip the question.");
111
+ const lines = createInterface({
112
+ input: process.stdin,
113
+ output: process.stdout
114
+ });
115
+ try {
116
+ return (await lines.question(question)).trim().toLowerCase();
117
+ } finally {
118
+ lines.close();
119
+ }
120
+ }
121
+ const terminalIo = {
122
+ confirm: async (question) => ["y", "yes"].includes(await ask(`${question} [y/N] `)),
123
+ async chooseStart() {
124
+ const answer = await ask("tau is not running here. Start it for this session (s), install it as a service (i), or neither (n)? [s/i/N] ");
125
+ return answer === "s" ? "serve" : answer === "i" ? "service" : "none";
126
+ }
127
+ };
128
+ //#endregion
129
+ //#region src/machines/connect-target.ts
130
+ const LOOPBACK = /* @__PURE__ */ new Set([
131
+ "127.0.0.1",
132
+ "localhost",
133
+ "[::1]"
134
+ ]);
135
+ /** `https://` always; `http://` only for loopback (the same-Mac test with `--hub http://127.0.0.1:<port>`). */
136
+ function normalizeHubUrl(input) {
137
+ let url;
138
+ try {
139
+ url = new URL(input);
140
+ } catch {
141
+ throw usageError(`"${input}" is not a hub URL. Example: https://abc123.opentunnel.xyz`);
142
+ }
143
+ const loopback = LOOPBACK.has(url.hostname);
144
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) throw usageError("The hub URL must start with https:// (http:// only for 127.0.0.1 or localhost).");
145
+ return url.origin;
146
+ }
147
+ /**
148
+ * `tau connect <link>` takes `https://<hostname>/pair#<code>[.<name>]`; `tau connect <short code> --hub <url>`
149
+ * takes the 8-character code. There is no unauthenticated endpoint that returns the hub's name, so it rides in the link.
150
+ */
151
+ function parseConnectTarget(input, hub) {
152
+ if (!input.includes("/")) {
153
+ if (hub === void 0) throw usageError("A short code needs the hub: tau connect <code> --hub <url>");
154
+ return {
155
+ hubUrl: normalizeHubUrl(hub),
156
+ code: input.replace(/\s+/g, "")
157
+ };
158
+ }
159
+ normalizeHubUrl(input);
160
+ const url = new URL(input);
161
+ const fragment = url.hash.replace(/^#/, "");
162
+ if (url.pathname !== "/pair" || fragment === "") throw usageError("That is not a link code. Make one in Settings → Machines on the hub.");
163
+ const dot = fragment.indexOf(".");
164
+ const code = dot < 0 ? fragment : fragment.slice(0, dot);
165
+ const name = dot < 0 ? "" : decodeURIComponent(fragment.slice(dot + 1));
166
+ return {
167
+ hubUrl: url.origin,
168
+ code,
169
+ ...name !== "" && { hubName: name }
170
+ };
171
+ }
172
+ //#endregion
173
+ //#region src/commands/client.ts
174
+ const URL_POLL_MS = 100;
175
+ async function waitForLockUrl(home, env) {
176
+ const deadline = env.now() + env.startupWaitMs;
177
+ for (;;) {
178
+ const info = await readRunningInstance(home);
179
+ if (!info) throw failure(NOT_RUNNING_MESSAGE);
180
+ if (info.url) return info.url;
181
+ if (env.now() >= deadline) throw failure("tau is starting but has not published its URL yet.");
182
+ await env.sleep(URL_POLL_MS);
183
+ }
184
+ }
185
+ async function readToken(home) {
186
+ try {
187
+ return (await readFile(join(home, "token"), "utf8")).trim();
188
+ } catch {
189
+ throw failure(NOT_RUNNING_MESSAGE);
190
+ }
191
+ }
192
+ /** The lock gives the URL and `<home>/token` the secret. Retries while a live server has no URL yet. */
193
+ async function discoverServer(env) {
194
+ return {
195
+ url: await waitForLockUrl(env.home, env),
196
+ token: await readToken(env.home)
197
+ };
198
+ }
199
+ const queryString = (query) => {
200
+ const entries = Object.entries(query ?? {});
201
+ return entries.length === 0 ? "" : `?${new URLSearchParams(entries).toString()}`;
202
+ };
203
+ async function errorMessageOf$1(response) {
204
+ const text = await response.text();
205
+ try {
206
+ const parsed = JSON.parse(text);
207
+ if (isErrorBody(parsed)) return `${parsed.error.code}: ${parsed.error.message}`;
208
+ } catch {}
209
+ return `HTTP ${response.status}${text ? `: ${text.slice(0, 200)}` : ""}`;
210
+ }
211
+ async function send(server, name, request, env) {
212
+ const route = routes[name];
213
+ const target = `${server.url}${fillPath(route.path, request.params)}${queryString(request.query)}`;
214
+ const hasBody = request.body !== void 0;
215
+ try {
216
+ return await env.fetch(target, {
217
+ method: route.method,
218
+ headers: {
219
+ authorization: `Bearer ${server.token}`,
220
+ ...hasBody ? { "content-type": "application/json" } : {}
221
+ },
222
+ body: hasBody ? JSON.stringify(request.body) : void 0
223
+ });
224
+ } catch {
225
+ throw failure(NOT_RUNNING_MESSAGE);
226
+ }
227
+ }
228
+ /** Calls one route. Returns the parsed JSON, or `undefined` for an empty answer. An `AppError` becomes a failure. */
229
+ async function callRoute(server, name, request, env) {
230
+ const response = await send(server, name, request, env);
231
+ if (!response.ok) throw failure(await errorMessageOf$1(response));
232
+ const text = await response.text();
233
+ return text ? JSON.parse(text) : void 0;
234
+ }
235
+ //#endregion
236
+ //#region src/commands/command-env.ts
237
+ const defaultOpenSocket = (url, headers) => {
238
+ return new WebSocket(url, { headers });
239
+ };
240
+ const homeOf = (env) => env.TAU_HOME ?? join(homedir(), ".tau");
241
+ function processEnv() {
242
+ return {
243
+ home: homeOf(process.env),
244
+ env: process.env,
245
+ stdout: (text) => void process.stdout.write(`${text}\n`),
246
+ stderr: (text) => void process.stderr.write(`${text}\n`),
247
+ fetch: (input, init) => fetch(input, init),
248
+ openSocket: defaultOpenSocket,
249
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
250
+ after: (ms, fn) => {
251
+ const timer = setTimeout(fn, ms);
252
+ return () => clearTimeout(timer);
253
+ },
254
+ now: Date.now,
255
+ startupWaitMs: 5e3,
256
+ pollMs: 2e3,
257
+ skillPaths: []
258
+ };
259
+ }
260
+ //#endregion
261
+ //#region src/commands/command-flags.ts
262
+ const WAIT_COMMANDS = ["threads start", "threads send"];
263
+ const COMMON_FLAGS = [
264
+ {
265
+ name: "json",
266
+ kind: "value",
267
+ valueKind: "output",
268
+ summary: "Whole request body as JSON (replaces the flags)"
269
+ },
270
+ {
271
+ name: "yes",
272
+ kind: "boolean",
273
+ valueKind: "output",
274
+ summary: "Confirm a destructive command"
275
+ },
276
+ {
277
+ name: "home",
278
+ kind: "value",
279
+ valueKind: "output",
280
+ summary: "Tau data home (default: TAU_HOME or ~/.tau)"
281
+ }
282
+ ];
283
+ const WAIT_FLAGS = [{
284
+ name: "wait",
285
+ kind: "boolean",
286
+ valueKind: "output",
287
+ summary: "Wait until the thread is done, then print the answer"
288
+ }, {
289
+ name: "timeout",
290
+ kind: "value",
291
+ valueKind: "output",
292
+ summary: "Seconds to wait (default 600)"
293
+ }];
294
+ const isLiteralBooleanString = (field) => field.node.anyOf?.every((member) => member.const === "true" || member.const === "false") ?? false;
295
+ function valueKindOf(field) {
296
+ if (isModelRefNode(field.node)) return "model";
297
+ if (isLiteralBooleanString(field)) return "booleanString";
298
+ if (field.boolean) return "boolean";
299
+ const type = field.node.type;
300
+ return type === "number" || type === "integer" ? "number" : "string";
301
+ }
302
+ const isFlagField = (def, field) => !def.positionals.includes(field.path) && def.fixed?.[field.path] === void 0 && (field.primitive || isModelRefNode(field.node));
303
+ function flagOf(field) {
304
+ const valueKind = valueKindOf(field);
305
+ const isBoolean = valueKind === "boolean" || valueKind === "booleanString";
306
+ return {
307
+ name: flagNameOf(field.name),
308
+ kind: isBoolean ? "boolean" : "value",
309
+ valueKind,
310
+ field,
311
+ summary: `${field.required ? "Required. " : ""}${field.source} field ${field.name}`
312
+ };
313
+ }
314
+ function commandFlags(def) {
315
+ const fieldFlags = routeFields(def.route).filter((field) => isFlagField(def, field)).map(flagOf);
316
+ const waitFlags = WAIT_COMMANDS.includes(def.command) ? WAIT_FLAGS : [];
317
+ return [
318
+ ...fieldFlags,
319
+ ...waitFlags,
320
+ ...COMMON_FLAGS
321
+ ];
322
+ }
323
+ const flagSpecOf = (flags) => Object.fromEntries(flags.map((flag) => [flag.name, flag.kind]));
324
+ //#endregion
325
+ //#region src/commands/scan-args.ts
326
+ const NEGATION = /^no-(.+)$/;
327
+ function booleanNameOf(name, spec) {
328
+ if (spec[name] === "boolean") return {
329
+ name,
330
+ value: true
331
+ };
332
+ const negated = NEGATION.exec(name)?.[1];
333
+ if (negated !== void 0 && spec[negated] === "boolean") return {
334
+ name: negated,
335
+ value: false
336
+ };
337
+ }
338
+ /** `--flag value`, `--flag=value`, `--flag` / `--no-flag` for booleans, `--` ends the flags. */
339
+ function scanArgs(argv, spec) {
340
+ const positionals = [];
341
+ const flags = /* @__PURE__ */ new Map();
342
+ for (let index = 0; index < argv.length; index++) {
343
+ const token = argv[index];
344
+ if (token === "--") {
345
+ positionals.push(...argv.slice(index + 1));
346
+ break;
347
+ }
348
+ if (!token.startsWith("--")) {
349
+ positionals.push(token);
350
+ continue;
351
+ }
352
+ const [rawName, inline] = splitInline(token.slice(2));
353
+ const boolean = booleanNameOf(rawName, spec);
354
+ if (boolean && inline === void 0) {
355
+ flags.set(boolean.name, boolean.value);
356
+ continue;
357
+ }
358
+ if (spec[rawName] !== "value") throw usageError(`Unknown flag: --${rawName}`);
359
+ const value = inline ?? argv[++index];
360
+ if (value === void 0) throw usageError(`Flag --${rawName} needs a value`);
361
+ flags.set(rawName, value);
362
+ }
363
+ return {
364
+ positionals,
365
+ flags
366
+ };
367
+ }
368
+ function splitInline(text) {
369
+ const at = text.indexOf("=");
370
+ return at < 0 ? [text, void 0] : [text.slice(0, at), text.slice(at + 1)];
371
+ }
372
+ const NONE = "none";
373
+ function parseModelRef(raw) {
374
+ const slash = raw.indexOf("/");
375
+ if (slash <= 0 || slash === raw.length - 1) throw usageError(`Expected provider/modelId, got: ${raw}`);
376
+ return {
377
+ provider: raw.slice(0, slash),
378
+ modelId: raw.slice(slash + 1)
379
+ };
380
+ }
381
+ function coerce(raw, kind, nullable) {
382
+ if (raw === NONE && nullable) return null;
383
+ if (typeof raw === "boolean") return kind === "booleanString" ? String(raw) : raw;
384
+ if (kind === "model") return parseModelRef(raw);
385
+ if (kind === "number") {
386
+ const value = Number(raw);
387
+ if (Number.isNaN(value)) throw usageError(`Expected a number, got: ${raw}`);
388
+ return value;
389
+ }
390
+ return raw;
391
+ }
392
+ function setPath(target, path, value) {
393
+ target[path.slice(path.indexOf(".") + 1)] = value;
394
+ }
395
+ function fieldAt(def, path) {
396
+ return routeFields(def.route).find((field) => field.path === path);
397
+ }
398
+ function targetOf(sources, path) {
399
+ if (path.startsWith("params.")) return sources.params;
400
+ return path.startsWith("query.") ? sources.query : sources.body;
401
+ }
402
+ function assignPositionals(def, values, sources) {
403
+ if (values.length < def.positionals.length) {
404
+ const missing = def.positionals.slice(values.length).map((path) => `<${path.split(".").pop()}>`);
405
+ throw usageError(`Missing ${missing.join(" ")}. Usage: tau ${def.command} ${usageOf(def)}`);
406
+ }
407
+ if (values.length > def.positionals.length) throw usageError(`Unexpected argument: ${values[def.positionals.length]}`);
408
+ let wholeBody;
409
+ def.positionals.forEach((path, index) => {
410
+ const raw = values[index];
411
+ if (path === "body") {
412
+ wholeBody = parseModelRef(raw);
413
+ return;
414
+ }
415
+ const field = fieldAt(def, path);
416
+ const kind = def.modelRefs?.includes(path) ? "model" : field ? valueKindOf(field) : "string";
417
+ setPath(targetOf(sources, path), path, coerce(raw, kind, field?.nullable ?? false));
418
+ });
419
+ return wholeBody;
420
+ }
421
+ function assignFlags(flags, given, sources) {
422
+ for (const flag of flags) {
423
+ const raw = given.get(flag.name);
424
+ if (raw === void 0 || !flag.field || flag.valueKind === "output") continue;
425
+ setPath(targetOf(sources, flag.field.path), flag.field.path, coerce(raw, flag.valueKind, flag.field.nullable));
426
+ }
427
+ }
428
+ function applyFixed(def, sources) {
429
+ for (const [path, value] of Object.entries(def.fixed ?? {})) setPath(targetOf(sources, path), path, value);
430
+ }
431
+ function defaultRequiredBooleans(def, sources) {
432
+ for (const field of routeFields(def.route)) {
433
+ const target = targetOf(sources, field.path);
434
+ if (field.source === "body" && field.required && field.boolean && target[field.name] === void 0) target[field.name] = false;
435
+ }
436
+ }
437
+ function fillAutoFields(def, body) {
438
+ for (const field of routeFields(def.route)) if (field.source === "body" && AUTO_FIELDS.includes(field.name) && body[field.name] === void 0) body[field.name] = randomUUID();
439
+ }
440
+ /** `tau mcp save docs --json '{…}'`: the body repeats the `name` the positional already gave. */
441
+ function fillFromParams(def, params, body) {
442
+ const bodyNames = routeFields(def.route).filter((field) => field.source === "body").map((field) => field.name);
443
+ for (const [name, value] of Object.entries(params)) if (bodyNames.includes(name) && body[name] === void 0) body[name] = value;
444
+ }
445
+ function parseJsonBody(raw) {
446
+ try {
447
+ const value = JSON.parse(raw);
448
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) return value;
449
+ } catch {}
450
+ throw usageError("--json needs a JSON object");
451
+ }
452
+ function checkSchema(schema, value, label) {
453
+ if (!schema) return;
454
+ const first = Array.from(Value.Errors(schema, value))[0];
455
+ if (!first) return;
456
+ const where = first.instancePath ? `${label}${first.instancePath.replaceAll("/", ".")}` : label;
457
+ throw usageError(`Invalid ${where}: ${first.message}`);
458
+ }
459
+ function validate(def, input) {
460
+ const route = routes[def.route];
461
+ checkSchema(route.params, input.params, "params");
462
+ checkSchema(route.query, input.query, "query");
463
+ checkSchema(route.body, input.body, "body");
464
+ }
465
+ function parseTimeout(raw) {
466
+ if (raw === void 0) return 600;
467
+ const seconds = Number(raw);
468
+ if (typeof raw === "boolean" || !(seconds > 0)) throw usageError("--timeout needs a number of seconds");
469
+ return seconds;
470
+ }
471
+ function bodyOf(def, sources, whole, json) {
472
+ if (!(routes[def.route].body !== void 0)) return void 0;
473
+ if (typeof json === "string") {
474
+ const body = parseJsonBody(json);
475
+ fillAutoFields(def, body);
476
+ fillFromParams(def, sources.params, body);
477
+ return body;
478
+ }
479
+ if (def.json) throw usageError(`tau ${def.command} needs --json '<body>'`);
480
+ if (whole !== void 0) return whole;
481
+ fillAutoFields(def, sources.body);
482
+ return sources.body;
483
+ }
484
+ const stringsOf = (record) => Object.fromEntries(Object.entries(record).map(([key, value]) => [key, String(value)]));
485
+ /** Usage text for the positionals, for example `<id> <text>`. */
486
+ function usageOf(def) {
487
+ return def.positionals.map((path) => `<${path === "body" ? "model" : path.split(".").pop()}>`).join(" ");
488
+ }
489
+ function findCommand(noun, verb) {
490
+ const def = verb === void 0 ? void 0 : commandOf(`${noun} ${verb}`);
491
+ if (def) return def;
492
+ const known = commands.filter((entry) => entry.command.startsWith(`${noun} `)).map((entry) => entry.command.slice(noun.length + 1));
493
+ throw usageError(`Unknown command: ${noun} ${verb ?? ""}. Try: ${known.join(", ")}`);
494
+ }
495
+ /** Argv after the verb, the table and the schemas give the route input. Throws a usage error. */
496
+ function parseCommand(def, argv) {
497
+ const flags = commandFlags(def);
498
+ const { positionals, flags: given } = scanArgs(argv, flagSpecOf(flags));
499
+ const sources = {
500
+ params: {},
501
+ query: {},
502
+ body: {}
503
+ };
504
+ const whole = assignPositionals(def, positionals, sources);
505
+ assignFlags(flags, given, sources);
506
+ applyFixed(def, sources);
507
+ defaultRequiredBooleans(def, sources);
508
+ const input = {
509
+ params: stringsOf(sources.params),
510
+ query: stringsOf(sources.query),
511
+ body: bodyOf(def, sources, whole, given.get("json"))
512
+ };
513
+ validate(def, input);
514
+ return {
515
+ def,
516
+ input,
517
+ yes: given.get("yes") === true,
518
+ wait: given.get("wait") === true,
519
+ timeoutSeconds: parseTimeout(given.get("timeout")),
520
+ home: typeof given.get("home") === "string" ? given.get("home") : void 0
521
+ };
522
+ }
523
+ //#endregion
524
+ //#region src/commands/help.ts
525
+ const THREAD_EXTRAS = [" wait <id> Wait until the thread is done, then print the answer (--timeout <s>)", " read <id> Print the thread as text (--last <n>)"];
526
+ const LIFECYCLE = [
527
+ " start, stop, restart, status, open, logs, service Run and manage the server",
528
+ " serve Run the server in the foreground",
529
+ " skill Print the tau skill for agents"
530
+ ];
531
+ const pad = (text) => text.padEnd(26);
532
+ function rootHelp() {
533
+ return [
534
+ "tau: chat with a coding agent from the browser, and control it from the shell.",
535
+ "",
536
+ "Usage: tau <command> [args] (no command starts the server and opens the browser)",
537
+ "",
538
+ "Server:",
539
+ ...LIFECYCLE,
540
+ "",
541
+ "API commands (output is JSON):",
542
+ ...COMMAND_NOUNS.map((noun) => ` ${noun}`),
543
+ ...FLOW_COMMANDS.map((word) => ` ${word}`),
544
+ "",
545
+ "Run `tau <noun> --help` for the commands of a noun, `tau <noun> <verb> --help` for one command."
546
+ ].join("\n");
547
+ }
548
+ function nounHelp(noun) {
549
+ const lines = commands.filter((entry) => entry.command.startsWith(`${noun} `)).map((entry) => ` ${pad(`${entry.command.slice(noun.length + 1)} ${usageOf(entry)}`)}${entry.summary}`);
550
+ const extras = noun === "threads" ? THREAD_EXTRAS : [];
551
+ return [
552
+ `tau ${noun} <command>`,
553
+ "",
554
+ ...lines,
555
+ ...extras
556
+ ].join("\n");
557
+ }
558
+ function commandHelp(command) {
559
+ const def = commandOf(command);
560
+ if (!def) throw usageError(`Unknown command: ${command}`);
561
+ const flags = commandFlags(def).map((flag) => {
562
+ const value = flag.kind === "value" ? " <value>" : "";
563
+ return ` ${pad(`--${flag.name}${value}`)}${flag.summary}`;
564
+ });
565
+ const destructive = def.destructive ? ["", "Destructive: needs --yes."] : [];
566
+ return [
567
+ `tau ${def.command} ${usageOf(def)}`,
568
+ def.summary,
569
+ "",
570
+ "Flags:",
571
+ ...flags,
572
+ ...destructive
573
+ ].join("\n");
574
+ }
575
+ /** Nouns, the commands of a noun, or one command's positionals and flags. */
576
+ function helpText(noun, verb) {
577
+ if (noun === void 0) return rootHelp();
578
+ if (!COMMAND_NOUNS.includes(noun)) throw usageError(`Unknown command: ${noun}`);
579
+ if (verb === void 0) return nounHelp(noun);
580
+ if (noun === "threads" && (verb === "wait" || verb === "read")) return nounHelp(noun);
581
+ return commandHelp(`${noun} ${verb}`);
582
+ }
583
+ /** `tau help [noun [verb]]` and `tau --help`. */
584
+ async function runHelp(argv) {
585
+ const words = argv.filter((word) => !word.startsWith("-"));
586
+ console.log(helpText(words[0], words[1]));
587
+ }
588
+ //#endregion
589
+ //#region src/commands/own-thread.ts
590
+ const GUARDED_COMMANDS = ["threads delete", "threads archive"];
591
+ function lineageOf(all, ownId) {
592
+ const lineage = /* @__PURE__ */ new Set();
593
+ let current = ownId;
594
+ while (current && !lineage.has(current)) {
595
+ lineage.add(current);
596
+ current = all.find((item) => item.id === current)?.parentId;
597
+ }
598
+ return lineage;
599
+ }
600
+ /** An agent must not delete or archive the thread it runs in, or a thread above it. */
601
+ async function assertNotOwnLineage(server, env, command, threadId) {
602
+ const ownId = env.env.TAU_THREAD_ID;
603
+ if (!ownId) return;
604
+ if (lineageOf(await callRoute(server, "listAllConversations", {}, env), ownId).has(threadId)) throw failure(`Refusing \`tau ${command}\`: ${threadId} is your own thread or one of its parents.`);
605
+ }
606
+ //#endregion
607
+ //#region src/commands/live.ts
608
+ const websocketUrlOf = (server) => `${server.url.replace(/^http/, "ws")}${LIVE_PATH}`;
609
+ var NodeLiveClient = class {
610
+ socket;
611
+ views = /* @__PURE__ */ new Map();
612
+ frameListeners = [];
613
+ closeListeners = [];
614
+ pending = [];
615
+ open = false;
616
+ constructor(server, env) {
617
+ this.socket = env.openSocket(websocketUrlOf(server), { authorization: `Bearer ${server.token}` });
618
+ this.socket.onopen = () => {
619
+ this.open = true;
620
+ for (const frame of this.pending.splice(0)) this.socket.send(frame);
621
+ };
622
+ this.socket.onmessage = (event) => this.receive(String(event.data));
623
+ this.socket.onclose = () => {
624
+ for (const listener of this.closeListeners) listener();
625
+ };
626
+ this.socket.onerror = () => void 0;
627
+ }
628
+ receive(raw) {
629
+ const frame = decodeServerFrame(raw);
630
+ if (!frame) return;
631
+ if (frame.type === "ops") try {
632
+ this.views.set(frame.conversationId, nextViewState(this.views.get(frame.conversationId), frame.ops));
633
+ } catch {
634
+ return;
635
+ }
636
+ for (const listener of this.frameListeners) listener();
637
+ }
638
+ subscribe(conversationId) {
639
+ const frame = encodeFrame({
640
+ type: "subscribe",
641
+ conversationId
642
+ });
643
+ if (this.open) this.socket.send(frame);
644
+ else this.pending.push(frame);
645
+ }
646
+ view(conversationId) {
647
+ return this.views.get(conversationId);
648
+ }
649
+ onFrame(listener) {
650
+ this.frameListeners.push(listener);
651
+ }
652
+ onClose(listener) {
653
+ this.closeListeners.push(listener);
654
+ }
655
+ close() {
656
+ this.socket.close();
657
+ }
658
+ };
659
+ /** A Node WebSocket client for `subscribe`, `ops` and `changed` frames, with Bearer auth on upgrade. */
660
+ const connectLive = (server, env) => new NodeLiveClient(server, env);
661
+ //#endregion
662
+ //#region src/commands/read.ts
663
+ const READ_TIMEOUT_MS = 1e4;
664
+ /** The first view frame of a thread, as text. `last` keeps the final N messages. */
665
+ function readThread(server, env, conversationId, last) {
666
+ const live = connectLive(server, env);
667
+ live.subscribe(conversationId);
668
+ return new Promise((resolve, reject) => {
669
+ const cancel = env.after(READ_TIMEOUT_MS, () => end(() => reject(failure("Timed out reading the thread."))));
670
+ const end = (settle) => {
671
+ cancel();
672
+ live.close();
673
+ settle();
674
+ };
675
+ live.onClose(() => end(() => reject(failure("Lost the connection to tau."))));
676
+ live.onFrame(() => {
677
+ const view = live.view(conversationId);
678
+ if (view === void 0) return;
679
+ if (view === null) return end(() => reject(failure(`not_found: Unknown conversation: ${conversationId}`)));
680
+ end(() => resolve(viewToText(view, { last })));
681
+ });
682
+ });
683
+ }
684
+ //#endregion
685
+ //#region src/commands/wait.ts
686
+ const descendantsOf = (all, id) => {
687
+ const children = all.filter((item) => item.parentId === id);
688
+ return [...children, ...children.flatMap((child) => descendantsOf(all, child.id))];
689
+ };
690
+ const runEndedAfter = (summary, endedAfter) => {
691
+ if (endedAfter === void 0) return true;
692
+ if (summary.lastRunEndedAt === null) return false;
693
+ return endedAfter === null || summary.lastRunEndedAt > endedAfter;
694
+ };
695
+ function errorMessageOf(view) {
696
+ for (const entry of [...view.entries].reverse()) {
697
+ const message = entry.model?.[0];
698
+ if (message?.role === "assistant" && message.errorMessage) return message.errorMessage;
699
+ }
700
+ return "the thread ended in error";
701
+ }
702
+ /** The 4-condition rule: idle, a run ended after R0, an empty queue, and no running child. */
703
+ function assessThread(all, conversationId, view, endedAfter) {
704
+ const summary = all.find((item) => item.id === conversationId);
705
+ if (!summary) throw failure(`not_found: Unknown conversation: ${conversationId}`);
706
+ if (summary.status === "error" && runEndedAfter(summary, endedAfter)) return {
707
+ state: "error",
708
+ message: errorMessageOf(view)
709
+ };
710
+ const idle = summary.status === "idle";
711
+ const live = readLive(view);
712
+ const queueEmpty = live.queued.length === 0;
713
+ const viewCaughtUp = !live.busy;
714
+ const childRunning = descendantsOf(all, conversationId).some((child) => child.agentStatus === "running");
715
+ return idle && runEndedAfter(summary, endedAfter) && queueEmpty && viewCaughtUp && !childRunning ? {
716
+ state: "done",
717
+ answer: lastAssistantText(view)
718
+ } : { state: "waiting" };
719
+ }
720
+ const listAll = async (server, env) => await callRoute(server, "listAllConversations", {}, env);
721
+ /** Resolves with the last assistant answer. Exits 1 on an error thread, 3 on timeout. */
722
+ function waitForThread(server, env, request) {
723
+ const live = connectLive(server, env);
724
+ live.subscribe(request.conversationId);
725
+ return new Promise((resolve, reject) => {
726
+ let finished = false;
727
+ let evaluating = false;
728
+ let again = false;
729
+ const cancels = [];
730
+ const finish = (settle) => {
731
+ if (finished) return;
732
+ finished = true;
733
+ for (const cancel of cancels) cancel();
734
+ live.close();
735
+ settle();
736
+ };
737
+ const check = async () => {
738
+ const view = live.view(request.conversationId);
739
+ if (view === void 0) return;
740
+ if (view === null) throw failure(`not_found: Unknown conversation: ${request.conversationId}`);
741
+ const result = assessThread(await listAll(server, env), request.conversationId, view, request.endedAfter);
742
+ if (result.state === "done") finish(() => resolve(result.answer));
743
+ if (result.state === "error") finish(() => reject(failure(result.message)));
744
+ };
745
+ const evaluate = async () => {
746
+ if (finished) return;
747
+ if (evaluating) {
748
+ again = true;
749
+ return;
750
+ }
751
+ evaluating = true;
752
+ try {
753
+ do {
754
+ again = false;
755
+ await check();
756
+ } while (again && !finished);
757
+ } catch (error) {
758
+ finish(() => reject(error));
759
+ }
760
+ evaluating = false;
761
+ };
762
+ const poll = () => {
763
+ cancels.push(env.after(env.pollMs, () => {
764
+ evaluate();
765
+ if (!finished) poll();
766
+ }));
767
+ };
768
+ live.onFrame(() => void evaluate());
769
+ live.onClose(() => finish(() => reject(failure("Lost the connection to tau."))));
770
+ cancels.push(env.after(request.timeoutSeconds * 1e3, () => finish(() => reject(new CliExit(3, `Timed out after ${request.timeoutSeconds}s`)))));
771
+ poll();
772
+ });
773
+ }
774
+ /** `lastRunEndedAt` of a thread now (R0), or `null` when it has never run. */
775
+ async function runEndedAtOf(server, env, conversationId) {
776
+ const found = (await listAll(server, env)).find((item) => item.id === conversationId);
777
+ if (!found) throw failure(`not_found: Unknown conversation: ${conversationId}`);
778
+ return found.lastRunEndedAt;
779
+ }
780
+ //#endregion
781
+ //#region src/commands/thread-extras.ts
782
+ const EXTRA_THREAD_VERBS = ["wait", "read"];
783
+ function positiveNumber(raw, flag) {
784
+ if (raw === void 0) return void 0;
785
+ const value = Number(raw);
786
+ if (typeof raw === "boolean" || !(value > 0)) throw usageError(`--${flag} needs a positive number`);
787
+ return value;
788
+ }
789
+ function threadIdOf(verb, positionals) {
790
+ if (positionals.length !== 1) throw usageError(`Usage: tau threads ${verb} <id>`);
791
+ return positionals[0];
792
+ }
793
+ /** `tau threads wait <id> [--timeout s]` and `tau threads read <id> [--last N]`. They are not routes. */
794
+ async function runThreadExtra(verb, argv, env) {
795
+ const { positionals, flags } = scanArgs(argv, verb === "wait" ? {
796
+ timeout: "value",
797
+ home: "value"
798
+ } : {
799
+ last: "value",
800
+ home: "value"
801
+ });
802
+ const id = threadIdOf(verb, positionals);
803
+ const home = flags.get("home");
804
+ const scoped = typeof home === "string" ? {
805
+ ...env,
806
+ home
807
+ } : env;
808
+ const server = await discoverServer(scoped);
809
+ if (verb === "read") {
810
+ const last = positiveNumber(flags.get("last"), "last");
811
+ env.stdout(await readThread(server, scoped, id, last));
812
+ return;
813
+ }
814
+ const timeoutSeconds = positiveNumber(flags.get("timeout"), "timeout") ?? 600;
815
+ env.stdout(await waitForThread(server, scoped, {
816
+ conversationId: id,
817
+ endedAfter: void 0,
818
+ timeoutSeconds
819
+ }));
820
+ }
821
+ //#endregion
822
+ //#region src/commands/run-command.ts
823
+ const wantsHelp$2 = (argv) => argv.includes("--help") || argv.includes("-h");
824
+ const printJson = (env, result) => env.stdout(JSON.stringify(result === void 0 ? { ok: true } : result, null, 2));
825
+ async function guardDestructive(server, env, parsed) {
826
+ const id = parsed.input.params.id;
827
+ if (id !== void 0 && GUARDED_COMMANDS.includes(parsed.def.command)) await assertNotOwnLineage(server, env, parsed.def.command, id);
828
+ }
829
+ async function runWithWait(server, env, parsed) {
830
+ const isStart = parsed.def.command === "threads start";
831
+ const knownId = parsed.input.params.id;
832
+ const endedAfter = parsed.wait && !isStart && knownId !== void 0 ? await runEndedAtOf(server, env, knownId) : null;
833
+ const result = await callRoute(server, parsed.def.route, parsed.input, env);
834
+ printJson(env, result);
835
+ if (!parsed.wait) return;
836
+ const answer = await waitForThread(server, env, {
837
+ conversationId: knownId ?? result.id,
838
+ endedAfter,
839
+ timeoutSeconds: parsed.timeoutSeconds
840
+ });
841
+ env.stdout(`\n${answer}`);
842
+ }
843
+ /** Runs one API command and throws `CliExit` on any failure. Testable with an injected env. */
844
+ async function executeApiCommand(noun, argv, env) {
845
+ const verb = argv[0]?.startsWith("-") ? void 0 : argv[0];
846
+ if (verb === void 0 || wantsHelp$2(argv)) return env.stdout(helpText(noun, verb));
847
+ if (noun === "threads" && EXTRA_THREAD_VERBS.includes(verb)) return runThreadExtra(verb, argv.slice(1), env);
848
+ const parsed = parseCommand(findCommand(noun, verb), argv.slice(1));
849
+ if (parsed.def.destructive && !parsed.yes) throw usageError(`tau ${parsed.def.command} is destructive. Add --yes to confirm.`);
850
+ const scoped = parsed.home === void 0 ? env : {
851
+ ...env,
852
+ home: parsed.home
853
+ };
854
+ const server = await discoverServer(scoped);
855
+ await guardDestructive(server, scoped, parsed);
856
+ await runWithWait(server, scoped, parsed);
857
+ }
858
+ /** `--home` may come before the verb (`tau --home X threads list`); it is lifted out of argv. */
859
+ function takeHomeFlag(argv) {
860
+ const rest = [];
861
+ let home;
862
+ for (let index = 0; index < argv.length; index++) {
863
+ const token = argv[index];
864
+ if (token === "--") {
865
+ rest.push(...argv.slice(index));
866
+ break;
867
+ }
868
+ if (token === "--home" && argv[index + 1] !== void 0) home = argv[++index];
869
+ else if (token.startsWith("--home=")) home = token.slice(7);
870
+ else rest.push(token);
871
+ }
872
+ return {
873
+ home,
874
+ rest
875
+ };
876
+ }
877
+ /** The `tau <noun> …` entry point. `argv` starts after the noun. */
878
+ async function runApiCommand(noun, argv) {
879
+ const env = processEnv();
880
+ try {
881
+ const { home, rest } = takeHomeFlag(argv);
882
+ await executeApiCommand(noun, rest, home === void 0 ? env : {
883
+ ...env,
884
+ home
885
+ });
886
+ } catch (error) {
887
+ if (!(error instanceof CliExit)) throw error;
888
+ env.stderr(error.message);
889
+ process.exitCode = error.exitCode;
890
+ }
891
+ }
892
+ //#endregion
893
+ //#region src/commands/connect-command.ts
894
+ const REDEEM_PATH = "/api/pair/redeem";
895
+ async function runningServer(env) {
896
+ try {
897
+ return await discoverServer(env);
898
+ } catch {
899
+ return;
900
+ }
901
+ }
902
+ async function linkedMachineCount(server, env) {
903
+ if (server) return (await callRoute(server, "listMachines", {}, env)).length;
904
+ try {
905
+ return (await readdir(join(env.home, "machines"))).length;
906
+ } catch {
907
+ return 0;
908
+ }
909
+ }
910
+ async function redeem(hubUrl, code, deviceName, env) {
911
+ let response;
912
+ try {
913
+ response = await env.fetch(`${hubUrl}${REDEEM_PATH}`, {
914
+ method: "POST",
915
+ headers: {
916
+ "content-type": "application/json",
917
+ origin: hubUrl
918
+ },
919
+ body: JSON.stringify({
920
+ code,
921
+ deviceName
922
+ })
923
+ });
924
+ } catch {
925
+ throw failure(`Could not reach the hub at ${hubUrl}. Is it running and online?`);
926
+ }
927
+ if (!response.ok) throw failure(response.status === 429 ? "Too many tries. Wait a few minutes and make a new code on the hub." : "The hub did not accept that code. It may have expired: make a new one in Settings → Machines.");
928
+ const body = await response.json().catch(() => void 0);
929
+ if (!Value.Check(RedeemPairingResponseSchema, body) || body.secret === void 0) throw failure("The hub answered without a machine secret. Make a new link code (kind: machine).");
930
+ return body.secret;
931
+ }
932
+ async function startAfterLink(choice, env) {
933
+ if (choice === "serve") {
934
+ const { serveInForeground } = await import("./serve-BcVZj5e2.js");
935
+ env.stdout("Starting tau for this session. Press Ctrl+C to stop.");
936
+ await serveInForeground([
937
+ "--no-open",
938
+ "--home",
939
+ env.home
940
+ ]);
941
+ } else if (choice === "service") {
942
+ const { runServiceCommand } = await import("./service-command-BVr33vlP.js");
943
+ await runServiceCommand([
944
+ "install",
945
+ "--home",
946
+ env.home
947
+ ]);
948
+ } else env.stdout("Run `tau` (or `tau service install`) to bring this machine online.");
949
+ }
950
+ /**
951
+ * `tau connect <link | code --hub <url>> [--name <label>] [--yes]`: redeem a link code on a hub and keep its
952
+ * machine secret in `hub.json`. A running server picks the file up on its own.
953
+ */
954
+ async function executeConnect(argv, env, io = terminalIo) {
955
+ const args = parseConnectArgs(argv);
956
+ const target = parseConnectTarget(args.target, args.hub);
957
+ const hubFile = createHubFileStore(env.home);
958
+ const refusal = alreadyLinkedRefusal(await hubFile.read() !== void 0);
959
+ if (refusal) throw usageError(refusal);
960
+ const server = await runningServer(env);
961
+ const loop = connectRefusal(await linkedMachineCount(server, env));
962
+ if (loop) throw usageError(loop);
963
+ const hubName = target.hubName ?? new URL(target.hubUrl).host;
964
+ if (!args.yes && !await io.confirm(`Link this machine to ${hubName} (${target.hubUrl})?`)) throw new CliExit(1, "Not linked.");
965
+ const machineSecret = await redeem(target.hubUrl, target.code, args.name, env);
966
+ await hubFile.write({
967
+ hubUrl: target.hubUrl,
968
+ machineSecret,
969
+ name: hubName,
970
+ machineName: args.name
971
+ });
972
+ env.stdout(`Linked to ${hubName} as "${args.name}".`);
973
+ if (server) return env.stdout("The running tau connects within a few seconds.");
974
+ await startAfterLink(args.yes ? "none" : await io.chooseStart(), env);
975
+ }
976
+ /** `tau disconnect`: tells the running server (it closes the link and forgets the hub), then removes `hub.json`. */
977
+ async function executeDisconnect(argv, env) {
978
+ if (argv.length > 0) throw usageError("Usage: tau disconnect");
979
+ const hubFile = createHubFileStore(env.home);
980
+ const file = await hubFile.read();
981
+ const server = await runningServer(env);
982
+ if (!file && !server) return env.stdout("This tau is not linked to a hub.");
983
+ if (server) {
984
+ if ((await callRoute(server, "getHubLink", {}, env)).state !== "none") await callRoute(server, "disconnectHub", {}, env);
985
+ }
986
+ await hubFile.delete();
987
+ if (!file) return env.stdout("This tau is not linked to a hub.");
988
+ env.stdout(`Disconnected from ${file.name}. Remove this machine in the hub's Settings → Machines to revoke it there.`);
989
+ }
990
+ function runner(execute) {
991
+ return async (argv) => {
992
+ const env = processEnv();
993
+ try {
994
+ const { home, rest } = takeHomeFlag(argv);
995
+ await execute(rest, home === void 0 ? env : {
996
+ ...env,
997
+ home
998
+ });
999
+ } catch (error) {
1000
+ if (!(error instanceof CliExit)) throw error;
1001
+ env.stderr(error.message);
1002
+ process.exitCode = error.exitCode;
1003
+ }
1004
+ };
1005
+ }
1006
+ const runConnect = runner(executeConnect);
1007
+ const runDisconnect = runner(executeDisconnect);
1008
+ //#endregion
1009
+ //#region src/commands/pair-flow.ts
1010
+ const REMOTE_WAIT_MS = 72e4;
1011
+ const LINK_POLL_MS = 2e3;
1012
+ const REMOTE_POLL_MS = 3e3;
1013
+ const PROGRESS_EVERY_MS = 15e3;
1014
+ const FULL_POWER_WARNING = "A linked device gets full power over this computer: agents, terminals and your files. Share this only with yourself.";
1015
+ function parsePairArgs(argv) {
1016
+ let name;
1017
+ for (let index = 0; index < argv.length; index++) {
1018
+ const token = argv[index];
1019
+ if (token === "--name" && argv[index + 1] !== void 0) name = argv[++index];
1020
+ else if (token.startsWith("--name=")) name = token.slice(7);
1021
+ else throw usageError(`Unknown argument "${token}". Usage: tau pair [--name <label>]`);
1022
+ }
1023
+ return { name: name?.trim() || hostname() };
1024
+ }
1025
+ const groupShortCode = (code) => code.match(/.{1,4}/g)?.join(" ") ?? code;
1026
+ const remoteOf = (server, env) => callRoute(server, "getRemoteStatus", {}, env);
1027
+ async function waitUntilOnline(server, env, first) {
1028
+ const deadline = env.now() + REMOTE_WAIT_MS;
1029
+ let status = first;
1030
+ let lastProgress = env.now();
1031
+ let lastMessage = null;
1032
+ while (status.state !== "online") {
1033
+ if (status.state === "error" || status.state === "off") throw new CliExit(1, `Remote access did not start: ${status.message ?? status.state}`);
1034
+ if (env.now() >= deadline) throw new CliExit(3, "Remote access is still starting. It keeps going in the server: run `tau pair` again in a few minutes.");
1035
+ if (status.message && status.message !== lastMessage) {
1036
+ lastMessage = status.message;
1037
+ env.stdout(status.message);
1038
+ } else if (env.now() - lastProgress >= PROGRESS_EVERY_MS) {
1039
+ lastProgress = env.now();
1040
+ env.stdout(`Still ${status.state}… (this takes a few minutes)`);
1041
+ }
1042
+ await env.sleep(REMOTE_POLL_MS);
1043
+ status = await remoteOf(server, env);
1044
+ }
1045
+ return status;
1046
+ }
1047
+ /** Turns remote on when needed and waits until the tunnel is online. */
1048
+ async function ensureRemoteOnline(server, env) {
1049
+ let status = await remoteOf(server, env);
1050
+ if (status.state === "online") return;
1051
+ if (status.state === "off") {
1052
+ env.stdout("Remote access is off. Turning it on: the first time takes a few minutes, please wait.");
1053
+ status = await callRoute(server, "enableRemote", {}, env);
1054
+ } else env.stdout("Remote access is starting: this takes a few minutes.");
1055
+ await waitUntilOnline(server, env, status);
1056
+ }
1057
+ async function printPairing(env, pairing, link) {
1058
+ const qr = await QRCode.toString(link, {
1059
+ type: "terminal",
1060
+ small: true
1061
+ });
1062
+ env.stdout(`\n${qr}`);
1063
+ env.stdout(`Open this link on the device:\n ${link}\n`);
1064
+ env.stdout(`Or open the same site and type this code: ${groupShortCode(pairing.shortCode)}\n`);
1065
+ env.stdout(FULL_POWER_WARNING);
1066
+ const minutes = Math.max(1, Math.round((pairing.expiresAt - env.now()) / 6e4));
1067
+ env.stdout(`The code works once and expires in ${minutes} minutes. Waiting for the device… (Ctrl+C to stop)`);
1068
+ }
1069
+ const listDevices = (server, env) => callRoute(server, "listDevices", {}, env);
1070
+ async function waitForLinkedDevice(server, env, knownIds, expiresAt) {
1071
+ while (env.now() < expiresAt) {
1072
+ await env.sleep(LINK_POLL_MS);
1073
+ const fresh = (await listDevices(server, env)).find((device) => !knownIds.has(device.id));
1074
+ if (fresh) return env.stdout(`Linked: ${fresh.name}`);
1075
+ }
1076
+ throw new CliExit(3, "The code expired before a device linked. Run `tau pair` again for a new one.");
1077
+ }
1078
+ async function runPairFlow(argv, env) {
1079
+ const { name } = parsePairArgs(argv);
1080
+ const server = await discoverServer(env);
1081
+ await ensureRemoteOnline(server, env);
1082
+ const known = new Set((await listDevices(server, env)).map((device) => device.id));
1083
+ const pairing = await callRoute(server, "mintPairing", { query: {
1084
+ kind: "device",
1085
+ name
1086
+ } }, env);
1087
+ if (pairing.link === null) throw new CliExit(1, "Remote access has no URL yet. Try again in a moment.");
1088
+ await printPairing(env, pairing, pairing.link);
1089
+ await waitForLinkedDevice(server, env, known, pairing.expiresAt);
1090
+ }
1091
+ //#endregion
1092
+ //#region src/commands/pair-command.ts
1093
+ const wantsHelp$1 = (argv) => argv.includes("--help") || argv.includes("-h");
1094
+ /** `tau pair new …` and help stay with the generic command table. */
1095
+ const isTableVerb = (argv) => argv[0] === "new" || wantsHelp$1(argv);
1096
+ /** `tau pair [--name <label>]`: link a phone or browser with a link, a short code and a QR. */
1097
+ async function executePair(argv, env) {
1098
+ if (isTableVerb(argv)) return executeApiCommand("pair", argv, env);
1099
+ await runPairFlow(argv, env);
1100
+ }
1101
+ async function runPair(argv) {
1102
+ const env = processEnv();
1103
+ try {
1104
+ const { home, rest } = takeHomeFlag(argv);
1105
+ await executePair(rest, home === void 0 ? env : {
1106
+ ...env,
1107
+ home
1108
+ });
1109
+ } catch (error) {
1110
+ if (!(error instanceof CliExit)) throw error;
1111
+ env.stderr(error.message);
1112
+ process.exitCode = error.exitCode;
1113
+ }
1114
+ }
1115
+ //#endregion
1116
+ //#region src/commands/skill.ts
1117
+ const here = (relative) => fileURLToPath(new URL(relative, import.meta.url));
1118
+ /** Built package (`dist/skills/builtin/tau` next to `dist/bin.js`), then the core source tree. */
1119
+ const SKILL_PATHS = [
1120
+ here("./skills/builtin/tau/SKILL.md"),
1121
+ here("../../core/src/skills/builtin/tau/SKILL.md"),
1122
+ here("../../../core/src/skills/builtin/tau/SKILL.md")
1123
+ ];
1124
+ const MISSING_MESSAGE = "The tau skill is not bundled in this build. Run `tau --help`: output is JSON, and `--wait` replaces polling.";
1125
+ async function firstReadable(paths) {
1126
+ for (const path of paths) try {
1127
+ return await readFile(path, "utf8");
1128
+ } catch {}
1129
+ }
1130
+ async function printSkill(env) {
1131
+ const paths = env.skillPaths.length > 0 ? env.skillPaths : SKILL_PATHS;
1132
+ env.stdout(await firstReadable(paths) ?? MISSING_MESSAGE);
1133
+ }
1134
+ /** `tau skill`: print the bundled SKILL.md. */
1135
+ async function runSkill(_argv) {
1136
+ await printSkill(processEnv());
1137
+ }
1138
+ //#endregion
1139
+ //#region src/lifecycle/status-extras.ts
1140
+ const NO_EXTRAS = {
1141
+ uptimeSeconds: null,
1142
+ lastExit: null,
1143
+ tunnel: null
1144
+ };
1145
+ const execFileAsync = promisify(execFile);
1146
+ /** `[[dd-]hh:]mm:ss` as printed by `ps -o etime=`. */
1147
+ function parseElapsed(text) {
1148
+ const match = /^(?:(?:(\d+)-)?(\d+):)?(\d+):(\d+)$/.exec(text.trim());
1149
+ if (!match) return null;
1150
+ const [, days, hours, minutes, seconds] = match.map((part) => Number(part ?? 0));
1151
+ return ((days ?? 0) * 24 + (hours ?? 0)) * 3600 + (minutes ?? 0) * 60 + (seconds ?? 0);
1152
+ }
1153
+ async function uptimeOf(pid) {
1154
+ try {
1155
+ const { stdout } = await execFileAsync("ps", [
1156
+ "-o",
1157
+ "etime=",
1158
+ "-p",
1159
+ String(pid)
1160
+ ]);
1161
+ return parseElapsed(stdout);
1162
+ } catch {
1163
+ return null;
1164
+ }
1165
+ }
1166
+ /** Missing or corrupt files give `null`. */
1167
+ async function readLastExit(home) {
1168
+ try {
1169
+ const value = JSON.parse(await readFile(join(home, "last-exit.json"), "utf8"));
1170
+ if (typeof value.reason !== "string" || typeof value.at !== "number") return null;
1171
+ return {
1172
+ reason: value.reason,
1173
+ at: value.at,
1174
+ ...value.detail ? { detail: String(value.detail) } : {}
1175
+ };
1176
+ } catch {
1177
+ return null;
1178
+ }
1179
+ }
1180
+ async function readTunnel(home, instance) {
1181
+ if (!instance?.url) return null;
1182
+ try {
1183
+ const token = (await readFile(join(home, "token"), "utf8")).trim();
1184
+ const response = await fetch(`${instance.url}/api/remote`, {
1185
+ headers: { authorization: `Bearer ${token}` },
1186
+ signal: AbortSignal.timeout(2e3)
1187
+ });
1188
+ if (!response.ok) return null;
1189
+ const { state, url, message } = await response.json();
1190
+ return {
1191
+ state,
1192
+ url,
1193
+ message
1194
+ };
1195
+ } catch {
1196
+ return null;
1197
+ }
1198
+ }
1199
+ async function realExtras(home, instance) {
1200
+ const [uptimeSeconds, lastExit, tunnel] = await Promise.all([
1201
+ instance ? uptimeOf(instance.pid) : null,
1202
+ readLastExit(home),
1203
+ readTunnel(home, instance)
1204
+ ]);
1205
+ return {
1206
+ uptimeSeconds,
1207
+ lastExit,
1208
+ tunnel
1209
+ };
1210
+ }
1211
+ function formatDuration(totalSeconds) {
1212
+ const days = Math.floor(totalSeconds / 86400);
1213
+ const hours = Math.floor(totalSeconds % 86400 / 3600);
1214
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1215
+ if (days > 0) return `${days}d ${hours}h`;
1216
+ if (hours > 0) return `${hours}h ${minutes}m`;
1217
+ return minutes > 0 ? `${minutes}m ${totalSeconds % 60}s` : `${totalSeconds}s`;
1218
+ }
1219
+ function renderExtras(extras) {
1220
+ const { uptimeSeconds, lastExit, tunnel } = extras;
1221
+ const exit = lastExit ? `${lastExit.reason}${lastExit.detail ? ` (${lastExit.detail})` : ""} at ${new Date(lastExit.at).toISOString()}` : "-";
1222
+ const tunnelText = tunnel ? `${tunnel.state}${tunnel.url ? ` ${tunnel.url}` : ""}` : "-";
1223
+ return [
1224
+ `uptime: ${uptimeSeconds === null ? "-" : formatDuration(uptimeSeconds)}`,
1225
+ `last exit: ${exit}`,
1226
+ `tunnel: ${tunnelText}`
1227
+ ];
1228
+ }
1229
+ //#endregion
1230
+ //#region src/lifecycle/deps.ts
1231
+ const withToken = (origin, token) => `${origin}/?token=${encodeURIComponent(token)}`;
1232
+ const managerOf = (deps, home) => createServiceManager(deps.context, deps.cli(), home);
1233
+ const NOT_RUNNING = "tau is not running. Run `tau` to start it.";
1234
+ async function realLifecycleDeps() {
1235
+ const { waitForUrl } = await import("./wait-for-lock-BMk6v3J5.js");
1236
+ const { existsSync } = await import("node:fs");
1237
+ return {
1238
+ context: realContext(),
1239
+ cli: () => cliEntry(),
1240
+ version: cliVersion,
1241
+ readInstance: readRunningInstance,
1242
+ waitForInstanceUrl: (home) => waitForUrl(home),
1243
+ readToken: readOrCreateToken,
1244
+ openBrowser: async (url) => {
1245
+ const { default: open } = await import("open");
1246
+ await open(url);
1247
+ },
1248
+ out: (message) => console.log(message),
1249
+ fileExists: existsSync,
1250
+ extras: realExtras
1251
+ };
1252
+ }
1253
+ //#endregion
1254
+ //#region src/lifecycle/open.ts
1255
+ /** Opens the browser on a running server; returns the URL it opened. */
1256
+ async function openRunning(deps, home) {
1257
+ const instance = await deps.waitForInstanceUrl(home);
1258
+ if (!instance?.url) throw new Error(NOT_RUNNING);
1259
+ const url = withToken(instance.url, await deps.readToken(home));
1260
+ try {
1261
+ await deps.openBrowser(url);
1262
+ } catch (error) {
1263
+ deps.out(`Could not open the browser (${error instanceof Error ? error.message : error}). Open ${url}`);
1264
+ }
1265
+ return url;
1266
+ }
1267
+ async function runOpenWith(deps, argv) {
1268
+ const { home } = lifecycleFlagsOf(argv, deps.context);
1269
+ if (!await deps.readInstance(home)) throw new Error(NOT_RUNNING);
1270
+ await openRunning(deps, home);
1271
+ }
1272
+ /** `tau open`. */
1273
+ async function runOpen(argv) {
1274
+ await runOpenWith(await realLifecycleDeps(), argv);
1275
+ }
1276
+ //#endregion
1277
+ //#region src/lifecycle/start.ts
1278
+ /** Starts the service of this home (a detached process when it has none), waits for the URL, prints it. */
1279
+ async function startAndWait(deps, home) {
1280
+ if (!await deps.readInstance(home)) await managerOf(deps, home).start();
1281
+ const instance = await deps.waitForInstanceUrl(home);
1282
+ if (!instance?.url) throw new Error("tau did not start. Run `tau logs` to see why.");
1283
+ return instance.url;
1284
+ }
1285
+ /** A detached server reads `TAU_FAUX`; the service definition never carries `--faux`. */
1286
+ function forwardFauxToServer(deps, argv) {
1287
+ if (argv.includes("--faux")) deps.context.env.TAU_FAUX = "1";
1288
+ }
1289
+ async function runStartWith(deps, argv) {
1290
+ forwardFauxToServer(deps, argv);
1291
+ const flags = lifecycleFlagsOf(argv, deps.context);
1292
+ if (flags.explicitPort !== void 0) writeStoredPort(flags.home, flags.explicitPort);
1293
+ const url = await startAndWait(deps, flags.home);
1294
+ deps.out(`tau is running: ${url}`);
1295
+ if (flags.open) await openRunning(deps, flags.home);
1296
+ }
1297
+ /** `tau start [--port --home --no-open]`. */
1298
+ async function runStart(argv) {
1299
+ await runStartWith(await realLifecycleDeps(), argv);
1300
+ }
1301
+ //#endregion
1302
+ //#region src/lifecycle/bare.ts
1303
+ /** Bare `tau`: install when missing, repair when stale, start when stopped, open the browser. */
1304
+ async function runBareWith(deps, argv, serveInForeground) {
1305
+ const flags = lifecycleFlagsOf(argv, deps.context);
1306
+ if (flags.explicitPort !== void 0) writeStoredPort(flags.home, flags.explicitPort);
1307
+ const manager = managerOf(deps, flags.home);
1308
+ if (!await manager.installed() || await manager.needsRepair()) try {
1309
+ for (const warning of await manager.install()) deps.out(`warning: ${warning}`);
1310
+ } catch (error) {
1311
+ if (!(error instanceof TemporaryEntryError)) throw error;
1312
+ deps.out(`warning: ${error.message} Serving in the foreground instead.`);
1313
+ return serveInForeground(argv);
1314
+ }
1315
+ const url = await startAndWait(deps, flags.home);
1316
+ deps.out(`tau is running: ${url}`);
1317
+ if (flags.open) await openRunning(deps, flags.home);
1318
+ }
1319
+ async function runBare(argv, serveInForeground) {
1320
+ await runBareWith(await realLifecycleDeps(), argv, serveInForeground);
1321
+ }
1322
+ //#endregion
1323
+ //#region src/lifecycle/logs.ts
1324
+ const TAIL_LINES = 200;
1325
+ const lastLinesOf = (text, count) => text.trimEnd().split("\n").slice(-count).join("\n");
1326
+ function follow(file, from, write) {
1327
+ let position = from;
1328
+ watchFile(file, { interval: 500 }, (current) => {
1329
+ if (current.size < position) position = 0;
1330
+ if (current.size === position) return;
1331
+ createReadStream(file, {
1332
+ start: position,
1333
+ end: current.size - 1,
1334
+ encoding: "utf8"
1335
+ }).on("data", (chunk) => write(String(chunk)));
1336
+ position = current.size;
1337
+ });
1338
+ }
1339
+ /** `tau logs [-f]`: the last lines of `<home>/logs/server.log`, then new ones with `-f`. */
1340
+ async function runLogs(argv, write = (text) => process.stdout.write(text)) {
1341
+ const flags = lifecycleFlagsOf(argv, realContext());
1342
+ const file = serverLogOf(flags.home);
1343
+ if (!existsSync(file)) throw new Error(`No log yet: ${file}`);
1344
+ write(`${lastLinesOf(readFileSync(file, "utf8"), TAIL_LINES)}\n`);
1345
+ if (!flags.follow) return;
1346
+ follow(file, statSync(file).size, write);
1347
+ await new Promise(() => void 0);
1348
+ }
1349
+ //#endregion
1350
+ //#region src/lifecycle/restart.ts
1351
+ /** Repairs a stale service, copies PATH again, and restarts. */
1352
+ async function runRestartWith(deps, argv) {
1353
+ const flags = lifecycleFlagsOf(argv, deps.context);
1354
+ if (flags.explicitPort !== void 0) writeStoredPort(flags.home, flags.explicitPort);
1355
+ const manager = managerOf(deps, flags.home);
1356
+ if (await manager.needsRepair()) deps.out("Repairing the service.");
1357
+ await manager.restart();
1358
+ const instance = await deps.waitForInstanceUrl(flags.home);
1359
+ deps.out(instance?.url ? `tau is running: ${instance.url}` : "tau restarted.");
1360
+ }
1361
+ /** `tau restart`. */
1362
+ async function runRestart(argv) {
1363
+ await runRestartWith(await realLifecycleDeps(), argv);
1364
+ }
1365
+ //#endregion
1366
+ //#region src/lifecycle/status.ts
1367
+ async function gatherStatus(deps, home) {
1368
+ const manager = managerOf(deps, home);
1369
+ const [installed, state, instance] = await Promise.all([
1370
+ manager.installed(),
1371
+ manager.state(),
1372
+ deps.readInstance(home)
1373
+ ]);
1374
+ const extras = await deps.extras?.(home, instance) ?? NO_EXTRAS;
1375
+ const recorded = manager.recorded();
1376
+ const nodeMissing = installed && recorded !== void 0 && !deps.fileExists(recorded.node);
1377
+ const cliVersion = deps.version();
1378
+ const managedPid = state.pid;
1379
+ const isService = state.active && (managedPid === void 0 || managedPid === instance?.pid);
1380
+ const warnings = [];
1381
+ if (instance?.version !== void 0 && instance.version !== cliVersion) warnings.push(`Running version ${instance.version} is not ${cliVersion}: run \`tau restart\`.`);
1382
+ if (nodeMissing) warnings.push("node missing: run `tau` to repair the service.");
1383
+ return {
1384
+ installed,
1385
+ running: instance ? isService ? "service" : "foreground" : false,
1386
+ pid: instance?.pid ?? null,
1387
+ url: instance?.url ?? null,
1388
+ version: instance?.version ?? null,
1389
+ cliVersion,
1390
+ home,
1391
+ node: recorded?.node ?? null,
1392
+ nodeMissing,
1393
+ warnings,
1394
+ ...extras
1395
+ };
1396
+ }
1397
+ function renderStatus(report) {
1398
+ return [
1399
+ `tau: ${report.running ? `running (${report.running})` : "not running"}`,
1400
+ `installed: ${report.installed ? "yes" : "no"}`,
1401
+ `pid: ${report.pid ?? "-"}`,
1402
+ `url: ${report.url ?? "-"}`,
1403
+ `version: ${report.version ?? "-"}`,
1404
+ `home: ${report.home}`,
1405
+ `node: ${report.node ?? "-"}${report.nodeMissing ? " (missing)" : ""}`,
1406
+ ...renderExtras(report),
1407
+ ...report.warnings.map((warning) => `warning: ${warning}`)
1408
+ ].join("\n");
1409
+ }
1410
+ /** `tau status [--json]`. */
1411
+ async function runStatusWith(deps, argv) {
1412
+ const flags = lifecycleFlagsOf(argv, deps.context);
1413
+ const report = await gatherStatus(deps, flags.home);
1414
+ deps.out(flags.json ? JSON.stringify(report) : renderStatus(report));
1415
+ }
1416
+ async function runStatus(argv) {
1417
+ await runStatusWith(await realLifecycleDeps(), argv);
1418
+ }
1419
+ //#endregion
1420
+ //#region src/lifecycle/stop.ts
1421
+ async function runStopWith(deps, argv) {
1422
+ const { home } = lifecycleFlagsOf(argv, deps.context);
1423
+ const status = await gatherStatus(deps, home);
1424
+ if (status.running === "foreground") {
1425
+ deps.out(`tau runs in the foreground (pid ${status.pid}). Stop it with Ctrl-C.`);
1426
+ return;
1427
+ }
1428
+ await managerOf(deps, home).stop();
1429
+ deps.out("tau stopped.");
1430
+ }
1431
+ /** `tau stop`: the service stays stopped until `tau start` or the next login. */
1432
+ async function runStop(argv) {
1433
+ await runStopWith(await realLifecycleDeps(), argv);
1434
+ }
1435
+ //#endregion
1436
+ //#region src/main.ts
1437
+ const apiCommandHandlers = Object.fromEntries(COMMAND_NOUNS.map((noun) => [noun, (argv) => runApiCommand(noun, argv)]));
1438
+ /** The one dispatch table: command word to handler. */
1439
+ const commandTable = {
1440
+ serve: serveInForeground,
1441
+ start: runStart,
1442
+ stop: runStop,
1443
+ restart: runRestart,
1444
+ status: runStatus,
1445
+ open: runOpen,
1446
+ logs: runLogs,
1447
+ service: runServiceCommand,
1448
+ skill: runSkill,
1449
+ help: runHelp,
1450
+ ...apiCommandHandlers,
1451
+ pair: runPair,
1452
+ connect: runConnect,
1453
+ disconnect: runDisconnect
1454
+ };
1455
+ const bareHandler = (argv) => runBare(argv, serveInForeground);
1456
+ const wantsHelp = (argv) => argv.includes("--help") || argv.includes("-h");
1457
+ var UnknownCommandError = class extends Error {
1458
+ constructor(word) {
1459
+ super(`Unknown command: ${word}. Run \`tau --help\`.`);
1460
+ this.name = "UnknownCommandError";
1461
+ }
1462
+ };
1463
+ /** Picks the handler and the argv it receives. */
1464
+ function resolveCommand(argv, table = commandTable, bare = bareHandler) {
1465
+ const { word, rest } = commandWordOf(argv);
1466
+ if (word === void 0) return wantsHelp(argv) ? {
1467
+ handler: table.help ?? bare,
1468
+ argv
1469
+ } : {
1470
+ handler: bare,
1471
+ argv
1472
+ };
1473
+ const handler = table[word];
1474
+ if (!handler) throw new UnknownCommandError(word);
1475
+ return {
1476
+ handler,
1477
+ argv: rest
1478
+ };
1479
+ }
1480
+ const wantsVersion = (argv) => argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v");
1481
+ async function main(argv) {
1482
+ if (wantsVersion(argv)) {
1483
+ console.log(cliVersion());
1484
+ return;
1485
+ }
1486
+ const { handler, argv: rest } = resolveCommand(argv);
1487
+ await handler(rest);
1488
+ }
1489
+ //#endregion
1490
+ export { main };