@impulselab/hepha 0.2.180 → 0.2.182

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/dist/index.js CHANGED
@@ -1,16 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/lib/config.ts
4
- import {
5
- chmodSync,
6
- mkdirSync,
7
- readFileSync,
8
- rmSync,
9
- writeFileSync
10
- } from "fs";
11
- import { homedir } from "os";
12
- import { dirname, join } from "path";
13
-
14
3
  // ../../packages/core/src/tagged-error.ts
15
4
  var TaggedError = class _TaggedError extends Error {
16
5
  _tag;
@@ -27,7 +16,7 @@ var TaggedError = class _TaggedError extends Error {
27
16
  }
28
17
  };
29
18
 
30
- // src/lib/errors.ts
19
+ // src/lib/shared/errors-constants.ts
31
20
  var CLI_ERROR_CODES = {
32
21
  BAD_USAGE: "BAD_USAGE",
33
22
  /** The session needs a person — the exit-3 family. */
@@ -43,6 +32,8 @@ var CLI_ERROR_CODES = {
43
32
  NON_JSON_RESPONSE: "NON_JSON_RESPONSE",
44
33
  ERROR: "ERROR"
45
34
  };
35
+
36
+ // src/lib/cli/cli-error.ts
46
37
  var CliError = class extends TaggedError {
47
38
  exitCode;
48
39
  details;
@@ -74,91 +65,7 @@ var CliError = class extends TaggedError {
74
65
  }
75
66
  };
76
67
 
77
- // src/lib/is-http-url.ts
78
- function isHttpUrl(value) {
79
- try {
80
- const parsed = new URL(value);
81
- return parsed.protocol === "http:" || parsed.protocol === "https:";
82
- } catch {
83
- return false;
84
- }
85
- }
86
-
87
- // src/lib/config.ts
88
- var CONFIG_PATH = join(homedir(), ".hepha", "config.json");
89
- var DEFAULT_BASE_URL = "https://hephaistos.impulselab.ai";
90
- function readFileAsObject() {
91
- try {
92
- const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
93
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
94
- return {};
95
- }
96
- return parsed;
97
- } catch {
98
- return {};
99
- }
100
- }
101
- function readString(value) {
102
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
103
- }
104
- function readUser(value) {
105
- if (!value || typeof value !== "object") {
106
- return void 0;
107
- }
108
- const candidate = value;
109
- const user = {
110
- id: readString(candidate.id),
111
- name: readString(candidate.name),
112
- email: readString(candidate.email)
113
- };
114
- return user.id && user.name && user.email ? { id: user.id, name: user.name, email: user.email } : void 0;
115
- }
116
- function readHttpUrl(value) {
117
- const raw = readString(value);
118
- return raw && isHttpUrl(raw) ? raw : void 0;
119
- }
120
- function isSameOrigin(a, b) {
121
- try {
122
- return new URL(a).origin === new URL(b).origin;
123
- } catch {
124
- return false;
125
- }
126
- }
127
- var config = {
128
- path: CONFIG_PATH,
129
- load() {
130
- const stored = readFileAsObject();
131
- const storedBaseUrl = readHttpUrl(stored.baseUrl);
132
- const override = readString(process.env.HEPHA_URL);
133
- if (override && !readHttpUrl(override)) {
134
- throw new CliError(
135
- `HEPHA_URL is not a valid http(s) URL: "${override}".`,
136
- 2
137
- );
138
- }
139
- const baseUrl = readHttpUrl(override) ?? storedBaseUrl ?? DEFAULT_BASE_URL;
140
- const token = storedBaseUrl && isSameOrigin(storedBaseUrl, baseUrl) ? readString(stored.token) : void 0;
141
- const user = token ? readUser(stored.user) : void 0;
142
- return {
143
- baseUrl,
144
- ...token ? { token } : {},
145
- ...user ? { user } : {}
146
- };
147
- },
148
- save(next) {
149
- mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
150
- writeFileSync(CONFIG_PATH, `${JSON.stringify(next, null, 2)}
151
- `, {
152
- mode: 384
153
- });
154
- chmodSync(CONFIG_PATH, 384);
155
- },
156
- clear() {
157
- rmSync(CONFIG_PATH, { force: true });
158
- }
159
- };
160
-
161
- // src/lib/output.ts
68
+ // src/lib/shared/output-constants.ts
162
69
  var COLORS = {
163
70
  reset: "\x1B[0m",
164
71
  dim: "\x1B[2m",
@@ -184,59 +91,68 @@ var STATUS_COLORS = {
184
91
  killed: "red",
185
92
  abstained: "yellow"
186
93
  };
187
- function supportsColor() {
94
+
95
+ // src/lib/ansi/strip-ansi.ts
96
+ function stripAnsi(text) {
97
+ return text.replace(/\u001b\[\d+m/g, "");
98
+ }
99
+
100
+ // src/lib/supports/supports-color.ts
101
+ function supportsColor(jsonMode) {
188
102
  return process.stdout.isTTY === true && process.env.NO_COLOR === void 0 && !jsonMode;
189
103
  }
190
- var jsonMode = false;
191
- var out = {
104
+
105
+ // src/lib/cli/cli-output.ts
106
+ var CliOutput = class {
107
+ jsonMode = false;
192
108
  /** Declared once per run, before anything can fail. */
193
109
  setJson(enabled) {
194
- jsonMode = enabled;
195
- },
110
+ this.jsonMode = enabled;
111
+ }
196
112
  isJson() {
197
- return jsonMode;
198
- },
113
+ return this.jsonMode;
114
+ }
199
115
  json(value) {
200
116
  process.stdout.write(`${JSON.stringify(value, null, 2)}
201
117
  `);
202
- },
118
+ }
203
119
  line(text = "") {
204
120
  process.stdout.write(`${text}
205
121
  `);
206
- },
122
+ }
207
123
  error(text) {
208
- process.stderr.write(`${out.paint(`error: ${out.safe(text)}`, "red")}
124
+ process.stderr.write(`${this.paint(`error: ${this.safe(text)}`, "red")}
209
125
  `);
210
- },
126
+ }
211
127
  /**
212
128
  * The one place a failure is reported. Under `--json` stdout must be
213
129
  * parseable, and it was not: failures went to stderr as prose. Success keeps
214
130
  * its bare shape, so nothing that already parses stdout has to change.
215
131
  */
216
132
  failure(error) {
217
- if (!jsonMode) {
218
- out.error(error.message);
133
+ if (!this.jsonMode) {
134
+ this.error(error.message);
219
135
  return;
220
136
  }
221
- out.json({
137
+ this.json({
222
138
  ok: false,
223
139
  error: {
224
140
  ...error.details.status === void 0 ? {} : { status: error.details.status },
225
141
  code: error.code,
226
- message: out.safe(error.message),
142
+ message: this.safe(error.message),
227
143
  ...error.details.issues ? { issues: error.details.issues } : {},
228
144
  ...error.details.retryAfterSeconds === void 0 ? {} : { retryAfterSeconds: error.details.retryAfterSeconds }
229
145
  },
230
146
  // Inside the envelope, so stdout stays one parseable document.
231
147
  ...error.result === void 0 ? {} : { result: error.result }
232
148
  });
233
- },
149
+ }
234
150
  paint(text, color) {
235
- if (!supportsColor()) {
151
+ if (!supportsColor(this.jsonMode)) {
236
152
  return text;
237
153
  }
238
154
  return `${COLORS[color] ?? ""}${text}${COLORS.reset}`;
239
- },
155
+ }
240
156
  /**
241
157
  * Neutralise terminal control sequences in text the platform gives us.
242
158
  *
@@ -249,222 +165,300 @@ var out = {
249
165
  */
250
166
  safe(text) {
251
167
  return text.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
252
- },
168
+ }
253
169
  /** `safe`, flattened to one line — anything going into a table cell. */
254
170
  cell(text) {
255
- return out.safe(text).replace(/\s+/g, " ").trim();
256
- },
171
+ return this.safe(text).replace(/\s+/g, " ").trim();
172
+ }
257
173
  status(status) {
258
- return out.paint(status, STATUS_COLORS[status] ?? "reset");
259
- },
174
+ return this.paint(status, STATUS_COLORS[status] ?? "reset");
175
+ }
260
176
  /** Left-aligned two-column block — the shape used by every list command. */
261
177
  table(rows) {
262
178
  if (rows.length === 0) {
263
179
  return;
264
180
  }
265
- const widths = rows[0].map(
181
+ const firstRow = rows[0];
182
+ if (firstRow === void 0) {
183
+ return;
184
+ }
185
+ const widths = firstRow.map(
266
186
  (_, column) => Math.max(...rows.map((row) => stripAnsi(row[column] ?? "").length))
267
187
  );
268
188
  for (const row of rows) {
269
189
  const line = row.map((cell, column) => {
270
- const padding = widths[column] - stripAnsi(cell).length;
190
+ const width = widths[column];
191
+ if (width === void 0) {
192
+ return cell;
193
+ }
194
+ const padding = width - stripAnsi(cell).length;
271
195
  return column === row.length - 1 ? cell : cell + " ".repeat(padding);
272
196
  }).join(" ");
273
- out.line(line.trimEnd());
197
+ this.line(line.trimEnd());
274
198
  }
275
199
  }
276
200
  };
277
- function stripAnsi(text) {
278
- return text.replace(/\u001b\[\d+m/g, "");
279
- }
280
201
 
281
- // src/lib/api.ts
282
- var DEFAULT_TIMEOUT_MS = 3e4;
283
- function resolveTimeout(override) {
284
- if (override !== void 0) {
285
- return override;
286
- }
287
- const raw = process.env.HEPHA_TIMEOUT_MS?.trim();
288
- if (!raw) {
289
- return DEFAULT_TIMEOUT_MS;
290
- }
291
- const parsed = Number(raw);
292
- if (!Number.isFinite(parsed) || parsed <= 0) {
293
- throw new CliError(
294
- `HEPHA_TIMEOUT_MS expects a positive number of milliseconds, got "${raw}".`,
295
- 2
296
- );
202
+ // src/lib/output/out.ts
203
+ var out = new CliOutput();
204
+
205
+ // src/lib/shared/config.ts
206
+ import { chmodSync, mkdirSync, rmSync, writeFileSync } from "fs";
207
+ import { dirname } from "path";
208
+
209
+ // src/lib/shared/config-constants.ts
210
+ var DEFAULT_BASE_URL = "https://hephaistos.impulselab.ai";
211
+
212
+ // src/lib/path/config-path-constants.ts
213
+ import { homedir } from "os";
214
+ import { join } from "path";
215
+ var CONFIG_PATH = join(homedir(), ".hepha", "config.json");
216
+
217
+ // src/lib/file/read-file-as-object.ts
218
+ import { readFileSync } from "fs";
219
+ function readFileAsObject() {
220
+ try {
221
+ const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
222
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
223
+ return {};
224
+ }
225
+ return parsed;
226
+ } catch {
227
+ return {};
297
228
  }
298
- return parsed;
299
229
  }
300
- function authHeaders(as) {
301
- if (as) {
302
- return { authorization: `Bearer ${as.token}` };
303
- }
304
- const apiKey = process.env.HEPHA_API_KEY;
305
- if (apiKey) {
306
- return { "x-api-key": apiKey };
307
- }
308
- const token = process.env.HEPHA_TOKEN ?? config.load().token;
309
- if (token) {
310
- return { authorization: `Bearer ${token}` };
311
- }
312
- throw new CliError(
313
- "Not signed in. Run `hepha login`, or set HEPHA_API_KEY to act as an agent.",
314
- 1,
315
- { code: CLI_ERROR_CODES.NOT_AUTHENTICATED }
316
- );
230
+
231
+ // src/lib/string/config-read-string.ts
232
+ function configReadString(value) {
233
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
317
234
  }
318
- function readIssues(payload) {
319
- if (!payload || typeof payload !== "object") {
320
- return void 0;
321
- }
322
- const data = payload.data;
323
- if (!data || typeof data !== "object") {
235
+
236
+ // src/lib/user/read-user.ts
237
+ function readUser(value) {
238
+ if (!value || typeof value !== "object") {
324
239
  return void 0;
325
240
  }
326
- const issues = data.issues;
327
- return Array.isArray(issues) ? issues : void 0;
241
+ const candidate = value;
242
+ const user = {
243
+ id: configReadString(candidate.id),
244
+ name: configReadString(candidate.name),
245
+ email: configReadString(candidate.email)
246
+ };
247
+ return user.id && user.name && user.email ? { id: user.id, name: user.name, email: user.email } : void 0;
328
248
  }
329
- function readString2(payload, key) {
330
- if (!payload || typeof payload !== "object") {
331
- return void 0;
249
+
250
+ // src/lib/http/is-http-url.ts
251
+ function isHttpUrl(value) {
252
+ try {
253
+ const parsed = new URL(value);
254
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
255
+ } catch {
256
+ return false;
332
257
  }
333
- const value = payload[key];
334
- return typeof value === "string" && value.length > 0 ? value : void 0;
335
258
  }
336
- function retryAfterSeconds(response) {
337
- const raw = response.headers.get("retry-after") ?? response.headers.get("x-retry-after");
338
- if (!raw) {
339
- return void 0;
340
- }
341
- const seconds = Number(raw.trim());
342
- return Number.isFinite(seconds) && seconds >= 0 ? seconds : void 0;
259
+
260
+ // src/lib/http/read-http-url.ts
261
+ function readHttpUrl(value) {
262
+ const raw = configReadString(value);
263
+ return raw && isHttpUrl(raw) ? raw : void 0;
343
264
  }
344
- function messageFrom(status, payload) {
345
- const message = readString2(payload, "message");
346
- if (message) {
347
- return status === 403 && process.env.HEPHA_API_KEY ? `${message} (HEPHA_API_KEY is set and takes precedence over your \`hepha login\` session \u2014 unset it to act as yourself.)` : message;
348
- }
349
- if (status === 401) {
350
- return "Unauthorized \u2014 your credential is missing, expired or revoked.";
265
+
266
+ // src/lib/same/is-same-origin.ts
267
+ function isSameOrigin(a, b) {
268
+ try {
269
+ return new URL(a).origin === new URL(b).origin;
270
+ } catch {
271
+ return false;
351
272
  }
352
- return `Request failed with status ${status}.`;
353
- }
354
- function excerpt(text) {
355
- const flattened = out.cell(text);
356
- return flattened.length > 200 ? `${flattened.slice(0, 200)}\u2026` : flattened;
357
273
  }
358
- async function api(requestPath, options = {}) {
359
- const baseUrl = options.as?.baseUrl ?? config.load().baseUrl;
360
- const url = new URL(`${baseUrl.replace(/\/$/, "")}/api/v1${requestPath}`);
361
- for (const [key, value] of Object.entries(options.query ?? {})) {
362
- if (value !== void 0) {
363
- url.searchParams.set(key, String(value));
274
+
275
+ // src/lib/shared/config.ts
276
+ var config = {
277
+ path: CONFIG_PATH,
278
+ load() {
279
+ const stored = readFileAsObject();
280
+ const storedBaseUrl = readHttpUrl(stored.baseUrl);
281
+ const override = configReadString(process.env.HEPHA_URL);
282
+ if (override && !readHttpUrl(override)) {
283
+ throw new CliError(
284
+ `HEPHA_URL is not a valid http(s) URL: "${override}".`,
285
+ 2
286
+ );
364
287
  }
288
+ const baseUrl = readHttpUrl(override) ?? storedBaseUrl ?? DEFAULT_BASE_URL;
289
+ const token = storedBaseUrl && isSameOrigin(storedBaseUrl, baseUrl) ? configReadString(stored.token) : void 0;
290
+ const user = token ? readUser(stored.user) : void 0;
291
+ return {
292
+ baseUrl,
293
+ ...token ? { token } : {},
294
+ ...user ? { user } : {}
295
+ };
296
+ },
297
+ save(next) {
298
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
299
+ writeFileSync(CONFIG_PATH, `${JSON.stringify(next, null, 2)}
300
+ `, {
301
+ mode: 384
302
+ });
303
+ chmodSync(CONFIG_PATH, 384);
304
+ },
305
+ clear() {
306
+ rmSync(CONFIG_PATH, { force: true });
365
307
  }
366
- const headers = {
367
- ...authHeaders(options.as),
368
- ...options.body === void 0 ? {} : { "content-type": "application/json" }
369
- };
370
- const method = options.method ?? "GET";
371
- const deadline = resolveTimeout(options.timeoutMs);
372
- const controller = new AbortController();
373
- let abortedBy = null;
374
- const timer = setTimeout(() => {
375
- abortedBy ??= "timeout";
376
- controller.abort();
377
- }, deadline);
378
- const onInterrupt = () => {
379
- abortedBy ??= "interrupt";
380
- controller.abort();
381
- };
382
- process.once("SIGINT", onInterrupt);
383
- let response;
384
- let text;
385
- try {
386
- response = await fetch(url, {
387
- method,
388
- headers,
389
- signal: controller.signal,
390
- ...options.body === void 0 ? {} : { body: JSON.stringify(options.body) }
391
- });
392
- text = await response.text();
393
- } catch (cause) {
394
- if (abortedBy === "interrupt") {
395
- throw new CliError(`${method} ${url.pathname} interrupted.`, 130, {
396
- code: CLI_ERROR_CODES.INTERRUPTED
397
- });
398
- }
399
- if (abortedBy === "timeout") {
400
- throw new CliError(
401
- `${method} ${url.pathname} timed out after ${deadline}ms. Raise HEPHA_TIMEOUT_MS if the platform is simply slow.`,
402
- 1,
403
- { code: CLI_ERROR_CODES.TIMEOUT }
404
- );
405
- }
406
- throw new CliError(
407
- `Cannot reach ${baseUrl}. Set HEPHA_URL if the platform lives elsewhere. (${String(cause)})`,
408
- 1,
409
- { code: CLI_ERROR_CODES.NETWORK }
410
- );
411
- } finally {
412
- clearTimeout(timer);
413
- process.removeListener("SIGINT", onInterrupt);
414
- }
415
- const retryAfter = retryAfterSeconds(response);
416
- let payload = null;
417
- if (text) {
308
+ };
309
+
310
+ // src/lib/version/read-version.ts
311
+ import { readFileSync as readFileSync2 } from "fs";
312
+ import { dirname as dirname2, join as join2 } from "path";
313
+ import { fileURLToPath } from "url";
314
+ function readVersion() {
315
+ let directory = dirname2(fileURLToPath(import.meta.url));
316
+ for (let depth = 0; depth < 5; depth += 1) {
418
317
  try {
419
- payload = JSON.parse(text);
420
- } catch {
421
- const body = excerpt(text);
422
- const contentType = response.headers.get("content-type") ?? "unknown";
423
- throw new CliError(
424
- response.ok ? `${method} ${url.pathname} answered ${response.status} with a non-JSON body (${contentType}). Is HEPHA_URL pointing at a Hephaistos deployment?${body ? ` Body: ${body}` : ""}` : `${method} ${url.pathname} failed with ${response.status} and a non-JSON body (${contentType}).${body ? ` Body: ${body}` : ""}`,
425
- 1,
426
- {
427
- status: response.status,
428
- code: CLI_ERROR_CODES.NON_JSON_RESPONSE,
429
- ...retryAfter === void 0 ? {} : { retryAfterSeconds: retryAfter }
430
- }
318
+ const manifest = JSON.parse(
319
+ readFileSync2(join2(directory, "package.json"), "utf8")
431
320
  );
432
- }
433
- }
434
- if (!response.ok) {
435
- const code = readString2(payload, "code") ?? readString2(payload, "error");
436
- const issues = readIssues(payload);
437
- throw new CliError(
438
- messageFrom(response.status, payload) + (response.status === 429 && retryAfter !== void 0 ? ` Retry in ${retryAfter}s.` : ""),
439
- 1,
440
- {
441
- status: response.status,
442
- ...code ? { code } : {},
443
- ...issues ? { issues } : {},
444
- ...retryAfter === void 0 ? {} : { retryAfterSeconds: retryAfter }
321
+ const version = manifest.version;
322
+ if (typeof version === "string" && version.length > 0) {
323
+ return version;
445
324
  }
446
- );
325
+ } catch {
326
+ }
327
+ const parent = dirname2(directory);
328
+ if (parent === directory) {
329
+ break;
330
+ }
331
+ directory = parent;
447
332
  }
448
- return payload;
333
+ return "unknown";
449
334
  }
450
335
 
451
- // src/lib/api-path.ts
452
- function path(strings, ...values) {
453
- return strings.reduce(
454
- (accumulator, literal, index) => accumulator + (index === 0 ? "" : encodeURIComponent(values[index - 1] ?? "")) + literal,
455
- ""
456
- );
336
+ // src/lib/version/version-constants.ts
337
+ var VERSION = readVersion();
338
+
339
+ // src/commands/help/help-constants.ts
340
+ var HELP = `hepha \u2014 orchestrate Hephaistos sessions from a terminal or an agent.
341
+
342
+ USAGE
343
+ hepha <command> [args] [--flags]
344
+
345
+ THE LOOP
346
+ hepha run "<prompt>" --repo owner/repo --follow
347
+ Start a session and follow it until it finishes or asks you something.
348
+ Exit 0 completed \xB7 3 waiting on a human \xB7 1 failed.
349
+ [--title "\u2026"] [--task-type implement|new-project|question|draft|orchestrator]
350
+ [--model <slug>] [--harness claude-code|codex] [--branch <name>]
351
+ [--skill <id>]\u2026
352
+ hepha questions list [--session <id>]
353
+ hepha questions show <batchId>
354
+ hepha questions answer <batchId> --option <questionId>=1,2 --text <questionId>="\u2026"
355
+ [--answers '<json array>']
356
+ hepha questions cancel <batchId>
357
+ hepha approvals list | show <id> | approve <id> | reject <id> --mode rework --feedback "\u2026"
358
+
359
+ SESSIONS
360
+ hepha sessions list [--status active|waiting|done] [--repo owner/repo]
361
+ [--search "\u2026"] [--page N] [--limit N] [--all]
362
+ hepha sessions get <id>
363
+ hepha sessions logs <id> [--after <sequence>] [--limit N] [--follow]
364
+ hepha sessions send <id> "<message>"
365
+ hepha sessions pause|resume|archive <id>
366
+ hepha sessions kill <id> [--reason "\u2026"]
367
+ hepha sessions checks|pr|verify <id>
368
+
369
+ CATALOG
370
+ hepha repos
371
+ hepha branches <owner/repo>
372
+ hepha models
373
+ hepha skills
374
+
375
+ AUTH
376
+ hepha login [--url https://\u2026] Device authorization: approve it in a browser.
377
+ hepha logout
378
+ hepha whoami
379
+ hepha keys list
380
+ hepha keys create <name> [--scope sessions:read]\u2026 [--expires-in-days 90]
381
+ hepha keys revoke <keyId>
382
+
383
+ ENVIRONMENT
384
+ HEPHA_API_KEY Act as an agent. Takes precedence over a stored login, so
385
+ the human-only commands (approvals approve/reject, keys)
386
+ answer 403 while it is set \u2014 unset it to act as yourself.
387
+ HEPHA_TOKEN A device-flow session token, instead of ~/.hepha/config.json.
388
+ HEPHA_URL Platform base URL.
389
+ HEPHA_TIMEOUT_MS Per-request timeout in milliseconds (default 30000).
390
+
391
+ GLOBAL FLAGS
392
+ --json Machine-readable output on stdout. Every command that
393
+ returns data honours it; login is interactive and does not.
394
+ Failures print {"ok":false,"error":{\u2026}} and keep their exit code.
395
+ --quiet Suppress the streamed transcript while following a session.
396
+ --version Print the version and exit.
397
+ --help Print this.
398
+
399
+ Unknown flags and surplus arguments are refused: for an agent, a typo must
400
+ fail rather than run with default options.
401
+
402
+ `;
403
+
404
+ // src/commands/command/help-command.ts
405
+ function helpCommand() {
406
+ const baseUrl = config.load().baseUrl.replace(/\/$/, "");
407
+ out.line(`${HELP}hepha ${VERSION} \xB7 full reference: ${baseUrl}/api/v1/docs`);
457
408
  }
458
409
 
459
- // src/lib/args.ts
410
+ // src/lib/args/args-constants.ts
460
411
  var GLOBAL_BOOLEANS = ["json", "help", "quiet"];
412
+
413
+ // src/lib/boolean/boolean-flags-constants.ts
461
414
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
462
415
  ...GLOBAL_BOOLEANS,
463
416
  "follow",
464
417
  "all",
465
418
  "version"
466
419
  ]);
467
- var args = {
420
+
421
+ // src/lib/args/parse-flag.ts
422
+ function parseFlag(argv, index, parsed) {
423
+ const token = argv[index];
424
+ if (token === void 0) {
425
+ return index;
426
+ }
427
+ const withoutDashes = token.slice(2);
428
+ const equalsAt = withoutDashes.indexOf("=");
429
+ let name;
430
+ let value;
431
+ if (equalsAt >= 0) {
432
+ name = withoutDashes.slice(0, equalsAt);
433
+ value = withoutDashes.slice(equalsAt + 1);
434
+ if (BOOLEAN_FLAGS.has(name)) {
435
+ throw new CliError(
436
+ `--${name} is a boolean flag: pass --${name} to enable it, or omit it. "--${name}=${value}" is not a value.`,
437
+ 2
438
+ );
439
+ }
440
+ } else {
441
+ name = withoutDashes;
442
+ const next = argv[index + 1];
443
+ if (!BOOLEAN_FLAGS.has(name) && next !== void 0 && !next.startsWith("--")) {
444
+ value = next;
445
+ index += 1;
446
+ }
447
+ }
448
+ if (value === void 0 || value.trim() === "") {
449
+ if (!BOOLEAN_FLAGS.has(name)) {
450
+ throw new CliError(`--${name} expects a value.`, 2);
451
+ }
452
+ parsed.booleans.add(name);
453
+ return index;
454
+ }
455
+ parsed.flags[name] = value;
456
+ (parsed.repeated[name] ??= []).push(value);
457
+ return index;
458
+ }
459
+
460
+ // src/lib/args/args-parser.ts
461
+ var ArgsParser = class {
468
462
  parse(argv) {
469
463
  const parsed = {
470
464
  positional: [],
@@ -475,6 +469,9 @@ var args = {
475
469
  let flagsEnded = false;
476
470
  for (let index = 0; index < argv.length; index += 1) {
477
471
  const token = argv[index];
472
+ if (token === void 0) {
473
+ break;
474
+ }
478
475
  if (!flagsEnded && token === "--") {
479
476
  flagsEnded = true;
480
477
  continue;
@@ -483,39 +480,10 @@ var args = {
483
480
  parsed.positional.push(token);
484
481
  continue;
485
482
  }
486
- const withoutDashes = token.slice(2);
487
- const equalsAt = withoutDashes.indexOf("=");
488
- let name;
489
- let value;
490
- if (equalsAt >= 0) {
491
- name = withoutDashes.slice(0, equalsAt);
492
- value = withoutDashes.slice(equalsAt + 1);
493
- if (BOOLEAN_FLAGS.has(name)) {
494
- throw new CliError(
495
- `--${name} is a boolean flag: pass --${name} to enable it, or omit it. "--${name}=${value}" is not a value.`,
496
- 2
497
- );
498
- }
499
- } else {
500
- name = withoutDashes;
501
- const next = argv[index + 1];
502
- if (!BOOLEAN_FLAGS.has(name) && next !== void 0 && !next.startsWith("--")) {
503
- value = next;
504
- index += 1;
505
- }
506
- }
507
- if (value === void 0 || value.trim() === "") {
508
- if (!BOOLEAN_FLAGS.has(name)) {
509
- throw new CliError(`--${name} expects a value.`, 2);
510
- }
511
- parsed.booleans.add(name);
512
- continue;
513
- }
514
- parsed.flags[name] = value;
515
- (parsed.repeated[name] ??= []).push(value);
483
+ index = parseFlag(argv, index, parsed);
516
484
  }
517
485
  return parsed;
518
- },
486
+ }
519
487
  /**
520
488
  * Refuse anything this action does not understand. Called once the action is
521
489
  * known, because `sessions list` and `sessions logs` accept different flags.
@@ -539,7 +507,7 @@ var args = {
539
507
  2
540
508
  );
541
509
  }
542
- },
510
+ }
543
511
  /** The action name, validated against what the group actually implements. */
544
512
  action(parsed, group, allowed, fallback) {
545
513
  const action = parsed.positional[0] ?? fallback;
@@ -550,14 +518,14 @@ var args = {
550
518
  );
551
519
  }
552
520
  return action;
553
- },
521
+ }
554
522
  required(parsed, index, name) {
555
523
  const value = parsed.positional[index];
556
524
  if (!value) {
557
525
  throw new CliError(`Missing <${name}>.`, 2);
558
526
  }
559
527
  return value;
560
- },
528
+ }
561
529
  /**
562
530
  * A whole number inside a range, refused locally: `--page -1` used to come
563
531
  * back from the API as a bare "Input validation failed".
@@ -582,7 +550,7 @@ var args = {
582
550
  throw new CliError(`--${name} must be at most ${bounds.max}.`, 2);
583
551
  }
584
552
  return value;
585
- },
553
+ }
586
554
  /** One of a fixed set, named in the error so the fix is obvious. */
587
555
  choice(parsed, name, allowed) {
588
556
  const raw = parsed.flags[name];
@@ -599,34 +567,254 @@ var args = {
599
567
  }
600
568
  };
601
569
 
602
- // src/lib/version.ts
603
- import { readFileSync as readFileSync2 } from "fs";
604
- import { dirname as dirname2, join as join2 } from "path";
605
- import { fileURLToPath } from "url";
606
- function readVersion() {
607
- let directory = dirname2(fileURLToPath(import.meta.url));
608
- for (let depth = 0; depth < 5; depth += 1) {
609
- try {
610
- const manifest = JSON.parse(
611
- readFileSync2(join2(directory, "package.json"), "utf8")
570
+ // src/lib/args/args.ts
571
+ var args = new ArgsParser();
572
+
573
+ // src/lib/wants/wants-json.ts
574
+ function wantsJson(argv) {
575
+ for (const token of argv) {
576
+ if (token === "--") {
577
+ return false;
578
+ }
579
+ if (token === "--json") {
580
+ return true;
581
+ }
582
+ }
583
+ return false;
584
+ }
585
+
586
+ // src/lib/api/api-constants.ts
587
+ var DEFAULT_TIMEOUT_MS = 3e4;
588
+
589
+ // src/lib/timeout/resolve-timeout.ts
590
+ function resolveTimeout(override) {
591
+ if (override !== void 0) {
592
+ return override;
593
+ }
594
+ const raw = process.env.HEPHA_TIMEOUT_MS?.trim();
595
+ if (!raw) {
596
+ return DEFAULT_TIMEOUT_MS;
597
+ }
598
+ const parsed = Number(raw);
599
+ if (!Number.isFinite(parsed) || parsed <= 0) {
600
+ throw new CliError(
601
+ `HEPHA_TIMEOUT_MS expects a positive number of milliseconds, got "${raw}".`,
602
+ 2
603
+ );
604
+ }
605
+ return parsed;
606
+ }
607
+
608
+ // src/lib/auth/auth-headers.ts
609
+ function authHeaders(as) {
610
+ if (as) {
611
+ return { authorization: `Bearer ${as.token}` };
612
+ }
613
+ const apiKey = process.env.HEPHA_API_KEY;
614
+ if (apiKey) {
615
+ return { "x-api-key": apiKey };
616
+ }
617
+ const token = process.env.HEPHA_TOKEN ?? config.load().token;
618
+ if (token) {
619
+ return { authorization: `Bearer ${token}` };
620
+ }
621
+ throw new CliError(
622
+ "Not signed in. Run `hepha login`, or set HEPHA_API_KEY to act as an agent.",
623
+ 1,
624
+ { code: CLI_ERROR_CODES.NOT_AUTHENTICATED }
625
+ );
626
+ }
627
+
628
+ // src/lib/retry/retry-after-seconds.ts
629
+ function retryAfterSeconds(response) {
630
+ const raw = response.headers.get("retry-after") ?? response.headers.get("x-retry-after");
631
+ if (!raw) {
632
+ return void 0;
633
+ }
634
+ const seconds = Number(raw.trim());
635
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds : void 0;
636
+ }
637
+
638
+ // src/lib/response/fetch-response.ts
639
+ async function fetchResponse({
640
+ url,
641
+ method,
642
+ headers,
643
+ body,
644
+ deadline,
645
+ baseUrl
646
+ }) {
647
+ const controller = new AbortController();
648
+ let abortedBy = null;
649
+ const timer = setTimeout(() => {
650
+ abortedBy ??= "timeout";
651
+ controller.abort();
652
+ }, deadline);
653
+ const onInterrupt = () => {
654
+ abortedBy ??= "interrupt";
655
+ controller.abort();
656
+ };
657
+ process.once("SIGINT", onInterrupt);
658
+ try {
659
+ const response = await fetch(url, {
660
+ method,
661
+ headers,
662
+ signal: controller.signal,
663
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
664
+ });
665
+ return [response, await response.text()];
666
+ } catch (cause) {
667
+ if (abortedBy === "interrupt") {
668
+ throw new CliError(`${method} ${url.pathname} interrupted.`, 130, {
669
+ code: CLI_ERROR_CODES.INTERRUPTED
670
+ });
671
+ }
672
+ if (abortedBy === "timeout") {
673
+ throw new CliError(
674
+ `${method} ${url.pathname} timed out after ${deadline}ms. Raise HEPHA_TIMEOUT_MS if the platform is simply slow.`,
675
+ 1,
676
+ { code: CLI_ERROR_CODES.TIMEOUT }
612
677
  );
613
- const version = manifest.version;
614
- if (typeof version === "string" && version.length > 0) {
615
- return version;
616
- }
617
- } catch {
618
678
  }
619
- const parent = dirname2(directory);
620
- if (parent === directory) {
621
- break;
679
+ throw new CliError(
680
+ `Cannot reach ${baseUrl}. Set HEPHA_URL if the platform lives elsewhere. (${String(cause)})`,
681
+ 1,
682
+ { code: CLI_ERROR_CODES.NETWORK }
683
+ );
684
+ } finally {
685
+ clearTimeout(timer);
686
+ process.removeListener("SIGINT", onInterrupt);
687
+ }
688
+ }
689
+
690
+ // src/lib/api/add-query-parameters.ts
691
+ function addQueryParameters(url, query) {
692
+ for (const [key, value] of Object.entries(query ?? {})) {
693
+ if (value !== void 0) {
694
+ url.searchParams.set(key, String(value));
622
695
  }
623
- directory = parent;
624
696
  }
625
- return "unknown";
626
697
  }
627
- var VERSION = readVersion();
628
698
 
629
- // src/lib/expect-shape.ts
699
+ // src/lib/excerpt/excerpt.ts
700
+ function excerpt(text) {
701
+ const flattened = out.cell(text);
702
+ return flattened.length > 200 ? `${flattened.slice(0, 200)}\u2026` : flattened;
703
+ }
704
+
705
+ // src/lib/api/parse-response-payload.ts
706
+ function parseResponsePayload(response, text, request) {
707
+ const { method, url, retryAfter } = request;
708
+ if (!text) {
709
+ return null;
710
+ }
711
+ try {
712
+ return JSON.parse(text);
713
+ } catch {
714
+ const body = excerpt(text);
715
+ const contentType = response.headers.get("content-type") ?? "unknown";
716
+ throw new CliError(
717
+ response.ok ? `${method} ${url.pathname} answered ${response.status} with a non-JSON body (${contentType}). Is HEPHA_URL pointing at a Hephaistos deployment?${body ? ` Body: ${body}` : ""}` : `${method} ${url.pathname} failed with ${response.status} and a non-JSON body (${contentType}).${body ? ` Body: ${body}` : ""}`,
718
+ 1,
719
+ {
720
+ status: response.status,
721
+ code: CLI_ERROR_CODES.NON_JSON_RESPONSE,
722
+ ...retryAfter === void 0 ? {} : { retryAfterSeconds: retryAfter }
723
+ }
724
+ );
725
+ }
726
+ }
727
+
728
+ // src/lib/issues/read-issues.ts
729
+ function readIssues(payload) {
730
+ if (!payload || typeof payload !== "object") {
731
+ return void 0;
732
+ }
733
+ const data = payload.data;
734
+ if (!data || typeof data !== "object") {
735
+ return void 0;
736
+ }
737
+ const issues = data.issues;
738
+ return Array.isArray(issues) ? issues : void 0;
739
+ }
740
+
741
+ // src/lib/string/read-string.ts
742
+ function readString(payload, key) {
743
+ if (!payload || typeof payload !== "object") {
744
+ return void 0;
745
+ }
746
+ const value = payload[key];
747
+ return typeof value === "string" && value.length > 0 ? value : void 0;
748
+ }
749
+
750
+ // src/lib/message/message-from.ts
751
+ function messageFrom(status, payload) {
752
+ const message = readString(payload, "message");
753
+ if (message) {
754
+ return status === 403 && process.env.HEPHA_API_KEY ? `${message} (HEPHA_API_KEY is set and takes precedence over your \`hepha login\` session \u2014 unset it to act as yourself.)` : message;
755
+ }
756
+ if (status === 401) {
757
+ return "Unauthorized \u2014 your credential is missing, expired or revoked.";
758
+ }
759
+ return `Request failed with status ${status}.`;
760
+ }
761
+
762
+ // src/lib/api/throw-response-error.ts
763
+ function throwResponseError(response, payload, retryAfter) {
764
+ const code = readString(payload, "code") ?? readString(payload, "error");
765
+ const issues = readIssues(payload);
766
+ throw new CliError(
767
+ messageFrom(response.status, payload) + (response.status === 429 && retryAfter !== void 0 ? ` Retry in ${retryAfter}s.` : ""),
768
+ 1,
769
+ {
770
+ status: response.status,
771
+ ...code ? { code } : {},
772
+ ...issues ? { issues } : {},
773
+ ...retryAfter === void 0 ? {} : { retryAfterSeconds: retryAfter }
774
+ }
775
+ );
776
+ }
777
+
778
+ // src/lib/api/api.ts
779
+ async function api(requestPath, options = {}) {
780
+ const baseUrl = options.as?.baseUrl ?? config.load().baseUrl;
781
+ const url = new URL(`${baseUrl.replace(/\/$/, "")}/api/v1${requestPath}`);
782
+ addQueryParameters(url, options.query);
783
+ const headers = {
784
+ ...authHeaders(options.as),
785
+ ...options.body === void 0 ? {} : { "content-type": "application/json" }
786
+ };
787
+ const method = options.method ?? "GET";
788
+ const deadline = resolveTimeout(options.timeoutMs);
789
+ const [response, text] = await fetchResponse({
790
+ url,
791
+ method,
792
+ headers,
793
+ body: options.body,
794
+ deadline,
795
+ baseUrl
796
+ });
797
+ const retryAfter = retryAfterSeconds(response);
798
+ const payload = parseResponsePayload(response, text, {
799
+ method,
800
+ url,
801
+ retryAfter
802
+ });
803
+ if (!response.ok) {
804
+ throwResponseError(response, payload, retryAfter);
805
+ }
806
+ return payload;
807
+ }
808
+
809
+ // src/lib/path/path.ts
810
+ function path(strings, ...values) {
811
+ return strings.reduce(
812
+ (accumulator, literal, index) => accumulator + (index === 0 ? "" : encodeURIComponent(values[index - 1] ?? "")) + literal,
813
+ ""
814
+ );
815
+ }
816
+
817
+ // src/lib/read/read.ts
630
818
  function read(payload, dotted) {
631
819
  return dotted.split(".").reduce((current, key) => {
632
820
  if (!current || typeof current !== "object") {
@@ -635,6 +823,8 @@ function read(payload, dotted) {
635
823
  return current[key];
636
824
  }, payload);
637
825
  }
826
+
827
+ // src/lib/expect/expect-shape.ts
638
828
  function expectShape(payload, endpoint, requirements) {
639
829
  for (const [field, predicate] of Object.entries(requirements)) {
640
830
  if (!predicate(read(payload, field))) {
@@ -648,7 +838,7 @@ function expectShape(payload, endpoint, requirements) {
648
838
  return payload;
649
839
  }
650
840
 
651
- // src/lib/is.ts
841
+ // src/lib/is/is.ts
652
842
  var is = {
653
843
  string: (value) => typeof value === "string",
654
844
  number: (value) => typeof value === "number",
@@ -656,36 +846,42 @@ var is = {
656
846
  object: (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value)
657
847
  };
658
848
 
659
- // src/commands/approvals.ts
849
+ // src/commands/approvals/approvals-constants.ts
660
850
  var ACTIONS = ["list", "show", "approve", "reject"];
661
851
  var REJECT_MODES = ["rework", "terminate"];
852
+
853
+ // src/commands/approvals/list-approvals.ts
854
+ async function listApprovals(asJson) {
855
+ const result = expectShape(
856
+ await api("/approvals"),
857
+ "GET /approvals",
858
+ { items: is.array }
859
+ );
860
+ if (asJson) {
861
+ out.json(result);
862
+ return;
863
+ }
864
+ if (result.items.length === 0) {
865
+ out.line("No guardrail waiting on you.");
866
+ return;
867
+ }
868
+ out.table(
869
+ result.items.map((item) => [
870
+ out.cell(item.id),
871
+ out.cell(item.guardrailKind),
872
+ out.cell(item.repoName ?? "\u2014"),
873
+ out.cell(item.summary)
874
+ ])
875
+ );
876
+ }
877
+
878
+ // src/commands/command/approvals-command.ts
662
879
  async function approvalsCommand(parsed) {
663
880
  const action = args.action(parsed, "approvals", ACTIONS, "list");
664
881
  const asJson = parsed.booleans.has("json");
665
882
  if (action === "list") {
666
883
  args.expect(parsed, { positional: 1 });
667
- const result2 = expectShape(
668
- await api("/approvals"),
669
- "GET /approvals",
670
- { items: is.array }
671
- );
672
- if (asJson) {
673
- out.json(result2);
674
- return;
675
- }
676
- if (result2.items.length === 0) {
677
- out.line("No guardrail waiting on you.");
678
- return;
679
- }
680
- out.table(
681
- result2.items.map((item) => [
682
- out.cell(item.id),
683
- out.cell(item.guardrailKind),
684
- out.cell(item.repoName ?? "\u2014"),
685
- out.cell(item.summary)
686
- ])
687
- );
688
- return;
884
+ return await listApprovals(asJson);
689
885
  }
690
886
  args.expect(parsed, {
691
887
  positional: 2,
@@ -748,169 +944,161 @@ async function approvalsCommand(parsed) {
748
944
  );
749
945
  }
750
946
 
751
- // src/commands/catalog.ts
752
- async function catalogCommand(topic, parsed) {
753
- const asJson = parsed.booleans.has("json");
754
- if (topic === "repos") {
755
- args.expect(parsed, {});
756
- const result = expectShape(
757
- await api("/repos"),
758
- "GET /repos",
759
- { repos: is.array }
947
+ // src/commands/catalog/list-catalog-branches.ts
948
+ async function listCatalogBranches(parsed, asJson) {
949
+ args.expect(parsed, { positional: 1 });
950
+ const repoFullName = args.required(parsed, 0, "owner/repo");
951
+ const segments = repoFullName.split("/");
952
+ const [owner, repo] = segments;
953
+ if (segments.length !== 2 || !owner || !repo) {
954
+ throw new CliError(
955
+ `Expected a repository as "owner/repo", got "${repoFullName}".`,
956
+ 2
760
957
  );
761
- if (asJson) {
762
- out.json(result);
763
- return;
764
- }
765
- for (const repo of result.repos) {
766
- out.line(out.cell(repo));
767
- }
768
- return;
769
958
  }
770
- if (topic === "branches") {
771
- args.expect(parsed, { positional: 1 });
772
- const repoFullName = args.required(parsed, 0, "owner/repo");
773
- const segments = repoFullName.split("/");
774
- const [owner, repo] = segments;
775
- if (segments.length !== 2 || !owner || !repo) {
776
- throw new CliError(
777
- `Expected a repository as "owner/repo", got "${repoFullName}".`,
778
- 2
779
- );
780
- }
781
- const result = await api(
782
- path`/repos/${owner}/${repo}/branches`
783
- );
784
- if (!result.result) {
785
- throw new CliError(`No GitHub installation covers ${repoFullName}.`);
786
- }
787
- if (asJson) {
788
- out.json(result);
789
- return;
790
- }
791
- for (const branch of result.result.branches) {
792
- out.line(
793
- branch === result.result.defaultBranch ? `${out.cell(branch)} ${out.paint("(default)", "dim")}` : out.cell(branch)
794
- );
795
- }
959
+ const result = await api(
960
+ path`/repos/${owner}/${repo}/branches`
961
+ );
962
+ if (!result.result) {
963
+ throw new CliError(`No GitHub installation covers ${repoFullName}.`);
964
+ }
965
+ if (asJson) {
966
+ out.json(result);
796
967
  return;
797
968
  }
798
- if (topic === "models") {
799
- args.expect(parsed, {});
800
- const result = expectShape(
801
- await api("/models"),
802
- "GET /models",
803
- { harnesses: is.array, gatewayModels: is.array }
969
+ for (const branch of result.result.branches) {
970
+ out.line(
971
+ branch === result.result.defaultBranch ? `${out.cell(branch)} ${out.paint("(default)", "dim")}` : out.cell(branch)
804
972
  );
805
- if (asJson) {
806
- out.json(result);
807
- return;
808
- }
809
- out.line(out.paint("harnesses", "dim"));
810
- for (const harness of result.harnesses) {
811
- out.line(` ${out.cell(harness)}`);
812
- }
813
- out.line();
814
- out.line(out.paint("gateway models", "dim"));
815
- for (const model of result.gatewayModels) {
816
- out.line(` ${out.cell(model.id)}`);
817
- }
973
+ }
974
+ }
975
+
976
+ // src/commands/catalog/list-catalog-models.ts
977
+ async function listCatalogModels(parsed, asJson) {
978
+ args.expect(parsed, {});
979
+ const result = expectShape(
980
+ await api("/models"),
981
+ "GET /models",
982
+ { harnesses: is.array, gatewayModels: is.array }
983
+ );
984
+ if (asJson) {
985
+ out.json(result);
818
986
  return;
819
987
  }
820
- if (topic === "skills") {
821
- args.expect(parsed, {});
822
- const result = expectShape(
823
- await api("/skills"),
824
- "GET /skills",
825
- { skills: is.array }
826
- );
827
- if (asJson) {
828
- out.json(result);
829
- return;
830
- }
831
- out.table(
832
- result.skills.map((skill) => [
833
- out.cell(skill.id),
834
- out.cell(skill.name),
835
- out.cell(skill.description ?? "")
836
- ])
837
- );
988
+ out.line(out.paint("harnesses", "dim"));
989
+ for (const harness of result.harnesses) {
990
+ out.line(` ${out.cell(harness)}`);
991
+ }
992
+ out.line();
993
+ out.line(out.paint("gateway models", "dim"));
994
+ for (const model of result.gatewayModels) {
995
+ out.line(` ${out.cell(model.id)}`);
996
+ }
997
+ }
998
+
999
+ // src/commands/catalog/read-catalog-list.ts
1000
+ async function readCatalogList(parsed, path2, field) {
1001
+ args.expect(parsed, {});
1002
+ return expectShape(await api(path2), `GET ${path2}`, { [field]: is.array });
1003
+ }
1004
+
1005
+ // src/commands/catalog/list-catalog-repos.ts
1006
+ async function listCatalogRepos(parsed, asJson) {
1007
+ const result = await readCatalogList(
1008
+ parsed,
1009
+ "/repos",
1010
+ "repos"
1011
+ );
1012
+ if (asJson) {
1013
+ out.json(result);
838
1014
  return;
839
1015
  }
840
- throw new CliError(`Unknown catalog topic "${topic}".`, 2);
1016
+ for (const repo of result.repos) {
1017
+ out.line(out.cell(repo));
1018
+ }
841
1019
  }
842
1020
 
843
- // src/commands/help.ts
844
- var HELP = `hepha \u2014 orchestrate Hephaistos sessions from a terminal or an agent.
845
-
846
- USAGE
847
- hepha <command> [args] [--flags]
848
-
849
- THE LOOP
850
- hepha run "<prompt>" --repo owner/repo --follow
851
- Start a session and follow it until it finishes or asks you something.
852
- Exit 0 completed \xB7 3 waiting on a human \xB7 1 failed.
853
- [--title "\u2026"] [--task-type implement|new-project|question|draft|orchestrator]
854
- [--model <slug>] [--harness claude-code|codex] [--branch <name>]
855
- [--skill <id>]\u2026
856
- hepha questions list [--session <id>]
857
- hepha questions show <batchId>
858
- hepha questions answer <batchId> --option <questionId>=1,2 --text <questionId>="\u2026"
859
- [--answers '<json array>']
860
- hepha questions cancel <batchId>
861
- hepha approvals list | show <id> | approve <id> | reject <id> --mode rework --feedback "\u2026"
862
-
863
- SESSIONS
864
- hepha sessions list [--status active|waiting|done] [--repo owner/repo]
865
- [--search "\u2026"] [--page N] [--limit N] [--all]
866
- hepha sessions get <id>
867
- hepha sessions logs <id> [--after <sequence>] [--limit N] [--follow]
868
- hepha sessions send <id> "<message>"
869
- hepha sessions pause|resume|archive <id>
870
- hepha sessions kill <id> [--reason "\u2026"]
871
- hepha sessions checks|pr|verify <id>
872
-
873
- CATALOG
874
- hepha repos
875
- hepha branches <owner/repo>
876
- hepha models
877
- hepha skills
878
-
879
- AUTH
880
- hepha login [--url https://\u2026] Device authorization: approve it in a browser.
881
- hepha logout
882
- hepha whoami
883
- hepha keys list
884
- hepha keys create <name> [--scope sessions:read]\u2026 [--expires-in-days 90]
885
- hepha keys revoke <keyId>
1021
+ // src/commands/catalog/list-catalog-skills.ts
1022
+ async function listCatalogSkills(parsed, asJson) {
1023
+ const result = await readCatalogList(
1024
+ parsed,
1025
+ "/skills",
1026
+ "skills"
1027
+ );
1028
+ if (asJson) {
1029
+ out.json(result);
1030
+ return;
1031
+ }
1032
+ out.table(
1033
+ result.skills.map((skill) => [
1034
+ out.cell(skill.id),
1035
+ out.cell(skill.name),
1036
+ out.cell(skill.description ?? "")
1037
+ ])
1038
+ );
1039
+ }
886
1040
 
887
- ENVIRONMENT
888
- HEPHA_API_KEY Act as an agent. Takes precedence over a stored login, so
889
- the human-only commands (approvals approve/reject, keys)
890
- answer 403 while it is set \u2014 unset it to act as yourself.
891
- HEPHA_TOKEN A device-flow session token, instead of ~/.hepha/config.json.
892
- HEPHA_URL Platform base URL.
893
- HEPHA_TIMEOUT_MS Per-request timeout in milliseconds (default 30000).
1041
+ // src/commands/catalog/catalog-topics.ts
1042
+ var catalogTopics = {
1043
+ branches: listCatalogBranches,
1044
+ models: listCatalogModels,
1045
+ repos: listCatalogRepos,
1046
+ skills: listCatalogSkills
1047
+ };
894
1048
 
895
- GLOBAL FLAGS
896
- --json Machine-readable output on stdout. Every command that
897
- returns data honours it; login is interactive and does not.
898
- Failures print {"ok":false,"error":{\u2026}} and keep their exit code.
899
- --quiet Suppress the streamed transcript while following a session.
900
- --version Print the version and exit.
901
- --help Print this.
1049
+ // src/commands/command/catalog-command.ts
1050
+ async function catalogCommand(topic, parsed) {
1051
+ const list = catalogTopics[topic];
1052
+ if (!list) {
1053
+ throw new CliError(`Unknown catalog topic "${topic}".`, 2);
1054
+ }
1055
+ await list(parsed, parsed.booleans.has("json"));
1056
+ }
902
1057
 
903
- Unknown flags and surplus arguments are refused: for an agent, a typo must
904
- fail rather than run with default options.
1058
+ // src/commands/keys/keys-constants.ts
1059
+ var ACTIONS2 = ["list", "create", "revoke"];
905
1060
 
906
- `;
907
- function helpCommand() {
908
- const baseUrl = config.load().baseUrl.replace(/\/$/, "");
909
- out.line(`${HELP}hepha ${VERSION} \xB7 full reference: ${baseUrl}/api/v1/docs`);
1061
+ // src/commands/key/create-key.ts
1062
+ async function createKey(parsed, asJson) {
1063
+ args.expect(parsed, { flags: ["scope", "expires-in-days"], positional: 2 });
1064
+ const name = args.required(parsed, 1, "name");
1065
+ const scopes = parsed.repeated.scope;
1066
+ const expiresInDays = args.integer(parsed, "expires-in-days", {
1067
+ min: 1,
1068
+ max: 365
1069
+ });
1070
+ const created = expectShape(
1071
+ await api("/keys", {
1072
+ method: "POST",
1073
+ body: {
1074
+ name,
1075
+ ...scopes && scopes.length > 0 ? { scopes } : {},
1076
+ ...expiresInDays !== void 0 ? { expiresInDays } : {}
1077
+ }
1078
+ }),
1079
+ "POST /keys",
1080
+ { key: is.string, scopes: is.array }
1081
+ );
1082
+ if (asJson) {
1083
+ out.json(created);
1084
+ return;
1085
+ }
1086
+ out.line(
1087
+ `${out.paint("\u2713", "green")} Created "${out.cell(created.name ?? name)}"`
1088
+ );
1089
+ out.line();
1090
+ out.line(` ${out.paint(created.key, "bold")}`);
1091
+ out.line();
1092
+ out.line(
1093
+ out.paint(
1094
+ " Copy it now \u2014 it is hashed at rest and never shown again.",
1095
+ "dim"
1096
+ )
1097
+ );
1098
+ out.line(out.paint(` Scopes: ${created.scopes.join(" ")}`, "dim"));
910
1099
  }
911
1100
 
912
- // src/commands/keys.ts
913
- var ACTIONS2 = ["list", "create", "revoke"];
1101
+ // src/commands/command/keys-command.ts
914
1102
  async function keysCommand(parsed) {
915
1103
  const action = args.action(parsed, "keys", ACTIONS2, "list");
916
1104
  const asJson = parsed.booleans.has("json");
@@ -946,46 +1134,7 @@ async function keysCommand(parsed) {
946
1134
  return;
947
1135
  }
948
1136
  if (action === "create") {
949
- args.expect(parsed, {
950
- flags: ["scope", "expires-in-days"],
951
- positional: 2
952
- });
953
- const name = args.required(parsed, 1, "name");
954
- const scopes = parsed.repeated.scope;
955
- const expiresInDays = args.integer(parsed, "expires-in-days", {
956
- min: 1,
957
- max: 365
958
- });
959
- const created = expectShape(
960
- await api("/keys", {
961
- method: "POST",
962
- body: {
963
- name,
964
- ...scopes && scopes.length > 0 ? { scopes } : {},
965
- ...expiresInDays !== void 0 ? { expiresInDays } : {}
966
- }
967
- }),
968
- "POST /keys",
969
- { key: is.string, scopes: is.array }
970
- );
971
- if (asJson) {
972
- out.json(created);
973
- return;
974
- }
975
- out.line(
976
- `${out.paint("\u2713", "green")} Created "${out.cell(created.name ?? name)}"`
977
- );
978
- out.line();
979
- out.line(` ${out.paint(created.key, "bold")}`);
980
- out.line();
981
- out.line(
982
- out.paint(
983
- " Copy it now \u2014 it is hashed at rest and never shown again.",
984
- "dim"
985
- )
986
- );
987
- out.line(out.paint(` Scopes: ${created.scopes.join(" ")}`, "dim"));
988
- return;
1137
+ return await createKey(parsed, asJson);
989
1138
  }
990
1139
  args.expect(parsed, { positional: 2 });
991
1140
  const keyId = args.required(parsed, 1, "keyId");
@@ -999,55 +1148,11 @@ async function keysCommand(parsed) {
999
1148
  out.line(`${out.paint("\u2713", "green")} Revoked ${out.cell(keyId)}`);
1000
1149
  }
1001
1150
 
1002
- // src/commands/login.ts
1151
+ // src/commands/command/login-command.ts
1003
1152
  import { createAuthClient } from "better-auth/client";
1004
1153
  import { deviceAuthorizationClient } from "better-auth/client/plugins";
1005
1154
 
1006
- // ../../packages/database/constants.ts
1007
- var USER_ROLES = {
1008
- ADMIN: "admin",
1009
- MEMBER: "member"
1010
- };
1011
- var ALL_USER_ROLES = Object.values(USER_ROLES);
1012
- var API_SCOPES = [
1013
- "sessions:read",
1014
- "sessions:write",
1015
- "questions:read",
1016
- "questions:answer",
1017
- "approvals:read",
1018
- "catalog:read",
1019
- "pull_requests:read"
1020
- ];
1021
- var DEFAULT_API_SCOPES = API_SCOPES.filter(
1022
- (scope) => scope.endsWith(":read")
1023
- );
1024
- var SESSION = {
1025
- /** Cookie + session row lifetime: 30 days. */
1026
- EXPIRES_IN: 60 * 60 * 24 * 30,
1027
- /** Refresh the expiry at most once a day, to avoid a write per request. */
1028
- UPDATE_AGE: 60 * 60 * 24,
1029
- /** Cache the session row in memory for this long, to avoid a read per request. */
1030
- COOKIE_CACHE_MAX_AGE: 60
1031
- };
1032
- var DEVICE_AUTHORIZATION = {
1033
- /** Only these clients may open a device flow. */
1034
- CLIENT_IDS: ["hepha-cli"],
1035
- /** Lifetime of an unapproved user code. */
1036
- EXPIRES_IN: "15m",
1037
- /** Minimum delay the CLI must wait between two token polls. */
1038
- INTERVAL: "5s",
1039
- /** Page a human visits to approve a pending device. */
1040
- VERIFICATION_PATH: "/device",
1041
- /**
1042
- * Per-IP ceiling on `/device/token`. The window is deliberately SHORTER than
1043
- * INTERVAL: Better Auth only resets its counter when two requests are further
1044
- * apart than the window, so a 5s poll under the default 60s window never reset
1045
- * it and every login 429'd partway through the code's lifetime.
1046
- */
1047
- POLL_RATE_LIMIT: { WINDOW_SECONDS: 4, MAX_REQUESTS: 10 }
1048
- };
1049
-
1050
- // src/lib/device-token-error.ts
1155
+ // src/lib/token/device-token-error.ts
1051
1156
  var deviceTokenError = {
1052
1157
  /** What the server said, when it said anything at all. */
1053
1158
  message(error) {
@@ -1075,7 +1180,7 @@ var deviceTokenError = {
1075
1180
  }
1076
1181
  };
1077
1182
 
1078
- // src/commands/login-backoff.ts
1183
+ // src/commands/next/next-poll-delay.ts
1079
1184
  function nextPollDelay(interval, remainingSeconds) {
1080
1185
  if (remainingSeconds <= 0) {
1081
1186
  return null;
@@ -1083,7 +1188,7 @@ function nextPollDelay(interval, remainingSeconds) {
1083
1188
  return Math.min(interval, remainingSeconds);
1084
1189
  }
1085
1190
 
1086
- // src/lib/open-browser.ts
1191
+ // src/lib/browser/open-browser.ts
1087
1192
  import { spawn } from "child_process";
1088
1193
  function openBrowser(url) {
1089
1194
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
@@ -1100,19 +1205,95 @@ function openBrowser(url) {
1100
1205
  }
1101
1206
  }
1102
1207
 
1103
- // src/commands/login.ts
1104
- var CLIENT_ID = DEVICE_AUTHORIZATION.CLIENT_IDS[0];
1208
+ // src/commands/login/login-constants.ts
1105
1209
  var MIN_BACKOFF = 10;
1210
+
1211
+ // ../../packages/database/shared/constants.ts
1212
+ var USER_ROLES = {
1213
+ ADMIN: "admin",
1214
+ MEMBER: "member"
1215
+ };
1216
+ var ALL_USER_ROLES = Object.values(USER_ROLES);
1217
+ var API_SCOPES = [
1218
+ "sessions:read",
1219
+ "sessions:write",
1220
+ "questions:read",
1221
+ "questions:answer",
1222
+ "approvals:read",
1223
+ "catalog:read",
1224
+ "pull_requests:read"
1225
+ ];
1226
+ var DEFAULT_API_SCOPES = API_SCOPES.filter(
1227
+ (scope) => scope.endsWith(":read")
1228
+ );
1229
+ var SESSION = {
1230
+ /** Cookie + session row lifetime: 30 days. */
1231
+ EXPIRES_IN: 60 * 60 * 24 * 30,
1232
+ /** Refresh the expiry at most once a day, to avoid a write per request. */
1233
+ UPDATE_AGE: 60 * 60 * 24,
1234
+ /** Cache the session row in memory for this long, to avoid a read per request. */
1235
+ COOKIE_CACHE_MAX_AGE: 60
1236
+ };
1237
+ var DEVICE_AUTHORIZATION = {
1238
+ /** Only these clients may open a device flow. */
1239
+ CLIENT_IDS: ["hepha-cli"],
1240
+ /** Lifetime of an unapproved user code. */
1241
+ EXPIRES_IN: "15m",
1242
+ /** Minimum delay the CLI must wait between two token polls. */
1243
+ INTERVAL: "5s",
1244
+ /** Page a human visits to approve a pending device. */
1245
+ VERIFICATION_PATH: "/device",
1246
+ /**
1247
+ * Per-IP ceiling on `/device/token`. The window is deliberately SHORTER than
1248
+ * INTERVAL: Better Auth only resets its counter when two requests are further
1249
+ * apart than the window, so a 5s poll under the default 60s window never reset
1250
+ * it and every login 429'd partway through the code's lifetime.
1251
+ */
1252
+ POLL_RATE_LIMIT: { WINDOW_SECONDS: 4, MAX_REQUESTS: 10 }
1253
+ };
1254
+
1255
+ // src/commands/client/client-id-constants.ts
1256
+ var CLIENT_ID = DEVICE_AUTHORIZATION.CLIENT_IDS[0];
1257
+
1258
+ // src/commands/sleep/sleep.ts
1106
1259
  function sleep(seconds) {
1107
1260
  return new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
1108
1261
  }
1109
- async function loginCommand(parsed) {
1110
- args.expect(parsed, { flags: ["url"] });
1262
+
1263
+ // src/commands/complete/complete-login.ts
1264
+ async function completeLogin(baseUrl, token) {
1265
+ const identity = await api("/me", {
1266
+ as: { baseUrl, token }
1267
+ });
1268
+ config.save({
1269
+ baseUrl,
1270
+ token,
1271
+ user: {
1272
+ id: identity.user.id,
1273
+ name: identity.user.name,
1274
+ email: identity.user.email
1275
+ }
1276
+ });
1277
+ out.line();
1278
+ out.line(
1279
+ `${out.paint("\u2713", "green")} Signed in as ${identity.user.email} on ${baseUrl}`
1280
+ );
1281
+ out.line(out.paint(` Session stored in ${config.path}`, "dim"));
1282
+ }
1283
+
1284
+ // src/commands/login/resolve-login-base-url.ts
1285
+ function resolveLoginBaseUrl(parsed) {
1111
1286
  const requested = parsed.flags.url?.trim();
1112
1287
  if (requested !== void 0 && !isHttpUrl(requested)) {
1113
1288
  throw new CliError(`--url is not a valid http(s) URL: "${requested}".`, 2);
1114
1289
  }
1115
- const baseUrl = (requested ?? config.load().baseUrl).replace(/\/$/, "");
1290
+ return (requested ?? config.load().baseUrl).replace(/\/$/, "");
1291
+ }
1292
+
1293
+ // src/commands/command/login-command.ts
1294
+ async function loginCommand(parsed) {
1295
+ args.expect(parsed, { flags: ["url"] });
1296
+ const baseUrl = resolveLoginBaseUrl(parsed);
1116
1297
  const authClient = createAuthClient({
1117
1298
  baseURL: baseUrl,
1118
1299
  plugins: [deviceAuthorizationClient()]
@@ -1154,23 +1335,7 @@ async function loginCommand(parsed) {
1154
1335
  continue;
1155
1336
  }
1156
1337
  if (token?.access_token) {
1157
- const identity = await api("/me", {
1158
- as: { baseUrl, token: token.access_token }
1159
- });
1160
- config.save({
1161
- baseUrl,
1162
- token: token.access_token,
1163
- user: {
1164
- id: identity.user.id,
1165
- name: identity.user.name,
1166
- email: identity.user.email
1167
- }
1168
- });
1169
- out.line();
1170
- out.line(
1171
- `${out.paint("\u2713", "green")} Signed in as ${identity.user.email} on ${baseUrl}`
1172
- );
1173
- out.line(out.paint(` Session stored in ${config.path}`, "dim"));
1338
+ await completeLogin(baseUrl, token.access_token);
1174
1339
  return;
1175
1340
  }
1176
1341
  if (!error) {
@@ -1193,7 +1358,7 @@ async function loginCommand(parsed) {
1193
1358
  throw new CliError("The code expired before it was approved. Try again.");
1194
1359
  }
1195
1360
 
1196
- // src/commands/logout.ts
1361
+ // src/commands/command/logout-command.ts
1197
1362
  function logoutCommand(parsed) {
1198
1363
  args.expect(parsed, {});
1199
1364
  config.clear();
@@ -1204,7 +1369,7 @@ function logoutCommand(parsed) {
1204
1369
  out.line("Signed out.");
1205
1370
  }
1206
1371
 
1207
- // src/commands/collect-answers.ts
1372
+ // src/commands/answers/collect-answers-constants.ts
1208
1373
  var LIMITS = {
1209
1374
  QUESTION_ID: 40,
1210
1375
  OPTION_ID: 40,
@@ -1212,48 +1377,62 @@ var LIMITS = {
1212
1377
  FREE_TEXT: 1e4,
1213
1378
  ANSWERS: 50
1214
1379
  };
1215
- function validateAnswer(answer, label) {
1216
- if (answer.questionId.trim().length === 0) {
1217
- throw new CliError(`${label} needs a non-empty "questionId".`, 2);
1380
+
1381
+ // src/commands/answer/validate-free-text.ts
1382
+ function validateFreeText(value, label) {
1383
+ if (value === void 0) {
1384
+ return;
1218
1385
  }
1219
- if (answer.questionId.length > LIMITS.QUESTION_ID) {
1386
+ const freeText = value.trim();
1387
+ if (freeText.length === 0) {
1388
+ throw new CliError(`${label} has empty free text.`, 2);
1389
+ }
1390
+ if (freeText.length > LIMITS.FREE_TEXT) {
1220
1391
  throw new CliError(
1221
- `${label} has a "questionId" longer than ${LIMITS.QUESTION_ID} characters.`,
1392
+ `${label} has free text longer than ${LIMITS.FREE_TEXT} characters.`,
1222
1393
  2
1223
1394
  );
1224
1395
  }
1225
- const options = answer.selectedOptionIds;
1226
- if (options !== void 0) {
1227
- if (options.length > LIMITS.OPTIONS_PER_ANSWER) {
1228
- throw new CliError(
1229
- `${label} selects more than ${LIMITS.OPTIONS_PER_ANSWER} options.`,
1230
- 2
1231
- );
1232
- }
1233
- for (const id of options) {
1234
- if (id.trim().length === 0) {
1235
- throw new CliError(`${label} has an empty option id.`, 2);
1236
- }
1237
- if (id.trim().length > LIMITS.OPTION_ID) {
1238
- throw new CliError(
1239
- `${label} has an option id longer than ${LIMITS.OPTION_ID} characters.`,
1240
- 2
1241
- );
1242
- }
1243
- }
1396
+ }
1397
+
1398
+ // src/commands/answer/validate-selected-option-ids.ts
1399
+ function validateSelectedOptionIds(options, label) {
1400
+ if (options === void 0) {
1401
+ return;
1244
1402
  }
1245
- if (answer.freeText !== void 0) {
1246
- const freeText = answer.freeText.trim();
1247
- if (freeText.length === 0) {
1248
- throw new CliError(`${label} has empty free text.`, 2);
1403
+ if (options.length > LIMITS.OPTIONS_PER_ANSWER) {
1404
+ throw new CliError(
1405
+ `${label} selects more than ${LIMITS.OPTIONS_PER_ANSWER} options.`,
1406
+ 2
1407
+ );
1408
+ }
1409
+ for (const id of options) {
1410
+ if (id.trim().length === 0) {
1411
+ throw new CliError(`${label} has an empty option id.`, 2);
1249
1412
  }
1250
- if (freeText.length > LIMITS.FREE_TEXT) {
1413
+ if (id.trim().length > LIMITS.OPTION_ID) {
1251
1414
  throw new CliError(
1252
- `${label} has free text longer than ${LIMITS.FREE_TEXT} characters.`,
1415
+ `${label} has an option id longer than ${LIMITS.OPTION_ID} characters.`,
1253
1416
  2
1254
1417
  );
1255
1418
  }
1256
1419
  }
1420
+ }
1421
+
1422
+ // src/commands/answer/validate-answer.ts
1423
+ function validateAnswer(answer, label) {
1424
+ if (answer.questionId.trim().length === 0) {
1425
+ throw new CliError(`${label} needs a non-empty "questionId".`, 2);
1426
+ }
1427
+ if (answer.questionId.length > LIMITS.QUESTION_ID) {
1428
+ throw new CliError(
1429
+ `${label} has a "questionId" longer than ${LIMITS.QUESTION_ID} characters.`,
1430
+ 2
1431
+ );
1432
+ }
1433
+ const options = answer.selectedOptionIds;
1434
+ validateSelectedOptionIds(options, label);
1435
+ validateFreeText(answer.freeText, label);
1257
1436
  if ((options === void 0 || options.length === 0) && answer.freeText === void 0) {
1258
1437
  throw new CliError(
1259
1438
  `${label} answers nothing. Give it at least one option id or some free text.`,
@@ -1261,6 +1440,31 @@ function validateAnswer(answer, label) {
1261
1440
  );
1262
1441
  }
1263
1442
  }
1443
+
1444
+ // src/commands/answers/parse-answer-entry.ts
1445
+ function parseAnswerEntry(entry, label) {
1446
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1447
+ throw new CliError(`${label} must be an object.`, 2);
1448
+ }
1449
+ const answer = entry;
1450
+ if (typeof answer.questionId !== "string") {
1451
+ throw new CliError(`${label} needs a non-empty "questionId".`, 2);
1452
+ }
1453
+ if (answer.selectedOptionIds !== void 0 && (!Array.isArray(answer.selectedOptionIds) || answer.selectedOptionIds.some((id) => typeof id !== "string"))) {
1454
+ throw new CliError(
1455
+ `${label}.selectedOptionIds must be an array of strings.`,
1456
+ 2
1457
+ );
1458
+ }
1459
+ if (answer.freeText !== void 0 && typeof answer.freeText !== "string") {
1460
+ throw new CliError(`${label}.freeText must be a string.`, 2);
1461
+ }
1462
+ const parsed = entry;
1463
+ validateAnswer(parsed, label);
1464
+ return parsed;
1465
+ }
1466
+
1467
+ // src/commands/answers/parse-answers-flag.ts
1264
1468
  function parseAnswersFlag(raw) {
1265
1469
  let decoded;
1266
1470
  try {
@@ -1281,27 +1485,12 @@ function parseAnswersFlag(raw) {
1281
1485
  );
1282
1486
  }
1283
1487
  for (const [index, entry] of decoded.entries()) {
1284
- const label = `--answers[${index}]`;
1285
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1286
- throw new CliError(`${label} must be an object.`, 2);
1287
- }
1288
- const answer = entry;
1289
- if (typeof answer.questionId !== "string") {
1290
- throw new CliError(`${label} needs a non-empty "questionId".`, 2);
1291
- }
1292
- if (answer.selectedOptionIds !== void 0 && (!Array.isArray(answer.selectedOptionIds) || answer.selectedOptionIds.some((id) => typeof id !== "string"))) {
1293
- throw new CliError(
1294
- `${label}.selectedOptionIds must be an array of strings.`,
1295
- 2
1296
- );
1297
- }
1298
- if (answer.freeText !== void 0 && typeof answer.freeText !== "string") {
1299
- throw new CliError(`${label}.freeText must be a string.`, 2);
1300
- }
1301
- validateAnswer(entry, label);
1488
+ parseAnswerEntry(entry, `--answers[${index}]`);
1302
1489
  }
1303
1490
  return decoded;
1304
1491
  }
1492
+
1493
+ // src/commands/answers/collect-answers.ts
1305
1494
  function collectAnswers(parsed) {
1306
1495
  if (parsed.flags.answers) {
1307
1496
  return parseAnswersFlag(parsed.flags.answers);
@@ -1346,8 +1535,61 @@ function collectAnswers(parsed) {
1346
1535
  return answers;
1347
1536
  }
1348
1537
 
1349
- // src/commands/questions.ts
1538
+ // src/commands/questions/questions-constants.ts
1350
1539
  var ACTIONS3 = ["list", "show", "answer", "cancel"];
1540
+
1541
+ // src/commands/questions/list-questions.ts
1542
+ async function listQuestions(sessionId, asJson) {
1543
+ const result = expectShape(
1544
+ await api(
1545
+ sessionId ? path`/sessions/${sessionId}/questions` : "/questions"
1546
+ ),
1547
+ "GET /questions",
1548
+ { items: is.array }
1549
+ );
1550
+ if (asJson) {
1551
+ out.json(result);
1552
+ return;
1553
+ }
1554
+ if (result.items.length === 0) {
1555
+ out.line("Nothing waiting on you.");
1556
+ return;
1557
+ }
1558
+ out.table(
1559
+ result.items.map((batch) => [
1560
+ out.cell(batch.id),
1561
+ out.cell(batch.sessionId),
1562
+ `${batch.questions.length} question(s)`,
1563
+ out.cell(batch.sessionTitle)
1564
+ ])
1565
+ );
1566
+ }
1567
+
1568
+ // src/commands/batch/print-batch-question.ts
1569
+ function printBatchQuestion(question) {
1570
+ out.line();
1571
+ if (question.header) {
1572
+ out.line(out.paint(out.cell(question.header), "dim"));
1573
+ }
1574
+ out.line(
1575
+ `${out.paint(out.cell(question.id), "dim")} ${out.safe(question.body)}`
1576
+ );
1577
+ if (question.image) {
1578
+ out.line(out.paint(` (image) ${out.cell(question.image)}`, "dim"));
1579
+ }
1580
+ for (const option of question.options) {
1581
+ const recommended = option.id === question.recommendedOptionId;
1582
+ const label = option.label ? out.cell(option.label) : option.image ? out.paint(`(image) ${out.cell(option.image)}`, "dim") : out.paint("(no label)", "dim");
1583
+ out.line(
1584
+ ` ${out.cell(option.id)}) ${label}${recommended ? out.paint(" \u2190 recommended", "green") : ""}`
1585
+ );
1586
+ }
1587
+ if (question.allowFreeText) {
1588
+ out.line(out.paint(" free text accepted", "dim"));
1589
+ }
1590
+ }
1591
+
1592
+ // src/commands/batch/print-batch.ts
1351
1593
  function printBatch(batch) {
1352
1594
  out.line(
1353
1595
  `${out.paint(out.cell(batch.sessionTitle), "bold")} ${out.paint(`round ${batch.round}`, "dim")}`
@@ -1359,56 +1601,17 @@ function printBatch(batch) {
1359
1601
  )
1360
1602
  );
1361
1603
  for (const question of batch.questions) {
1362
- out.line();
1363
- if (question.header) {
1364
- out.line(out.paint(out.cell(question.header), "dim"));
1365
- }
1366
- out.line(
1367
- `${out.paint(out.cell(question.id), "dim")} ${out.safe(question.body)}`
1368
- );
1369
- if (question.image) {
1370
- out.line(out.paint(` (image) ${out.cell(question.image)}`, "dim"));
1371
- }
1372
- for (const option of question.options) {
1373
- const recommended = option.id === question.recommendedOptionId;
1374
- const label = option.label ? out.cell(option.label) : option.image ? out.paint(`(image) ${out.cell(option.image)}`, "dim") : out.paint("(no label)", "dim");
1375
- out.line(
1376
- ` ${out.cell(option.id)}) ${label}${recommended ? out.paint(" \u2190 recommended", "green") : ""}`
1377
- );
1378
- }
1379
- if (question.allowFreeText) {
1380
- out.line(out.paint(" free text accepted", "dim"));
1381
- }
1604
+ printBatchQuestion(question);
1382
1605
  }
1383
1606
  }
1607
+
1608
+ // src/commands/command/questions-command.ts
1384
1609
  async function questionsCommand(parsed) {
1385
1610
  const action = args.action(parsed, "questions", ACTIONS3, "list");
1386
1611
  const asJson = parsed.booleans.has("json");
1387
1612
  if (action === "list") {
1388
1613
  args.expect(parsed, { flags: ["session"], positional: 1 });
1389
- const session = parsed.flags.session;
1390
- const result2 = expectShape(
1391
- await api(session ? path`/sessions/${session}/questions` : "/questions"),
1392
- "GET /questions",
1393
- { items: is.array }
1394
- );
1395
- if (asJson) {
1396
- out.json(result2);
1397
- return;
1398
- }
1399
- if (result2.items.length === 0) {
1400
- out.line("Nothing waiting on you.");
1401
- return;
1402
- }
1403
- out.table(
1404
- result2.items.map((batch) => [
1405
- out.cell(batch.id),
1406
- out.cell(batch.sessionId),
1407
- `${batch.questions.length} question(s)`,
1408
- out.cell(batch.sessionTitle)
1409
- ])
1410
- );
1411
- return;
1614
+ return await listQuestions(parsed.flags.session, asJson);
1412
1615
  }
1413
1616
  args.expect(parsed, {
1414
1617
  positional: 2,
@@ -1459,11 +1662,17 @@ async function questionsCommand(parsed) {
1459
1662
  );
1460
1663
  }
1461
1664
 
1462
- // src/lib/format-event.ts
1665
+ // src/lib/follow/follow-constants.ts
1666
+ var POLL_SECONDS = 3;
1667
+ var PAGE_SIZE = 200;
1668
+
1669
+ // src/lib/truncate/truncate.ts
1463
1670
  function truncate(text, max = 2e3) {
1464
1671
  const trimmed = out.safe(text).trim();
1465
1672
  return trimmed.length > max ? `${trimmed.slice(0, max)}\u2026` : trimmed;
1466
1673
  }
1674
+
1675
+ // src/lib/event/format-event.ts
1467
1676
  function formatEvent(event) {
1468
1677
  const part = event.part;
1469
1678
  switch (part.kind) {
@@ -1515,14 +1724,33 @@ function formatEvent(event) {
1515
1724
  }
1516
1725
  }
1517
1726
 
1518
- // src/lib/follow.ts
1727
+ // src/lib/session/consume-session-events.ts
1728
+ function consumeSessionEvents(events, cursor, quiet) {
1729
+ for (const event of events) {
1730
+ cursor = Math.max(cursor, event.sequence);
1731
+ if (quiet) {
1732
+ continue;
1733
+ }
1734
+ const line = formatEvent(event);
1735
+ if (line) {
1736
+ out.line(line);
1737
+ }
1738
+ }
1739
+ return cursor;
1740
+ }
1741
+
1742
+ // src/lib/terminal/terminal-constants.ts
1519
1743
  var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "killed", "abstained"]);
1744
+
1745
+ // src/lib/blocked/blocked-constants.ts
1520
1746
  var BLOCKED = /* @__PURE__ */ new Set(["waiting_human", "awaiting_approval"]);
1521
- var POLL_SECONDS = 3;
1522
- var PAGE_SIZE = 200;
1747
+
1748
+ // src/lib/sleep/sleep.ts
1523
1749
  function sleep2(seconds) {
1524
1750
  return new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
1525
1751
  }
1752
+
1753
+ // src/lib/session/follow-session.ts
1526
1754
  async function followSession(sessionId, options = {}) {
1527
1755
  let cursor = options.after ?? 0;
1528
1756
  for (; ; ) {
@@ -1533,16 +1761,7 @@ async function followSession(sessionId, options = {}) {
1533
1761
  "GET /sessions/{id}/events",
1534
1762
  { items: is.array }
1535
1763
  );
1536
- for (const event of page.items) {
1537
- cursor = Math.max(cursor, event.sequence);
1538
- if (options.quiet) {
1539
- continue;
1540
- }
1541
- const line = formatEvent(event);
1542
- if (line) {
1543
- out.line(line);
1544
- }
1545
- }
1764
+ cursor = consumeSessionEvents(page.items, cursor, options.quiet);
1546
1765
  if (page.nextCursor !== null) {
1547
1766
  cursor = Math.max(cursor, page.nextCursor);
1548
1767
  }
@@ -1557,11 +1776,8 @@ async function followSession(sessionId, options = {}) {
1557
1776
  }
1558
1777
  }
1559
1778
 
1560
- // src/lib/print-session.ts
1561
- function printSession(session) {
1562
- out.line(
1563
- `${out.paint(out.cell(session.title), "bold")} ${out.status(session.status)}`
1564
- );
1779
+ // src/lib/session/build-session-detail-rows.ts
1780
+ function buildSessionDetailRows(session) {
1565
1781
  const rows = [["id", out.cell(session.id)]];
1566
1782
  if (session.statusReason) {
1567
1783
  rows.push(["reason", out.cell(session.statusReason)]);
@@ -1592,23 +1808,39 @@ function printSession(session) {
1592
1808
  rows.push([out.cell(deliverable.type), out.cell(deliverable.url)]);
1593
1809
  }
1594
1810
  }
1595
- out.table(rows.map(([label, value]) => [out.paint(label, "dim"), value]));
1596
- const steps = session.playbook?.steps ?? [];
1597
- if (steps.length > 0) {
1598
- out.line();
1599
- for (const step of steps) {
1600
- const done = step.todos.filter(
1601
- (todo) => todo.status === "completed"
1602
- ).length;
1603
- const marker = done === step.todos.length && step.todos.length > 0 ? "\u2713" : "\xB7";
1604
- out.line(
1605
- ` ${marker} ${out.cell(step.title)} ${out.paint(`${done}/${step.todos.length}`, "dim")}`
1606
- );
1607
- }
1811
+ return rows;
1812
+ }
1813
+
1814
+ // src/lib/session/print-playbook-steps.ts
1815
+ function printPlaybookSteps(steps) {
1816
+ if (steps.length === 0) {
1817
+ return;
1818
+ }
1819
+ out.line();
1820
+ for (const step of steps) {
1821
+ const done = step.todos.filter(
1822
+ (todo) => todo.status === "completed"
1823
+ ).length;
1824
+ const marker = done === step.todos.length && step.todos.length > 0 ? "\u2713" : "\xB7";
1825
+ out.line(
1826
+ ` ${marker} ${out.cell(step.title)} ${out.paint(`${done}/${step.todos.length}`, "dim")}`
1827
+ );
1608
1828
  }
1609
1829
  }
1610
1830
 
1611
- // src/lib/session-outcome.ts
1831
+ // src/lib/session/print-session.ts
1832
+ function printSession(session) {
1833
+ out.line(
1834
+ `${out.paint(out.cell(session.title), "bold")} ${out.status(session.status)}`
1835
+ );
1836
+ const rows = buildSessionDetailRows(session);
1837
+ out.table(
1838
+ rows.map(([label, value]) => [out.paint(label ?? "", "dim"), value ?? ""])
1839
+ );
1840
+ printPlaybookSteps(session.playbook?.steps ?? []);
1841
+ }
1842
+
1843
+ // src/lib/session/session-outcome.ts
1612
1844
  function sessionOutcome(session) {
1613
1845
  switch (session.status) {
1614
1846
  case "completed":
@@ -1637,7 +1869,26 @@ function sessionOutcome(session) {
1637
1869
  }
1638
1870
  }
1639
1871
 
1640
- // src/commands/run.ts
1872
+ // src/lib/session/write-followed-session-outcome.ts
1873
+ function writeFollowedSessionOutcome(session, asJson) {
1874
+ const outcome = sessionOutcome(session);
1875
+ if (asJson) {
1876
+ if (!outcome) out.json(session);
1877
+ } else {
1878
+ out.line();
1879
+ printSession(session);
1880
+ }
1881
+ if (outcome) throw outcome;
1882
+ }
1883
+
1884
+ // src/commands/harnesses/harnesses-constants.ts
1885
+ var HARNESSES = [
1886
+ "claude-code",
1887
+ "codex",
1888
+ "cursor"
1889
+ ];
1890
+
1891
+ // src/commands/task/task-types-constants.ts
1641
1892
  var TASK_TYPES = [
1642
1893
  "implement",
1643
1894
  "new-project",
@@ -1645,11 +1896,24 @@ var TASK_TYPES = [
1645
1896
  "draft",
1646
1897
  "orchestrator"
1647
1898
  ];
1648
- var HARNESSES = [
1649
- "claude-code",
1650
- "codex",
1651
- "cursor"
1652
- ];
1899
+
1900
+ // src/commands/command/create-run-input.ts
1901
+ function createRunInput(parsed, prompt) {
1902
+ const taskType = args.choice(parsed, "task-type", TASK_TYPES);
1903
+ const harness = args.choice(parsed, "harness", HARNESSES);
1904
+ return {
1905
+ prompt,
1906
+ ...parsed.flags.title ? { title: parsed.flags.title } : {},
1907
+ ...taskType ? { taskType } : {},
1908
+ ...parsed.flags.model ? { modelSelection: parsed.flags.model } : {},
1909
+ ...harness ? { harnessPin: harness } : {},
1910
+ ...parsed.flags.repo ? { repoFullName: parsed.flags.repo } : {},
1911
+ ...parsed.flags.branch ? { branch: parsed.flags.branch } : {},
1912
+ ...parsed.repeated.skill ? { pinnedSkillIds: parsed.repeated.skill } : {}
1913
+ };
1914
+ }
1915
+
1916
+ // src/commands/command/run-command.ts
1653
1917
  async function runCommand(parsed) {
1654
1918
  args.expect(parsed, {
1655
1919
  flags: [
@@ -1666,18 +1930,7 @@ async function runCommand(parsed) {
1666
1930
  });
1667
1931
  const prompt = args.required(parsed, 0, "prompt");
1668
1932
  const asJson = parsed.booleans.has("json");
1669
- const taskType = args.choice(parsed, "task-type", TASK_TYPES);
1670
- const harness = args.choice(parsed, "harness", HARNESSES);
1671
- const body = {
1672
- prompt,
1673
- ...parsed.flags.title ? { title: parsed.flags.title } : {},
1674
- ...taskType ? { taskType } : {},
1675
- ...parsed.flags.model ? { modelSelection: parsed.flags.model } : {},
1676
- ...harness ? { harnessPin: harness } : {},
1677
- ...parsed.flags.repo ? { repoFullName: parsed.flags.repo } : {},
1678
- ...parsed.flags.branch ? { branch: parsed.flags.branch } : {},
1679
- ...parsed.repeated.skill ? { pinnedSkillIds: parsed.repeated.skill } : {}
1680
- };
1933
+ const body = createRunInput(parsed, prompt);
1681
1934
  const session = expectShape(
1682
1935
  await api("/sessions", { method: "POST", body }),
1683
1936
  "POST /sessions",
@@ -1699,21 +1952,10 @@ async function runCommand(parsed) {
1699
1952
  out.line();
1700
1953
  }
1701
1954
  const final = await followSession(session.id, { quiet });
1702
- const outcome = sessionOutcome(final);
1703
- if (asJson) {
1704
- if (!outcome) {
1705
- out.json(final);
1706
- }
1707
- } else {
1708
- out.line();
1709
- printSession(final);
1710
- }
1711
- if (outcome) {
1712
- throw outcome;
1713
- }
1955
+ writeFollowedSessionOutcome(final, asJson);
1714
1956
  }
1715
1957
 
1716
- // src/commands/sessions.ts
1958
+ // src/commands/sessions/sessions-constants.ts
1717
1959
  var ACTIONS4 = [
1718
1960
  "list",
1719
1961
  "get",
@@ -1742,9 +1984,8 @@ var SESSION_FIELDS = {
1742
1984
  repos: is.array,
1743
1985
  deliverables: is.array
1744
1986
  };
1745
- function detail(payload, endpoint) {
1746
- return expectShape(payload, endpoint, SESSION_FIELDS);
1747
- }
1987
+
1988
+ // src/commands/sessions/list-sessions.ts
1748
1989
  async function listSessions(parsed) {
1749
1990
  args.expect(parsed, {
1750
1991
  flags: ["status", "repo", "search", "page", "limit"],
@@ -1791,6 +2032,39 @@ async function listSessions(parsed) {
1791
2032
  ])
1792
2033
  );
1793
2034
  }
2035
+
2036
+ // src/commands/session/send-session-message.ts
2037
+ async function sendSessionMessage(parsed, asJson) {
2038
+ args.expect(parsed, { positional: 3 });
2039
+ const sessionId = args.required(parsed, 1, "sessionId");
2040
+ const message = args.required(parsed, 2, "message");
2041
+ const result = expectShape(
2042
+ await api(path`/sessions/${sessionId}/messages`, {
2043
+ method: "POST",
2044
+ body: { message }
2045
+ }),
2046
+ "POST /sessions/{id}/messages",
2047
+ { delivery: is.string }
2048
+ );
2049
+ if (asJson) {
2050
+ out.json(result);
2051
+ return;
2052
+ }
2053
+ out.line(
2054
+ result.delivery === "interrupt" ? "Delivered to the running agent." : "Queued \u2014 the agent picks it up at the end of its turn."
2055
+ );
2056
+ }
2057
+
2058
+ // src/commands/show/follow-session-logs.ts
2059
+ async function followSessionLogs(parsed, sessionId, after, asJson) {
2060
+ const session = await followSession(sessionId, {
2061
+ quiet: asJson || parsed.booleans.has("quiet"),
2062
+ ...after !== void 0 ? { after } : {}
2063
+ });
2064
+ writeFollowedSessionOutcome(session, asJson);
2065
+ }
2066
+
2067
+ // src/commands/show/show-logs.ts
1794
2068
  async function showLogs(parsed) {
1795
2069
  args.expect(parsed, {
1796
2070
  flags: ["after", "limit"],
@@ -1801,22 +2075,7 @@ async function showLogs(parsed) {
1801
2075
  const asJson = parsed.booleans.has("json");
1802
2076
  const after = args.integer(parsed, "after", { min: 0 });
1803
2077
  if (parsed.booleans.has("follow")) {
1804
- const session = await followSession(sessionId, {
1805
- quiet: asJson || parsed.booleans.has("quiet"),
1806
- ...after !== void 0 ? { after } : {}
1807
- });
1808
- const outcome = sessionOutcome(session);
1809
- if (asJson) {
1810
- if (!outcome) {
1811
- out.json(session);
1812
- }
1813
- } else {
1814
- out.line();
1815
- printSession(session);
1816
- }
1817
- if (outcome) {
1818
- throw outcome;
1819
- }
2078
+ await followSessionLogs(parsed, sessionId, after, asJson);
1820
2079
  return;
1821
2080
  }
1822
2081
  const page = expectShape(
@@ -1849,36 +2108,41 @@ async function showLogs(parsed) {
1849
2108
  );
1850
2109
  }
1851
2110
  }
1852
- async function sessionsCommand(parsed) {
1853
- const action = args.action(parsed, "sessions", ACTIONS4, "list");
1854
- const asJson = parsed.booleans.has("json");
1855
- if (action === "list") {
1856
- return await listSessions(parsed);
1857
- }
1858
- if (action === "logs") {
1859
- return await showLogs(parsed);
1860
- }
1861
- if (action === "send") {
1862
- args.expect(parsed, { positional: 3 });
1863
- const sessionId2 = args.required(parsed, 1, "sessionId");
1864
- const message = args.required(parsed, 2, "message");
1865
- const result = expectShape(
1866
- await api(path`/sessions/${sessionId2}/messages`, {
1867
- method: "POST",
1868
- body: { message }
1869
- }),
1870
- "POST /sessions/{id}/messages",
1871
- { delivery: is.string }
1872
- );
1873
- if (asJson) {
1874
- out.json(result);
1875
- return;
1876
- }
1877
- out.line(
1878
- result.delivery === "interrupt" ? "Delivered to the running agent." : "Queued \u2014 the agent picks it up at the end of its turn."
1879
- );
2111
+
2112
+ // src/commands/sessions/session-actions.ts
2113
+ var sessionActions = {
2114
+ list: (parsed) => listSessions(parsed),
2115
+ logs: (parsed) => showLogs(parsed),
2116
+ send: sendSessionMessage
2117
+ };
2118
+
2119
+ // src/commands/detail/detail.ts
2120
+ function detail(payload, endpoint) {
2121
+ return expectShape(payload, endpoint, SESSION_FIELDS);
2122
+ }
2123
+
2124
+ // src/commands/command/run-session-control-action.ts
2125
+ async function runSessionControlAction(parsed, action, sessionId, asJson) {
2126
+ const controlPath = CONTROL_PATHS[action];
2127
+ if (controlPath === void 0) {
2128
+ throw new CliError(`Unknown session action: ${action}`, 2);
2129
+ }
2130
+ const session = detail(
2131
+ await api(path`/sessions/${sessionId}/${controlPath}`, {
2132
+ method: "POST",
2133
+ body: action === "kill" && parsed.flags.reason ? { reason: parsed.flags.reason } : {}
2134
+ }),
2135
+ `POST /sessions/{id}/${controlPath}`
2136
+ );
2137
+ if (asJson) {
2138
+ out.json(session);
1880
2139
  return;
1881
2140
  }
2141
+ printSession(session);
2142
+ }
2143
+
2144
+ // src/commands/command/run-session-operation.ts
2145
+ async function runSessionOperation(parsed, action, asJson) {
1882
2146
  args.expect(parsed, {
1883
2147
  positional: 2,
1884
2148
  ...action === "kill" ? { flags: ["reason"] } : {}
@@ -1897,19 +2161,7 @@ async function sessionsCommand(parsed) {
1897
2161
  return;
1898
2162
  }
1899
2163
  if (Object.hasOwn(CONTROL_PATHS, action)) {
1900
- const controlPath = CONTROL_PATHS[action];
1901
- const session = detail(
1902
- await api(path`/sessions/${sessionId}/${controlPath}`, {
1903
- method: "POST",
1904
- body: action === "kill" && parsed.flags.reason ? { reason: parsed.flags.reason } : {}
1905
- }),
1906
- `POST /sessions/{id}/${controlPath}`
1907
- );
1908
- if (asJson) {
1909
- out.json(session);
1910
- return;
1911
- }
1912
- printSession(session);
2164
+ await runSessionControlAction(parsed, action, sessionId, asJson);
1913
2165
  return;
1914
2166
  }
1915
2167
  if (action === "checks") {
@@ -1936,7 +2188,18 @@ async function sessionsCommand(parsed) {
1936
2188
  );
1937
2189
  }
1938
2190
 
1939
- // src/commands/whoami.ts
2191
+ // src/commands/command/sessions-command.ts
2192
+ async function sessionsCommand(parsed) {
2193
+ const action = args.action(parsed, "sessions", ACTIONS4, "list");
2194
+ const asJson = parsed.booleans.has("json");
2195
+ const dedicated = sessionActions[action];
2196
+ if (dedicated) {
2197
+ return await dedicated(parsed, asJson);
2198
+ }
2199
+ await runSessionOperation(parsed, action, asJson);
2200
+ }
2201
+
2202
+ // src/commands/command/whoami-command.ts
1940
2203
  async function whoamiCommand(parsed) {
1941
2204
  args.expect(parsed, {});
1942
2205
  const identity = expectShape(
@@ -1970,21 +2233,15 @@ async function whoamiCommand(parsed) {
1970
2233
  ]);
1971
2234
  }
1972
2235
 
1973
- // src/lib/wants-json.ts
1974
- function wantsJson(argv) {
1975
- for (const token of argv) {
1976
- if (token === "--") {
1977
- return false;
1978
- }
1979
- if (token === "--json") {
1980
- return true;
1981
- }
1982
- }
1983
- return false;
1984
- }
2236
+ // src/catalog-topics-constants.ts
2237
+ var CATALOG_TOPICS = /* @__PURE__ */ new Set([
2238
+ "repos",
2239
+ "branches",
2240
+ "models",
2241
+ "skills"
2242
+ ]);
1985
2243
 
1986
- // src/index.ts
1987
- var CATALOG_TOPICS = /* @__PURE__ */ new Set(["repos", "branches", "models", "skills"]);
2244
+ // src/dispatch.ts
1988
2245
  async function dispatch(command, argv) {
1989
2246
  const parsed = args.parse(argv);
1990
2247
  if (parsed.booleans.has("help")) {
@@ -2016,6 +2273,8 @@ async function dispatch(command, argv) {
2016
2273
  throw new CliError(`Unknown command "${command}".`, 2);
2017
2274
  }
2018
2275
  }
2276
+
2277
+ // src/main.ts
2019
2278
  async function main() {
2020
2279
  const argv = process.argv.slice(2);
2021
2280
  out.setJson(wantsJson(argv));
@@ -2035,9 +2294,19 @@ async function main() {
2035
2294
  }
2036
2295
  await dispatch(command, rest);
2037
2296
  }
2038
- main().catch((error) => {
2039
- const failure = error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
2040
- out.failure(failure);
2041
- process.exit(failure.exitCode);
2042
- });
2297
+
2298
+ // src/cli-entry.ts
2299
+ async function cliEntry() {
2300
+ try {
2301
+ await main();
2302
+ } catch (error) {
2303
+ const failure = error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
2304
+ out.failure(failure);
2305
+ process.exit(failure.exitCode);
2306
+ }
2307
+ }
2308
+ void cliEntry();
2309
+ export {
2310
+ cliEntry
2311
+ };
2043
2312
  //# sourceMappingURL=index.js.map