@lobehub/cli 0.0.60 → 0.0.61

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 (3) hide show
  1. package/dist/index.js +1443 -356
  2. package/man/man1/lh.1 +1 -1
  3. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import * as fs$2 from "node:fs";
4
- import fs, { appendFileSync, closeSync, constants, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync as readFileSync$1, readSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, unlink, writeFileSync } from "node:fs";
4
+ import fs, { appendFileSync, closeSync, constants, createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, unlink, writeFileSync } from "node:fs";
5
5
  import * as os$1 from "node:os";
6
6
  import os, { EOL, constants as constants$1, homedir, platform, tmpdir } from "node:os";
7
7
  import * as path$1 from "node:path";
@@ -18934,7 +18934,7 @@ var init_heteroSelectorCapabilities = __esmMin((() => {
18934
18934
  kind: "config"
18935
18935
  }],
18936
18936
  resolve: resolveCodexModel,
18937
- source: "static"
18937
+ source: "catalog"
18938
18938
  },
18939
18939
  speed: {
18940
18940
  encodings: [{
@@ -19394,6 +19394,53 @@ var init_agent = __esmMin((() => {
19394
19394
  //#region ../../packages/types/src/agentAccount.ts
19395
19395
  var init_agentAccount = __esmMin((() => {}));
19396
19396
 
19397
+ //#endregion
19398
+ //#region ../../packages/types/src/device.ts
19399
+ var gitLinkedPullRequestSchema, workingDirConfigSchema;
19400
+ var init_device = __esmMin((() => {
19401
+ init_zod$1();
19402
+ gitLinkedPullRequestSchema = object$2({
19403
+ ciStatus: _enum([
19404
+ "failure",
19405
+ "pending",
19406
+ "success",
19407
+ "unknown"
19408
+ ]).optional(),
19409
+ isDraft: boolean().optional(),
19410
+ mergeable: string().optional(),
19411
+ mergeStateStatus: string().optional(),
19412
+ mergedAt: string().nullable().optional(),
19413
+ number: number(),
19414
+ reviewDecision: string().optional(),
19415
+ state: string(),
19416
+ title: string(),
19417
+ url: string()
19418
+ });
19419
+ workingDirConfigSchema = object$2({
19420
+ git: object$2({
19421
+ activeWorktree: string().optional(),
19422
+ branch: string().optional(),
19423
+ detached: boolean().optional(),
19424
+ github: object$2({
19425
+ extraPullRequestCount: number().optional(),
19426
+ pullRequest: gitLinkedPullRequestSchema.nullable().optional(),
19427
+ pullRequestStatus: _enum([
19428
+ "error",
19429
+ "gh-missing",
19430
+ "ok"
19431
+ ]).optional()
19432
+ }).optional(),
19433
+ isWorktree: boolean().optional(),
19434
+ upstream: object$2({
19435
+ branch: string(),
19436
+ remote: string()
19437
+ }).optional()
19438
+ }).optional(),
19439
+ path: string(),
19440
+ repoType: _enum(["git", "github"]).optional()
19441
+ });
19442
+ }));
19443
+
19397
19444
  //#endregion
19398
19445
  //#region ../../packages/types/src/agentExecution/credentialFacts.ts
19399
19446
  var init_credentialFacts = __esmMin((() => {}));
@@ -19404,9 +19451,26 @@ var init_modelFacts = __esmMin((() => {}));
19404
19451
 
19405
19452
  //#endregion
19406
19453
  //#region ../../packages/types/src/agentExecution/index.ts
19454
+ var initialTopicMetadataSchema;
19407
19455
  var init_agentExecution = __esmMin((() => {
19456
+ init_zod$1();
19457
+ init_device();
19408
19458
  init_credentialFacts();
19409
19459
  init_modelFacts();
19460
+ initialTopicMetadataSchema = object$2({
19461
+ repos: array(string()).optional(),
19462
+ /**
19463
+ * Cloud-sandbox instance the composer chose before any topic existed. The
19464
+ * choice is made on a conversation that has nothing to write to yet, so it
19465
+ * travels with the first send instead — the server cannot read the client's
19466
+ * pending selection, and without this the new topic would be born unbound and
19467
+ * silently run at the workspace root.
19468
+ */
19469
+ sandboxInstanceId: string().optional(),
19470
+ sandboxMode: _enum(["ephemeral", "persistent"]).optional(),
19471
+ workingDirectory: string().optional(),
19472
+ workingDirectoryConfig: workingDirConfigSchema.optional()
19473
+ });
19410
19474
  }));
19411
19475
 
19412
19476
  //#endregion
@@ -20637,55 +20701,12 @@ var init_conversation = __esmMin((() => {}));
20637
20701
  var init_creds = __esmMin((() => {}));
20638
20702
 
20639
20703
  //#endregion
20640
- //#region ../../packages/types/src/dbMigration.ts
20641
- var init_dbMigration = __esmMin((() => {}));
20704
+ //#region ../../packages/types/src/dashboard/index.ts
20705
+ var init_dashboard = __esmMin((() => {}));
20642
20706
 
20643
20707
  //#endregion
20644
- //#region ../../packages/types/src/device.ts
20645
- var gitLinkedPullRequestSchema, workingDirConfigSchema;
20646
- var init_device = __esmMin((() => {
20647
- init_zod$1();
20648
- gitLinkedPullRequestSchema = object$2({
20649
- ciStatus: _enum([
20650
- "failure",
20651
- "pending",
20652
- "success",
20653
- "unknown"
20654
- ]).optional(),
20655
- isDraft: boolean().optional(),
20656
- mergeable: string().optional(),
20657
- mergeStateStatus: string().optional(),
20658
- mergedAt: string().nullable().optional(),
20659
- number: number(),
20660
- reviewDecision: string().optional(),
20661
- state: string(),
20662
- title: string(),
20663
- url: string()
20664
- });
20665
- workingDirConfigSchema = object$2({
20666
- git: object$2({
20667
- activeWorktree: string().optional(),
20668
- branch: string().optional(),
20669
- detached: boolean().optional(),
20670
- github: object$2({
20671
- extraPullRequestCount: number().optional(),
20672
- pullRequest: gitLinkedPullRequestSchema.nullable().optional(),
20673
- pullRequestStatus: _enum([
20674
- "error",
20675
- "gh-missing",
20676
- "ok"
20677
- ]).optional()
20678
- }).optional(),
20679
- isWorktree: boolean().optional(),
20680
- upstream: object$2({
20681
- branch: string(),
20682
- remote: string()
20683
- }).optional()
20684
- }).optional(),
20685
- path: string(),
20686
- repoType: _enum(["git", "github"]).optional()
20687
- });
20688
- }));
20708
+ //#region ../../packages/types/src/dbMigration.ts
20709
+ var init_dbMigration = __esmMin((() => {}));
20689
20710
 
20690
20711
  //#endregion
20691
20712
  //#region ../../packages/types/src/deviceMetric.ts
@@ -21170,22 +21191,43 @@ var init_goal = __esmMin((() => {}));
21170
21191
 
21171
21192
  //#endregion
21172
21193
  //#region ../../packages/types/src/goalReport.ts
21173
- var idList, goalReportDetourKinds, GoalReportDetourSchema, GoalReportChapterSchema, GoalReportNextStepSchema, GoalReportMainlineSchema, GoalReportMetadataSchema;
21194
+ var idList, NO_VALUE_TOKENS, optionalId, goalReportDetourKinds, GOAL_REPORT_MAX_DETOUR_TITLE_LENGTH, GOAL_REPORT_MAX_DETOUR_TEXT_LENGTH, GoalReportDetourSchema, GOAL_REPORT_MAX_DETOURS_PER_CHAPTER, GoalReportChapterSchema, GoalReportNextStepSchema, GoalReportMainlineSchema, GoalReportMetadataSchema;
21174
21195
  var init_goalReport = __esmMin((() => {
21175
21196
  init_zod$1();
21176
21197
  idList = array(string().trim().min(1)).max(200);
21198
+ NO_VALUE_TOKENS = new Set([
21199
+ "",
21200
+ "-",
21201
+ "—",
21202
+ "n/a",
21203
+ "na",
21204
+ "nil",
21205
+ "none",
21206
+ "null",
21207
+ "undefined"
21208
+ ]);
21209
+ optionalId = preprocess((value) => {
21210
+ if (value === null || value === void 0) return void 0;
21211
+ if (typeof value !== "string") return value;
21212
+ const trimmed = value.trim();
21213
+ if (trimmed === "" || NO_VALUE_TOKENS.has(trimmed.toLowerCase())) return void 0;
21214
+ return trimmed;
21215
+ }, string().trim().min(1).optional());
21177
21216
  goalReportDetourKinds = [
21178
21217
  "dead_end",
21179
21218
  "superseded",
21180
21219
  "retry"
21181
21220
  ];
21221
+ GOAL_REPORT_MAX_DETOUR_TITLE_LENGTH = 200;
21222
+ GOAL_REPORT_MAX_DETOUR_TEXT_LENGTH = 2e3;
21182
21223
  GoalReportDetourSchema = object$2({
21183
21224
  kind: _enum(goalReportDetourKinds),
21184
- lesson: string().trim().min(1).max(2e3),
21225
+ lesson: string().trim().min(1).max(GOAL_REPORT_MAX_DETOUR_TEXT_LENGTH),
21185
21226
  nodeIds: idList.min(1),
21186
- reason: string().trim().min(1).max(2e3),
21227
+ reason: string().trim().min(1).max(GOAL_REPORT_MAX_DETOUR_TEXT_LENGTH),
21187
21228
  title: string().trim().min(1).max(200)
21188
21229
  }).strict();
21230
+ GOAL_REPORT_MAX_DETOURS_PER_CHAPTER = 20;
21189
21231
  GoalReportChapterSchema = object$2({
21190
21232
  detours: array(GoalReportDetourSchema).max(20).default([]),
21191
21233
  findingIds: idList.default([]),
@@ -21205,7 +21247,12 @@ var init_goalReport = __esmMin((() => {
21205
21247
  }).strict();
21206
21248
  GoalReportMetadataSchema = object$2({
21207
21249
  chapters: array(GoalReportChapterSchema).min(1).max(30),
21208
- deliverableWorkId: string().trim().min(1).optional(),
21250
+ /**
21251
+ * The Goal's final deliverable, when it produced one. A placeholder such as
21252
+ * `""` or `"none"` is read as "no deliverable" rather than rejected, so a
21253
+ * Goal without one can still report.
21254
+ */
21255
+ deliverableWorkId: optionalId,
21209
21256
  /** The newest `goal_events.id` the report was written against. */
21210
21257
  graphCursor: string().trim().min(1),
21211
21258
  headline: string().trim().min(1).max(500),
@@ -21968,6 +22015,9 @@ var init_topic$1 = __esmMin((() => {
21968
22015
  messageId: string(),
21969
22016
  reservedAt: string()
21970
22017
  }).nullable().optional(),
22018
+ sandboxCwd: string().optional(),
22019
+ sandboxInstanceId: string().optional(),
22020
+ sandboxMode: _enum(["ephemeral", "persistent"]).optional(),
21971
22021
  scheduledRun: topicScheduledRunSchema.nullish(),
21972
22022
  workingDirectory: string().optional(),
21973
22023
  workingDirectoryConfig: workingDirConfigSchema.optional()
@@ -22163,7 +22213,13 @@ var init_preference = __esmMin((() => {
22163
22213
  * expose a port on the working device through a tunnel link, so a dev server
22164
22214
  * running there can be opened from this UI
22165
22215
  */
22166
- enableDeviceTunnel: boolean().optional()
22216
+ enableDeviceTunnel: boolean().optional(),
22217
+ /**
22218
+ * opt this user's cloud-sandbox runs into a persistent workspace: the working
22219
+ * directory survives session recycling and is shared across their topics,
22220
+ * instead of being thrown away with the sandbox
22221
+ */
22222
+ enablePersistentSandbox: boolean().optional()
22167
22223
  });
22168
22224
  GithubIntegrationPreferenceSchema = object$2({
22169
22225
  acceptOnMerge: boolean().optional(),
@@ -22607,6 +22663,10 @@ var init_userMemory = __esmMin((() => {
22607
22663
  //#region ../../packages/types/src/verify.ts
22608
22664
  var init_verify = __esmMin((() => {}));
22609
22665
 
22666
+ //#endregion
22667
+ //#region ../../packages/types/src/widget/index.ts
22668
+ var init_widget = __esmMin((() => {}));
22669
+
22610
22670
  //#endregion
22611
22671
  //#region ../../packages/types/src/work.ts
22612
22672
  var WORK_PROVIDER_RESOURCE_TYPES, RESOURCE_TYPE_TO_PROVIDER;
@@ -22723,6 +22783,7 @@ var init_src$1 = __esmMin((() => {
22723
22783
  init_chunk$1();
22724
22784
  init_conversation();
22725
22785
  init_creds();
22786
+ init_dashboard();
22726
22787
  init_dbMigration();
22727
22788
  init_device();
22728
22789
  init_deviceMetric();
@@ -22774,6 +22835,7 @@ var init_src$1 = __esmMin((() => {
22774
22835
  init_user();
22775
22836
  init_userMemory();
22776
22837
  init_verify();
22838
+ init_widget();
22777
22839
  init_work();
22778
22840
  init_acceptanceComment();
22779
22841
  init_acceptanceFlow();
@@ -23127,6 +23189,7 @@ const TRPC_NAMESPACE_API_KEY_RULES = {
23127
23189
  composio: "blocked",
23128
23190
  config: "open",
23129
23191
  connector: "blocked",
23192
+ dashboard: rw("agent:read", null),
23130
23193
  device: "blocked",
23131
23194
  deviceMetric: "blocked",
23132
23195
  document: rw("knowledge:read", "knowledge:write"),
@@ -23167,6 +23230,7 @@ const TRPC_NAMESPACE_API_KEY_RULES = {
23167
23230
  referral: "blocked",
23168
23231
  resourcePermission: "blocked",
23169
23232
  resourceTransferRequest: "blocked",
23233
+ sandboxStorage: "blocked",
23170
23234
  search: rw("chat:read", null),
23171
23235
  scm: "blocked",
23172
23236
  session: rw("chat:read", "chat:write"),
@@ -23192,6 +23256,7 @@ const TRPC_NAMESPACE_API_KEY_RULES = {
23192
23256
  video: { any: "model:invoke" },
23193
23257
  waitlist: "blocked",
23194
23258
  webBrowsing: { any: "model:invoke" },
23259
+ widget: rw("agent:read", null),
23195
23260
  work: rw("agent:read", "agent:write"),
23196
23261
  workspace: rw("workspace:read", "workspace:write"),
23197
23262
  workspaceAuditLog: rw("workspace:read", null),
@@ -25371,33 +25436,199 @@ async function getAgentStreamAuthInfo(workspaceId) {
25371
25436
  };
25372
25437
  }
25373
25438
 
25439
+ //#endregion
25440
+ //#region src/utils/agentRunOutcome.ts
25441
+ /**
25442
+ * Exit status contract of `lh agent run`. Scripts can tell a parked run apart
25443
+ * from a finished one, and both apart from a run the CLI could not confirm.
25444
+ */
25445
+ const AGENT_RUN_EXIT_CODES = {
25446
+ completed: 0,
25447
+ failed: 1,
25448
+ interrupted: 1,
25449
+ unknown: 3,
25450
+ waiting_for_human: 2
25451
+ };
25452
+ const COMPLETED_STATUSES = new Set([
25453
+ "completed",
25454
+ "done",
25455
+ "success"
25456
+ ]);
25457
+ const FAILED_STATUSES = new Set(["error", "failed"]);
25458
+ const INTERRUPTED_STATUSES = new Set([
25459
+ "aborted",
25460
+ "canceled",
25461
+ "cancelled",
25462
+ "interrupted"
25463
+ ]);
25464
+ /** Non-terminal runtime statuses: the run is still going (or about to resume). */
25465
+ const ACTIVE_STATUSES = new Set([
25466
+ "idle",
25467
+ "pending",
25468
+ "processing",
25469
+ "running",
25470
+ "waiting_for_async_tool"
25471
+ ]);
25472
+ /**
25473
+ * Classify a runtime status or `agent_runtime_end` reason. Returns `active`
25474
+ * for a run that is still going and `undefined` for a value the CLI does not
25475
+ * recognise.
25476
+ */
25477
+ const classifyRunStatus = (status) => {
25478
+ if (!status) return void 0;
25479
+ if (COMPLETED_STATUSES.has(status)) return "completed";
25480
+ if (FAILED_STATUSES.has(status)) return "failed";
25481
+ if (INTERRUPTED_STATUSES.has(status)) return "interrupted";
25482
+ if (status === "waiting_for_human") return "waiting_for_human";
25483
+ if (ACTIVE_STATUSES.has(status)) return "active";
25484
+ };
25485
+ const errorText = (error) => {
25486
+ if (!error) return void 0;
25487
+ if (typeof error === "string") return error;
25488
+ if (typeof error === "object" && "message" in error) return String(error.message);
25489
+ return JSON.stringify(error);
25490
+ };
25491
+ /**
25492
+ * Read `aiAgent.getOperationStatus`. The server answers with an envelope —
25493
+ * `{ currentState: { status, stepCount, usage, cost, error }, isCompleted,
25494
+ * hasError, needsHumanInput, isActive, ... }` — so the run's fields live under
25495
+ * `currentState`, not at the top level. The flat reads are kept as fallbacks
25496
+ * for older servers, and the envelope's summary flags stand in when there is
25497
+ * no status string at all.
25498
+ */
25499
+ const readOperationStatus = (r) => {
25500
+ const state = r?.currentState ?? {};
25501
+ let status = state.status ?? r?.status ?? r?.state;
25502
+ if (!status) {
25503
+ if (r?.isCompleted) status = r.hasError ? "error" : "done";
25504
+ else if (r?.needsHumanInput) status = "waiting_for_human";
25505
+ else if (r?.isActive) status = "running";
25506
+ }
25507
+ return {
25508
+ cost: state.cost ?? r?.cost,
25509
+ error: errorText(state.error ?? r?.error),
25510
+ status,
25511
+ stepCount: state.stepCount ?? r?.stepCount ?? r?.stats?.totalSteps,
25512
+ usage: state.usage ?? r?.usage
25513
+ };
25514
+ };
25515
+ const colorStatus$1 = (status) => {
25516
+ switch (classifyRunStatus(status)) {
25517
+ case "completed": return import_picocolors.default.green(status);
25518
+ case "failed": return import_picocolors.default.red(status);
25519
+ case "interrupted":
25520
+ case "waiting_for_human": return import_picocolors.default.yellow(status);
25521
+ case "active": return import_picocolors.default.cyan(status);
25522
+ default: return import_picocolors.default.dim(status);
25523
+ }
25524
+ };
25525
+ /** One-line human summary of an outcome, used by the stream and poll paths. */
25526
+ const describeOutcome = (outcome) => {
25527
+ switch (outcome.kind) {
25528
+ case "completed": return `${import_picocolors.default.green("✓")} Agent finished`;
25529
+ case "failed": return `${import_picocolors.default.red("✗")} Agent failed${outcome.error ? `: ${outcome.error}` : ""}`;
25530
+ case "interrupted": return `${import_picocolors.default.yellow("■")} Agent interrupted`;
25531
+ case "waiting_for_human": return `${import_picocolors.default.yellow("⏸")} Agent paused: waiting for human approval — approve or answer it in LobeHub, the CLI does not wait for it`;
25532
+ case "unknown": return `${import_picocolors.default.yellow("?")} Agent run outcome unknown${outcome.status ? ` (status: ${outcome.status})` : ""}`;
25533
+ }
25534
+ };
25535
+
25374
25536
  //#endregion
25375
25537
  //#region src/utils/agentStream.ts
25376
25538
  /**
25539
+ * Map an `agent_runtime_end` event to the run outcome. The server ends the
25540
+ * stream on `done`, `error`, `interrupted` and `waiting_for_human` alike, so the
25541
+ * end event alone does not mean the run succeeded — `data.reason` says how.
25542
+ */
25543
+ const outcomeFromEndEvent = (event) => {
25544
+ const reason = event.data?.reason;
25545
+ const kind = classifyRunStatus(reason);
25546
+ const error = event.data?.finalState?.error;
25547
+ return {
25548
+ error: typeof error === "string" ? error : error?.message ?? void 0,
25549
+ kind: !reason ? "completed" : kind && kind !== "active" ? kind : "unknown",
25550
+ status: reason
25551
+ };
25552
+ };
25553
+ const STALL_TIMEOUT = 6e4;
25554
+ const outcomeFromErrorEvent = (event) => ({
25555
+ error: event.data?.message || event.data?.error || "Unknown error",
25556
+ kind: "failed",
25557
+ status: "error"
25558
+ });
25559
+ /**
25377
25560
  * Connect to the agent SSE stream and render events to the terminal.
25378
- * Resolves when the stream ends (agent_runtime_end or connection close).
25561
+ * Resolves with the run outcome once a terminal event arrives, or `undefined`
25562
+ * when the stream closed without one (the caller should check the status).
25563
+ * Rejects when the stream cannot be opened — the run may still be executing
25564
+ * server-side, so the caller falls back to polling rather than exiting here.
25379
25565
  */
25380
25566
  async function streamAgentEvents(url, headers, options = {}) {
25381
- const res = await fetch(url, { headers });
25382
- if (!res.ok) {
25383
- const text = await res.text();
25384
- log$13.error(`Agent stream failed: ${res.status} ${text}`);
25385
- process.exit(1);
25386
- }
25387
- if (!res.body) {
25388
- log$13.error("No response body received from agent stream");
25389
- process.exit(1);
25567
+ const { onStall, stallTimeoutMs = STALL_TIMEOUT } = options;
25568
+ const jsonEvents = [];
25569
+ let jsonPrinted = false;
25570
+ const printJsonOnce = () => {
25571
+ if (!options.json || jsonPrinted) return;
25572
+ jsonPrinted = true;
25573
+ console.log(JSON.stringify(jsonEvents, null, 2));
25574
+ };
25575
+ let res;
25576
+ try {
25577
+ res = await fetch(url, { headers });
25578
+ if (!res.ok) {
25579
+ const text = await res.text();
25580
+ throw new Error(`Agent stream failed: ${res.status} ${text}`);
25581
+ }
25582
+ if (!res.body) throw new Error("No response body received from agent stream");
25583
+ } catch (error) {
25584
+ printJsonOnce();
25585
+ throw error;
25390
25586
  }
25391
25587
  const reader = res.body.getReader();
25392
25588
  const decoder = new TextDecoder();
25393
25589
  let buffer = "";
25394
- const jsonEvents = [];
25395
25590
  const ctx = createRenderContext();
25396
25591
  let eventType = "";
25397
25592
  let eventData = "";
25593
+ let finished = false;
25594
+ let stallTimer;
25595
+ let stalled;
25596
+ const armStallTimer = () => {
25597
+ if (!onStall) return;
25598
+ if (stallTimer) clearTimeout(stallTimer);
25599
+ stallTimer = setTimeout(async () => {
25600
+ if (finished) return;
25601
+ const silence = `Agent stream sent no progress for ${Math.round(stallTimeoutMs / 1e3)}s`;
25602
+ log$13.debug(`${silence}; checking the run status`);
25603
+ try {
25604
+ const outcome = await onStall();
25605
+ if (finished) return;
25606
+ if (!outcome) {
25607
+ armStallTimer();
25608
+ return;
25609
+ }
25610
+ stalled = { outcome };
25611
+ } catch (error) {
25612
+ if (finished) return;
25613
+ stalled = { error: /* @__PURE__ */ new Error(`${silence}; status check failed: ${error.message}`) };
25614
+ }
25615
+ reader.cancel();
25616
+ }, stallTimeoutMs);
25617
+ };
25618
+ armStallTimer();
25398
25619
  try {
25399
25620
  while (true) {
25400
25621
  const { done, value } = await reader.read();
25622
+ if (stalled) {
25623
+ finished = true;
25624
+ if ("error" in stalled) {
25625
+ printJsonOnce();
25626
+ throw stalled.error;
25627
+ }
25628
+ if (options.json) printJsonOnce();
25629
+ else renderOutcome(stalled.outcome);
25630
+ return stalled.outcome;
25631
+ }
25401
25632
  if (done) break;
25402
25633
  buffer += decoder.decode(value, { stream: true });
25403
25634
  const lines = buffer.split("\n");
@@ -25415,19 +25646,22 @@ async function streamAgentEvents(url, headers, options = {}) {
25415
25646
  eventData = "";
25416
25647
  continue;
25417
25648
  }
25649
+ armStallTimer();
25418
25650
  try {
25419
25651
  const event = JSON.parse(eventData);
25420
25652
  if (options.json) jsonEvents.push(event);
25421
25653
  else renderEvent(event, ctx, options);
25422
25654
  if (event.type === "agent_runtime_end") {
25423
- if (options.json) console.log(JSON.stringify(jsonEvents, null, 2));
25424
- else renderEnd(event);
25425
- return;
25655
+ const outcome = outcomeFromEndEvent(event);
25656
+ if (options.json) printJsonOnce();
25657
+ else renderEnd(event, outcome);
25658
+ return outcome;
25426
25659
  }
25427
25660
  if (event.type === "error") {
25428
- if (options.json) console.log(JSON.stringify(jsonEvents, null, 2));
25429
- log$13.error(`Agent error: ${event.data?.message || event.data?.error || "Unknown error"}`);
25430
- process.exit(1);
25661
+ printJsonOnce();
25662
+ const outcome = outcomeFromErrorEvent(event);
25663
+ log$13.error(`Agent error: ${outcome.error}`);
25664
+ return outcome;
25431
25665
  }
25432
25666
  } catch {}
25433
25667
  eventType = "";
@@ -25435,18 +25669,26 @@ async function streamAgentEvents(url, headers, options = {}) {
25435
25669
  }
25436
25670
  }
25437
25671
  }
25438
- if (options.json && jsonEvents.length > 0) console.log(JSON.stringify(jsonEvents, null, 2));
25672
+ printJsonOnce();
25673
+ return;
25439
25674
  } finally {
25675
+ finished = true;
25676
+ if (stallTimer) clearTimeout(stallTimer);
25440
25677
  reader.releaseLock();
25441
25678
  }
25442
25679
  }
25443
25680
  /**
25444
25681
  * Replay previously saved JSON events (from --json output) to the terminal.
25445
- * No network calls needed.
25682
+ * No network calls needed. Returns the outcome carried by the recorded terminal
25683
+ * event, or `undefined` when the recording has none (e.g. a truncated capture).
25446
25684
  */
25447
25685
  function replayAgentEvents(events, options = {}) {
25448
25686
  if (options.json) {
25449
25687
  console.log(JSON.stringify(events, null, 2));
25688
+ for (const event of events) {
25689
+ if (event.type === "agent_runtime_end") return outcomeFromEndEvent(event);
25690
+ if (event.type === "error") return outcomeFromErrorEvent(event);
25691
+ }
25450
25692
  return;
25451
25693
  }
25452
25694
  const ctx = createRenderContext();
@@ -25454,22 +25696,26 @@ function replayAgentEvents(events, options = {}) {
25454
25696
  if (!event.type) continue;
25455
25697
  renderEvent(event, ctx, options);
25456
25698
  if (event.type === "agent_runtime_end") {
25457
- renderEnd(event);
25458
- return;
25699
+ const outcome = outcomeFromEndEvent(event);
25700
+ renderEnd(event, outcome);
25701
+ return outcome;
25459
25702
  }
25460
25703
  if (event.type === "error") {
25461
- log$13.error(`Agent error: ${event.data?.message || event.data?.error || "Unknown error"}`);
25462
- return;
25704
+ const outcome = outcomeFromErrorEvent(event);
25705
+ log$13.error(`Agent error: ${outcome.error}`);
25706
+ return outcome;
25463
25707
  }
25464
25708
  }
25465
25709
  }
25466
25710
  const HEARTBEAT_INTERVAL$1 = 3e4;
25467
25711
  /**
25468
25712
  * Connect to the Agent Gateway via WebSocket and render events to the terminal.
25469
- * Resolves when the session completes or the connection closes.
25713
+ * Resolves with the run outcome once a terminal event arrives, `undefined` when
25714
+ * the gateway completed the session without one, and rejects when the
25715
+ * connection fails so the caller can fall back to polling.
25470
25716
  */
25471
25717
  async function streamAgentEventsViaWebSocket(options) {
25472
- const { gatewayUrl, operationId, serverUrl, token, tokenType = "jwt", ...streamOpts } = options;
25718
+ const { gatewayUrl, onStall, operationId, serverUrl, stallTimeoutMs = STALL_TIMEOUT, token, tokenType = "jwt", ...streamOpts } = options;
25473
25719
  const wsUrl = urlJoin(gatewayUrl.replace(/^http/, "ws"), `/ws?operationId=${encodeURIComponent(operationId)}`);
25474
25720
  log$13.debug(`Connecting to gateway: ${wsUrl} (auth: ${tokenType})`);
25475
25721
  return new Promise((resolve, reject) => {
@@ -25477,12 +25723,56 @@ async function streamAgentEventsViaWebSocket(options) {
25477
25723
  const jsonEvents = [];
25478
25724
  const ctx = createRenderContext();
25479
25725
  let heartbeatTimer;
25726
+ let stallTimer;
25480
25727
  let isSettled = false;
25481
25728
  let jsonPrinted = false;
25482
25729
  const cleanup = () => {
25483
25730
  if (heartbeatTimer) clearInterval(heartbeatTimer);
25731
+ if (stallTimer) clearTimeout(stallTimer);
25484
25732
  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close();
25485
25733
  };
25734
+ const printJsonOnce = () => {
25735
+ if (streamOpts.json && !jsonPrinted) {
25736
+ jsonPrinted = true;
25737
+ console.log(JSON.stringify(jsonEvents, null, 2));
25738
+ }
25739
+ };
25740
+ const settle = (outcome) => {
25741
+ if (isSettled) return;
25742
+ isSettled = true;
25743
+ cleanup();
25744
+ resolve(outcome);
25745
+ };
25746
+ const fail = (error) => {
25747
+ if (isSettled) return;
25748
+ isSettled = true;
25749
+ cleanup();
25750
+ printJsonOnce();
25751
+ reject(error);
25752
+ };
25753
+ const armStallTimer = () => {
25754
+ if (stallTimer) clearTimeout(stallTimer);
25755
+ stallTimer = setTimeout(async () => {
25756
+ if (isSettled) return;
25757
+ const silence = `Agent gateway WebSocket sent no progress for ${Math.round(stallTimeoutMs / 1e3)}s`;
25758
+ if (!onStall) {
25759
+ fail(/* @__PURE__ */ new Error(`${silence} and never reported completion`));
25760
+ return;
25761
+ }
25762
+ log$13.debug(`${silence}; checking the run status`);
25763
+ try {
25764
+ const outcome = await onStall();
25765
+ if (isSettled) return;
25766
+ if (outcome) {
25767
+ if (!streamOpts.json) renderOutcome(outcome);
25768
+ printJsonOnce();
25769
+ settle(outcome);
25770
+ } else armStallTimer();
25771
+ } catch (error) {
25772
+ fail(/* @__PURE__ */ new Error(`${silence}; status check failed: ${error.message}`));
25773
+ }
25774
+ }, stallTimeoutMs);
25775
+ };
25486
25776
  ws.onopen = () => {
25487
25777
  ws.send(JSON.stringify({
25488
25778
  serverUrl,
@@ -25490,9 +25780,11 @@ async function streamAgentEventsViaWebSocket(options) {
25490
25780
  tokenType,
25491
25781
  type: "auth"
25492
25782
  }));
25783
+ armStallTimer();
25493
25784
  };
25494
25785
  ws.onmessage = (event) => {
25495
25786
  const msg = JSON.parse(event.data);
25787
+ if (msg.type !== "heartbeat_ack") armStallTimer();
25496
25788
  if (msg.type === "auth_success") {
25497
25789
  log$13.debug("Gateway authenticated");
25498
25790
  ws.send(JSON.stringify({
@@ -25502,11 +25794,11 @@ async function streamAgentEventsViaWebSocket(options) {
25502
25794
  heartbeatTimer = setInterval(() => {
25503
25795
  if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "heartbeat" }));
25504
25796
  }, HEARTBEAT_INTERVAL$1);
25797
+ heartbeatTimer.unref?.();
25505
25798
  return;
25506
25799
  }
25507
25800
  if (msg.type === "auth_failed") {
25508
- cleanup();
25509
- reject(/* @__PURE__ */ new Error(`Gateway auth failed: ${msg.reason}`));
25801
+ fail(/* @__PURE__ */ new Error(`Gateway auth failed: ${msg.reason}`));
25510
25802
  return;
25511
25803
  }
25512
25804
  if (msg.type === "heartbeat_ack") return;
@@ -25516,57 +25808,33 @@ async function streamAgentEventsViaWebSocket(options) {
25516
25808
  if (streamOpts.json) jsonEvents.push(agentEvent);
25517
25809
  else renderEvent(agentEvent, ctx, streamOpts);
25518
25810
  if (agentEvent.type === "agent_runtime_end") {
25519
- if (streamOpts.json && !jsonPrinted) {
25520
- jsonPrinted = true;
25521
- console.log(JSON.stringify(jsonEvents, null, 2));
25522
- } else if (!streamOpts.json) renderEnd(agentEvent);
25523
25811
  if (isSettled) return;
25524
- isSettled = true;
25525
- cleanup();
25526
- resolve();
25812
+ const outcome = outcomeFromEndEvent(agentEvent);
25813
+ if (!streamOpts.json) renderEnd(agentEvent, outcome);
25814
+ printJsonOnce();
25815
+ settle(outcome);
25527
25816
  return;
25528
25817
  }
25529
25818
  if (agentEvent.type === "error") {
25530
- if (streamOpts.json && !jsonPrinted) {
25531
- jsonPrinted = true;
25532
- console.log(JSON.stringify(jsonEvents, null, 2));
25533
- }
25534
- log$13.error(`Agent error: ${agentEvent.data?.message || agentEvent.data?.error || "Unknown error"}`);
25535
- cleanup();
25536
- process.exit(1);
25819
+ if (isSettled) return;
25820
+ const outcome = outcomeFromErrorEvent(agentEvent);
25821
+ printJsonOnce();
25822
+ log$13.error(`Agent error: ${outcome.error}`);
25823
+ settle(outcome);
25824
+ return;
25537
25825
  }
25538
25826
  }
25539
25827
  if (msg.type === "session_complete") {
25540
- if (streamOpts.json && jsonEvents.length > 0 && !jsonPrinted) {
25541
- jsonPrinted = true;
25542
- console.log(JSON.stringify(jsonEvents, null, 2));
25543
- }
25544
- if (isSettled) return;
25545
- isSettled = true;
25546
- cleanup();
25547
- resolve();
25828
+ printJsonOnce();
25829
+ settle(void 0);
25548
25830
  }
25549
25831
  };
25550
25832
  ws.onerror = (err) => {
25551
- cleanup();
25552
- if (isSettled) return;
25553
- if (streamOpts.json && jsonEvents.length > 0 && !jsonPrinted) {
25554
- jsonPrinted = true;
25555
- console.log(JSON.stringify(jsonEvents, null, 2));
25556
- }
25557
- isSettled = true;
25558
- reject(/* @__PURE__ */ new Error(`Agent gateway WebSocket failed: ${String(err)}`));
25833
+ fail(/* @__PURE__ */ new Error(`Agent gateway WebSocket failed: ${String(err)}`));
25559
25834
  };
25560
25835
  ws.onclose = (event) => {
25561
- if (heartbeatTimer) clearInterval(heartbeatTimer);
25562
- if (isSettled) return;
25563
- if (streamOpts.json && jsonEvents.length > 0 && !jsonPrinted) {
25564
- jsonPrinted = true;
25565
- console.log(JSON.stringify(jsonEvents, null, 2));
25566
- }
25567
- isSettled = true;
25568
25836
  const reason = event.reason ? `: ${event.reason}` : "";
25569
- reject(/* @__PURE__ */ new Error(`Agent gateway WebSocket closed before completion (code ${event.code}${reason})`));
25837
+ fail(/* @__PURE__ */ new Error(`Agent gateway WebSocket closed before completion (code ${event.code}${reason})`));
25570
25838
  };
25571
25839
  });
25572
25840
  }
@@ -25621,10 +25889,17 @@ function renderEvent(event, ctx, options) {
25621
25889
  case "step_complete": break;
25622
25890
  }
25623
25891
  }
25624
- function renderEnd(event) {
25892
+ function renderOutcome(outcome) {
25893
+ console.log();
25894
+ console.log(describeOutcome(outcome));
25895
+ }
25896
+ function renderEnd(event, outcome) {
25625
25897
  console.log();
25626
- const data = event.data || {};
25627
- const parts = [`${import_picocolors.default.green("✓")} Agent finished`];
25898
+ const data = {
25899
+ ...event.data?.finalState,
25900
+ ...event.data
25901
+ };
25902
+ const parts = [describeOutcome(outcome)];
25628
25903
  if (data.stepCount !== void 0) parts.push(`${data.stepCount} step${data.stepCount !== 1 ? "s" : ""}`);
25629
25904
  if (data.usage?.total_tokens) parts.push(`${data.usage.total_tokens} tokens`);
25630
25905
  if (data.cost?.total !== void 0) parts.push(`$${data.cost.total.toFixed(4)}`);
@@ -25988,6 +26263,133 @@ async function resolveAgentId$1(client, opts) {
25988
26263
  return "";
25989
26264
  }
25990
26265
 
26266
+ //#endregion
26267
+ //#region src/commands/agent/runStatus.ts
26268
+ const RUN_POLL_INTERVAL_MS = 1e4;
26269
+ /** Upper bound for one status request, so a hung request cannot stall the wait. */
26270
+ const RUN_STATUS_REQUEST_TIMEOUT_MS = 3e4;
26271
+ /** Consecutive failed / unreadable answers tolerated before giving up. */
26272
+ const RUN_STATUS_MAX_MISSES = 3;
26273
+ const sleep$7 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
26274
+ const info$2 = (json, msg) => {
26275
+ if (json) console.error(msg);
26276
+ else log$13.info(msg);
26277
+ };
26278
+ const MISSING_RESULT_MESSAGE = "The server returned no status for this operation (unknown id, not visible to this account, or its run state already expired) — the run's outcome cannot be confirmed.";
26279
+ /**
26280
+ * Query `getOperationStatus` with a hard deadline. The abort signal cancels
26281
+ * the request; the race guarantees the deadline even if a transport ignores it.
26282
+ */
26283
+ const queryOperationStatus = async (client, operationId, timeoutMs = RUN_STATUS_REQUEST_TIMEOUT_MS) => {
26284
+ const controller = new AbortController();
26285
+ let timer;
26286
+ const deadline = new Promise((_, reject) => {
26287
+ timer = setTimeout(() => {
26288
+ controller.abort();
26289
+ reject(/* @__PURE__ */ new Error(`status request got no answer within ${Math.round(timeoutMs / 1e3)}s`));
26290
+ }, timeoutMs);
26291
+ });
26292
+ try {
26293
+ return await Promise.race([client.aiAgent.getOperationStatus.query({ operationId }, { signal: controller.signal }), deadline]);
26294
+ } finally {
26295
+ clearTimeout(timer);
26296
+ }
26297
+ };
26298
+ /**
26299
+ * One status check while the live stream is still open but quiet. Returns the
26300
+ * outcome once the run has ended, `undefined` while it is still active, and
26301
+ * throws when the status cannot be fetched (the caller falls back to polling).
26302
+ */
26303
+ const probeRunOutcome = async (client, operationId, options = {}) => {
26304
+ const r = await queryOperationStatus(client, operationId, options.requestTimeoutMs);
26305
+ if (!r) {
26306
+ log$13.error(MISSING_RESULT_MESSAGE);
26307
+ return { kind: "unknown" };
26308
+ }
26309
+ const read = readOperationStatus(r);
26310
+ const kind = classifyRunStatus(read.status);
26311
+ if (kind === "active") return void 0;
26312
+ if (!kind) throw new Error(`unreadable run status (${read.status ?? "none"})`);
26313
+ return {
26314
+ error: read.error,
26315
+ kind,
26316
+ status: read.status
26317
+ };
26318
+ };
26319
+ /**
26320
+ * Fallback when the live stream (gateway WebSocket / SSE) is unavailable or
26321
+ * ended without a terminal event: the run may still be executing server-side,
26322
+ * so poll its status until it reaches a terminal state.
26323
+ *
26324
+ * Never reports success it cannot see: failure / interruption statuses come
26325
+ * back as such, and a missing, unreadable or unreachable status ends as
26326
+ * `unknown` instead of being read as "finished".
26327
+ */
26328
+ const pollAgentRunStatus = async (client, operationId, options = {}) => {
26329
+ const { intervalMs = RUN_POLL_INTERVAL_MS, json, maxMisses = 3, requestTimeoutMs } = options;
26330
+ const finish = (outcome) => {
26331
+ if (!json) {
26332
+ console.log();
26333
+ console.log(describeOutcome(outcome));
26334
+ }
26335
+ return outcome;
26336
+ };
26337
+ let lastStatus = "";
26338
+ let failures = 0;
26339
+ let unreadable = 0;
26340
+ for (let i = 0;; i++) {
26341
+ if (i > 0) await sleep$7(intervalMs);
26342
+ let r;
26343
+ try {
26344
+ r = await queryOperationStatus(client, operationId, requestTimeoutMs);
26345
+ failures = 0;
26346
+ } catch (error) {
26347
+ failures++;
26348
+ log$13.warn(`Status poll failed (${failures}/${maxMisses}): ${error.message}`);
26349
+ if (failures >= maxMisses) {
26350
+ log$13.error("Giving up on the run status; the run may still be executing server-side.");
26351
+ return finish({
26352
+ error: error.message,
26353
+ kind: "unknown"
26354
+ });
26355
+ }
26356
+ continue;
26357
+ }
26358
+ if (!r) {
26359
+ log$13.error(MISSING_RESULT_MESSAGE);
26360
+ return finish({ kind: "unknown" });
26361
+ }
26362
+ const read = readOperationStatus(r);
26363
+ const kind = classifyRunStatus(read.status);
26364
+ if (!kind) {
26365
+ unreadable++;
26366
+ if (unreadable >= maxMisses) {
26367
+ const keys = typeof r === "object" ? Object.keys(r).join(", ") : typeof r;
26368
+ log$13.error(`Could not read a run status from the server's answer (status: ${read.status ?? "none"}; keys: ${keys}).`);
26369
+ return finish({
26370
+ kind: "unknown",
26371
+ status: read.status
26372
+ });
26373
+ }
26374
+ continue;
26375
+ }
26376
+ unreadable = 0;
26377
+ if (read.status !== lastStatus) {
26378
+ lastStatus = read.status;
26379
+ const steps = read.stepCount !== void 0 ? ` · ${read.stepCount} step(s)` : "";
26380
+ info$2(json, `Run status: ${colorStatus$1(read.status)}${steps}`);
26381
+ }
26382
+ if (kind !== "active") {
26383
+ if (read.error) log$13.error(`Run error: ${read.error}`);
26384
+ return finish({
26385
+ error: read.error,
26386
+ kind,
26387
+ status: read.status
26388
+ });
26389
+ }
26390
+ }
26391
+ };
26392
+
25991
26393
  //#endregion
25992
26394
  //#region node_modules/.pnpm/dayjs@1.11.23/node_modules/dayjs/dayjs.min.js
25993
26395
  var require_dayjs_min = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -26428,7 +26830,7 @@ function formatFsLongListing(nodes) {
26428
26830
  return lines;
26429
26831
  }
26430
26832
  async function readFsContentInput(options) {
26431
- if (options.contentFile) return readFileSync$1(options.contentFile, "utf8");
26833
+ if (options.contentFile) return readFileSync(options.contentFile, "utf8");
26432
26834
  if (options.content !== void 0) return options.content;
26433
26835
  if (process.stdin.isTTY === false) return text(process.stdin);
26434
26836
  return "";
@@ -26989,14 +27391,20 @@ function registerAgentCommand(program) {
26989
27391
  const r = await client.agent.duplicateAgent.mutate(input);
26990
27392
  console.log(`${import_picocolors.default.green("✓")} Duplicated agent → ${import_picocolors.default.bold(r.agentId || r.id || "done")}`);
26991
27393
  });
26992
- agent.command("run").description("Run an agent with a prompt").option("-a, --agent-id <id>", "Agent ID").option("-s, --slug <slug>", "Agent slug").option("-p, --prompt <text>", "User prompt").option("-t, --topic-id <id>", "Reuse an existing topic").option("--no-auto-start", "Do not auto-start the agent").option("--device <target>", "Target device ID, or use \"local\" for the current connected device").option("--no-headless", "Disable headless mode and wait for human approval on tool calls (default: headless — tools auto-run, matching the CLI's non-interactive nature)").option("--json", "Output full JSON event stream").option("-v, --verbose", "Show detailed tool call info").option("--replay <file>", "Replay events from a saved JSON file (offline)").option("--sse", "Force SSE stream instead of WebSocket gateway").action(async (options) => {
27394
+ agent.command("run").description("Run an agent with a prompt").option("-a, --agent-id <id>", "Agent ID").option("-s, --slug <slug>", "Agent slug").option("-p, --prompt <text>", "User prompt").option("-t, --topic-id <id>", "Reuse an existing topic").option("--no-auto-start", "Do not auto-start the agent").option("--device <target>", "Target device ID, or use \"local\" for the current connected device").option("--no-headless", "Disable headless mode: tool calls that need approval park the run for a human (default: headless — tools auto-run, matching the CLI's non-interactive nature). The CLI does not wait for that approval; it returns with exit code 2 and the run continues once it is answered in LobeHub").option("--json", "Output full JSON event stream").option("-v, --verbose", "Show detailed tool call info").option("--replay <file>", "Replay events from a saved JSON file (offline)").option("--sse", "Force SSE stream instead of WebSocket gateway").option("--timeout <seconds>", "Stop waiting after this many seconds (exit code 3; the run keeps going server-side). Default: wait as long as the server reports the run active", parsePositiveSeconds).addHelpText("after", `
27395
+ Exit codes:
27396
+ 0 the run finished (done)
27397
+ 1 the run failed or was interrupted
27398
+ 2 the run is paused waiting for human approval / input
27399
+ 3 the outcome could not be confirmed (no or unreadable status, status checks kept failing, or --timeout reached)`).action(async (options) => {
26993
27400
  if (options.verbose) setVerbose(true);
26994
27401
  if (options.replay) {
26995
27402
  const data = readFileSync(options.replay, "utf8");
26996
- replayAgentEvents(JSON.parse(data), {
27403
+ const replayExitCode = AGENT_RUN_EXIT_CODES[(replayAgentEvents(JSON.parse(data), {
26997
27404
  json: options.json,
26998
27405
  verbose: options.verbose
26999
- });
27406
+ }) ?? { kind: "unknown" }).kind];
27407
+ if (replayExitCode !== 0) process.exitCode = replayExitCode;
27000
27408
  return;
27001
27409
  }
27002
27410
  if (!options.agentId && !options.slug) {
@@ -27051,24 +27459,45 @@ function registerAgentCommand(program) {
27051
27459
  if (!options.json) log$13.info(`Operation: ${import_picocolors.default.dim(operationId)} · Topic: ${import_picocolors.default.dim(r.topicId || "n/a")}`);
27052
27460
  const { serverUrl, headers, token, tokenType } = await getAgentStreamAuthInfo();
27053
27461
  const agentGatewayUrl = options.sse ? void 0 : resolveAgentGatewayUrl();
27054
- try {
27055
- if (agentGatewayUrl) await streamAgentEventsViaWebSocket({
27056
- gatewayUrl: agentGatewayUrl,
27057
- json: options.json,
27058
- operationId,
27059
- serverUrl,
27060
- token,
27061
- tokenType,
27062
- verbose: options.verbose
27063
- });
27064
- else await streamAgentEvents(`${serverUrl}/api/agent/stream?operationId=${encodeURIComponent(operationId)}`, headers, {
27065
- json: options.json,
27066
- verbose: options.verbose
27067
- });
27068
- } catch (error) {
27069
- if (options.json) throw error;
27070
- log$13.warn(`Live stream unavailable (${error.message}). Polling run status every 10s…`);
27071
- await pollAgentRunStatus(client, operationId);
27462
+ const waitForRun = async () => {
27463
+ let streamed;
27464
+ try {
27465
+ if (agentGatewayUrl) streamed = await streamAgentEventsViaWebSocket({
27466
+ gatewayUrl: agentGatewayUrl,
27467
+ json: options.json,
27468
+ onStall: () => probeRunOutcome(client, operationId, { json: options.json }),
27469
+ operationId,
27470
+ serverUrl,
27471
+ token,
27472
+ tokenType,
27473
+ verbose: options.verbose
27474
+ });
27475
+ else streamed = await streamAgentEvents(`${serverUrl}/api/agent/stream?operationId=${encodeURIComponent(operationId)}`, headers, {
27476
+ json: options.json,
27477
+ onStall: () => probeRunOutcome(client, operationId, { json: options.json }),
27478
+ verbose: options.verbose
27479
+ });
27480
+ } catch (error) {
27481
+ log$13.warn(`Live stream unavailable (${error.message}). Polling run status every ${RUN_POLL_INTERVAL_MS / 1e3}s…`);
27482
+ return pollAgentRunStatus(client, operationId, { json: options.json });
27483
+ }
27484
+ return streamed ?? pollAgentRunStatus(client, operationId, { json: options.json });
27485
+ };
27486
+ let timeoutTimer;
27487
+ let didTimeOut = false;
27488
+ const timedOut = options.timeout === void 0 ? void 0 : new Promise((resolve) => {
27489
+ timeoutTimer = setTimeout(() => {
27490
+ didTimeOut = true;
27491
+ log$13.error(`Stopped waiting after ${options.timeout}s (--timeout). The run may still be executing — check it with 'lh agent status ${operationId}'.`);
27492
+ resolve({ kind: "unknown" });
27493
+ }, options.timeout * 1e3);
27494
+ });
27495
+ const outcome = await (timedOut ? Promise.race([waitForRun(), timedOut]) : waitForRun());
27496
+ clearTimeout(timeoutTimer);
27497
+ const exitCode = AGENT_RUN_EXIT_CODES[outcome.kind];
27498
+ if (exitCode !== 0) {
27499
+ if (didTimeOut) process.exit(exitCode);
27500
+ process.exitCode = exitCode;
27072
27501
  }
27073
27502
  });
27074
27503
  agent.command("pin <agentId>").description("Pin an agent").action(async (agentId) => {
@@ -27204,13 +27633,19 @@ function registerAgentCommand(program) {
27204
27633
  outputJson(r, typeof options.json === "string" ? options.json : void 0);
27205
27634
  return;
27206
27635
  }
27636
+ if (!r) {
27637
+ log$13.error(`No status for operation ${operationId} (unknown id, not visible to this account, or its run state already expired).`);
27638
+ process.exit(1);
27639
+ return;
27640
+ }
27641
+ const read = readOperationStatus(r);
27207
27642
  console.log(import_picocolors.default.bold("Operation Status"));
27208
27643
  console.log(` ID: ${operationId}`);
27209
- console.log(` Status: ${colorStatus$1(r.status || r.state || "unknown")}`);
27210
- if (r.stepCount !== void 0) console.log(` Steps: ${r.stepCount}`);
27211
- if (r.usage?.total_tokens) console.log(` Tokens: ${r.usage.total_tokens}`);
27212
- if (r.cost?.total !== void 0) console.log(` Cost: $${r.cost.total.toFixed(4)}`);
27213
- if (r.error) console.log(` Error: ${import_picocolors.default.red(r.error)}`);
27644
+ console.log(` Status: ${colorStatus$1(read.status || "unknown")}`);
27645
+ if (read.stepCount !== void 0) console.log(` Steps: ${read.stepCount}`);
27646
+ if (read.usage?.total_tokens) console.log(` Tokens: ${read.usage.total_tokens}`);
27647
+ if (read.cost?.total !== void 0) console.log(` Cost: $${read.cost.total.toFixed(4)}`);
27648
+ if (read.error) console.log(` Error: ${import_picocolors.default.red(read.error)}`);
27214
27649
  if (r.createdAt) console.log(` Started: ${r.createdAt}`);
27215
27650
  if (r.completedAt) console.log(` Ended: ${r.completedAt}`);
27216
27651
  });
@@ -27232,60 +27667,10 @@ function registerAgentCommand(program) {
27232
27667
  else console.log(`${import_picocolors.default.yellow("!")} Interrupt not acknowledged for ${import_picocolors.default.bold(label)} (already finished?)`);
27233
27668
  });
27234
27669
  }
27235
- function colorStatus$1(status) {
27236
- switch (status) {
27237
- case "completed":
27238
- case "success": return import_picocolors.default.green(status);
27239
- case "failed":
27240
- case "error": return import_picocolors.default.red(status);
27241
- case "processing":
27242
- case "running": return import_picocolors.default.yellow(status);
27243
- default: return import_picocolors.default.dim(status);
27244
- }
27245
- }
27246
- const TERMINAL_RUN_STATUSES = new Set([
27247
- "completed",
27248
- "done",
27249
- "success",
27250
- "failed",
27251
- "error",
27252
- "cancelled",
27253
- "canceled",
27254
- "aborted"
27255
- ]);
27256
- /**
27257
- * Fallback when the live stream (gateway WebSocket / SSE) drops before the run
27258
- * finishes: the run is still executing server-side, so poll its status every 10s
27259
- * until it reaches a terminal state (or is no longer tracked, which also means it
27260
- * has finished). Avoids hard-exiting on a transient gateway disconnect.
27261
- */
27262
- async function pollAgentRunStatus(client, operationId) {
27263
- const POLL_MS = 1e4;
27264
- let lastStatus = "";
27265
- for (let i = 0;; i++) {
27266
- if (i > 0) await new Promise((resolve) => setTimeout(resolve, POLL_MS));
27267
- let r;
27268
- try {
27269
- r = await client.aiAgent.getOperationStatus.query({ operationId });
27270
- } catch (error) {
27271
- log$13.error(`Status poll failed: ${error.message}`);
27272
- process.exit(1);
27273
- }
27274
- if (!r) {
27275
- log$13.info("Run is no longer tracked — finished (or expired).");
27276
- return;
27277
- }
27278
- const status = r.status || r.state || "unknown";
27279
- if (status !== lastStatus) {
27280
- lastStatus = status;
27281
- const steps = r.stepCount !== void 0 ? ` · ${r.stepCount} step(s)` : "";
27282
- log$13.info(`Run status: ${colorStatus$1(status)}${steps}`);
27283
- }
27284
- if (TERMINAL_RUN_STATUSES.has(status)) {
27285
- if (r.error) log$13.error(`Run error: ${r.error}`);
27286
- return;
27287
- }
27288
- }
27670
+ function parsePositiveSeconds(value) {
27671
+ const seconds = Number(value);
27672
+ if (!Number.isFinite(seconds) || seconds <= 0) throw new InvalidArgumentError("Expected a positive number of seconds.");
27673
+ return seconds;
27289
27674
  }
27290
27675
 
27291
27676
  //#endregion
@@ -38457,6 +38842,7 @@ const isObjectLike = (value) => typeof value === "object" && value !== null;
38457
38842
  */
38458
38843
  const isRecord$6 = (value) => isObjectLike(value) && !Array.isArray(value);
38459
38844
  const toRecord = (value) => isRecord$6(value) ? value : void 0;
38845
+ const pickString$1 = (value) => typeof value === "string" ? value : void 0;
38460
38846
 
38461
38847
  //#endregion
38462
38848
  //#region node_modules/.pnpm/partial-json@0.1.7/node_modules/partial-json/dist/options.js
@@ -41667,6 +42053,503 @@ var init_cliSpawn = __esmMin((() => {
41667
42053
  };
41668
42054
  }));
41669
42055
 
42056
+ //#endregion
42057
+ //#region ../../packages/heterogeneous-agents/src/codex/CodexAppServerClient.ts
42058
+ init_cliSpawn();
42059
+ const APP_SERVER_RPC_TIMEOUT_MS = 3e4;
42060
+ const DEFAULT_RECONNECT_BASE_DELAY_MS = 250;
42061
+ const DEFAULT_RECONNECT_MAX_DELAY_MS = 4e3;
42062
+ const DEFAULT_RECONNECT_MAX_ATTEMPTS = 5;
42063
+ const APPROVAL_REQUEST_METHODS = new Set(["item/commandExecution/requestApproval", "item/fileChange/requestApproval"]);
42064
+ var CodexAppServerRpcError = class extends Error {
42065
+ constructor(message, code, data, method) {
42066
+ super(message);
42067
+ this.code = code;
42068
+ this.data = data;
42069
+ this.method = method;
42070
+ this.name = "CodexAppServerRpcError";
42071
+ }
42072
+ };
42073
+ var CodexAppServerConnectionError = class extends Error {
42074
+ phase;
42075
+ constructor(message, options) {
42076
+ super(message, options);
42077
+ this.name = "CodexAppServerConnectionError";
42078
+ this.phase = options?.phase;
42079
+ }
42080
+ };
42081
+ /**
42082
+ * One long-lived, bidirectional NDJSON client for `codex app-server`.
42083
+ *
42084
+ * stdout has exactly one reader here. It correlates RPC responses, publishes
42085
+ * notifications to the owning thread session, and separately routes
42086
+ * server-initiated requests back to that thread.
42087
+ */
42088
+ var CodexAppServerClient = class {
42089
+ activeGeneration = 0;
42090
+ closedByHost = false;
42091
+ connectionState;
42092
+ connectionError;
42093
+ connected = false;
42094
+ consumerCount = 0;
42095
+ generationSequence = 0;
42096
+ hasConnected = false;
42097
+ nextRequestId = 0;
42098
+ pendingRequests = /* @__PURE__ */ new Map();
42099
+ notificationHandlers = /* @__PURE__ */ new Map();
42100
+ serverRequestHandlers = /* @__PURE__ */ new Map();
42101
+ disconnectHandlers = /* @__PURE__ */ new Set();
42102
+ rawMessageHandlers = /* @__PURE__ */ new Map();
42103
+ stderrHandlers = /* @__PURE__ */ new Set();
42104
+ threadRegistrations = /* @__PURE__ */ new Map();
42105
+ processGeneration;
42106
+ reconnectAttempt = 0;
42107
+ reconnectEpoch = 0;
42108
+ reconnectExhaustedEpoch;
42109
+ reconnectTimer;
42110
+ constructor(options) {
42111
+ this.options = options;
42112
+ }
42113
+ get isConnected() {
42114
+ return this.connected && !this.connectionError;
42115
+ }
42116
+ get hasConsumers() {
42117
+ return this.consumerCount > 0;
42118
+ }
42119
+ acquireConsumer() {
42120
+ if (this.closedByHost) throw new CodexAppServerConnectionError("Codex app-server client closed by host");
42121
+ this.consumerCount += 1;
42122
+ let released = false;
42123
+ return () => {
42124
+ if (released) return;
42125
+ released = true;
42126
+ this.consumerCount -= 1;
42127
+ };
42128
+ }
42129
+ /** Process-global options must stay identical while this long-lived client is reused. */
42130
+ canReuseFor(options) {
42131
+ const currentArgs = this.options.args ?? [];
42132
+ const nextArgs = options.args ?? [];
42133
+ const commandIsRelativePath = !path.isAbsolute(this.options.commandPath) && (this.options.commandPath.includes("/") || this.options.commandPath.includes("\\"));
42134
+ if (this.options.commandPath !== options.commandPath || commandIsRelativePath && this.options.cwd !== options.cwd || currentArgs.length !== nextArgs.length || currentArgs.some((arg, index) => arg !== nextArgs[index])) return false;
42135
+ const currentEnv = Object.entries(this.options.env).filter(([, value]) => value !== void 0);
42136
+ const nextEnv = Object.entries(options.env).filter(([, value]) => value !== void 0);
42137
+ return currentEnv.length === nextEnv.length && currentEnv.every(([key, value]) => options.env[key] === value);
42138
+ }
42139
+ connect() {
42140
+ if (this.closedByHost) return Promise.reject(new CodexAppServerConnectionError("Codex app-server client closed by host"));
42141
+ if (this.connectionState && !this.connectionError) return this.connectionState.promise;
42142
+ this.startUserRecoveryEpoch();
42143
+ return this.startConnection();
42144
+ }
42145
+ async request(method, params) {
42146
+ if (!this.connected) await this.connect();
42147
+ if (this.connectionError) throw this.connectionError;
42148
+ return this.requestForGeneration(this.activeGeneration, method, params);
42149
+ }
42150
+ notify(notification) {
42151
+ this.writeForGeneration(this.activeGeneration, { ...notification });
42152
+ }
42153
+ subscribe(threadId, handler) {
42154
+ const handlers = this.notificationHandlers.get(threadId) ?? /* @__PURE__ */ new Set();
42155
+ handlers.add(handler);
42156
+ this.notificationHandlers.set(threadId, handlers);
42157
+ return () => this.removeHandler(this.notificationHandlers, threadId, handler);
42158
+ }
42159
+ subscribeServerRequests(threadId, handler) {
42160
+ const handlers = this.serverRequestHandlers.get(threadId) ?? /* @__PURE__ */ new Set();
42161
+ handlers.add(handler);
42162
+ this.serverRequestHandlers.set(threadId, handlers);
42163
+ return () => this.removeHandler(this.serverRequestHandlers, threadId, handler);
42164
+ }
42165
+ registerThread(threadId, params, registration) {
42166
+ if (params.threadId !== threadId) throw new Error(`Codex resume params do not match registered thread: ${threadId}`);
42167
+ const existing = this.threadRegistrations.get(threadId);
42168
+ if (existing && JSON.stringify(existing.params) !== JSON.stringify(params)) throw new Error(`Conflicting Codex resume params for thread: ${threadId}`);
42169
+ const group = existing ?? {
42170
+ params,
42171
+ registrations: /* @__PURE__ */ new Set()
42172
+ };
42173
+ group.registrations.add(registration);
42174
+ this.threadRegistrations.set(threadId, group);
42175
+ if (!this.connected && this.connectionError) this.scheduleReconnect();
42176
+ return () => {
42177
+ group.registrations.delete(registration);
42178
+ if (group.registrations.size === 0) this.threadRegistrations.delete(threadId);
42179
+ if (this.threadRegistrations.size === 0 && this.reconnectTimer) {
42180
+ clearTimeout(this.reconnectTimer);
42181
+ this.reconnectTimer = void 0;
42182
+ }
42183
+ };
42184
+ }
42185
+ onDisconnect(handler) {
42186
+ this.disconnectHandlers.add(handler);
42187
+ return () => this.disconnectHandlers.delete(handler);
42188
+ }
42189
+ onRawMessage(threadId, handler) {
42190
+ const handlers = this.rawMessageHandlers.get(threadId) ?? /* @__PURE__ */ new Set();
42191
+ handlers.add(handler);
42192
+ this.rawMessageHandlers.set(threadId, handlers);
42193
+ return () => this.removeHandler(this.rawMessageHandlers, threadId, handler);
42194
+ }
42195
+ onStderr(handler) {
42196
+ this.stderrHandlers.add(handler);
42197
+ return () => this.stderrHandlers.delete(handler);
42198
+ }
42199
+ close() {
42200
+ if (this.closedByHost) return;
42201
+ this.closedByHost = true;
42202
+ this.reconnectEpoch += 1;
42203
+ if (this.reconnectTimer) {
42204
+ clearTimeout(this.reconnectTimer);
42205
+ this.reconnectTimer = void 0;
42206
+ }
42207
+ const error = new CodexAppServerConnectionError("Codex app-server client closed by host");
42208
+ const generation = this.activeGeneration;
42209
+ this.activeGeneration = ++this.generationSequence;
42210
+ this.connected = false;
42211
+ this.connectionError = error;
42212
+ this.connectionState = void 0;
42213
+ const processGeneration = this.processGeneration;
42214
+ this.processGeneration = void 0;
42215
+ this.rejectPendingRequests(generation, error);
42216
+ this.emitDisconnect(error);
42217
+ this.terminateChild(processGeneration?.child);
42218
+ }
42219
+ startConnection() {
42220
+ if (this.closedByHost) return Promise.reject(new CodexAppServerConnectionError("Codex app-server client closed by host"));
42221
+ const generation = ++this.generationSequence;
42222
+ const isReconnect = this.hasConnected;
42223
+ this.activeGeneration = generation;
42224
+ this.connected = false;
42225
+ this.connectionError = void 0;
42226
+ const promise = this.initialize(generation, isReconnect);
42227
+ this.connectionState = {
42228
+ generation,
42229
+ promise
42230
+ };
42231
+ return promise;
42232
+ }
42233
+ async initialize(generation, isReconnect) {
42234
+ try {
42235
+ await this.startProcess(generation);
42236
+ const params = {
42237
+ capabilities: {
42238
+ experimentalApi: false,
42239
+ requestAttestation: false
42240
+ },
42241
+ clientInfo: {
42242
+ name: "lobehub-desktop",
42243
+ title: "LobeHub Desktop",
42244
+ version: this.options.clientVersion
42245
+ }
42246
+ };
42247
+ const response = await this.requestForGeneration(generation, "initialize", params);
42248
+ this.writeForGeneration(generation, { method: "initialized" });
42249
+ if (!this.isCurrentGeneration(generation)) throw new CodexAppServerConnectionError("Codex app-server connection was replaced");
42250
+ this.connected = true;
42251
+ this.hasConnected = true;
42252
+ if (isReconnect) {
42253
+ await this.resumeRegisteredThreads(generation);
42254
+ if (!this.isCurrentGeneration(generation) || !this.connected || this.connectionError) throw this.connectionError ?? new CodexAppServerConnectionError("Codex app-server disconnected while resuming threads");
42255
+ }
42256
+ this.reconnectAttempt = 0;
42257
+ this.reconnectExhaustedEpoch = void 0;
42258
+ return response;
42259
+ } catch (error) {
42260
+ const failure = this.toConnectionError(error, "Codex app-server handshake failed");
42261
+ if (!this.isCurrentGeneration(generation)) throw failure;
42262
+ const connectionError = !isReconnect && failure instanceof CodexAppServerConnectionError ? new CodexAppServerConnectionError(failure.message, {
42263
+ cause: failure,
42264
+ phase: "initialize"
42265
+ }) : failure;
42266
+ this.fail(connectionError, generation);
42267
+ throw connectionError;
42268
+ }
42269
+ }
42270
+ async startProcess(generation) {
42271
+ const spawnPlan = await resolveCliSpawnPlan(this.options.commandPath, [...this.options.args ?? [], "app-server"]);
42272
+ if (!this.isCurrentGeneration(generation)) throw new CodexAppServerConnectionError(this.closedByHost ? "Codex app-server client closed by host" : "Codex app-server connection was replaced");
42273
+ const child = spawnManaged(spawnPlan.command, spawnPlan.args, {
42274
+ cwd: this.options.cwd,
42275
+ detached: process.platform !== "win32",
42276
+ env: this.options.env,
42277
+ stdio: [
42278
+ "pipe",
42279
+ "pipe",
42280
+ "pipe"
42281
+ ]
42282
+ });
42283
+ const processGeneration = {
42284
+ child,
42285
+ generation,
42286
+ stdoutBuffer: ""
42287
+ };
42288
+ if (!this.isCurrentGeneration(generation)) {
42289
+ this.terminateChild(child);
42290
+ throw new CodexAppServerConnectionError("Codex app-server connection was replaced");
42291
+ }
42292
+ this.processGeneration = processGeneration;
42293
+ child.stdin?.on("error", () => {});
42294
+ child.stdout?.on("data", (chunk) => {
42295
+ if (!this.ownsProcess(processGeneration)) return;
42296
+ this.consumeStdout(processGeneration, chunk);
42297
+ });
42298
+ child.stderr?.on("data", (chunk) => {
42299
+ if (!this.ownsProcess(processGeneration)) return;
42300
+ this.emitText(this.stderrHandlers, chunk.toString());
42301
+ });
42302
+ child.once("error", (error) => {
42303
+ if (!this.ownsProcess(processGeneration)) return;
42304
+ this.fail(new CodexAppServerConnectionError(`Failed to start Codex app-server: ${error.message}`, { cause: error }), generation);
42305
+ });
42306
+ child.once("exit", (code, signal) => {
42307
+ if (!this.ownsProcess(processGeneration)) return;
42308
+ this.fail(new CodexAppServerConnectionError(`Codex app-server exited (code ${code ?? "null"}, signal ${signal ?? "null"})`), generation);
42309
+ });
42310
+ }
42311
+ requestForGeneration(generation, method, params) {
42312
+ const processGeneration = this.processGeneration;
42313
+ if (!processGeneration || !this.ownsProcess(processGeneration) || generation !== this.activeGeneration) return Promise.reject(new CodexAppServerConnectionError("Codex app-server stdin is unavailable"));
42314
+ const id = ++this.nextRequestId;
42315
+ const key = String(id);
42316
+ return new Promise((resolve, reject) => {
42317
+ const timeout = setTimeout(() => {
42318
+ const current = this.pendingRequests.get(key);
42319
+ if (!current || current.generation !== generation) return;
42320
+ this.pendingRequests.delete(key);
42321
+ const error = new CodexAppServerConnectionError(`Codex app-server request timed out: ${method}`);
42322
+ reject(error);
42323
+ this.fail(error, generation);
42324
+ }, APP_SERVER_RPC_TIMEOUT_MS);
42325
+ timeout.unref?.();
42326
+ const pending = {
42327
+ generation,
42328
+ method,
42329
+ reject,
42330
+ resolve: (result) => resolve(result),
42331
+ threadId: isRecord$6(params) ? pickString$1(params.threadId) : void 0,
42332
+ timeout
42333
+ };
42334
+ this.pendingRequests.set(key, pending);
42335
+ try {
42336
+ this.writeToProcess(processGeneration, {
42337
+ id,
42338
+ method,
42339
+ params
42340
+ });
42341
+ } catch (error) {
42342
+ if (this.pendingRequests.get(key) === pending) this.pendingRequests.delete(key);
42343
+ clearTimeout(timeout);
42344
+ const connectionError = this.toConnectionError(error, `Failed to write Codex app-server request: ${method}`);
42345
+ reject(connectionError);
42346
+ this.fail(connectionError, generation);
42347
+ }
42348
+ });
42349
+ }
42350
+ consumeStdout(processGeneration, chunk) {
42351
+ processGeneration.stdoutBuffer += chunk.toString("utf8");
42352
+ let newlineIndex;
42353
+ while ((newlineIndex = processGeneration.stdoutBuffer.indexOf("\n")) >= 0) {
42354
+ const line = processGeneration.stdoutBuffer.slice(0, newlineIndex).trim();
42355
+ processGeneration.stdoutBuffer = processGeneration.stdoutBuffer.slice(newlineIndex + 1);
42356
+ if (!line) continue;
42357
+ try {
42358
+ const message = JSON.parse(line);
42359
+ const threadId = this.getMessageThreadId(processGeneration.generation, message);
42360
+ const rawHandlers = threadId ? this.rawMessageHandlers.get(threadId) : void 0;
42361
+ if (rawHandlers) this.emitText(rawHandlers, `${line}\n`);
42362
+ this.routeMessage(processGeneration.generation, message);
42363
+ } catch (error) {
42364
+ this.fail(new CodexAppServerConnectionError("Codex app-server emitted invalid NDJSON", { cause: error }), processGeneration.generation);
42365
+ return;
42366
+ }
42367
+ }
42368
+ }
42369
+ getMessageThreadId(generation, message) {
42370
+ if (!isRecord$6(message)) return;
42371
+ if (isRecord$6(message.params)) {
42372
+ const threadId = pickString$1(message.params.threadId);
42373
+ if (threadId) return threadId;
42374
+ }
42375
+ const id = message.id;
42376
+ if (id === void 0) return;
42377
+ const pending = this.pendingRequests.get(String(id));
42378
+ return pending?.generation === generation ? pending.threadId : void 0;
42379
+ }
42380
+ routeMessage(generation, message) {
42381
+ if (!this.isCurrentGeneration(generation)) return;
42382
+ if (!isRecord$6(message)) throw new TypeError("Expected an app-server RPC object");
42383
+ const method = pickString$1(message.method);
42384
+ const id = message.id;
42385
+ if (method) {
42386
+ if (id !== void 0) this.routeServerRequest(generation, id, method, message.params);
42387
+ else this.routeNotification(method, message.params);
42388
+ return;
42389
+ }
42390
+ if (id === void 0) return;
42391
+ const key = String(id);
42392
+ const pending = this.pendingRequests.get(key);
42393
+ if (!pending || pending.generation !== generation) return;
42394
+ this.pendingRequests.delete(key);
42395
+ clearTimeout(pending.timeout);
42396
+ if (isRecord$6(message.error)) pending.reject(new CodexAppServerRpcError(pickString$1(message.error.message) ?? `Codex app-server request failed: ${pending.method}`, typeof message.error.code === "number" ? message.error.code : void 0, message.error.data, pending.method));
42397
+ else pending.resolve(message.result);
42398
+ }
42399
+ routeNotification(method, params) {
42400
+ const threadId = isRecord$6(params) ? pickString$1(params.threadId) : void 0;
42401
+ if (!threadId) return;
42402
+ for (const handler of this.notificationHandlers.get(threadId) ?? []) handler(method, params);
42403
+ }
42404
+ async routeServerRequest(generation, id, method, params) {
42405
+ const threadId = isRecord$6(params) ? pickString$1(params.threadId) : void 0;
42406
+ const handler = (threadId ? this.serverRequestHandlers.get(threadId) : void 0)?.values().next().value;
42407
+ let response;
42408
+ try {
42409
+ if (handler) response = {
42410
+ id,
42411
+ result: await handler(method, params)
42412
+ };
42413
+ else if (APPROVAL_REQUEST_METHODS.has(method)) response = {
42414
+ id,
42415
+ result: { decision: "cancel" }
42416
+ };
42417
+ else response = {
42418
+ error: {
42419
+ code: -32601,
42420
+ message: `Unsupported Codex app-server request: ${method}`
42421
+ },
42422
+ id
42423
+ };
42424
+ } catch (error) {
42425
+ response = {
42426
+ error: {
42427
+ code: -32603,
42428
+ message: error instanceof Error ? error.message : "Codex server request failed"
42429
+ },
42430
+ id
42431
+ };
42432
+ }
42433
+ if (!this.isCurrentGeneration(generation)) return;
42434
+ try {
42435
+ this.writeForGeneration(generation, response);
42436
+ } catch (error) {
42437
+ this.fail(this.toConnectionError(error, "Failed to write Codex app-server response"), generation);
42438
+ }
42439
+ }
42440
+ writeForGeneration(generation, message) {
42441
+ const processGeneration = this.processGeneration;
42442
+ if (!processGeneration || processGeneration.generation !== generation) throw new CodexAppServerConnectionError("Codex app-server stdin is unavailable");
42443
+ this.writeToProcess(processGeneration, message);
42444
+ }
42445
+ writeToProcess(processGeneration, message) {
42446
+ if (!this.ownsProcess(processGeneration) || !processGeneration.child.stdin) throw new CodexAppServerConnectionError("Codex app-server stdin is unavailable");
42447
+ processGeneration.child.stdin.write(`${JSON.stringify(message)}\n`);
42448
+ }
42449
+ emitText(handlers, data) {
42450
+ for (const handler of handlers) handler(data);
42451
+ }
42452
+ removeHandler(map, key, handler) {
42453
+ const handlers = map.get(key);
42454
+ handlers?.delete(handler);
42455
+ if (handlers?.size === 0) map.delete(key);
42456
+ }
42457
+ fail(error, generation, reconnect = true) {
42458
+ if (!this.isCurrentGeneration(generation) || this.connectionError) return;
42459
+ this.connectionError = error;
42460
+ this.connected = false;
42461
+ if (this.connectionState?.generation === generation) this.connectionState = void 0;
42462
+ const processGeneration = this.processGeneration;
42463
+ if (processGeneration?.generation === generation) {
42464
+ this.processGeneration = void 0;
42465
+ this.terminateChild(processGeneration.child);
42466
+ }
42467
+ this.rejectPendingRequests(generation, error);
42468
+ this.emitDisconnect(error);
42469
+ if (reconnect) this.scheduleReconnect();
42470
+ }
42471
+ scheduleReconnect() {
42472
+ if (this.closedByHost || this.reconnectTimer || this.threadRegistrations.size === 0) return;
42473
+ const maxAttempts = Math.max(0, Math.floor(this.options.reconnectMaxAttempts ?? DEFAULT_RECONNECT_MAX_ATTEMPTS));
42474
+ const epoch = this.reconnectEpoch;
42475
+ if (this.reconnectAttempt >= maxAttempts) {
42476
+ if (this.reconnectExhaustedEpoch === epoch) return;
42477
+ this.reconnectExhaustedEpoch = epoch;
42478
+ const error = new CodexAppServerConnectionError(`Codex app-server reconnect exhausted after ${maxAttempts} attempts`);
42479
+ this.notifyResumeErrors(error);
42480
+ return;
42481
+ }
42482
+ const baseDelay = this.options.reconnectBaseDelayMs ?? DEFAULT_RECONNECT_BASE_DELAY_MS;
42483
+ const maxDelay = this.options.reconnectMaxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
42484
+ const delay = Math.min(baseDelay * 2 ** this.reconnectAttempt, maxDelay);
42485
+ this.reconnectAttempt += 1;
42486
+ this.reconnectTimer = setTimeout(() => {
42487
+ this.reconnectTimer = void 0;
42488
+ if (epoch !== this.reconnectEpoch || this.closedByHost) return;
42489
+ this.startConnection().catch(() => {});
42490
+ }, delay);
42491
+ this.reconnectTimer.unref?.();
42492
+ }
42493
+ async resumeRegisteredThreads(generation) {
42494
+ await Promise.all([...this.threadRegistrations.values()].map(async ({ params, registrations }) => {
42495
+ try {
42496
+ const response = await this.requestForGeneration(generation, "thread/resume", params);
42497
+ if (!this.isCurrentGeneration(generation) || !this.connected) return;
42498
+ await Promise.allSettled([...registrations].map(({ onResume }) => onResume(response)));
42499
+ } catch (error) {
42500
+ if (!this.isCurrentGeneration(generation) || !this.connected) return;
42501
+ const resumeError = error instanceof Error ? error : new Error(String(error));
42502
+ await Promise.allSettled([...registrations].map(({ onResumeError }) => onResumeError(resumeError)));
42503
+ }
42504
+ }));
42505
+ }
42506
+ startUserRecoveryEpoch() {
42507
+ if (this.reconnectTimer) {
42508
+ clearTimeout(this.reconnectTimer);
42509
+ this.reconnectTimer = void 0;
42510
+ }
42511
+ this.reconnectAttempt = 0;
42512
+ this.reconnectEpoch += 1;
42513
+ this.reconnectExhaustedEpoch = void 0;
42514
+ }
42515
+ notifyResumeErrors(error) {
42516
+ Promise.allSettled([...this.threadRegistrations.values()].flatMap(({ registrations }) => [...registrations].map(({ onResumeError }) => onResumeError(error))));
42517
+ }
42518
+ rejectPendingRequests(generation, error) {
42519
+ for (const [key, pending] of this.pendingRequests) {
42520
+ if (pending.generation !== generation) continue;
42521
+ this.pendingRequests.delete(key);
42522
+ clearTimeout(pending.timeout);
42523
+ pending.reject(error);
42524
+ }
42525
+ }
42526
+ emitDisconnect(error) {
42527
+ for (const handler of this.disconnectHandlers) try {
42528
+ handler(error);
42529
+ } catch (handlerError) {
42530
+ console.error("Codex app-server disconnect handler failed:", handlerError);
42531
+ }
42532
+ }
42533
+ isCurrentGeneration(generation) {
42534
+ return !this.closedByHost && this.activeGeneration === generation;
42535
+ }
42536
+ ownsProcess(processGeneration) {
42537
+ return this.isCurrentGeneration(processGeneration.generation) && this.processGeneration === processGeneration;
42538
+ }
42539
+ toConnectionError(error, message) {
42540
+ if (error instanceof CodexAppServerRpcError || error instanceof CodexAppServerConnectionError) return error;
42541
+ return new CodexAppServerConnectionError(message, { cause: error });
42542
+ }
42543
+ terminateChild(child) {
42544
+ if (!child?.pid || child.killed) return;
42545
+ if (process.platform !== "win32") try {
42546
+ process.kill(-child.pid, "SIGTERM");
42547
+ return;
42548
+ } catch {}
42549
+ child.kill("SIGTERM");
42550
+ }
42551
+ };
42552
+
41670
42553
  //#endregion
41671
42554
  //#region node_modules/.pnpm/diff@8.0.4/node_modules/diff/libesm/diff/base.js
41672
42555
  var Diff = class {
@@ -50954,7 +51837,7 @@ const buildSpawnArgs = (params) => {
50954
51837
  default: throw new Error(`spawnAgent: unsupported agent type "${params.agentType}"`);
50955
51838
  }
50956
51839
  };
50957
- const killProcessTree$1 = (proc, signal, detached) => {
51840
+ const killProcessTree = (proc, signal, detached) => {
50958
51841
  if (!proc.pid || proc.killed) return;
50959
51842
  if (platform() === "win32") {
50960
51843
  try {
@@ -51357,7 +52240,7 @@ const spawnAgent = async (options) => {
51357
52240
  exit,
51358
52241
  kill: (signal = "SIGINT") => {
51359
52242
  killedByUs = true;
51360
- killProcessTree$1(proc, signal, detached);
52243
+ killProcessTree(proc, signal, detached);
51361
52244
  },
51362
52245
  pid: proc.pid,
51363
52246
  get sessionId() {
@@ -248553,6 +249436,13 @@ const MAX_MISSED_HEARTBEATS = 3;
248553
249436
  */
248554
249437
  const CONNECT_TIMEOUT = 15e3;
248555
249438
  const METRICS_ACK_TIMEOUT_MS = 15e3;
249439
+ /**
249440
+ * Close reason the gateway sends when a newer socket with the same
249441
+ * `connectionId` takes over. `connectionId` is persisted per install, so this
249442
+ * is how a second process of the same install (two `lh connect` sharing one
249443
+ * home) shows up.
249444
+ */
249445
+ const REPLACED_CLOSE_REASON = "Replaced by new connection";
248556
249446
  const noopLogger = {
248557
249447
  debug: () => {},
248558
249448
  error: () => {},
@@ -248570,6 +249460,15 @@ var GatewayClient = class extends EventEmitter {
248570
249460
  status = "disconnected";
248571
249461
  pendingMetricAcks = /* @__PURE__ */ new Map();
248572
249462
  intentionalDisconnect = false;
249463
+ /**
249464
+ * When each socket abandoned mid-handshake was abandoned. Only such a socket
249465
+ * can reach the gateway after its successor and knock it off with the
249466
+ * takeover reason: an opened socket was registered before the successor
249467
+ * existed. Each can do that at most once, and only within `connectTimeoutMs`
249468
+ * (the handshake budget), so a takeover consumes one live entry and anything
249469
+ * beyond them is another client.
249470
+ */
249471
+ inFlightAbandons = [];
248573
249472
  deviceId;
248574
249473
  connectionId;
248575
249474
  channel;
@@ -248848,6 +249747,13 @@ var GatewayClient = class extends EventEmitter {
248848
249747
  this.rejectPendingMetricAcks();
248849
249748
  this.clearConnectWatchdog();
248850
249749
  this.ws = null;
249750
+ if (!this.intentionalDisconnect && reason.toString() === REPLACED_CLOSE_REASON && !this.consumeInFlightAbandon()) {
249751
+ this.logger.warn("Connection taken over by another client with the same connection id; not reconnecting");
249752
+ this.setStatus("disconnected");
249753
+ this.emit("replaced");
249754
+ this.emit("disconnected");
249755
+ return;
249756
+ }
248851
249757
  if (!this.intentionalDisconnect && this.autoReconnect) {
248852
249758
  this.setStatus("reconnecting");
248853
249759
  this.scheduleReconnect();
@@ -248856,6 +249762,12 @@ var GatewayClient = class extends EventEmitter {
248856
249762
  this.emit("disconnected");
248857
249763
  }
248858
249764
  };
249765
+ /** Attribute a takeover to one of our own late sockets, if one can still land. */
249766
+ consumeInFlightAbandon() {
249767
+ const now = Date.now();
249768
+ this.inFlightAbandons = this.inFlightAbandons.filter((at) => now - at <= this.connectTimeoutMs);
249769
+ return this.inFlightAbandons.shift() !== void 0;
249770
+ }
248859
249771
  handleError = (error) => {
248860
249772
  this.logger.error("WebSocket error:", error.message);
248861
249773
  this.emit("error", error);
@@ -248945,6 +249857,7 @@ var GatewayClient = class extends EventEmitter {
248945
249857
  this.rejectPendingMetricAcks();
248946
249858
  if (!this.ws) return;
248947
249859
  const ws = this.ws;
249860
+ if (ws.readyState === wrapper_default.CONNECTING) this.inFlightAbandons.push(Date.now());
248948
249861
  const suppressCloseError = (error) => {
248949
249862
  this.logger.debug(`Ignoring WebSocket error during close: ${error.message}`);
248950
249863
  };
@@ -248992,6 +249905,95 @@ const getHeterogeneousTypeLabel = (type) => {
248992
249905
  return Object.hasOwn(HETEROGENEOUS_TYPE_LABELS, type) ? HETEROGENEOUS_TYPE_LABELS[type] : type;
248993
249906
  };
248994
249907
 
249908
+ //#endregion
249909
+ //#region ../../packages/heterogeneous-agents/src/models/codex.ts
249910
+ /** Discovery has no thread: provider overrides must be applied at process startup. */
249911
+ const catalogConfig = (source, initialCwd) => {
249912
+ const args = [];
249913
+ let cwd = initialCwd;
249914
+ const valueFlags = [
249915
+ "-c",
249916
+ "--config",
249917
+ "-C",
249918
+ "--cd",
249919
+ "--enable",
249920
+ "--disable"
249921
+ ];
249922
+ for (let index = 0; index < source.length; index += 1) {
249923
+ const arg = source[index];
249924
+ if (arg === "--") break;
249925
+ if ([
249926
+ "-p",
249927
+ "--profile",
249928
+ "--oss",
249929
+ "--local-provider",
249930
+ "--ignore-user-config"
249931
+ ].some((flag) => arg === flag || arg.startsWith(`${flag}=`) || flag === "-p" && arg.startsWith("-p"))) throw Object.assign(/* @__PURE__ */ new Error("Unsupported Codex catalog configuration"), { code: "unsupported_configuration" });
249932
+ const flag = valueFlags.find((candidate) => arg === candidate || arg.startsWith(`${candidate}=`) || candidate.length === 2 && arg.startsWith(candidate));
249933
+ if (!flag) continue;
249934
+ const value = arg === flag ? source[++index] : arg.slice(flag.length).replace(/^=/, "");
249935
+ if (!value) throw new Error(`Missing value for Codex ${flag}`);
249936
+ if (flag === "-C" || flag === "--cd") cwd = path.resolve(initialCwd, value);
249937
+ else args.push(flag, value);
249938
+ }
249939
+ return {
249940
+ args,
249941
+ cwd
249942
+ };
249943
+ };
249944
+ /** A short-lived catalog connection, independent of the exec/app-server task transport. */
249945
+ const listCodexModels = async ({ args = [], commandPath, cwd, env, timeoutMs }) => {
249946
+ const client = new CodexAppServerClient({
249947
+ ...catalogConfig(args, cwd),
249948
+ clientVersion: "1.0.0",
249949
+ commandPath,
249950
+ env,
249951
+ reconnectMaxAttempts: 0
249952
+ });
249953
+ let timer;
249954
+ const readPages = async () => {
249955
+ const models = /* @__PURE__ */ new Map();
249956
+ const cursors = /* @__PURE__ */ new Set();
249957
+ let cursor;
249958
+ do {
249959
+ const page = await client.request("model/list", {
249960
+ ...cursor ? { cursor } : {},
249961
+ includeHidden: false,
249962
+ limit: 100
249963
+ });
249964
+ if (!isRecord$6(page) || !Array.isArray(page.data)) throw new Error("Invalid Codex model catalog");
249965
+ for (const item of page.data) {
249966
+ if (!isRecord$6(item) || typeof item.model !== "string" || !item.model.trim()) throw new Error("Invalid Codex model catalog entry");
249967
+ if (item.hidden === true || models.has(item.model)) continue;
249968
+ models.set(item.model, {
249969
+ id: item.model,
249970
+ ...typeof item.displayName === "string" && item.displayName.trim() ? { label: item.displayName } : {},
249971
+ modelId: item.model,
249972
+ providerId: "codex"
249973
+ });
249974
+ }
249975
+ if (page.nextCursor == null) break;
249976
+ if (typeof page.nextCursor !== "string" || !page.nextCursor || cursors.has(page.nextCursor)) throw new Error("Invalid Codex model catalog cursor");
249977
+ cursor = page.nextCursor;
249978
+ cursors.add(cursor);
249979
+ } while (cursor);
249980
+ return [...models.values()];
249981
+ };
249982
+ try {
249983
+ return await Promise.race([readPages(), new Promise((_, reject) => {
249984
+ timer = setTimeout(() => {
249985
+ reject(Object.assign(/* @__PURE__ */ new Error("Codex model discovery timed out"), { code: "ETIMEDOUT" }));
249986
+ }, timeoutMs);
249987
+ })]);
249988
+ } catch (error) {
249989
+ if (error instanceof CodexAppServerRpcError && error.code === -32601) throw Object.assign(/* @__PURE__ */ new Error("Codex model discovery is not supported"), { code: "unsupported_client" });
249990
+ throw error;
249991
+ } finally {
249992
+ clearTimeout(timer);
249993
+ client.close();
249994
+ }
249995
+ };
249996
+
248995
249997
  //#endregion
248996
249998
  //#region ../../packages/heterogeneous-agents/src/models/listHeterogeneousAgentModels.ts
248997
249999
  init_cliSpawn();
@@ -249181,8 +250183,11 @@ const parseQoderModelCatalog = (stdout) => {
249181
250183
  };
249182
250184
  const getErrorRecord = (error) => error;
249183
250185
  const classifyCatalogError = (error) => {
250186
+ if (error instanceof Error && error.cause) return classifyCatalogError(error.cause);
249184
250187
  const { code, killed, signal } = getErrorRecord(error);
249185
250188
  if (code === "ENOENT") return "cli_not_found";
250189
+ if (code === "unsupported_client") return "unsupported_client";
250190
+ if (code === "unsupported_configuration") return "unsupported_configuration";
249186
250191
  if (code === "ETIMEDOUT" || killed || signal === "SIGTERM") return "timeout";
249187
250192
  return "command_failed";
249188
250193
  };
@@ -249210,6 +250215,17 @@ const listHeterogeneousAgentModels = async (params) => {
249210
250215
  ...mergedPath ? { PATH: mergedPath } : {}
249211
250216
  };
249212
250217
  try {
250218
+ if (params.type === "codex") return {
250219
+ models: await listCodexModels({
250220
+ args: params.args,
250221
+ commandPath: resolved.command,
250222
+ cwd: params.cwd ?? process.cwd(),
250223
+ env,
250224
+ timeoutMs: MODEL_CATALOG_TIMEOUT_MS
250225
+ }),
250226
+ status: "success",
250227
+ updatedAt
250228
+ };
249213
250229
  if (params.type === "droid") return {
249214
250230
  models: await listDroidAcpModels({
249215
250231
  args: params.args,
@@ -252822,7 +253838,7 @@ const getTargetFd = ({ value, optionName, fdNumber, direction }) => {
252822
253838
  if (tty.isatty(targetFdNumber)) throw new TypeError(`The \`${optionName}: ${serializeOptionValue(value)}\` option is invalid: it cannot be a TTY with synchronous methods.`);
252823
253839
  return {
252824
253840
  type: "uint8Array",
252825
- value: bufferToUint8Array(readFileSync$1(targetFdNumber)),
253841
+ value: bufferToUint8Array(readFileSync(targetFdNumber)),
252826
253842
  optionName
252827
253843
  };
252828
253844
  };
@@ -253103,8 +254119,8 @@ const addProperties$1 = {
253103
254119
  const addPropertiesSync = {
253104
254120
  input: {
253105
254121
  ...addProperties$1,
253106
- fileUrl: ({ value }) => ({ contents: [bufferToUint8Array(readFileSync$1(value))] }),
253107
- filePath: ({ value: { file } }) => ({ contents: [bufferToUint8Array(readFileSync$1(file))] }),
254122
+ fileUrl: ({ value }) => ({ contents: [bufferToUint8Array(readFileSync(value))] }),
254123
+ filePath: ({ value: { file } }) => ({ contents: [bufferToUint8Array(readFileSync(file))] }),
253108
254124
  fileNumber: forbiddenIfSync,
253109
254125
  iterable: ({ value }) => ({ contents: [...value] }),
253110
254126
  string: ({ value }) => ({ contents: [value] }),
@@ -256682,65 +257698,6 @@ const createFileSearchModule = (toolDetector) => {
256682
257698
  }
256683
257699
  };
256684
257700
 
256685
- //#endregion
256686
- //#region ../../packages/local-file-shell/src/shell/clixml.ts
256687
- /**
256688
- * Decode PowerShell CLIXML blocks embedded in redirected stderr.
256689
- *
256690
- * When powershell.exe / pwsh.exe runs with redirected stderr, records from its
256691
- * non-stdout streams (error, warning, verbose, information / Write-Host) are
256692
- * serialized to stderr as CLIXML: `#< CLIXML\n<Objs ...>...</Objs>`. Shown
256693
- * verbatim this is unreadable noise for the model and the user, so we extract
256694
- * the human-readable messages and replace each block in place — the same
256695
- * approach Ansible's `_parse_clixml` takes.
256696
- */
256697
- const CLIXML_MARKER = "#< CLIXML";
256698
- /** CLIXML escapes characters as `_xHHHH_` (UTF-16 code units). */
256699
- const unescapeClixmlText = (text) => text.replaceAll(/_x([\dA-Fa-f]{4})_/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))).replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", "\"").replaceAll("&apos;", "'").replaceAll("&amp;", "&");
256700
- /** Stream-typed string records: `<S S="Error">text</S>` (PS 5.1 style). */
256701
- const STREAM_ENTRY = /<S S="(?:Error|Warning|Verbose|Debug|Info)">([\S\s]*?)<\/S>/g;
256702
- /**
256703
- * Rendered text of serialized records (`InformationRecord` / `HostInformationMessage`
256704
- * from Write-Host end up here). Nested objects repeat the same message in their
256705
- * own `<ToString>`, so consecutive duplicates are collapsed by the caller.
256706
- */
256707
- const TO_STRING_ENTRY = /<ToString>([\S\s]*?)<\/ToString>/g;
256708
- const extractBlockMessages = (block) => {
256709
- const streamTexts = [...block.matchAll(STREAM_ENTRY)].map((m) => m[1]);
256710
- const texts = streamTexts.length > 0 ? streamTexts : [...block.matchAll(TO_STRING_ENTRY)].map((m) => m[1]);
256711
- const messages = [];
256712
- for (const raw of texts) {
256713
- const text = unescapeClixmlText(raw);
256714
- if (messages.at(-1) === text) continue;
256715
- messages.push(text);
256716
- }
256717
- return messages;
256718
- };
256719
- /**
256720
- * Replace every CLIXML block in `text` with its decoded messages. Plain text
256721
- * around the blocks is preserved; input without a marker is returned as-is.
256722
- * Truncated blocks (output preview cuts) decode whatever entries survived.
256723
- */
256724
- const decodeClixml = (text) => {
256725
- if (!text.includes(CLIXML_MARKER)) return text;
256726
- let result = "";
256727
- let cursor = 0;
256728
- while (cursor < text.length) {
256729
- const markerIndex = text.indexOf(CLIXML_MARKER, cursor);
256730
- if (markerIndex === -1) {
256731
- result += text.slice(cursor);
256732
- break;
256733
- }
256734
- result += text.slice(cursor, markerIndex);
256735
- const closeIndex = text.indexOf("</Objs>", markerIndex);
256736
- const blockEnd = closeIndex === -1 ? text.length : closeIndex + 7;
256737
- const messages = extractBlockMessages(text.slice(markerIndex, blockEnd));
256738
- if (messages.length > 0) result += messages.map((m) => m.endsWith("\n") ? m : `${m}\n`).join("");
256739
- cursor = blockEnd;
256740
- }
256741
- return result;
256742
- };
256743
-
256744
257701
  //#endregion
256745
257702
  //#region node_modules/.pnpm/tree-kill@1.2.2/node_modules/tree-kill/index.js
256746
257703
  var require_tree_kill = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -256838,6 +257795,118 @@ var require_tree_kill = /* @__PURE__ */ __commonJSMin(((exports, module) => {
256838
257795
  }
256839
257796
  }));
256840
257797
 
257798
+ //#endregion
257799
+ //#region ../../packages/local-file-shell/src/shell/child-process-backend.ts
257800
+ var import_tree_kill = /* @__PURE__ */ __toESM(require_tree_kill());
257801
+ const KILL_SIGNAL = "SIGKILL";
257802
+ /**
257803
+ * Non-interactive backend: a plain `child_process` with stdin left as an
257804
+ * unused pipe and stdout/stderr inherited straight into the output files.
257805
+ */
257806
+ var ChildProcessBackend = class {
257807
+ capabilities = {
257808
+ interactive: false,
257809
+ persistent: false,
257810
+ streaming: false
257811
+ };
257812
+ name = "child-process";
257813
+ processes = /* @__PURE__ */ new Map();
257814
+ /**
257815
+ * @param spawnProcess - spawn implementation; a caller can pass one that
257816
+ * registers each process with a tracker. Defaults to `child_process.spawn`.
257817
+ */
257818
+ constructor(spawnProcess = spawn) {
257819
+ this.spawnProcess = spawnProcess;
257820
+ }
257821
+ spawn({ cmd, args }, { cwd, env, outputFiles, shellId }) {
257822
+ const childProcess = this.spawnProcess(cmd, args, {
257823
+ cwd,
257824
+ detached: process.platform !== "win32",
257825
+ env,
257826
+ shell: false,
257827
+ stdio: [
257828
+ "pipe",
257829
+ outputFiles.stdout.fd,
257830
+ outputFiles.stderr.fd
257831
+ ],
257832
+ windowsHide: true
257833
+ });
257834
+ this.processes.set(shellId, childProcess);
257835
+ return childProcess;
257836
+ }
257837
+ kill(shellId) {
257838
+ const childProcess = this.processes.get(shellId);
257839
+ if (!childProcess) throw new Error(`Shell ID ${shellId} not found`);
257840
+ if (childProcess.pid) {
257841
+ (0, import_tree_kill.default)(childProcess.pid, KILL_SIGNAL);
257842
+ return;
257843
+ }
257844
+ childProcess.kill(KILL_SIGNAL);
257845
+ }
257846
+ release(shellId) {
257847
+ this.processes.delete(shellId);
257848
+ }
257849
+ };
257850
+
257851
+ //#endregion
257852
+ //#region ../../packages/local-file-shell/src/shell/clixml.ts
257853
+ /**
257854
+ * Decode PowerShell CLIXML blocks embedded in redirected stderr.
257855
+ *
257856
+ * When powershell.exe / pwsh.exe runs with redirected stderr, records from its
257857
+ * non-stdout streams (error, warning, verbose, information / Write-Host) are
257858
+ * serialized to stderr as CLIXML: `#< CLIXML\n<Objs ...>...</Objs>`. Shown
257859
+ * verbatim this is unreadable noise for the model and the user, so we extract
257860
+ * the human-readable messages and replace each block in place — the same
257861
+ * approach Ansible's `_parse_clixml` takes.
257862
+ */
257863
+ const CLIXML_MARKER = "#< CLIXML";
257864
+ /** CLIXML escapes characters as `_xHHHH_` (UTF-16 code units). */
257865
+ const unescapeClixmlText = (text) => text.replaceAll(/_x([\dA-Fa-f]{4})_/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))).replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", "\"").replaceAll("&apos;", "'").replaceAll("&amp;", "&");
257866
+ /** Stream-typed string records: `<S S="Error">text</S>` (PS 5.1 style). */
257867
+ const STREAM_ENTRY = /<S S="(?:Error|Warning|Verbose|Debug|Info)">([\S\s]*?)<\/S>/g;
257868
+ /**
257869
+ * Rendered text of serialized records (`InformationRecord` / `HostInformationMessage`
257870
+ * from Write-Host end up here). Nested objects repeat the same message in their
257871
+ * own `<ToString>`, so consecutive duplicates are collapsed by the caller.
257872
+ */
257873
+ const TO_STRING_ENTRY = /<ToString>([\S\s]*?)<\/ToString>/g;
257874
+ const extractBlockMessages = (block) => {
257875
+ const streamTexts = [...block.matchAll(STREAM_ENTRY)].map((m) => m[1]);
257876
+ const texts = streamTexts.length > 0 ? streamTexts : [...block.matchAll(TO_STRING_ENTRY)].map((m) => m[1]);
257877
+ const messages = [];
257878
+ for (const raw of texts) {
257879
+ const text = unescapeClixmlText(raw);
257880
+ if (messages.at(-1) === text) continue;
257881
+ messages.push(text);
257882
+ }
257883
+ return messages;
257884
+ };
257885
+ /**
257886
+ * Replace every CLIXML block in `text` with its decoded messages. Plain text
257887
+ * around the blocks is preserved; input without a marker is returned as-is.
257888
+ * Truncated blocks (output preview cuts) decode whatever entries survived.
257889
+ */
257890
+ const decodeClixml = (text) => {
257891
+ if (!text.includes(CLIXML_MARKER)) return text;
257892
+ let result = "";
257893
+ let cursor = 0;
257894
+ while (cursor < text.length) {
257895
+ const markerIndex = text.indexOf(CLIXML_MARKER, cursor);
257896
+ if (markerIndex === -1) {
257897
+ result += text.slice(cursor);
257898
+ break;
257899
+ }
257900
+ result += text.slice(cursor, markerIndex);
257901
+ const closeIndex = text.indexOf("</Objs>", markerIndex);
257902
+ const blockEnd = closeIndex === -1 ? text.length : closeIndex + 7;
257903
+ const messages = extractBlockMessages(text.slice(markerIndex, blockEnd));
257904
+ if (messages.length > 0) result += messages.map((m) => m.endsWith("\n") ? m : `${m}\n`).join("");
257905
+ cursor = blockEnd;
257906
+ }
257907
+ return result;
257908
+ };
257909
+
256841
257910
  //#endregion
256842
257911
  //#region ../../packages/local-file-shell/src/shell/utils.ts
256843
257912
  /** Maximum preview bytes returned inline to prevent context explosion */
@@ -257287,7 +258356,6 @@ if (-not \$__lobeExecOk) {
257287
258356
 
257288
258357
  //#endregion
257289
258358
  //#region ../../packages/local-file-shell/src/shell/process-manager.ts
257290
- var import_tree_kill = /* @__PURE__ */ __toESM(require_tree_kill());
257291
258359
  const DEFAULT_OBSERVATION_TIMEOUT_MS = 6e4;
257292
258360
  /**
257293
258361
  * Ceiling on a single observation window. Sized for the real unit of work an
@@ -257336,7 +258404,6 @@ const GET_COMMAND_OUTPUT_HEAD_RATIO = 0;
257336
258404
  const OUTPUT_PREVIEW_TOTAL_MAX_BYTES = 22 * 1024;
257337
258405
  const OUTPUT_PREVIEW_STREAM_MAX_BYTES = 18 * 1024;
257338
258406
  const OUTPUT_PREVIEW_SECONDARY_MIN_BYTES = 4 * 1024;
257339
- const KILL_SIGNAL = "SIGKILL";
257340
258407
  /**
257341
258408
  * Not-found text for a shell id this process never issued. Shell ids travel
257342
258409
  * through the model and the device gateway, so a lookup can land on a device
@@ -257346,6 +258413,8 @@ const KILL_SIGNAL = "SIGKILL";
257346
258413
  */
257347
258414
  const shellNotFoundError = (shellId) => `Shell ID ${shellId} not found in this device process. It may have been started by a different or restarted process; its output is not available here.`;
257348
258415
  var ShellProcessManager = class {
258416
+ /** Default backend for commands spawned through this manager. */
258417
+ backend;
257349
258418
  nextShellId = 1;
257350
258419
  /**
257351
258420
  * Per-instance random prefix for shell ids. A bare counter restarts at 1 in
@@ -257356,7 +258425,8 @@ var ShellProcessManager = class {
257356
258425
  shellIdToken = randomBytes(3).toString("hex");
257357
258426
  outputRunDir;
257358
258427
  processes = /* @__PURE__ */ new Map();
257359
- constructor(outputRoot) {
258428
+ constructor(outputRoot, backend = new ChildProcessBackend()) {
258429
+ this.backend = backend;
257360
258430
  const date = /* @__PURE__ */ new Date();
257361
258431
  this.outputRunDir = path.join(path.resolve(outputRoot ?? path.join(os.tmpdir(), "lobehub", "shell")), `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`, process.pid.toString());
257362
258432
  fs.mkdirSync(this.outputRunDir, {
@@ -257424,9 +258494,9 @@ var ShellProcessManager = class {
257424
258494
  stdout: "",
257425
258495
  success: false
257426
258496
  };
257427
- const { process: childProcess } = shellProcess;
257428
- let exitCode = childProcess.exitCode ?? shellProcess.exitCode;
257429
- if (exitCode === null && childProcess.signalCode == null) {
258497
+ const { process: handle } = shellProcess;
258498
+ let exitCode = handle.exitCode ?? shellProcess.exitCode;
258499
+ if (exitCode === null && handle.signalCode == null) {
257430
258500
  const waitTimeout = resolveWaitTimeout(typeof timeout === "number" && Number.isFinite(timeout) ? Math.min(Math.max(Math.trunc(timeout), 0), MAX_OBSERVATION_TIMEOUT_MS) : DEFAULT_OBSERVATION_TIMEOUT_MS);
257431
258501
  if (waitTimeout > 0) {
257432
258502
  let timer;
@@ -257436,11 +258506,11 @@ var ShellProcessManager = class {
257436
258506
  await Promise.race([
257437
258507
  new Promise((resolve) => {
257438
258508
  onError = resolve;
257439
- childProcess.once("error", onError);
258509
+ handle.once("error", onError);
257440
258510
  }),
257441
258511
  new Promise((resolve) => {
257442
258512
  onExit = resolve;
257443
- childProcess.once("exit", onExit);
258513
+ handle.once("exit", onExit);
257444
258514
  }),
257445
258515
  new Promise((resolve) => {
257446
258516
  timer = setTimeout(resolve, waitTimeout);
@@ -257448,12 +258518,12 @@ var ShellProcessManager = class {
257448
258518
  ]);
257449
258519
  } finally {
257450
258520
  if (timer) clearTimeout(timer);
257451
- if (onError) childProcess.off("error", onError);
257452
- if (onExit) childProcess.off("exit", onExit);
258521
+ if (onError) handle.off("error", onError);
258522
+ if (onExit) handle.off("exit", onExit);
257453
258523
  }
257454
258524
  }
257455
258525
  }
257456
- exitCode = childProcess.exitCode ?? shellProcess.exitCode;
258526
+ exitCode = handle.exitCode ?? shellProcess.exitCode;
257457
258527
  if (exitCode !== null) {
257458
258528
  shellProcess.endedAt ??= Date.now();
257459
258529
  await shellProcess.closed;
@@ -257472,7 +258542,7 @@ var ShellProcessManager = class {
257472
258542
  } catch {}
257473
258543
  const startedAt = shellProcess.startedAt ?? Date.now();
257474
258544
  const durationMs = Math.max(0, (shellProcess.endedAt ?? Date.now()) - startedAt);
257475
- const signal = childProcess.signalCode ?? void 0;
258545
+ const signal = handle.signalCode ?? void 0;
257476
258546
  const running = exitCode === null && !signal && !shellProcess.spawnError;
257477
258547
  return {
257478
258548
  duration_ms: durationMs,
@@ -257505,7 +258575,7 @@ var ShellProcessManager = class {
257505
258575
  success: false
257506
258576
  };
257507
258577
  try {
257508
- killProcessTree(shellProcess.process);
258578
+ (shellProcess.backend ?? this.backend).kill(shell_id);
257509
258579
  return { success: true };
257510
258580
  } catch (error) {
257511
258581
  return {
@@ -257516,9 +258586,11 @@ var ShellProcessManager = class {
257516
258586
  }
257517
258587
  cleanupAll() {
257518
258588
  for (const [id, sp] of this.processes) {
258589
+ const backend = sp.backend ?? this.backend;
257519
258590
  try {
257520
- killProcessTree(sp.process);
258591
+ backend.kill(id);
257521
258592
  } catch {}
258593
+ backend.release?.(id);
257522
258594
  this.closeOutputFiles(sp.outputFiles);
257523
258595
  this.processes.delete(id);
257524
258596
  }
@@ -257544,14 +258616,6 @@ var ShellProcessManager = class {
257544
258616
  };
257545
258617
  }
257546
258618
  };
257547
- const killProcessTree = (childProcess) => {
257548
- const { pid } = childProcess;
257549
- if (pid) {
257550
- (0, import_tree_kill.default)(pid, KILL_SIGNAL);
257551
- return;
257552
- }
257553
- childProcess.kill(KILL_SIGNAL);
257554
- };
257555
258619
  const allocateOutputPreviewBytes = (stdoutSize, stderrSize) => {
257556
258620
  if (stdoutSize + stderrSize <= OUTPUT_PREVIEW_TOTAL_MAX_BYTES) return {
257557
258621
  stderr: stderrSize,
@@ -279889,12 +280953,12 @@ function writeTrustBundle(dir, caCertPem, extraCaCertPaths) {
279889
280953
  const parts = [caCertPem.trim(), ...rootCertificates];
279890
280954
  const extra = process.env.NODE_EXTRA_CA_CERTS;
279891
280955
  if (extra) try {
279892
- parts.push(readFileSync$1(extra, "utf8").trim());
280956
+ parts.push(readFileSync(extra, "utf8").trim());
279893
280957
  } catch {}
279894
280958
  for (const extraPath of extraCaCertPaths ?? []) {
279895
280959
  let raw;
279896
280960
  try {
279897
- raw = readFileSync$1(extraPath, "utf8");
280961
+ raw = readFileSync(extraPath, "utf8");
279898
280962
  } catch (err) {
279899
280963
  logForDebugging(`[mitm-ca] extraCaCertPaths: cannot read ${extraPath} (${err.code ?? String(err)}); skipping`, { level: "warn" });
279900
280964
  continue;
@@ -280039,7 +281103,7 @@ function generateEphemeralCA(extraCaCertPaths) {
280039
281103
  function readPem(path, label, field) {
280040
281104
  let pem;
280041
281105
  try {
280042
- pem = readFileSync$1(path, "utf8");
281106
+ pem = readFileSync(path, "utf8");
280043
281107
  } catch (err) {
280044
281108
  const code = err.code ?? String(err);
280045
281109
  throw new Error(`${field}: cannot read ${path} (${code})`);
@@ -290697,11 +291761,12 @@ const checkWorkingDirectory = async (cwd) => {
290697
291761
  return `Working directory does not exist on ${os.hostname()}: ${cwd}. The shell is fine — run the command from a directory that exists on this device.`;
290698
291762
  }
290699
291763
  };
290700
- async function runCommand$1({ command, cwd, description, env: extraEnv, run_in_background, timeout = DEFAULT_OBSERVATION_TIMEOUT_MS }, { processManager, logger, onSandboxUnavailable, sandboxPolicy, spawnProcess = spawn }) {
291764
+ async function runCommand$1({ command, cwd, description, env: extraEnv, run_in_background, timeout = DEFAULT_OBSERVATION_TIMEOUT_MS }, { backend: backendOverride, processManager, logger, onSandboxUnavailable, sandboxPolicy, spawnProcess }) {
290701
291765
  if (!command) return {
290702
291766
  error: "command is required",
290703
291767
  success: false
290704
291768
  };
291769
+ const backend = backendOverride ?? (spawnProcess ? new ChildProcessBackend(spawnProcess) : processManager.backend);
290705
291770
  const logPrefix = `[runCommand: ${description || command.slice(0, 50)}]`;
290706
291771
  logger?.debug(`${logPrefix} Starting`, {
290707
291772
  background: run_in_background,
@@ -290748,37 +291813,35 @@ async function runCommand$1({ command, cwd, description, env: extraEnv, run_in_b
290748
291813
  const shellId = processManager.createShellId();
290749
291814
  const shellOutputFiles = processManager.createOutputFiles(shellId);
290750
291815
  outputFiles = shellOutputFiles;
290751
- const childProcess = spawnProcess(launchCommand.cmd, launchCommand.args, {
291816
+ const handle = backend.spawn({
291817
+ args: launchCommand.args,
291818
+ cmd: launchCommand.cmd
291819
+ }, {
290752
291820
  cwd,
290753
- detached: process.platform !== "win32",
290754
291821
  env: launchEnv,
290755
- shell: false,
290756
- stdio: [
290757
- "pipe",
290758
- shellOutputFiles.stdout.fd,
290759
- shellOutputFiles.stderr.fd
290760
- ],
290761
- windowsHide: true
291822
+ outputFiles: shellOutputFiles,
291823
+ shellId
290762
291824
  });
290763
291825
  const shellProcess = {
291826
+ backend,
290764
291827
  exitCode: null,
290765
291828
  outputFiles: shellOutputFiles,
290766
- process: childProcess
291829
+ process: handle
290767
291830
  };
290768
- childProcess.on("exit", (code) => {
291831
+ handle.on("exit", (code) => {
290769
291832
  logger?.debug(`${logPrefix} Process exited`, {
290770
291833
  code,
290771
291834
  shellId
290772
291835
  });
290773
291836
  shellProcess.exitCode = code ?? 0;
290774
291837
  });
290775
- childProcess.on("error", (error) => {
291838
+ handle.on("error", (error) => {
290776
291839
  logger?.error(`${logPrefix} Command failed:`, error);
290777
291840
  const cwdContext = cwd ? ` (working directory: ${cwd})` : "";
290778
291841
  shellProcess.spawnError = new Error(`Failed to start command${cwdContext}: ${error.message}`, { cause: error });
290779
291842
  shellProcess.exitCode = 1;
290780
291843
  });
290781
- childProcess.once("close", () => releaseSandbox?.());
291844
+ handle.once("close", () => releaseSandbox?.());
290782
291845
  processManager.register(shellId, shellProcess);
290783
291846
  processManager.closeOutputFiles(shellOutputFiles);
290784
291847
  logger?.info?.(`${logPrefix} Started session`, {
@@ -504097,6 +505160,10 @@ async function runConnect(options, isDaemonChild) {
504097
505160
  error(`Could not refresh workspace ${wsId} connect token. Closing share connection.`);
504098
505161
  closeWorkspaceConnection(wsId);
504099
505162
  });
505163
+ wsClient.on("replaced", () => {
505164
+ error(`Workspace ${wsId} share connection was taken over by another '${"lh"} connect' on this machine. Closing it here.`);
505165
+ closeWorkspaceConnection(wsId);
505166
+ });
504100
505167
  wsClient.on("auth_failed", (reason) => {
504101
505168
  error(`Workspace ${wsId} authentication failed: ${reason}. Closing share connection.`);
504102
505169
  closeWorkspaceConnection(wsId);
@@ -504202,6 +505269,11 @@ async function runConnect(options, isDaemonChild) {
504202
505269
  cleanup();
504203
505270
  process.exit(1);
504204
505271
  });
505272
+ client.on("replaced", () => {
505273
+ error(`Another '${"lh"} connect' on this machine took over this device's gateway connection. Only one can be connected at a time; stopping this one.`);
505274
+ cleanup();
505275
+ process.exit(1);
505276
+ });
504205
505277
  client.on("error", (err) => {
504206
505278
  error(`Connection error: ${err.message}`);
504207
505279
  });
@@ -509628,7 +510700,20 @@ const hasOperationToken = () => {
509628
510700
  };
509629
510701
  function registerGoalCommand$1(program) {
509630
510702
  const goal = program.command("goal").description("Run long-horizon Goal Graphs");
509631
- goal.command("plan <id>").description("Atomically submit the current main Agent plan").requiredOption("--file <path>", "JSON plan: action tasks/verify/retry/escalate, reason and action fields").requiredOption("--token <token>", "Current server-issued planning turn token").option("--operation <id>", "Defaults to LOBEHUB_OPERATION_ID").option("--json", "Output JSON").action(async (id, options) => {
510703
+ goal.command("wake <id>").description("Deliver an external result to a specific Goal wait").requiredOption("--token <token>", "managerState.token from the waiting Goal").requiredOption("--event <id>", "Stable delivery ID").requiredOption("--type <type>", "Event type declared by the wait").requiredOption("--key <key>", "Event correlation key declared by the wait").option("--summary <text>", "Short observation").option("--reference <reference>", "Evidence reference, not an inline dataset").option("--json", "Output JSON").action(async (id, options) => {
510704
+ const result = await (await getTrpcClient()).goal.wake.mutate({
510705
+ id,
510706
+ waitToken: options.token,
510707
+ eventId: options.event,
510708
+ type: options.type,
510709
+ key: options.key,
510710
+ summary: options.summary,
510711
+ reference: options.reference
510712
+ });
510713
+ if (options.json) outputJson(result.data);
510714
+ else console.log(result.data.accepted ? "Event recorded; Goal continuation queued." : `Event not applied: ${result.data.reason}`);
510715
+ });
510716
+ goal.command("plan <id>").description("Atomically submit the current main Agent plan").requiredOption("--file <path>", "JSON plan: action tasks/wait/verify/retry/escalate, reason and action fields").requiredOption("--token <token>", "Current server-issued planning turn token").option("--operation <id>", "Defaults to LOBEHUB_OPERATION_ID").option("--json", "Output JSON").action(async (id, options) => {
509632
510717
  const operationId = options.operation ?? process.env.LOBEHUB_OPERATION_ID;
509633
510718
  if (!operationId) throw new Error("Current manager operation ID required");
509634
510719
  const client = await getTrpcClient();
@@ -509656,31 +510741,31 @@ function registerGoalCommand$1(program) {
509656
510741
  if (options.json) outputJson(result.data);
509657
510742
  else console.log("Goal report recorded.");
509658
510743
  });
509659
- goal.command("create <title>").option("--max-manager-turns <n>", "Add a main Agent with this turn limit; it takes over problems the system planner cannot route").description("Create a standalone goal and seed its graph").option("-r, --requirement <text>", "Acceptance requirement").option("-i, --instruction <text>", "The ask in the user's own words, shown on the problem node").option("-t, --task <title...>", "Initial task node titles (omit to let the planner decompose)").option("--agent <id>", "Goal agent: supervises and plans the goal (defaults to the current agent)").option("--task-agent <id>", "Dedicated executor for the goal's tasks (defaults to the goal agent)").option("--project <id>", "Project ID").option("--explore <instruction>", "Explore alternatives using completed experiment results").option("--max-experiments <n>", "Maximum experiment nodes (requires --explore, default 10)").option("--supervise", "Enable bounded recovery supervision in an independent topic").option("--max-supervision-incidents <n>", "Maximum supervised interruptions (default 10, maximum 100)").option("--max-rounds <n>", "Maximum goal rounds").option("--max-cost <usd>", "Maximum total cost in USD").option("--max-attempts-per-task <n>", "Attempts per Task before opening a decision gate").option("--max-concurrent-tasks <n>", "How many of this goal's tasks may run at once (default 3)").option("--max-steps-per-run <n>", "Optional agent step cap per Task run (for example 500)").option("--operation-lease-timeout-ms <n>", "Reclaim a Task operation after this idle time (minimum: 60000)").option("--conversation", "Create the goal from the current conversation run (/goal): this agent supervises it from this conversation, and the output carries a turnToken for the first plan").option("--criterion <text...>", "Acceptance criterion (repeatable)").option("--json [fields]", "Output JSON").action(async (title, options) => {
510744
+ goal.command("create <title>").option("--max-manager-turns <n>", "Add a main Agent with this turn limit; it takes over problems the system planner cannot route").description("Create a standalone goal and seed its graph").option("-r, --requirement <text>", "Acceptance requirement").option("-i, --instruction <text>", "The ask in the user's own words, shown on the problem node").option("-t, --task <title...>", "Initial task node titles (omit to let the planner decompose)").option("--agent <id>", "Goal agent: supervises and plans the goal (defaults to the current agent)").option("--task-agent <id>", "Dedicated executor for the goal's tasks (defaults to the goal agent)").option("--project <id>", "Project ID").option("--explore <instruction>", "Explore alternatives using completed experiment results").option("--max-experiments <n>", "Maximum experiment nodes (requires --explore, default 10)").option("--max-supervision-incidents <n>", "Maximum supervised interruptions (supervision is always on; default 10, maximum 100)").option("--max-rounds <n>", "Maximum goal rounds").option("--max-cost <usd>", "Maximum total cost in USD").option("--max-attempts-per-task <n>", "Attempts per Task before opening a decision gate").option("--max-concurrent-tasks <n>", "How many of this goal's tasks may run at once (default 3)").option("--max-steps-per-run <n>", "Optional agent step cap per Task run (for example 500)").option("--operation-lease-timeout-ms <n>", "Reclaim a Task operation after this idle time (minimum: 60000)").option("--conversation", "Link the goal to the current conversation. Use it whenever you create a goal for the user from a chat, not only for /goal: without it the goal is standalone and never shows on this conversation's goal tray. This agent supervises the goal from this conversation; the output carries a turnToken, so submit the first plan with `lh goal plan <id> --token <turnToken> --file <plan.json>`").option("--criterion <text...>", "Acceptance criterion (repeatable)").option("--json [fields]", "Output JSON").action(async (title, options) => {
509660
510745
  if (options.maxExperiments && !options.explore) throw new Error("--max-experiments requires --explore");
509661
510746
  const operationId = process.env.LOBEHUB_OPERATION_ID;
509662
510747
  if (options.conversation && !operationId) throw new Error("--conversation must run inside an agent conversation (LOBEHUB_OPERATION_ID)");
509663
510748
  const client = await getTrpcClient();
509664
510749
  const buildUrl = await resolveAppUrlBuilder(client);
509665
510750
  const goalInput = {
509666
- config: options.maxManagerTurns || options.explore || options.supervise || options.maxAttemptsPerTask || options.maxStepsPerRun || options.operationLeaseTimeoutMs || options.maxConcurrentTasks || options.taskAgent ? {
509667
- taskAgentId: options.taskAgent,
509668
- manager: options.maxManagerTurns ? { maxTurns: Number(options.maxManagerTurns) } : void 0,
510751
+ config: {
509669
510752
  exploration: options.explore ? {
509670
510753
  instruction: options.explore,
509671
510754
  maxExperiments: Number(options.maxExperiments ?? 10)
509672
510755
  } : void 0,
509673
- supervision: options.supervise ? {
509674
- enabled: true,
509675
- maxIncidents: options.maxSupervisionIncidents ? Number.parseInt(options.maxSupervisionIncidents, 10) : void 0
509676
- } : void 0,
510756
+ manager: options.maxManagerTurns ? { maxTurns: Number(options.maxManagerTurns) } : void 0,
509677
510757
  maxConcurrentTasks: options.maxConcurrentTasks ? Number.parseInt(options.maxConcurrentTasks, 10) : void 0,
509678
510758
  recovery: {
509679
510759
  maxAttemptsPerTask: options.maxAttemptsPerTask ? Number.parseInt(options.maxAttemptsPerTask, 10) : void 0,
509680
510760
  maxStepsPerRun: options.maxStepsPerRun ? Number.parseInt(options.maxStepsPerRun, 10) : void 0,
509681
510761
  operationLeaseTimeoutMs: options.operationLeaseTimeoutMs ? Number.parseInt(options.operationLeaseTimeoutMs, 10) : void 0
509682
- }
509683
- } : void 0,
510762
+ },
510763
+ supervision: {
510764
+ enabled: true,
510765
+ ...options.maxSupervisionIncidents ? { maxIncidents: Number.parseInt(options.maxSupervisionIncidents, 10) } : {}
510766
+ },
510767
+ taskAgentId: options.taskAgent
510768
+ },
509684
510769
  criteria: options.criterion?.map((criterion) => ({ title: criterion })),
509685
510770
  maxRounds: options.maxRounds ? Number.parseInt(options.maxRounds, 10) : void 0,
509686
510771
  maxTotalCost: options.maxCost ? Number.parseFloat(options.maxCost) : void 0,
@@ -513776,7 +514861,7 @@ function object(shape, params) {
513776
514861
  }
513777
514862
 
513778
514863
  //#endregion
513779
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
514864
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
513780
514865
  function isZ4Schema(s) {
513781
514866
  return !!s._zod;
513782
514867
  }
@@ -513900,7 +514985,7 @@ function getLiteralValue(schema) {
513900
514985
  }
513901
514986
 
513902
514987
  //#endregion
513903
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
514988
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
513904
514989
  const LATEST_PROTOCOL_VERSION = "2025-11-25";
513905
514990
  const DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26";
513906
514991
  const SUPPORTED_PROTOCOL_VERSIONS = [
@@ -515866,7 +516951,7 @@ var UrlElicitationRequiredError = class extends McpError {
515866
516951
  };
515867
516952
 
515868
516953
  //#endregion
515869
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js
516954
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js
515870
516955
  /**
515871
516956
  * Experimental task interfaces for MCP SDK.
515872
516957
  * WARNING: These APIs are experimental and may change without notice.
@@ -517009,7 +518094,7 @@ const zodToJsonSchema = (schema, options) => {
517009
518094
  };
517010
518095
 
517011
518096
  //#endregion
517012
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js
518097
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js
517013
518098
  function mapMiniTarget(t) {
517014
518099
  if (!t) return "draft-7";
517015
518100
  if (t === "jsonSchema7" || t === "draft-7") return "draft-7";
@@ -517040,7 +518125,7 @@ function parseWithCompat(schema, data) {
517040
518125
  }
517041
518126
 
517042
518127
  //#endregion
517043
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
518128
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
517044
518129
  /**
517045
518130
  * The default request timeout, in miliseconds.
517046
518131
  */
@@ -518218,7 +519303,7 @@ var require_dist = /* @__PURE__ */ __commonJSMin(((exports, module) => {
518218
519303
  }));
518219
519304
 
518220
519305
  //#endregion
518221
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/validation/ajv-provider.js
519306
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/validation/ajv-provider.js
518222
519307
  /**
518223
519308
  * AJV-based JSON Schema validator provider
518224
519309
  */
@@ -518297,7 +519382,7 @@ var AjvJsonSchemaValidator = class {
518297
519382
  };
518298
519383
 
518299
519384
  //#endregion
518300
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
519385
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
518301
519386
  /**
518302
519387
  * Experimental server task features for MCP SDK.
518303
519388
  * WARNING: These APIs are experimental and may change without notice.
@@ -518518,7 +519603,7 @@ var ExperimentalServerTasks = class {
518518
519603
  };
518519
519604
 
518520
519605
  //#endregion
518521
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
519606
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
518522
519607
  /**
518523
519608
  * Experimental task capability assertion helpers.
518524
519609
  * WARNING: These APIs are experimental and may change without notice.
@@ -518570,7 +519655,7 @@ function assertClientRequestTaskCapability(requests, method, entityName) {
518570
519655
  }
518571
519656
 
518572
519657
  //#endregion
518573
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js
519658
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js
518574
519659
  /**
518575
519660
  * An MCP server on top of a pluggable transport.
518576
519661
  *
@@ -518905,7 +519990,7 @@ var Server = class extends Protocol {
518905
519990
  };
518906
519991
 
518907
519992
  //#endregion
518908
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/completable.js
519993
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/completable.js
518909
519994
  const COMPLETABLE_SYMBOL = Symbol.for("mcp.completable");
518910
519995
  /**
518911
519996
  * Checks if a schema is completable (has completion metadata).
@@ -518925,7 +520010,7 @@ var McpZodTypeKind;
518925
520010
  })(McpZodTypeKind || (McpZodTypeKind = {}));
518926
520011
 
518927
520012
  //#endregion
518928
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/toolNameValidation.js
520013
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/toolNameValidation.js
518929
520014
  /**
518930
520015
  * Tool name validation utilities according to SEP: Specify Format for Tool Names
518931
520016
  *
@@ -518997,7 +520082,7 @@ function validateAndWarnToolName(name) {
518997
520082
  }
518998
520083
 
518999
520084
  //#endregion
519000
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/mcp-server.js
520085
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/mcp-server.js
519001
520086
  /**
519002
520087
  * Experimental McpServer task features for MCP SDK.
519003
520088
  * WARNING: These APIs are experimental and may change without notice.
@@ -519029,7 +520114,7 @@ var ExperimentalMcpServerTasks = class {
519029
520114
  };
519030
520115
 
519031
520116
  //#endregion
519032
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js
520117
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js
519033
520118
  init_zod$1();
519034
520119
  function toolInputElementCount(value, max) {
519035
520120
  let count = 0;
@@ -519683,11 +520768,11 @@ const EMPTY_COMPLETION_RESULT = { completion: {
519683
520768
  } };
519684
520769
 
519685
520770
  //#endregion
519686
- //#region node_modules/.pnpm/@hono+node-server@2.1.3_hono@4.13.12/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs
520771
+ //#region node_modules/.pnpm/@hono+node-server@2.1.3_hono@4.13.13/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs
519687
520772
  const X_ALREADY_SENT = "x-hono-already-sent";
519688
520773
 
519689
520774
  //#endregion
519690
- //#region node_modules/.pnpm/hono@4.13.12/node_modules/hono/dist/helper/websocket/index.js
520775
+ //#region node_modules/.pnpm/hono@4.13.13/node_modules/hono/dist/helper/websocket/index.js
519691
520776
  /**
519692
520777
  * Create a WebSocket adapter/helper
519693
520778
  */
@@ -519712,7 +520797,7 @@ const defineWebSocketHelper = (handler) => {
519712
520797
  };
519713
520798
 
519714
520799
  //#endregion
519715
- //#region node_modules/.pnpm/@hono+node-server@2.1.3_hono@4.13.12/node_modules/@hono/node-server/dist/index.mjs
520800
+ //#region node_modules/.pnpm/@hono+node-server@2.1.3_hono@4.13.13/node_modules/@hono/node-server/dist/index.mjs
519716
520801
  var RequestError = class extends Error {
519717
520802
  constructor(message, options) {
519718
520803
  super(message, options);
@@ -520978,7 +522063,7 @@ var require_content_type = /* @__PURE__ */ __commonJSMin(((exports) => {
520978
522063
  }));
520979
522064
 
520980
522065
  //#endregion
520981
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/mediaType.js
522066
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/mediaType.js
520982
522067
  var import_content_type = /* @__PURE__ */ __toESM(require_content_type(), 1);
520983
522068
  /**
520984
522069
  * Extracts the media type (the lowercased `type/subtype` pair, without
@@ -521025,7 +522110,7 @@ function isJsonContentType(header) {
521025
522110
  }
521026
522111
 
521027
522112
  //#endregion
521028
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/requestBody.js
522113
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/requestBody.js
521029
522114
  /** Default upper bound, in bytes, on a request body read by the HTTP entry points (4 MiB). */
521030
522115
  const DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024 * 1024;
521031
522116
  /** The message answered with 413 for a request body over `maxBytes`. */
@@ -521076,7 +522161,7 @@ async function readRequestBody(request, maxBytes = DEFAULT_MAX_REQUEST_BODY_SIZE
521076
522161
  }
521077
522162
 
521078
522163
  //#endregion
521079
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sseKeepAlive.js
522164
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sseKeepAlive.js
521080
522165
  /** Default interval between SSE keep-alive comment frames. */
521081
522166
  const DEFAULT_SSE_KEEP_ALIVE_MS = 15e3;
521082
522167
  const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
@@ -521089,7 +522174,7 @@ function armSseKeepAlive(intervalMs, onTick) {
521089
522174
  }
521090
522175
 
521091
522176
  //#endregion
521092
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js
522177
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js
521093
522178
  /**
521094
522179
  * Web Standards Streamable HTTP Server Transport
521095
522180
  *
@@ -521757,7 +522842,7 @@ var WebStandardStreamableHTTPServerTransport = class {
521757
522842
  };
521758
522843
 
521759
522844
  //#endregion
521760
- //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.0_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/streamableHttp.js
522845
+ //#region node_modules/.pnpm/@modelcontextprotocol+sdk@1.32.1_zod@4.6.5/node_modules/@modelcontextprotocol/sdk/dist/esm/server/streamableHttp.js
521761
522846
  /**
521762
522847
  * Node.js HTTP Streamable HTTP Server Transport
521763
522848
  *
@@ -528305,10 +529390,10 @@ function registerLifecycleCommands(task) {
528305
529390
  return;
528306
529391
  }
528307
529392
  const { serverUrl, headers } = await getAuthInfo();
528308
- await streamAgentEvents(`${serverUrl}/api/agent/stream?operationId=${encodeURIComponent(result.operationId)}`, headers, {
529393
+ if ((await streamAgentEvents(`${serverUrl}/api/agent/stream?operationId=${encodeURIComponent(result.operationId)}`, headers, {
528309
529394
  json: options.json,
528310
529395
  verbose: options.verbose
528311
- });
529396
+ }))?.kind === "failed") process.exit(1);
528312
529397
  try {
528313
529398
  await client.task.heartbeat.mutate({ id });
528314
529399
  } catch {}
@@ -528353,10 +529438,10 @@ function registerLifecycleCommands(task) {
528353
529438
  return;
528354
529439
  }
528355
529440
  const { serverUrl, headers } = await getAuthInfo();
528356
- await streamAgentEvents(`${serverUrl}/api/agent/stream?operationId=${encodeURIComponent(operationId)}`, headers, {
529441
+ if ((await streamAgentEvents(`${serverUrl}/api/agent/stream?operationId=${encodeURIComponent(operationId)}`, headers, {
528357
529442
  json: options.json,
528358
529443
  verbose: options.verbose
528359
- });
529444
+ }))?.kind === "failed") process.exit(1);
528360
529445
  try {
528361
529446
  await client.task.heartbeat.mutate({ id });
528362
529447
  } catch {}
@@ -530847,7 +531932,7 @@ function evidenceTypeForFile(file) {
530847
531932
  function inlineTextEvidenceForFile(file, type) {
530848
531933
  if (!INLINE_TEXT_EVIDENCE_TYPES.has(type)) return void 0;
530849
531934
  try {
530850
- const buffer = readFileSync$1(file);
531935
+ const buffer = readFileSync(file);
530851
531936
  if (buffer.includes(0)) return void 0;
530852
531937
  const content = buffer.toString("utf8");
530853
531938
  return content.length > 0 && content.length < 5e3 ? content : void 0;
@@ -531387,7 +532472,7 @@ function interactionCostFromReportDir(dir, result) {
531387
532472
  if (!existsSync(tracePath)) return void 0;
531388
532473
  let raw;
531389
532474
  try {
531390
- raw = readFileSync$1(tracePath, "utf8");
532475
+ raw = readFileSync(tracePath, "utf8");
531391
532476
  } catch {
531392
532477
  log$13.warn(`${GOMS_KLM_TRACE_FILE} could not be read — publishing without interaction cost`);
531393
532478
  return;
@@ -532018,7 +533103,7 @@ async function ingestReportAction(reportDir, options) {
532018
533103
  }
532019
533104
  let result;
532020
533105
  try {
532021
- result = JSON.parse(readFileSync$1(resultPath, "utf8"));
533106
+ result = JSON.parse(readFileSync(resultPath, "utf8"));
532022
533107
  } catch {
532023
533108
  log$13.error("result.json is not valid JSON");
532024
533109
  process.exit(1);
@@ -532043,9 +533128,9 @@ async function ingestReportAction(reportDir, options) {
532043
533128
  });
532044
533129
  const summary = result.summary ?? {};
532045
533130
  const reportMdPath = path.join(dir, "report.md");
532046
- const content = existsSync(reportMdPath) ? readFileSync$1(reportMdPath, "utf8") : void 0;
533131
+ const content = existsSync(reportMdPath) ? readFileSync(reportMdPath, "utf8") : void 0;
532047
533132
  const proposalMdPath = path.join(dir, "proposal.md");
532048
- const proposal = (existsSync(proposalMdPath) ? readFileSync$1(proposalMdPath, "utf8") : typeof result.proposal === "string" ? result.proposal : "").trim();
533133
+ const proposal = (existsSync(proposalMdPath) ? readFileSync(proposalMdPath, "utf8") : typeof result.proposal === "string" ? result.proposal : "").trim();
532049
533134
  const scenario = scenarioFromResult(result);
532050
533135
  let context;
532051
533136
  let pullRequest;
@@ -532073,6 +533158,7 @@ async function ingestReportAction(reportDir, options) {
532073
533158
  process.exit(1);
532074
533159
  }
532075
533160
  let subject = subjectFromResult(result);
533161
+ let foldTaskRunTopic = false;
532076
533162
  if (!requestedAcceptanceId && options.subject) {
532077
533163
  const ref = parseSubjectRef(options.subject);
532078
533164
  if (!ref) {
@@ -532089,6 +533175,7 @@ async function ingestReportAction(reportDir, options) {
532089
533175
  } else if (!requestedAcceptanceId && !subject) {
532090
533176
  const ref = subjectFromEnv();
532091
533177
  if (ref) subject = { ref };
533178
+ foldTaskRunTopic = Boolean(ref);
532092
533179
  }
532093
533180
  if (!requestedAcceptanceId && !subject) subject = { ref: {
532094
533181
  subjectId: randomUUID(),
@@ -532122,8 +533209,16 @@ async function ingestReportAction(reportDir, options) {
532122
533209
  requirement,
532123
533210
  subjectId: subject.ref.subjectId,
532124
533211
  subjectType: subject.ref.subjectType,
533212
+ ...foldTaskRunTopic ? { foldTaskRunTopic } : {},
532125
533213
  ...subject.ref.subjectType === "standalone" && (title || goal) ? { title: title || goal } : {}
532126
533214
  });
533215
+ subject = {
533216
+ ...subject,
533217
+ ref: {
533218
+ subjectId: acceptance.subjectId,
533219
+ subjectType: acceptance.subjectType
533220
+ }
533221
+ };
532127
533222
  bundle = await client.acceptance.getBundle.query({ id: acceptance.id });
532128
533223
  }
532129
533224
  plan = reuseSourceCriteria(plan, bundle?.checks);
@@ -532949,20 +534044,12 @@ function registerVerifyCommand(program) {
532949
534044
  console.log(`${import_picocolors.default.green("✓")} Rubric ${import_picocolors.default.bold(rubricId)} now has ${criterionIds.length} criterion(s)`);
532950
534045
  });
532951
534046
  const plan = verify.command("plan").description("Per-run check plan lifecycle");
532952
- plan.command("generate <operationId>").description("Generate a draft check plan for a run").requiredOption("--goal <goal>", "The run's task/instruction the plan must satisfy").option("--rubric <id>", "Mounted rubric id").option("--criteria <ids>", "Ad-hoc criterion ids (comma-separated)").option("--ai", "Let the LLM propose additional criteria").option("--max-ai <n>", "Max AI-proposed criteria").option("--model <model>", "Model (required with --ai)").option("--provider <provider>", "Provider (required with --ai)").option("--context <text>", "Extra context for the AI prompt").option("--json [fields]", "Output JSON").action(async (operationId, options) => {
532953
- if (options.ai && (!options.model || !options.provider)) {
532954
- log$13.error("--ai requires --model and --provider");
532955
- process.exit(1);
532956
- }
534047
+ plan.command("generate <operationId>").description("Generate a draft check plan for a run").requiredOption("--goal <goal>", "The run's task/instruction the plan must satisfy").option("--rubric <id>", "Mounted rubric id").option("--criteria <ids>", "Ad-hoc criterion ids (comma-separated)").option("--ai", "Let the LLM propose additional criteria").option("--max-ai <n>", "Max AI-proposed criteria").option("--context <text>", "Extra context for the AI prompt").option("--json [fields]", "Output JSON").action(async (operationId, options) => {
532957
534048
  const items = await (await getTrpcClient()).verify.generateDraftPlan.mutate({
532958
534049
  context: options.context,
532959
534050
  enableAiGeneration: options.ai,
532960
534051
  goal: options.goal,
532961
534052
  maxAiCriteria: options.maxAi ? Number.parseInt(options.maxAi, 10) : void 0,
532962
- modelConfig: options.model && options.provider ? {
532963
- model: options.model,
532964
- provider: options.provider
532965
- } : void 0,
532966
534053
  operationId,
532967
534054
  verifyCriteriaIds: options.criteria?.split(",").map((s) => s.trim()).filter(Boolean),
532968
534055
  verifyRubricId: options.rubric ?? null