@dashup/connector 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -670,13 +670,13 @@ var require_common = __commonJS({
670
670
  }
671
671
  }
672
672
  }
673
- function matchesTemplate(search, template) {
673
+ function matchesTemplate(search2, template) {
674
674
  let searchIndex = 0;
675
675
  let templateIndex = 0;
676
676
  let starIndex = -1;
677
677
  let matchIndex = 0;
678
- while (searchIndex < search.length) {
679
- if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === "*")) {
678
+ while (searchIndex < search2.length) {
679
+ if (templateIndex < template.length && (template[templateIndex] === search2[searchIndex] || template[templateIndex] === "*")) {
680
680
  if (template[templateIndex] === "*") {
681
681
  starIndex = templateIndex;
682
682
  matchIndex = searchIndex;
@@ -3802,7 +3802,7 @@ function bridgeAcp(socket, onUpdate) {
3802
3802
  if (session && direction === "agent" && message.method === methods.client.session.requestPermission) {
3803
3803
  const tool = message.params.toolCall ?? {};
3804
3804
  const diff = (tool.content ?? []).find((part) => part.type === "diff");
3805
- const approval3 = {
3805
+ const approval2 = {
3806
3806
  id: randomUUID(),
3807
3807
  title: String(message.params.title ?? tool.title ?? "Agent needs approval"),
3808
3808
  detail: String(message.params.description ?? ""),
@@ -3820,10 +3820,10 @@ ${String(diff.newText).split("\n").map((line) => "+" + line).join("\n")}`
3820
3820
  }
3821
3821
  } : {}
3822
3822
  };
3823
- session.approvals.set(String(message.id), approval3);
3823
+ session.approvals.set(String(message.id), approval2);
3824
3824
  permissions.set(String(message.id), session);
3825
3825
  session.status = "attention";
3826
- publish(session, approval3);
3826
+ publish(session, approval2);
3827
3827
  }
3828
3828
  if (direction === "editor" && !message.method) {
3829
3829
  const pending = permissions.get(String(message.id));
@@ -4048,7 +4048,7 @@ import * as ui from "@clack/prompts";
4048
4048
  import { z } from "zod";
4049
4049
 
4050
4050
  // package.json
4051
- var version = "0.1.15";
4051
+ var version = "0.1.17";
4052
4052
 
4053
4053
  // src/pair.ts
4054
4054
  var Invitation = z.object({
@@ -4215,6 +4215,24 @@ var attached = (id, mime, data, name) => ({
4215
4215
  });
4216
4216
  var MAX_PREVIEW = 64 * 1024;
4217
4217
  var NO_PREVIEW = "Review the full request on your computer before approving.";
4218
+ function questionApproval(id, items, multiple) {
4219
+ const asked = items.map((item) => ({
4220
+ ...typeof item?.header === "string" ? { header: item.header } : {},
4221
+ question: String(item?.question ?? ""),
4222
+ options: (Array.isArray(item?.options) ? item.options : []).filter((option2) => typeof option2?.label === "string").map((option2) => ({
4223
+ label: String(option2.label),
4224
+ ...typeof option2.description === "string" ? { description: option2.description } : {}
4225
+ })),
4226
+ ...multiple(item) ? { multiple: true } : {}
4227
+ }));
4228
+ return {
4229
+ id,
4230
+ title: asked.length === 1 ? asked[0].question : `${asked.length} questions`,
4231
+ detail: "",
4232
+ choices: [{ id: "skip", label: "Skip" }],
4233
+ questions: asked
4234
+ };
4235
+ }
4218
4236
 
4219
4237
  // src/harness/history.ts
4220
4238
  var SAMPLE_BYTES = 128 * 1024;
@@ -4222,8 +4240,11 @@ var READ_BYTES = 64 * 1024;
4222
4240
  var TITLE_LIMIT = 120;
4223
4241
  var LATEST_BYTES = 8 * 1024 * 1024;
4224
4242
  var JOURNALS = 16;
4243
+ var SEARCH_BYTES = 1024 * 1024;
4244
+ var MARKED = 512;
4225
4245
  var journals = /* @__PURE__ */ new Map();
4226
4246
  var metadata = /* @__PURE__ */ new Map();
4247
+ var marked = /* @__PURE__ */ new Map();
4227
4248
  function object(value2) {
4228
4249
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : void 0;
4229
4250
  }
@@ -4288,6 +4309,40 @@ function described(path, until, describe2) {
4288
4309
  metadata.set(path, { stamp, value: value2 });
4289
4310
  return value2;
4290
4311
  }
4312
+ function newest(path, marker) {
4313
+ const key = `${path}\0${marker}`;
4314
+ const stamp = statSync(path);
4315
+ const saved = marked.get(key);
4316
+ if (saved && unchanged(saved.stamp, stamp)) return saved.row;
4317
+ const grown = saved && saved.stamp.ino === stamp.ino && saved.stamp.size <= stamp.size;
4318
+ const start2 = grown ? Math.max(0, saved.stamp.size - SEARCH_BYTES) : 0;
4319
+ const row = search(path, marker, start2, stamp.size) ?? (grown ? saved.row : void 0);
4320
+ marked.delete(key);
4321
+ marked.set(key, { stamp, row });
4322
+ if (marked.size > MARKED) marked.delete(marked.keys().next().value);
4323
+ return row;
4324
+ }
4325
+ function search(path, marker, from, to) {
4326
+ let carry = Buffer.alloc(0);
4327
+ let position = to;
4328
+ while (position > from) {
4329
+ const begin = Math.max(from, position - SEARCH_BYTES);
4330
+ const buffer = Buffer.concat([readRange(path, begin, position - begin), carry]);
4331
+ const first = begin > from ? buffer.indexOf(10) + 1 : 0;
4332
+ position = begin;
4333
+ if (begin > from && !first) {
4334
+ carry = buffer;
4335
+ continue;
4336
+ }
4337
+ const lines = buffer.subarray(first).toString("utf8").split("\n");
4338
+ for (let index = lines.length - 1; index >= 0; index--) {
4339
+ const row = lines[index].includes(marker) ? parse(lines[index]) : void 0;
4340
+ if (row) return row;
4341
+ }
4342
+ carry = buffer.subarray(0, first);
4343
+ }
4344
+ return void 0;
4345
+ }
4291
4346
  var windowed = (path, messages5) => journals.get(path)?.start ? [EARLIER, ...messages5] : messages5;
4292
4347
  function journal(path, latest = false) {
4293
4348
  const stamp = statSync(path);
@@ -4544,20 +4599,18 @@ function claudeConversation(conversation, rows) {
4544
4599
  }
4545
4600
  function claudeSettings(path) {
4546
4601
  const rows = journal(path, true);
4547
- const settings = {};
4602
+ const marker = object(newest(path, '"ultra_effort_')?.attachment)?.type;
4548
4603
  for (let index = rows.length - 1; index >= 0; index--) {
4549
4604
  const row = rows[index];
4550
4605
  const message = object(row.message);
4551
- const marker = row.type === "attachment" ? object(row.attachment)?.type : void 0;
4552
- if (settings.model === void 0 && row.type === "assistant" && !row.isSidechain && typeof message?.model === "string" && message.model !== "<synthetic>") {
4553
- settings.model = message.model;
4554
- settings.fast = object(message.usage)?.speed === "fast";
4555
- }
4556
- if (settings.ultracode === void 0 && (marker === "ultra_effort_enter" || marker === "ultra_effort_exit"))
4557
- settings.ultracode = marker === "ultra_effort_enter";
4558
- if (settings.model !== void 0 && settings.ultracode !== void 0) break;
4606
+ if (row.type === "assistant" && !row.isSidechain && typeof message?.model === "string" && message.model !== "<synthetic>")
4607
+ return {
4608
+ model: message.model,
4609
+ fast: object(message.usage)?.speed === "fast",
4610
+ ultracode: marker === "ultra_effort_enter"
4611
+ };
4559
4612
  }
4560
- return { ultracode: false, ...settings };
4613
+ return { ultracode: marker === "ultra_effort_enter" };
4561
4614
  }
4562
4615
  function claude(root, scan) {
4563
4616
  for (const path of files(join2(root, "projects"), scan, 1).filter((file) => file.endsWith(".jsonl"))) {
@@ -4614,6 +4667,21 @@ function applyPatch(patch) {
4614
4667
  +++ ${kind === "Delete" ? "/dev/null" : path}`
4615
4668
  ).replace(/^\*\*\* .*\n?/gm, "");
4616
4669
  }
4670
+ function codexSettings(path) {
4671
+ const rows = journal(path, true);
4672
+ const applied = object(newest(path, '"thread_settings_applied"')?.payload?.thread_settings);
4673
+ const fast = applied ? { fast: applied.service_tier === "priority" } : {};
4674
+ for (let index = rows.length - 1; index >= 0; index--) {
4675
+ const row = rows[index];
4676
+ const turn = row.type === "turn_context" ? object(row.payload) : void 0;
4677
+ const settings = row.payload?.type === "thread_settings_applied" ? object(row.payload.thread_settings) : void 0;
4678
+ const model = settings?.model ?? turn?.model;
4679
+ const effort = settings?.reasoning_effort ?? turn?.effort;
4680
+ if (typeof model === "string")
4681
+ return { model, ...typeof effort === "string" ? { effort } : {}, ...fast };
4682
+ }
4683
+ return fast;
4684
+ }
4617
4685
  function codexMessages(path) {
4618
4686
  const rows = journal(path, true);
4619
4687
  const outputs = /* @__PURE__ */ new Map();
@@ -4706,7 +4774,8 @@ function codex(root, scan) {
4706
4774
  "Codex thread"
4707
4775
  ),
4708
4776
  files: [path, ...indexed ? [index] : []],
4709
- read: () => codexMessages(path)
4777
+ read: () => codexMessages(path),
4778
+ settings: () => codexSettings(path)
4710
4779
  });
4711
4780
  } catch {
4712
4781
  scan.warnings.push(`Cannot read Codex session: ${path}`);
@@ -4882,7 +4951,7 @@ function openCodeLegacy(root, scan) {
4882
4951
  continue;
4883
4952
  scan.paths.add(root);
4884
4953
  const messages5 = join2(root, "message", row.id);
4885
- const newest = files(messages5, scan, 0).filter((file) => file.endsWith(".json")).sort().at(-1);
4954
+ const newest2 = files(messages5, scan, 0).filter((file) => file.endsWith(".json")).sort().at(-1);
4886
4955
  scan.sessions.push({
4887
4956
  harness: "opencode",
4888
4957
  id: row.id,
@@ -4890,7 +4959,7 @@ function openCodeLegacy(root, scan) {
4890
4959
  title: title(row.title, "OpenCode thread"),
4891
4960
  parent: typeof row.parentID === "string" ? row.parentID : void 0,
4892
4961
  updatedAt: date(
4893
- newest ? jsonFile(newest)?.time?.created : row.time?.created,
4962
+ newest2 ? jsonFile(newest2)?.time?.created : row.time?.created,
4894
4963
  statSync(path).mtimeMs
4895
4964
  ),
4896
4965
  files: [path, messages5, join2(root, "part")],
@@ -13246,22 +13315,22 @@ var FrontendAdapter = class {
13246
13315
  if (options.subscribe === false) requestData.__subscribe = false;
13247
13316
  const result = await this.transport.get(path, requestData);
13248
13317
  const items = result?.[(0, import_pluralize.default)(modelClass.type)] ?? result?.[modelClass.type + "s"] ?? result?.items ?? [];
13249
- const models = items.map(
13318
+ const models2 = items.map(
13250
13319
  (row) => modelClass.hydrate(this, row)
13251
13320
  );
13252
13321
  if (result?.__queryHash) {
13253
- Object.defineProperty(models, "__queryHash", {
13322
+ Object.defineProperty(models2, "__queryHash", {
13254
13323
  value: result.__queryHash,
13255
13324
  enumerable: false
13256
13325
  });
13257
13326
  }
13258
13327
  if (typeof result?.totalCount === "number") {
13259
- Object.defineProperty(models, "__totalCount", {
13328
+ Object.defineProperty(models2, "__totalCount", {
13260
13329
  value: result.totalCount,
13261
13330
  enumerable: false
13262
13331
  });
13263
13332
  }
13264
- return models;
13333
+ return models2;
13265
13334
  };
13266
13335
  chain.first = async () => {
13267
13336
  const all = await chain.find();
@@ -16433,720 +16502,460 @@ async function shareAttachment(config2, pair, project, thread, messages5, id) {
16433
16502
  import { promisify as promisify5 } from "node:util";
16434
16503
  import { execFile as execFile5 } from "node:child_process";
16435
16504
 
16436
- // src/harness/codex.ts
16437
- import WebSocket2 from "ws";
16505
+ // src/harness/headless.ts
16506
+ import { randomUUID as randomUUID3 } from "node:crypto";
16507
+ import { createInterface as createInterface3 } from "node:readline";
16508
+ import { spawn as spawn3 } from "node:child_process";
16509
+
16510
+ // src/harness/claude.ts
16438
16511
  import { homedir as homedir4 } from "node:os";
16439
- import { connect } from "node:net";
16440
- import { promisify as promisify2 } from "node:util";
16441
16512
  import { randomUUID as randomUUID2 } from "node:crypto";
16442
- import { execFile as execFile2 } from "node:child_process";
16443
- import { join as join4, isAbsolute as isAbsolute2 } from "node:path";
16444
- import { stat, readdir, readFile } from "node:fs/promises";
16445
- var REQUEST_MS = 4e3;
16446
- var MAX_BYTES = 8 * 1024 * 1024;
16447
- var MAX_SESSIONS = 100;
16448
- var MAX_SOCKETS = 32;
16449
- var MAX_PROCESSES = 4096;
16450
- var SOCKET_PARTS = ["app-server-control", "app-server-control.sock"];
16451
- var DECISIONS = [
16452
- { id: "accept", label: "Allow once" },
16453
- { id: "decline", label: "Reject" }
16454
- ];
16455
- function approval(id, method, params) {
16456
- const command2 = method === "item/commandExecution/requestApproval";
16457
- let detail = [
16458
- command2 && typeof params.command === "string" ? params.command : "",
16459
- typeof params.cwd === "string" ? `Working directory: ${params.cwd}` : "",
16460
- typeof params.grantRoot === "string" ? `Write access root: ${params.grantRoot}` : "",
16461
- typeof params.reason === "string" ? params.reason : ""
16462
- ].filter(Boolean).join("\n\n");
16463
- const oversized = Buffer.byteLength(detail) > MAX_PREVIEW;
16464
- const unavailable = !command2 || params.kind !== void 0 && params.kind !== "command" || typeof params.command !== "string" || !params.command.trim() || oversized;
16465
- if (oversized) detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
16466
- if (unavailable) detail = [detail, NO_PREVIEW].filter(Boolean).join("\n\n");
16513
+ import { dirname as dirname4, join as join4, basename as basename2 } from "node:path";
16514
+ import { spawn as spawn2 } from "node:child_process";
16515
+ import { createInterface as createInterface2 } from "node:readline";
16516
+ import { watch as watch2, readFileSync as readFileSync3 } from "node:fs";
16517
+ import { mkdir, readdir, readFile, readlink, rm } from "node:fs/promises";
16518
+ import { createServer as createServer2 } from "node:http";
16519
+ var MODELS_MS = 3e4;
16520
+ var COMMANDS = {
16521
+ model: (value2) => `/model ${value2}`,
16522
+ fast: (value2) => `/fast ${value2 ? "on" : "off"}`,
16523
+ ultracode: (value2) => `/effort ultracode ${value2 ? "on" : "off"}`
16524
+ };
16525
+ var offered = /* @__PURE__ */ new Map();
16526
+ var asking = /* @__PURE__ */ new Set();
16527
+ var forgetModels = () => offered.clear();
16528
+ function claudeModels(changed, command2 = "claude", version2 = "") {
16529
+ const key = `${command2}\0${version2}`;
16530
+ if (!offered.has(key) && !asking.has(key)) {
16531
+ asking.add(key);
16532
+ void listModels(command2).then((models2) => {
16533
+ asking.delete(key);
16534
+ if (!models2.length) return;
16535
+ offered.set(key, models2);
16536
+ changed();
16537
+ });
16538
+ }
16539
+ return offered.get(key) ?? [];
16540
+ }
16541
+ function listModels(command2) {
16542
+ return new Promise((resolve3) => {
16543
+ const child = spawn2(
16544
+ command2,
16545
+ ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"],
16546
+ { cwd: homedir4(), stdio: "pipe", shell: process.platform === "win32" }
16547
+ );
16548
+ const timer = setTimeout(() => child.kill(), MODELS_MS);
16549
+ child.stdin.on("error", () => {
16550
+ });
16551
+ child.once("error", () => resolve3([]));
16552
+ child.once("exit", () => {
16553
+ clearTimeout(timer);
16554
+ resolve3([]);
16555
+ });
16556
+ createInterface2({ input: child.stdout }).on("line", (line) => {
16557
+ try {
16558
+ const message = JSON.parse(line);
16559
+ if (message?.type !== "control_response" || message.response?.request_id !== "models")
16560
+ return;
16561
+ const models2 = message.response.response?.models;
16562
+ resolve3(
16563
+ (Array.isArray(models2) ? models2 : []).filter(
16564
+ (model) => typeof model?.value === "string" && typeof model.displayName === "string"
16565
+ )
16566
+ );
16567
+ child.stdin.end();
16568
+ } catch {
16569
+ }
16570
+ });
16571
+ child.stdin.write(
16572
+ `${JSON.stringify({ type: "control_request", request_id: "models", request: { subtype: "initialize" } })}
16573
+ `
16574
+ );
16575
+ });
16576
+ }
16577
+ function claudeOptions(models2, recorded) {
16578
+ const ran = typeof recorded.model === "string" ? recorded.model : "";
16579
+ const current = models2.find((model) => model.value !== "default" && model.resolvedModel === ran) ?? models2.find((model) => model.value === ran);
16580
+ const choices = models2.map((model) => ({ value: model.value, name: model.displayName }));
16581
+ if (!ran) return [];
16582
+ if (!current) choices.push({ value: ran, name: ran });
16583
+ return [
16584
+ { id: "model", name: "Model", value: current?.value ?? ran, choices },
16585
+ ...current?.supportsFastMode ? [{ id: "fast", name: "Fast", value: recorded.fast === true }] : [],
16586
+ ...current?.supportedEffortLevels?.includes("xhigh") ? [{ id: "ultracode", name: "Ultracode", value: recorded.ultracode === true }] : []
16587
+ ];
16588
+ }
16589
+ var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
16590
+ var SCOPE = 32;
16591
+ var preview2 = (text3) => Buffer.byteLength(text3) > MAX_PREVIEW ? Buffer.from(text3).subarray(0, MAX_PREVIEW).toString("utf8") : text3;
16592
+ function hunk(before, after, offset = 0) {
16593
+ const old = before ? before.replace(/\n$/, "").split("\n") : [];
16594
+ const next = after ? after.replace(/\n$/, "").split("\n") : [];
16595
+ let start2 = 0;
16596
+ while (start2 < old.length && start2 < next.length && old[start2] === next[start2]) start2++;
16597
+ let end = 0;
16598
+ while (end < old.length - start2 && end < next.length - start2 && old[old.length - 1 - end] === next[next.length - 1 - end])
16599
+ end++;
16600
+ const removed = old.slice(start2, old.length - end);
16601
+ const added2 = next.slice(start2, next.length - end);
16602
+ return {
16603
+ lines: [
16604
+ `@@ -${offset + start2 + 1},${removed.length} +${offset + start2 + 1},${added2.length} @@`,
16605
+ ...old.slice(Math.max(0, start2 - 3), start2).map((line) => ` ${line}`),
16606
+ ...removed.map((line) => `-${line}`),
16607
+ ...added2.map((line) => `+${line}`),
16608
+ ...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
16609
+ ],
16610
+ added: added2.length,
16611
+ deleted: removed.length
16612
+ };
16613
+ }
16614
+ function change(tool, input) {
16615
+ const path = input.file_path;
16616
+ if (typeof path !== "string") return void 0;
16617
+ let current = "";
16618
+ try {
16619
+ current = readFileSync3(path, "utf8");
16620
+ } catch {
16621
+ }
16622
+ const edit = (value2) => {
16623
+ const before = String(value2?.old_string ?? "");
16624
+ const at = before ? current.indexOf(before) : -1;
16625
+ return hunk(
16626
+ before,
16627
+ String(value2?.new_string ?? ""),
16628
+ at < 0 ? 0 : current.slice(0, at).split("\n").length - 1
16629
+ );
16630
+ };
16631
+ const hunks = tool === "Write" && typeof input.content === "string" ? [hunk(current, input.content)] : tool === "Edit" ? [edit(input)] : tool === "MultiEdit" && Array.isArray(input.edits) ? input.edits.map(edit) : [];
16632
+ if (!hunks.length) return void 0;
16633
+ const diff = [`--- ${path}`, `+++ ${path}`, ...hunks.flatMap((item) => item.lines)].join("\n");
16634
+ if (Buffer.byteLength(diff) > MAX_PREVIEW) return void 0;
16635
+ return {
16636
+ path,
16637
+ diff,
16638
+ added: hunks.reduce((sum, item) => sum + item.added, 0),
16639
+ deleted: hunks.reduce((sum, item) => sum + item.deleted, 0)
16640
+ };
16641
+ }
16642
+ function remember(suggestions = []) {
16643
+ const updates = suggestions.filter(
16644
+ (update) => update?.type === "addRules" && update.behavior === "allow" || update?.type === "addDirectories"
16645
+ );
16646
+ const rule = updates.flatMap((update) => update.rules ?? [])[0];
16647
+ const folder = updates.flatMap((update) => update.directories ?? [])[0];
16648
+ const scope2 = rule ? String(rule.ruleContent ?? rule.toolName) : folder ? `access to ${basename2(String(folder))}` : "";
16649
+ if (scope2)
16650
+ return {
16651
+ label: `Always allow ${scope2.length > SCOPE ? `${scope2.slice(0, SCOPE - 1)}\u2026` : scope2}`,
16652
+ updates
16653
+ };
16654
+ const edits = suggestions.find(
16655
+ (update) => update?.type === "setMode" && update.mode === "acceptEdits"
16656
+ );
16657
+ return edits ? { label: "Allow all edits", updates: [edits] } : void 0;
16658
+ }
16659
+ function ask(id, request2, host) {
16660
+ const { tool, input } = request2;
16661
+ if (tool === "AskUserQuestion" && Array.isArray(input.questions))
16662
+ return questionApproval(id, input.questions, (item) => item.multiSelect === true);
16663
+ if (tool === "ExitPlanMode")
16664
+ return {
16665
+ id,
16666
+ title: "Approve this plan?",
16667
+ detail: host ? "" : "Approve it on your computer, or tell Claude what to change.",
16668
+ choices: [
16669
+ ...host ? [{ id: "accept", label: "Approve" }] : [],
16670
+ { id: "revise", label: "Keep planning", text: true }
16671
+ ],
16672
+ plan: preview2(String(input.plan ?? ""))
16673
+ };
16674
+ const diff = change(tool, input);
16675
+ const always = remember(request2.suggestions);
16467
16676
  return {
16468
16677
  id,
16469
- title: command2 ? "Allow command?" : "Allow file changes?",
16470
- detail,
16471
- choices: DECISIONS.filter((choice) => !unavailable || choice.id === "decline").map((choice) => ({ ...choice }))
16678
+ title: tool === "Bash" ? "Allow command?" : EDITS.has(tool) ? "Allow file changes?" : `Allow ${request2.label ?? tool}?`,
16679
+ detail: preview2(
16680
+ [
16681
+ request2.description ?? (typeof input.description === "string" ? input.description : ""),
16682
+ typeof input.command === "string" ? input.command : diff ? "" : JSON.stringify(input, null, 2)
16683
+ ].filter(Boolean).join("\n\n")
16684
+ ),
16685
+ ...diff ? { change: diff } : {},
16686
+ choices: [
16687
+ { id: "accept", label: "Allow once" },
16688
+ ...always ? [{ id: "always", label: always.label }] : [],
16689
+ { id: "decline", label: "Reject" }
16690
+ ]
16472
16691
  };
16473
16692
  }
16474
- var codexHome = () => process.env.DASHUP_CODEX_HOME ?? process.env.CODEX_HOME ?? join4(homedir4(), ".codex");
16475
- async function sockets() {
16476
- const explicit = process.env.DASHUP_CODEX_SOCKET;
16477
- if (explicit !== void 0) return explicit && isAbsolute2(explicit) ? [explicit] : [];
16693
+ function decide(request2, approval2, reply) {
16694
+ const input = request2.input;
16695
+ if ("answers" in reply)
16696
+ return {
16697
+ behavior: "allow",
16698
+ updatedInput: {
16699
+ ...input,
16700
+ answers: Object.fromEntries(
16701
+ (approval2.questions ?? []).map((item, index) => [
16702
+ item.question,
16703
+ reply.answers[index].join(", ")
16704
+ ])
16705
+ )
16706
+ }
16707
+ };
16708
+ if (reply.choice === "accept")
16709
+ return {
16710
+ behavior: "allow",
16711
+ updatedInput: input,
16712
+ ...approval2.plan === void 0 ? {} : { updatedPermissions: [{ type: "setMode", mode: "default", destination: "session" }] }
16713
+ };
16714
+ if (reply.choice === "always")
16715
+ return {
16716
+ behavior: "allow",
16717
+ updatedInput: input,
16718
+ updatedPermissions: remember(request2.suggestions)?.updates ?? []
16719
+ };
16720
+ if (reply.choice === "revise")
16721
+ return { behavior: "deny", message: reply.text?.trim() || "Keep planning." };
16722
+ return {
16723
+ behavior: "deny",
16724
+ message: reply.choice === "skip" ? "The person skipped these questions." : "The person rejected this from their phone."
16725
+ };
16726
+ }
16727
+ var MAX_SESSIONS = 100;
16728
+ var MAX_REQUESTS = 1e3;
16729
+ var MAX_PROMPT = 4 * 1024 * 1024;
16730
+ var SESSION = /^\/sessions\/([\w-]{1,100})(\/prompts)?$/;
16731
+ var STATUS = {
16732
+ busy: "working",
16733
+ idle: "idle",
16734
+ shell: "idle",
16735
+ waiting: "attention"
16736
+ };
16737
+ async function registrations() {
16478
16738
  if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
16479
- const homes = /* @__PURE__ */ new Set([codexHome()]);
16480
- if (process.platform === "linux") {
16481
- const entries2 = await readdir("/proc").catch(() => []);
16482
- for (const pid of entries2.filter((value2) => /^\d+$/.test(value2)).slice(0, MAX_PROCESSES)) {
16483
- if (homes.size >= MAX_SOCKETS) break;
16739
+ const directory2 = join4(claudeHome(), "sessions");
16740
+ const names = await readdir(directory2).catch(() => []);
16741
+ const found = await Promise.all(
16742
+ names.filter((name) => /^\d+\.json$/.test(name)).slice(0, MAX_SESSIONS).map(async (name) => {
16484
16743
  try {
16485
- const directory2 = `/proc/${pid}`;
16486
- const owner = await stat(directory2);
16487
- if (process.getuid && owner.uid !== process.getuid()) continue;
16488
- const command2 = (await readFile(join4(directory2, "cmdline"), "utf8")).slice(0, 4096);
16489
- if (!/(?:^|[\/\0 ])codex(?:[\0 .]|$)/.test(command2)) continue;
16490
- const environment = await readFile(join4(directory2, "environ"), "utf8");
16491
- const home = environment.split("\0").find((value2) => value2.startsWith("CODEX_HOME="))?.slice("CODEX_HOME=".length);
16492
- if (home && isAbsolute2(home)) homes.add(home);
16744
+ const value2 = JSON.parse(await readFile(join4(directory2, name), "utf8"));
16745
+ if (typeof value2.sessionId !== "string" || typeof value2.cwd !== "string") return [];
16746
+ process.kill(value2.pid, 0);
16747
+ const binary2 = await readlink(`/proc/${value2.pid}/exe`).catch(() => "");
16748
+ return [{ ...value2, command: /[\\/]claude(?:\.exe)?$/.test(binary2) ? binary2 : "claude" }];
16493
16749
  } catch {
16750
+ return [];
16494
16751
  }
16495
- }
16496
- }
16497
- return [...homes].map((home) => join4(home, ...SOCKET_PARTS));
16498
- }
16499
- function messages2(thread) {
16500
- if (!Array.isArray(thread.turns)) return [];
16501
- return thread.turns.flatMap((turn) => Array.isArray(turn?.items) ? turn.items.flatMap((item) => {
16502
- if (typeof item?.id !== "string") return [];
16503
- if (item.type === "userMessage" && Array.isArray(item.content)) {
16504
- const text3 = item.content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
16505
- return text3 ? [{ id: item.id, role: "user", text: text3 }] : [];
16506
- }
16507
- if (item.type === "agentMessage" && typeof item.text === "string")
16508
- return [{ id: item.id, role: "assistant", text: item.text }];
16509
- if (item.type === "commandExecution" && typeof item.command === "string")
16510
- return [
16511
- {
16512
- id: item.id,
16513
- role: "tool",
16514
- text: [item.command, item.aggregatedOutput].filter((value2) => typeof value2 === "string").join("\n\n")
16515
- }
16516
- ];
16517
- return [];
16518
- }) : []);
16752
+ })
16753
+ );
16754
+ return found.flat();
16519
16755
  }
16520
- function status(thread) {
16521
- if (thread.status?.type === "idle") return "idle";
16522
- if (thread.status?.type === "active") {
16523
- return thread.status.activeFlags?.some(
16524
- (flag) => flag === "waitingOnApproval" || flag === "waitingOnUserInput"
16525
- ) ? "attention" : "working";
16756
+ var Claude = class {
16757
+ constructor(changed, socket = join4(dirname4(configPath()), "claude.sock")) {
16758
+ this.changed = changed;
16759
+ this.socket = socket;
16526
16760
  }
16527
- return "unknown";
16528
- }
16529
- var Client = class {
16530
- constructor(path, changed) {
16531
- this.path = path;
16532
- this.changed = changed;
16533
- }
16534
- path;
16535
16761
  changed;
16536
16762
  socket;
16537
- connecting;
16538
- stopped = false;
16539
- sequence = 0;
16540
- controls = false;
16763
+ watcher;
16541
16764
  notify;
16542
- pending = /* @__PURE__ */ new Map();
16543
- approvals = /* @__PURE__ */ new Map();
16765
+ server;
16766
+ streams = /* @__PURE__ */ new Map();
16767
+ prompts = /* @__PURE__ */ new Map();
16544
16768
  requests = /* @__PURE__ */ new Map();
16545
- // The reply each thread is writing, as far as it has streamed, and the item it belongs to.
16546
- typing = /* @__PURE__ */ new Map();
16547
- async connect() {
16548
- if (this.socket?.readyState === WebSocket2.OPEN) return;
16549
- if (this.connecting) return this.connecting;
16550
- this.connecting = (async () => {
16551
- const file = await stat(this.path);
16552
- if (!file.isSocket() || process.getuid && file.uid !== process.getuid())
16553
- throw new Error("Codex control socket is not owned by this user");
16554
- const socket = this.socket = new WebSocket2("ws://localhost/rpc", {
16555
- createConnection: () => connect(this.path),
16556
- maxPayload: MAX_BYTES,
16557
- handshakeTimeout: REQUEST_MS
16558
- });
16559
- socket.on("error", () => {
16560
- });
16561
- socket.on("message", (bytes) => {
16562
- let message;
16563
- try {
16564
- message = JSON.parse(bytes.toString());
16565
- } catch {
16566
- return;
16567
- }
16568
- if (typeof message.id === "number" && !message.method) {
16569
- const pending = this.pending.get(message.id);
16570
- if (!pending) return;
16571
- this.pending.delete(message.id);
16572
- clearTimeout(pending.timer);
16573
- if (message.error)
16574
- pending.reject(
16575
- new Error(`Codex native request failed (${String(message.error.code ?? "unknown")})`)
16576
- );
16577
- else pending.resolve(message.result ?? {});
16578
- return;
16579
- }
16580
- if (typeof message.method !== "string") return;
16581
- const params = message.params;
16582
- if (message.method === "item/agentMessage/delta" && typeof params?.threadId === "string" && typeof params.delta === "string") {
16583
- const known = this.typing.get(params.threadId);
16584
- this.typing.set(params.threadId, {
16585
- item: params.itemId,
16586
- text: (known && known.item === params.itemId ? known.text : "") + params.delta
16587
- });
16588
- }
16589
- if (message.method === "serverRequest/resolved") {
16590
- for (const [id, pending] of this.approvals)
16591
- if (pending.requestId === message.params?.requestId) this.approvals.delete(id);
16592
- }
16593
- if ((message.method === "item/commandExecution/requestApproval" || message.method === "item/fileChange/requestApproval") && (typeof message.id === "string" || typeof message.id === "number") && typeof message.params?.threadId === "string") {
16594
- const id = randomUUID2();
16595
- this.approvals.set(id, {
16596
- requestId: message.id,
16597
- threadId: params.threadId,
16598
- approval: approval(id, message.method, params)
16599
- });
16600
- }
16601
- if (/^(thread|turn|item|serverRequest)\//.test(message.method) && !this.notify) {
16602
- this.notify = setTimeout(() => {
16603
- this.notify = void 0;
16604
- this.changed();
16605
- }, 100);
16606
- this.notify.unref();
16607
- }
16608
- });
16609
- socket.on("close", () => {
16610
- if (this.socket !== socket) return;
16611
- this.socket = void 0;
16612
- this.controls = false;
16613
- this.approvals.clear();
16614
- for (const pending of this.pending.values()) {
16615
- clearTimeout(pending.timer);
16616
- pending.reject(new Error("Codex control socket disconnected"));
16617
- }
16618
- this.pending.clear();
16619
- if (!this.stopped) this.changed();
16620
- });
16621
- await new Promise((resolve3, reject) => {
16622
- socket.once("open", resolve3);
16623
- socket.once("error", reject);
16624
- socket.once("close", () => reject(new Error("Codex control socket closed before initialization")));
16625
- });
16626
- const initialized = await this.call("initialize", {
16627
- clientInfo: { name: "dashup", title: "Dashup", version },
16628
- capabilities: { experimentalApi: false, requestAttestation: false }
16769
+ async refresh() {
16770
+ this.watch();
16771
+ return (await registrations()).map((session) => {
16772
+ const plugin = this.streams.has(session.sessionId);
16773
+ const models2 = claudeModels(this.changed, session.command, session.version);
16774
+ const approvals = [...this.prompts.values()].filter((prompt) => prompt.session === session.sessionId).map((prompt) => prompt.approval);
16775
+ return {
16776
+ id: session.sessionId,
16777
+ harness: "claude",
16778
+ cwd: session.cwd,
16779
+ updatedAt: new Date(session.updatedAt ?? 0).toISOString(),
16780
+ status: approvals.length ? "attention" : STATUS[session.status ?? ""] ?? "unknown",
16781
+ messages: [],
16782
+ approvals,
16783
+ approve: approvals.length ? (requestId, approvalId, reply) => this.once(requestId, async () => {
16784
+ const prompt = this.prompts.get(approvalId);
16785
+ if (prompt?.session !== session.sessionId)
16786
+ throw new Error("Claude Code approval is no longer pending");
16787
+ prompt.answer(decide(prompt.request, prompt.approval, reply));
16788
+ }) : void 0,
16789
+ send: plugin ? (requestId, text3, now) => this.once(requestId, () => this.deliver(session.sessionId, now ? { text: text3, now } : { text: text3 })) : void 0,
16790
+ stop: plugin ? (requestId) => this.once(requestId, () => this.deliver(session.sessionId, { now: true })) : void 0,
16791
+ now: plugin,
16792
+ options: (recorded) => claudeOptions(models2, recorded),
16793
+ configure: plugin ? (requestId, option2, value2) => this.once(requestId, () => this.deliver(session.sessionId, { text: COMMANDS[option2](value2) })) : void 0
16794
+ };
16795
+ });
16796
+ }
16797
+ // Only the connector serves the plugin; one-off commands such as `dashup doctor` read alone.
16798
+ async listen() {
16799
+ if (this.server || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
16800
+ const server = this.server = createServer2((request2, response) => {
16801
+ const [, id, prompt] = SESSION.exec(request2.url ?? "") ?? [];
16802
+ if (id && prompt && request2.method === "POST") return this.prompt(id, request2, response);
16803
+ if (!id || prompt || request2.method !== "GET") return void response.writeHead(404).end();
16804
+ this.streams.get(id)?.end();
16805
+ this.streams.set(id, response);
16806
+ response.once("close", () => {
16807
+ if (this.streams.get(id) === response) this.streams.delete(id);
16808
+ this.changed();
16629
16809
  });
16630
- socket.send(JSON.stringify({ method: "initialized", params: {} }));
16631
- const version2 = typeof initialized.userAgent === "string" ? /\b0\.(\d+)\.\d+\b/.exec(initialized.userAgent) : null;
16632
- this.controls = !!version2 && Number(version2[1]) >= 140;
16633
- })();
16634
- try {
16635
- await this.connecting;
16636
- } catch (error) {
16637
- this.socket?.terminate();
16638
- throw error;
16639
- } finally {
16640
- this.connecting = void 0;
16641
- }
16810
+ response.writeHead(200, { "Content-Type": "application/x-ndjson" }).flushHeaders();
16811
+ this.changed();
16812
+ });
16813
+ await mkdir(dirname4(this.socket), { recursive: true, mode: 448 });
16814
+ await rm(this.socket, { force: true });
16815
+ if (this.server !== server) return;
16816
+ await new Promise((resolve3, reject) => {
16817
+ server.once("error", reject);
16818
+ server.listen(this.socket, resolve3);
16819
+ });
16642
16820
  }
16643
- call(method, params) {
16644
- const socket = this.socket;
16645
- if (this.stopped || socket?.readyState !== WebSocket2.OPEN)
16646
- return Promise.reject(new Error("Codex control socket is disconnected"));
16647
- if (this.pending.size >= 32) return Promise.reject(new Error("Codex request limit reached"));
16648
- const id = ++this.sequence;
16649
- return new Promise((resolve3, reject) => {
16650
- const timer = setTimeout(() => {
16651
- this.pending.delete(id);
16652
- reject(new Error("Codex native request timed out"));
16653
- }, REQUEST_MS);
16654
- timer.unref();
16655
- this.pending.set(id, { resolve: resolve3, reject, timer });
16656
- socket.send(JSON.stringify({ id, method, params }), (error) => {
16657
- if (error) {
16658
- clearTimeout(timer);
16659
- this.pending.delete(id);
16660
- reject(new Error("Codex native request could not be sent"));
16821
+ // Asks the phone what the session's permission request asks the person.
16822
+ prompt(session, request2, response) {
16823
+ const chunks = [];
16824
+ let bytes = 0;
16825
+ request2.on("data", (chunk) => {
16826
+ bytes += chunk.length;
16827
+ if (bytes > MAX_PROMPT) request2.destroy();
16828
+ else chunks.push(chunk);
16829
+ });
16830
+ request2.once("end", () => {
16831
+ let asked;
16832
+ try {
16833
+ asked = JSON.parse(Buffer.concat(chunks).toString("utf8"));
16834
+ } catch {
16835
+ }
16836
+ if (typeof asked?.tool !== "string" || !asked.input || typeof asked.input !== "object")
16837
+ return void response.writeHead(400).end();
16838
+ const id = randomUUID2();
16839
+ const prompted = {
16840
+ tool: asked.tool,
16841
+ input: asked.input,
16842
+ ...Array.isArray(asked.suggestions) ? { suggestions: asked.suggestions } : {}
16843
+ };
16844
+ this.prompts.set(id, {
16845
+ session,
16846
+ request: prompted,
16847
+ approval: ask(id, prompted, false),
16848
+ answer: (decision) => {
16849
+ this.prompts.delete(id);
16850
+ response.end(JSON.stringify(decision));
16851
+ this.changed();
16661
16852
  }
16662
16853
  });
16854
+ response.once("close", () => {
16855
+ if (this.prompts.delete(id)) this.changed();
16856
+ });
16857
+ response.writeHead(200, { "Content-Type": "application/json" }).flushHeaders();
16858
+ this.changed();
16663
16859
  });
16664
16860
  }
16861
+ deliver(id, delivery) {
16862
+ const stream = this.streams.get(id);
16863
+ if (!stream) return Promise.reject(new Error("Claude Code is not taking messages"));
16864
+ return new Promise((resolve3, reject) => stream.write(`${JSON.stringify(delivery)}
16865
+ `, (error) => error ? reject(error) : resolve3()));
16866
+ }
16867
+ // Each request delivers at most once.
16665
16868
  once(id, action) {
16666
16869
  const known = this.requests.get(id);
16667
16870
  if (known) return known;
16668
- if (this.requests.size >= 1e3)
16669
- throw new Error("Codex command history is full; reconnect before sending");
16871
+ if (this.requests.size >= MAX_REQUESTS)
16872
+ this.requests.delete(this.requests.keys().next().value);
16670
16873
  const result = action();
16671
16874
  this.requests.set(id, result);
16672
16875
  return result;
16673
16876
  }
16674
- // Starts a thread in `cwd` with Codex's own settings, or loads a saved one, then a turn.
16675
- async start(cwd, text3, saved) {
16676
- await this.connect();
16677
- if (!this.controls) throw new Error("Update Codex to start threads from Dashup");
16678
- const id = saved ? (await this.call("thread/resume", { threadId: saved, excludeTurns: true })).thread?.id : (await this.call("thread/start", { cwd })).thread?.id;
16679
- if (typeof id !== "string") throw new Error("Codex did not open the thread");
16680
- await this.call("turn/start", { threadId: id, input: [{ type: "text", text: text3, text_elements: [] }] });
16681
- this.changed();
16682
- return id;
16683
- }
16684
- async read() {
16685
- await this.connect();
16686
- const loaded = await this.call("thread/loaded/list", { limit: MAX_SESSIONS });
16687
- if (!Array.isArray(loaded.data)) return [];
16688
- const sessions = [];
16689
- for (const id of loaded.data.slice(0, MAX_SESSIONS)) {
16690
- if (typeof id !== "string") continue;
16691
- try {
16692
- const read = await this.call("thread/read", { threadId: id, includeTurns: true });
16693
- const thread = read.thread;
16694
- if (thread?.id !== id || typeof thread.cwd !== "string" || !thread.cwd) continue;
16695
- const approvals = [...this.approvals.values()].filter((value2) => value2.threadId === id).map((value2) => value2.approval);
16696
- sessions.push({
16697
- id,
16698
- harness: "codex",
16699
- cwd: thread.cwd,
16700
- title: typeof thread.name === "string" && thread.name ? thread.name : typeof thread.preview === "string" && thread.preview ? thread.preview : "Codex thread",
16701
- updatedAt: new Date(Number(thread.updatedAt ?? 0) * 1e3).toISOString(),
16702
- status: approvals.length ? "attention" : status(thread),
16703
- messages: messages2(thread),
16704
- typing: this.typing.get(id)?.text,
16705
- approvals,
16706
- // An interrupt for a finished turn is never answered, so the active turn is read first.
16707
- stop: this.controls ? (requestId) => this.once(requestId, async () => {
16708
- const native = (await this.call("thread/read", { threadId: id, includeTurns: true })).thread;
16709
- const turn = native?.status?.type === "active" && Array.isArray(native.turns) ? native.turns.at(-1) : void 0;
16710
- if (turn?.status !== "inProgress" || typeof turn.id !== "string")
16711
- throw new Error("Nothing is running in this Codex thread");
16712
- await this.call("turn/interrupt", { threadId: id, turnId: turn.id });
16713
- this.changed();
16714
- }) : void 0,
16715
- send: this.controls && thread.status?.type === "idle" ? (requestId, text3) => this.once(requestId, async () => {
16716
- if (!text3.trim() || Buffer.byteLength(text3) > MAX_BYTES / 2)
16717
- throw new Error("Message is empty or too large");
16718
- const current = await this.call("thread/loaded/list", { limit: MAX_SESSIONS });
16719
- if (!Array.isArray(current.data) || !current.data.includes(id))
16720
- throw new Error("Codex thread is no longer loaded");
16721
- const native = (await this.call("thread/read", { threadId: id, includeTurns: false })).thread;
16722
- if (native?.id !== id || native.status?.type !== "idle")
16723
- throw new Error("Codex thread is busy or unavailable");
16724
- await this.call("turn/start", {
16725
- threadId: id,
16726
- clientUserMessageId: requestId,
16727
- input: [{ type: "text", text: text3, text_elements: [] }]
16728
- });
16729
- this.changed();
16730
- }) : void 0,
16731
- approve: approvals.length ? (requestId, approvalId, choice) => this.once(requestId, async () => {
16732
- const approval3 = this.approvals.get(approvalId);
16733
- const socket = this.socket;
16734
- if (!approval3 || approval3.threadId !== id || !approval3.approval.choices.some((value2) => value2.id === choice) || socket?.readyState !== WebSocket2.OPEN)
16735
- throw new Error("Codex approval is no longer pending");
16736
- await new Promise((resolve3, reject) => socket.send(
16737
- JSON.stringify({ id: approval3.requestId, result: { decision: choice } }),
16738
- (error) => error ? reject(new Error("Codex approval could not be sent")) : resolve3()
16739
- ));
16740
- this.approvals.delete(approvalId);
16741
- this.changed();
16742
- }) : void 0
16743
- });
16744
- } catch {
16745
- }
16746
- }
16747
- return sessions;
16877
+ watch() {
16878
+ if (this.watcher || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
16879
+ try {
16880
+ const watcher = this.watcher = watch2(join4(claudeHome(), "sessions"), () => {
16881
+ if (this.notify) return;
16882
+ this.notify = setTimeout(() => {
16883
+ this.notify = void 0;
16884
+ this.changed();
16885
+ }, 100);
16886
+ this.notify.unref();
16887
+ });
16888
+ watcher.on("error", () => {
16889
+ watcher.close();
16890
+ if (this.watcher === watcher) this.watcher = void 0;
16891
+ });
16892
+ } catch {
16893
+ }
16748
16894
  }
16749
16895
  stop() {
16750
- this.stopped = true;
16751
16896
  clearTimeout(this.notify);
16752
- this.socket?.terminate();
16753
- for (const pending of this.pending.values()) {
16754
- clearTimeout(pending.timer);
16755
- pending.reject(new Error("Codex observation stopped"));
16756
- }
16757
- this.pending.clear();
16758
- this.approvals.clear();
16759
- }
16760
- };
16761
- var Codex = class {
16762
- constructor(changed) {
16763
- this.changed = changed;
16764
- }
16765
- changed;
16766
- clients = /* @__PURE__ */ new Map();
16767
- // Control sockets of daemons this connector started, which discovery may not look for.
16768
- started = /* @__PURE__ */ new Set();
16769
- // Codex's own daemon runs the threads Dashup starts or resumes, so they outlive this connector.
16770
- async start(cwd, text3, saved) {
16771
- const path = process.env.DASHUP_CODEX_SOCKET || join4(codexHome(), ...SOCKET_PARTS);
16772
- await promisify2(execFile2)("codex", ["app-server", "daemon", "start"], {
16773
- timeout: 6e4,
16774
- shell: process.platform === "win32"
16775
- });
16776
- this.started.add(path);
16777
- let client = this.clients.get(path);
16778
- if (!client) {
16779
- client = new Client(path, this.changed);
16780
- this.clients.set(path, client);
16781
- }
16782
- return client.start(cwd, text3, saved);
16783
- }
16784
- async resume(id, cwd, text3) {
16785
- await this.start(cwd, text3, id);
16786
- }
16787
- async refresh() {
16788
- const paths = /* @__PURE__ */ new Set([...await sockets(), ...this.started]);
16789
- for (const [path, client] of this.clients)
16790
- if (!paths.has(path)) {
16791
- client.stop();
16792
- this.clients.delete(path);
16793
- }
16794
- const results = await Promise.allSettled(
16795
- [...paths].map(async (path) => {
16796
- let client = this.clients.get(path);
16797
- if (!client) {
16798
- client = new Client(path, this.changed);
16799
- this.clients.set(path, client);
16800
- }
16801
- return client.read();
16802
- })
16803
- );
16804
- return results.flatMap((result) => result.status === "fulfilled" ? result.value : []);
16805
- }
16806
- stop() {
16807
- for (const client of this.clients.values()) client.stop();
16808
- this.clients.clear();
16897
+ this.watcher?.close();
16898
+ this.watcher = void 0;
16899
+ this.prompts.clear();
16900
+ this.server?.closeAllConnections();
16901
+ this.server?.close();
16902
+ this.server = void 0;
16809
16903
  }
16810
16904
  };
16811
16905
 
16812
16906
  // src/harness/headless.ts
16813
- import { randomUUID as randomUUID3 } from "node:crypto";
16814
- import { readFileSync as readFileSync3 } from "node:fs";
16815
- import { createInterface as createInterface3 } from "node:readline";
16816
- import { spawn as spawn3 } from "node:child_process";
16817
-
16818
- // src/harness/claude.ts
16819
- import { homedir as homedir5 } from "node:os";
16820
- import { dirname as dirname4, join as join5 } from "node:path";
16821
- import { spawn as spawn2 } from "node:child_process";
16822
- import { createInterface as createInterface2 } from "node:readline";
16823
- import { watch as watch2 } from "node:fs";
16824
- import { mkdir, readdir as readdir2, readFile as readFile2, readlink, rm } from "node:fs/promises";
16825
- import { createServer as createServer2 } from "node:http";
16826
- var MODELS_MS = 3e4;
16827
- var COMMANDS = {
16828
- model: (value2) => `/model ${value2}`,
16829
- fast: (value2) => `/fast ${value2 ? "on" : "off"}`,
16830
- ultracode: (value2) => `/effort ultracode ${value2 ? "on" : "off"}`
16831
- };
16832
- var offered = /* @__PURE__ */ new Map();
16833
- var asking = /* @__PURE__ */ new Set();
16834
- var forgetModels = () => offered.clear();
16835
- function claudeModels(changed, command2 = "claude", version2 = "") {
16836
- const key = `${command2}\0${version2}`;
16837
- if (!offered.has(key) && !asking.has(key)) {
16838
- asking.add(key);
16839
- void listModels(command2).then((models) => {
16840
- asking.delete(key);
16841
- if (!models.length) return;
16842
- offered.set(key, models);
16843
- changed();
16844
- });
16845
- }
16846
- return offered.get(key) ?? [];
16847
- }
16848
- function listModels(command2) {
16849
- return new Promise((resolve3) => {
16850
- const child = spawn2(
16851
- command2,
16852
- ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"],
16853
- { cwd: homedir5(), stdio: "pipe", shell: process.platform === "win32" }
16854
- );
16855
- const timer = setTimeout(() => child.kill(), MODELS_MS);
16856
- child.stdin.on("error", () => {
16857
- });
16858
- child.once("error", () => resolve3([]));
16859
- child.once("exit", () => {
16860
- clearTimeout(timer);
16861
- resolve3([]);
16862
- });
16863
- createInterface2({ input: child.stdout }).on("line", (line) => {
16864
- try {
16865
- const message = JSON.parse(line);
16866
- if (message?.type !== "control_response" || message.response?.request_id !== "models")
16867
- return;
16868
- const models = message.response.response?.models;
16869
- resolve3(
16870
- (Array.isArray(models) ? models : []).filter(
16871
- (model) => typeof model?.value === "string" && typeof model.displayName === "string"
16872
- )
16873
- );
16874
- child.stdin.end();
16875
- } catch {
16876
- }
16877
- });
16878
- child.stdin.write(
16879
- `${JSON.stringify({ type: "control_request", request_id: "models", request: { subtype: "initialize" } })}
16880
- `
16881
- );
16882
- });
16883
- }
16884
- function claudeOptions(models, recorded) {
16885
- const ran = typeof recorded.model === "string" ? recorded.model : "";
16886
- const current = models.find((model) => model.value !== "default" && model.resolvedModel === ran) ?? models.find((model) => model.value === ran);
16887
- const choices = models.map((model) => ({ value: model.value, name: model.displayName }));
16888
- if (ran && !current) choices.push({ value: ran, name: ran });
16889
- if (!choices.length) return [];
16890
- return [
16891
- { id: "model", name: "Model", value: current?.value ?? ran, choices },
16892
- ...current?.supportsFastMode ? [{ id: "fast", name: "Fast", value: recorded.fast === true }] : [],
16893
- ...current?.supportedEffortLevels?.includes("xhigh") ? [{ id: "ultracode", name: "Ultracode", value: recorded.ultracode === true }] : []
16894
- ];
16895
- }
16896
- var MAX_SESSIONS2 = 100;
16897
- var MAX_REQUESTS = 1e3;
16898
- var SESSION = /^\/sessions\/([\w-]{1,100})$/;
16899
- var STATUS = {
16900
- busy: "working",
16901
- idle: "idle",
16902
- shell: "idle",
16903
- waiting: "attention"
16904
- };
16905
- async function registrations() {
16906
- if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
16907
- const directory2 = join5(claudeHome(), "sessions");
16908
- const names = await readdir2(directory2).catch(() => []);
16909
- const found = await Promise.all(
16910
- names.filter((name) => /^\d+\.json$/.test(name)).slice(0, MAX_SESSIONS2).map(async (name) => {
16911
- try {
16912
- const value2 = JSON.parse(await readFile2(join5(directory2, name), "utf8"));
16913
- if (typeof value2.sessionId !== "string" || typeof value2.cwd !== "string") return [];
16914
- process.kill(value2.pid, 0);
16915
- const binary2 = await readlink(`/proc/${value2.pid}/exe`).catch(() => "");
16916
- return [{ ...value2, command: /[\\/]claude(?:\.exe)?$/.test(binary2) ? binary2 : "claude" }];
16917
- } catch {
16918
- return [];
16919
- }
16920
- })
16921
- );
16922
- return found.flat();
16923
- }
16924
- var Claude = class {
16925
- constructor(changed, socket = join5(dirname4(configPath()), "claude.sock")) {
16907
+ var MAX_REQUESTS2 = 1e3;
16908
+ var NOTIFY_MS = 100;
16909
+ var SWITCHES = { fast: "fastMode", ultracode: "ultracode" };
16910
+ var Session = class {
16911
+ // A resumed session is `saved` and keeps the title its history gives.
16912
+ constructor(id, cwd, title2, changed, saved = false) {
16913
+ this.id = id;
16914
+ this.cwd = cwd;
16915
+ this.title = title2;
16926
16916
  this.changed = changed;
16927
- this.socket = socket;
16917
+ this.saved = saved;
16928
16918
  }
16919
+ id;
16920
+ cwd;
16921
+ title;
16929
16922
  changed;
16930
- socket;
16931
- watcher;
16932
- notify;
16933
- server;
16934
- streams = /* @__PURE__ */ new Map();
16923
+ saved;
16924
+ messages = [];
16925
+ status = "idle";
16926
+ updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16927
+ turn;
16928
+ // Messages written to this turn's process that Claude Code has not finished answering.
16929
+ open = 0;
16930
+ // The text Claude Code is writing, as far as it has streamed.
16931
+ typing = "";
16932
+ approvals = /* @__PURE__ */ new Map();
16935
16933
  requests = /* @__PURE__ */ new Map();
16936
- async refresh() {
16937
- this.watch();
16938
- return (await registrations()).map((session) => {
16939
- const plugin = this.streams.has(session.sessionId);
16940
- const models = claudeModels(this.changed, session.command, session.version);
16941
- return {
16942
- id: session.sessionId,
16943
- harness: "claude",
16944
- cwd: session.cwd,
16945
- updatedAt: new Date(session.updatedAt ?? 0).toISOString(),
16946
- status: STATUS[session.status ?? ""] ?? "unknown",
16947
- messages: [],
16948
- approvals: [],
16949
- send: plugin ? (requestId, text3, now) => this.once(requestId, () => this.deliver(session.sessionId, now ? { text: text3, now } : { text: text3 })) : void 0,
16950
- stop: plugin ? (requestId) => this.once(requestId, () => this.deliver(session.sessionId, { now: true })) : void 0,
16951
- now: plugin,
16952
- options: (recorded) => claudeOptions(models, recorded),
16953
- configure: plugin ? (requestId, option2, value2) => this.once(requestId, () => this.deliver(session.sessionId, { text: COMMANDS[option2](value2) })) : void 0
16954
- };
16955
- });
16934
+ // What the person chose for this session, applied to each turn as it starts.
16935
+ chosen = {};
16936
+ write(value2) {
16937
+ this.turn?.stdin.write(`${JSON.stringify(value2)}
16938
+ `);
16956
16939
  }
16957
- // Only the connector serves the plugin; one-off commands such as `dashup doctor` read alone.
16958
- async listen() {
16959
- if (this.server || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
16960
- const server = this.server = createServer2((request2, response) => {
16961
- const id = request2.method === "GET" ? SESSION.exec(request2.url ?? "")?.[1] : void 0;
16962
- if (!id) return void response.writeHead(404).end();
16963
- this.streams.get(id)?.end();
16964
- this.streams.set(id, response);
16965
- response.once("close", () => {
16966
- if (this.streams.get(id) === response) this.streams.delete(id);
16967
- this.changed();
16968
- });
16969
- response.writeHead(200, { "Content-Type": "application/x-ndjson" }).flushHeaders();
16970
- this.changed();
16971
- });
16972
- await mkdir(dirname4(this.socket), { recursive: true, mode: 448 });
16973
- await rm(this.socket, { force: true });
16974
- if (this.server !== server) return;
16975
- await new Promise((resolve3, reject) => {
16976
- server.once("error", reject);
16977
- server.listen(this.socket, resolve3);
16940
+ interrupt() {
16941
+ this.write({
16942
+ type: "control_request",
16943
+ request_id: randomUUID3(),
16944
+ request: { subtype: "interrupt" }
16978
16945
  });
16979
16946
  }
16980
- deliver(id, delivery) {
16981
- const stream = this.streams.get(id);
16982
- if (!stream) return Promise.reject(new Error("Claude Code is not taking messages"));
16983
- return new Promise((resolve3, reject) => stream.write(`${JSON.stringify(delivery)}
16984
- `, (error) => error ? reject(error) : resolve3()));
16947
+ apply(option2, value2) {
16948
+ this.write({
16949
+ type: "control_request",
16950
+ request_id: randomUUID3(),
16951
+ request: option2 === "model" ? { subtype: "set_model", model: value2 } : { subtype: "apply_flag_settings", settings: { [SWITCHES[option2]]: value2 } }
16952
+ });
16985
16953
  }
16986
- // Each request delivers at most once.
16987
16954
  once(id, action) {
16988
16955
  const known = this.requests.get(id);
16989
16956
  if (known) return known;
16990
- if (this.requests.size >= MAX_REQUESTS)
16991
- this.requests.delete(this.requests.keys().next().value);
16992
- const result = action();
16993
- this.requests.set(id, result);
16994
- return result;
16995
- }
16996
- watch() {
16997
- if (this.watcher || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
16998
- try {
16999
- const watcher = this.watcher = watch2(join5(claudeHome(), "sessions"), () => {
17000
- if (this.notify) return;
17001
- this.notify = setTimeout(() => {
17002
- this.notify = void 0;
17003
- this.changed();
17004
- }, 100);
17005
- this.notify.unref();
17006
- });
17007
- watcher.on("error", () => {
17008
- watcher.close();
17009
- if (this.watcher === watcher) this.watcher = void 0;
17010
- });
17011
- } catch {
17012
- }
17013
- }
17014
- stop() {
17015
- clearTimeout(this.notify);
17016
- this.watcher?.close();
17017
- this.watcher = void 0;
17018
- this.server?.closeAllConnections();
17019
- this.server?.close();
17020
- this.server = void 0;
17021
- }
17022
- };
17023
-
17024
- // src/harness/headless.ts
17025
- var CHOICES = [
17026
- { id: "accept", label: "Allow once" },
17027
- { id: "decline", label: "Reject" }
17028
- ];
17029
- var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
17030
- var MAX_REQUESTS2 = 1e3;
17031
- var NOTIFY_MS = 100;
17032
- var SWITCHES = { fast: "fastMode", ultracode: "ultracode" };
17033
- function hunk(before, after, offset = 0) {
17034
- const old = before ? before.replace(/\n$/, "").split("\n") : [];
17035
- const next = after ? after.replace(/\n$/, "").split("\n") : [];
17036
- let start2 = 0;
17037
- while (start2 < old.length && start2 < next.length && old[start2] === next[start2]) start2++;
17038
- let end = 0;
17039
- while (end < old.length - start2 && end < next.length - start2 && old[old.length - 1 - end] === next[next.length - 1 - end])
17040
- end++;
17041
- const removed = old.slice(start2, old.length - end);
17042
- const added2 = next.slice(start2, next.length - end);
17043
- return {
17044
- lines: [
17045
- `@@ -${offset + start2 + 1},${removed.length} +${offset + start2 + 1},${added2.length} @@`,
17046
- ...old.slice(Math.max(0, start2 - 3), start2).map((line) => ` ${line}`),
17047
- ...removed.map((line) => `-${line}`),
17048
- ...added2.map((line) => `+${line}`),
17049
- ...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
17050
- ],
17051
- added: added2.length,
17052
- deleted: removed.length
17053
- };
17054
- }
17055
- function change(tool, input) {
17056
- const path = input.file_path;
17057
- if (typeof path !== "string") return void 0;
17058
- let current = "";
17059
- try {
17060
- current = readFileSync3(path, "utf8");
17061
- } catch {
17062
- }
17063
- const edit = (value2) => {
17064
- const before = String(value2?.old_string ?? "");
17065
- const at = before ? current.indexOf(before) : -1;
17066
- return hunk(
17067
- before,
17068
- String(value2?.new_string ?? ""),
17069
- at < 0 ? 0 : current.slice(0, at).split("\n").length - 1
17070
- );
17071
- };
17072
- const hunks = tool === "Write" && typeof input.content === "string" ? [hunk(current, input.content)] : tool === "Edit" ? [edit(input)] : tool === "MultiEdit" && Array.isArray(input.edits) ? input.edits.map(edit) : [];
17073
- if (!hunks.length) return void 0;
17074
- const diff = [`--- ${path}`, `+++ ${path}`, ...hunks.flatMap((item) => item.lines)].join("\n");
17075
- if (Buffer.byteLength(diff) > MAX_PREVIEW) return void 0;
17076
- return {
17077
- path,
17078
- diff,
17079
- added: hunks.reduce((sum, item) => sum + item.added, 0),
17080
- deleted: hunks.reduce((sum, item) => sum + item.deleted, 0)
17081
- };
17082
- }
17083
- function approval2(id, request2) {
17084
- const input = request2.input ?? {};
17085
- const tool = String(request2.display_name ?? request2.tool_name ?? "a tool");
17086
- const diff = change(request2.tool_name, input);
17087
- let detail = [
17088
- typeof request2.description === "string" ? request2.description : "",
17089
- typeof input.command === "string" ? input.command : diff ? "" : JSON.stringify(input, null, 2)
17090
- ].filter(Boolean).join("\n\n");
17091
- if (Buffer.byteLength(detail) > MAX_PREVIEW)
17092
- detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
17093
- return {
17094
- id,
17095
- title: request2.tool_name === "Bash" ? "Allow command?" : EDITS.has(request2.tool_name) ? "Allow file changes?" : `Allow ${tool}?`,
17096
- detail,
17097
- ...diff ? { change: diff } : {},
17098
- choices: CHOICES.map((choice) => ({ ...choice }))
17099
- };
17100
- }
17101
- var Session = class {
17102
- // A resumed session is `saved` and keeps the title its history gives.
17103
- constructor(id, cwd, title2, changed, saved = false) {
17104
- this.id = id;
17105
- this.cwd = cwd;
17106
- this.title = title2;
17107
- this.changed = changed;
17108
- this.saved = saved;
17109
- }
17110
- id;
17111
- cwd;
17112
- title;
17113
- changed;
17114
- saved;
17115
- messages = [];
17116
- status = "idle";
17117
- updatedAt = (/* @__PURE__ */ new Date()).toISOString();
17118
- turn;
17119
- // Messages written to this turn's process that Claude Code has not finished answering.
17120
- open = 0;
17121
- // The text Claude Code is writing, as far as it has streamed.
17122
- typing = "";
17123
- approvals = /* @__PURE__ */ new Map();
17124
- requests = /* @__PURE__ */ new Map();
17125
- // What the person chose for this session, applied to each turn as it starts.
17126
- chosen = {};
17127
- write(value2) {
17128
- this.turn?.stdin.write(`${JSON.stringify(value2)}
17129
- `);
17130
- }
17131
- interrupt() {
17132
- this.write({
17133
- type: "control_request",
17134
- request_id: randomUUID3(),
17135
- request: { subtype: "interrupt" }
17136
- });
17137
- }
17138
- apply(option2, value2) {
17139
- this.write({
17140
- type: "control_request",
17141
- request_id: randomUUID3(),
17142
- request: option2 === "model" ? { subtype: "set_model", model: value2 } : { subtype: "apply_flag_settings", settings: { [SWITCHES[option2]]: value2 } }
17143
- });
17144
- }
17145
- once(id, action) {
17146
- const known = this.requests.get(id);
17147
- if (known) return known;
17148
- if (this.requests.size >= MAX_REQUESTS2)
17149
- throw new Error("Claude Code command history is full; restart Dashup before sending");
16957
+ if (this.requests.size >= MAX_REQUESTS2)
16958
+ throw new Error("Claude Code command history is full; restart Dashup before sending");
17150
16959
  const result = action();
17151
16960
  this.requests.set(id, result);
17152
16961
  return result;
@@ -17214,130 +17023,575 @@ var Session = class {
17214
17023
  receive(line) {
17215
17024
  let message;
17216
17025
  try {
17217
- message = JSON.parse(line);
17218
- } catch {
17219
- return false;
17026
+ message = JSON.parse(line);
17027
+ } catch {
17028
+ return false;
17029
+ }
17030
+ const init = message.type === "system" && message.subtype === "init";
17031
+ if (init) this.saved = true;
17032
+ else if (message.type === "assistant") {
17033
+ const text3 = (Array.isArray(message.message?.content) ? message.message.content : []).filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n\n");
17034
+ if (!text3) return false;
17035
+ this.messages.push({ id: String(message.uuid ?? randomUUID3()), role: "assistant", text: text3 });
17036
+ } else if (message.type === "control_request" && message.request?.subtype === "can_use_tool") {
17037
+ const id = randomUUID3();
17038
+ const asked = message.request;
17039
+ const request2 = {
17040
+ tool: String(asked.tool_name),
17041
+ input: asked.input ?? {},
17042
+ label: typeof asked.display_name === "string" ? asked.display_name : void 0,
17043
+ description: typeof asked.description === "string" ? asked.description : void 0,
17044
+ suggestions: Array.isArray(asked.permission_suggestions) ? asked.permission_suggestions : void 0
17045
+ };
17046
+ this.approvals.set(id, {
17047
+ control: String(message.request_id),
17048
+ request: request2,
17049
+ approval: ask(id, request2, true)
17050
+ });
17051
+ this.status = "attention";
17052
+ } else if (message.type === "control_cancel_request") {
17053
+ for (const [id, pending] of this.approvals)
17054
+ if (pending.control === message.request_id) this.approvals.delete(id);
17055
+ if (this.status === "attention" && !this.approvals.size) this.status = "working";
17056
+ } else if (message.type === "result") {
17057
+ if (--this.open <= 0) this.turn?.stdin.end();
17058
+ } else if (message.type === "stream_event" && !message.parent_tool_use_id) {
17059
+ const event = message.event;
17060
+ if (event?.type === "content_block_start") this.typing = "";
17061
+ else if (event?.type === "content_block_delta" && event.delta?.type === "text_delta")
17062
+ this.typing += String(event.delta.text ?? "");
17063
+ else return false;
17064
+ this.changed();
17065
+ return false;
17066
+ } else return false;
17067
+ this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
17068
+ this.changed();
17069
+ return init;
17070
+ }
17071
+ live(models2) {
17072
+ return {
17073
+ id: this.id,
17074
+ cwd: this.cwd,
17075
+ title: this.title,
17076
+ harness: "claude",
17077
+ updatedAt: this.updatedAt,
17078
+ status: this.status,
17079
+ messages: [...this.messages],
17080
+ typing: this.typing,
17081
+ approvals: [...this.approvals.values()].map((pending) => pending.approval),
17082
+ send: (requestId, text3, now) => this.once(requestId, () => this.say(text3, now)),
17083
+ now: true,
17084
+ options: (recorded) => claudeOptions(models2, recorded),
17085
+ configure: (requestId, option2, value2) => this.once(requestId, async () => {
17086
+ this.chosen[option2] = value2;
17087
+ this.apply(option2, value2);
17088
+ }),
17089
+ stop: this.turn ? (requestId) => this.once(requestId, async () => {
17090
+ if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
17091
+ this.interrupt();
17092
+ }) : void 0,
17093
+ approve: this.approvals.size ? (requestId, approvalId, reply) => this.once(requestId, async () => {
17094
+ const pending = this.approvals.get(approvalId);
17095
+ if (!pending || !this.turn)
17096
+ throw new Error("Claude Code approval is no longer pending");
17097
+ this.write({
17098
+ type: "control_response",
17099
+ response: {
17100
+ subtype: "success",
17101
+ request_id: pending.control,
17102
+ response: decide(pending.request, pending.approval, reply)
17103
+ }
17104
+ });
17105
+ this.approvals.delete(approvalId);
17106
+ if (!this.approvals.size) this.status = "working";
17107
+ this.changed();
17108
+ }) : void 0
17109
+ };
17110
+ }
17111
+ end() {
17112
+ this.turn?.kill();
17113
+ }
17114
+ };
17115
+ var Headless = class {
17116
+ constructor(changed) {
17117
+ this.changed = changed;
17118
+ }
17119
+ changed;
17120
+ sessions = /* @__PURE__ */ new Map();
17121
+ notify;
17122
+ soon = () => {
17123
+ if (this.notify) return;
17124
+ this.notify = setTimeout(() => {
17125
+ this.notify = void 0;
17126
+ this.changed();
17127
+ }, NOTIFY_MS);
17128
+ this.notify.unref();
17129
+ };
17130
+ // Starts a session in `cwd` with its first message, and resolves with the session's id.
17131
+ async start(cwd, text3) {
17132
+ const title2 = text3.trim().split("\n")[0].slice(0, 80);
17133
+ const session = new Session(randomUUID3(), cwd, title2, this.soon);
17134
+ this.sessions.set(session.id, session);
17135
+ try {
17136
+ await session.say(text3);
17137
+ } catch (error) {
17138
+ this.sessions.delete(session.id);
17139
+ throw error;
17140
+ }
17141
+ return session.id;
17142
+ }
17143
+ // Continues a saved session with a message; Claude Code resumes it from its own transcript.
17144
+ async resume(id, cwd, text3) {
17145
+ const known = this.sessions.get(id);
17146
+ const session = known ?? new Session(id, cwd, void 0, this.soon, true);
17147
+ this.sessions.set(id, session);
17148
+ try {
17149
+ await session.say(text3);
17150
+ } catch (error) {
17151
+ if (!known) this.sessions.delete(id);
17152
+ throw error;
17153
+ }
17154
+ }
17155
+ async refresh() {
17156
+ const models2 = claudeModels(this.changed);
17157
+ return [...this.sessions.values()].map((session) => session.live(models2));
17158
+ }
17159
+ stop() {
17160
+ clearTimeout(this.notify);
17161
+ for (const session of this.sessions.values()) session.end();
17162
+ }
17163
+ };
17164
+
17165
+ // src/harness/codex.ts
17166
+ import WebSocket2 from "ws";
17167
+ import { homedir as homedir5 } from "node:os";
17168
+ import { connect } from "node:net";
17169
+ import { promisify as promisify2 } from "node:util";
17170
+ import { randomUUID as randomUUID4 } from "node:crypto";
17171
+ import { execFile as execFile2 } from "node:child_process";
17172
+ import { join as join5, isAbsolute as isAbsolute2 } from "node:path";
17173
+ import { statSync as statSync2, readFileSync as readFileSync4 } from "node:fs";
17174
+ import { stat, readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
17175
+ var REQUEST_MS = 4e3;
17176
+ var MAX_BYTES = 8 * 1024 * 1024;
17177
+ var MAX_SESSIONS2 = 100;
17178
+ var MAX_SOCKETS = 32;
17179
+ var MAX_PROCESSES = 4096;
17180
+ var SOCKET_PARTS = ["app-server-control", "app-server-control.sock"];
17181
+ var DECISIONS = [
17182
+ { id: "accept", label: "Allow once" },
17183
+ { id: "decline", label: "Reject" }
17184
+ ];
17185
+ function approval(id, method, params) {
17186
+ const command2 = method === "item/commandExecution/requestApproval";
17187
+ let detail = [
17188
+ command2 && typeof params.command === "string" ? params.command : "",
17189
+ typeof params.cwd === "string" ? `Working directory: ${params.cwd}` : "",
17190
+ typeof params.grantRoot === "string" ? `Write access root: ${params.grantRoot}` : "",
17191
+ typeof params.reason === "string" ? params.reason : ""
17192
+ ].filter(Boolean).join("\n\n");
17193
+ const oversized = Buffer.byteLength(detail) > MAX_PREVIEW;
17194
+ const unavailable = !command2 || params.kind !== void 0 && params.kind !== "command" || typeof params.command !== "string" || !params.command.trim() || oversized;
17195
+ if (oversized) detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
17196
+ if (unavailable) detail = [detail, NO_PREVIEW].filter(Boolean).join("\n\n");
17197
+ return {
17198
+ id,
17199
+ title: command2 ? "Allow command?" : "Allow file changes?",
17200
+ detail,
17201
+ choices: DECISIONS.filter((choice) => !unavailable || choice.id === "decline").map((choice) => ({ ...choice }))
17202
+ };
17203
+ }
17204
+ var codexHome = () => process.env.DASHUP_CODEX_HOME ?? process.env.CODEX_HOME ?? join5(homedir5(), ".codex");
17205
+ var catalog;
17206
+ function models() {
17207
+ const path = join5(codexHome(), "models_cache.json");
17208
+ try {
17209
+ const { mtimeMs } = statSync2(path);
17210
+ if (catalog?.path !== path || catalog.mtimeMs !== mtimeMs) {
17211
+ const listed = JSON.parse(readFileSync4(path, "utf8"))?.models;
17212
+ catalog = {
17213
+ path,
17214
+ mtimeMs,
17215
+ models: (Array.isArray(listed) ? listed : []).filter(
17216
+ (model) => typeof model?.slug === "string"
17217
+ )
17218
+ };
17219
+ }
17220
+ return catalog.models;
17221
+ } catch {
17222
+ return [];
17223
+ }
17224
+ }
17225
+ function codexOptions(recorded) {
17226
+ const listed = models();
17227
+ const ran = typeof recorded.model === "string" ? recorded.model : "";
17228
+ const current = listed.find((model) => model.slug === ran);
17229
+ const choices = listed.filter((model) => model.visibility !== "hide").map((model) => ({ value: model.slug, name: model.display_name ?? model.slug }));
17230
+ if (!ran) return [];
17231
+ if (!choices.some((choice) => choice.value === ran))
17232
+ choices.push({ value: ran, name: current?.display_name ?? ran });
17233
+ const effort = typeof recorded.effort === "string" ? recorded.effort : current?.default_reasoning_level;
17234
+ const efforts = [...(current?.supported_reasoning_levels ?? []).map((level) => level.effort)];
17235
+ if (effort && !efforts.includes(effort)) efforts.push(effort);
17236
+ const fast = current?.service_tiers?.find((tier) => tier.id === "priority");
17237
+ return [
17238
+ { id: "model", name: "Model", value: ran, choices },
17239
+ ...effort ? [
17240
+ {
17241
+ id: "effort",
17242
+ name: "Effort",
17243
+ value: effort,
17244
+ choices: efforts.map((value2) => ({ value: value2, name: `${value2} effort` }))
17245
+ }
17246
+ ] : [],
17247
+ ...fast ? [{ id: "fast", name: fast.name ?? "Fast", value: recorded.fast === true }] : []
17248
+ ];
17249
+ }
17250
+ async function sockets() {
17251
+ const explicit = process.env.DASHUP_CODEX_SOCKET;
17252
+ if (explicit !== void 0) return explicit && isAbsolute2(explicit) ? [explicit] : [];
17253
+ if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17254
+ const homes = /* @__PURE__ */ new Set([codexHome()]);
17255
+ if (process.platform === "linux") {
17256
+ const entries2 = await readdir2("/proc").catch(() => []);
17257
+ for (const pid of entries2.filter((value2) => /^\d+$/.test(value2)).slice(0, MAX_PROCESSES)) {
17258
+ if (homes.size >= MAX_SOCKETS) break;
17259
+ try {
17260
+ const directory2 = `/proc/${pid}`;
17261
+ const owner = await stat(directory2);
17262
+ if (process.getuid && owner.uid !== process.getuid()) continue;
17263
+ const command2 = (await readFile2(join5(directory2, "cmdline"), "utf8")).slice(0, 4096);
17264
+ if (!/(?:^|[\/\0 ])codex(?:[\0 .]|$)/.test(command2)) continue;
17265
+ const environment = await readFile2(join5(directory2, "environ"), "utf8");
17266
+ const home = environment.split("\0").find((value2) => value2.startsWith("CODEX_HOME="))?.slice("CODEX_HOME=".length);
17267
+ if (home && isAbsolute2(home)) homes.add(home);
17268
+ } catch {
17269
+ }
17270
+ }
17271
+ }
17272
+ return [...homes].map((home) => join5(home, ...SOCKET_PARTS));
17273
+ }
17274
+ function messages2(thread) {
17275
+ if (!Array.isArray(thread.turns)) return [];
17276
+ return thread.turns.flatMap((turn) => Array.isArray(turn?.items) ? turn.items.flatMap((item) => {
17277
+ if (typeof item?.id !== "string") return [];
17278
+ if (item.type === "userMessage" && Array.isArray(item.content)) {
17279
+ const text3 = item.content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
17280
+ return text3 ? [{ id: item.id, role: "user", text: text3 }] : [];
17281
+ }
17282
+ if (item.type === "agentMessage" && typeof item.text === "string")
17283
+ return [{ id: item.id, role: "assistant", text: item.text }];
17284
+ if (item.type === "commandExecution" && typeof item.command === "string")
17285
+ return [
17286
+ {
17287
+ id: item.id,
17288
+ role: "tool",
17289
+ text: [item.command, item.aggregatedOutput].filter((value2) => typeof value2 === "string").join("\n\n")
17290
+ }
17291
+ ];
17292
+ return [];
17293
+ }) : []);
17294
+ }
17295
+ function status(thread) {
17296
+ if (thread.status?.type === "idle") return "idle";
17297
+ if (thread.status?.type === "active") {
17298
+ return thread.status.activeFlags?.some(
17299
+ (flag) => flag === "waitingOnApproval" || flag === "waitingOnUserInput"
17300
+ ) ? "attention" : "working";
17301
+ }
17302
+ return "unknown";
17303
+ }
17304
+ var Client = class {
17305
+ constructor(path, changed) {
17306
+ this.path = path;
17307
+ this.changed = changed;
17308
+ }
17309
+ path;
17310
+ changed;
17311
+ socket;
17312
+ connecting;
17313
+ stopped = false;
17314
+ sequence = 0;
17315
+ controls = false;
17316
+ notify;
17317
+ pending = /* @__PURE__ */ new Map();
17318
+ approvals = /* @__PURE__ */ new Map();
17319
+ requests = /* @__PURE__ */ new Map();
17320
+ // The reply each thread is writing, as far as it has streamed, and the item it belongs to.
17321
+ typing = /* @__PURE__ */ new Map();
17322
+ // What the person chose for each thread, which its next turn takes and keeps.
17323
+ chosen = /* @__PURE__ */ new Map();
17324
+ async connect() {
17325
+ if (this.socket?.readyState === WebSocket2.OPEN) return;
17326
+ if (this.connecting) return this.connecting;
17327
+ this.connecting = (async () => {
17328
+ const file = await stat(this.path);
17329
+ if (!file.isSocket() || process.getuid && file.uid !== process.getuid())
17330
+ throw new Error("Codex control socket is not owned by this user");
17331
+ const socket = this.socket = new WebSocket2("ws://localhost/rpc", {
17332
+ createConnection: () => connect(this.path),
17333
+ maxPayload: MAX_BYTES,
17334
+ handshakeTimeout: REQUEST_MS
17335
+ });
17336
+ socket.on("error", () => {
17337
+ });
17338
+ socket.on("message", (bytes) => {
17339
+ let message;
17340
+ try {
17341
+ message = JSON.parse(bytes.toString());
17342
+ } catch {
17343
+ return;
17344
+ }
17345
+ if (typeof message.id === "number" && !message.method) {
17346
+ const pending = this.pending.get(message.id);
17347
+ if (!pending) return;
17348
+ this.pending.delete(message.id);
17349
+ clearTimeout(pending.timer);
17350
+ if (message.error)
17351
+ pending.reject(
17352
+ new Error(`Codex native request failed (${String(message.error.code ?? "unknown")})`)
17353
+ );
17354
+ else pending.resolve(message.result ?? {});
17355
+ return;
17356
+ }
17357
+ if (typeof message.method !== "string") return;
17358
+ const params = message.params;
17359
+ if (message.method === "item/agentMessage/delta" && typeof params?.threadId === "string" && typeof params.delta === "string") {
17360
+ const known = this.typing.get(params.threadId);
17361
+ this.typing.set(params.threadId, {
17362
+ item: params.itemId,
17363
+ text: (known && known.item === params.itemId ? known.text : "") + params.delta
17364
+ });
17365
+ }
17366
+ if (message.method === "serverRequest/resolved") {
17367
+ for (const [id, pending] of this.approvals)
17368
+ if (pending.requestId === message.params?.requestId) this.approvals.delete(id);
17369
+ }
17370
+ if ((message.method === "item/commandExecution/requestApproval" || message.method === "item/fileChange/requestApproval") && (typeof message.id === "string" || typeof message.id === "number") && typeof message.params?.threadId === "string") {
17371
+ const id = randomUUID4();
17372
+ this.approvals.set(id, {
17373
+ requestId: message.id,
17374
+ threadId: params.threadId,
17375
+ approval: approval(id, message.method, params)
17376
+ });
17377
+ }
17378
+ if (/^(thread|turn|item|serverRequest)\//.test(message.method) && !this.notify) {
17379
+ this.notify = setTimeout(() => {
17380
+ this.notify = void 0;
17381
+ this.changed();
17382
+ }, 100);
17383
+ this.notify.unref();
17384
+ }
17385
+ });
17386
+ socket.on("close", () => {
17387
+ if (this.socket !== socket) return;
17388
+ this.socket = void 0;
17389
+ this.controls = false;
17390
+ this.approvals.clear();
17391
+ for (const pending of this.pending.values()) {
17392
+ clearTimeout(pending.timer);
17393
+ pending.reject(new Error("Codex control socket disconnected"));
17394
+ }
17395
+ this.pending.clear();
17396
+ if (!this.stopped) this.changed();
17397
+ });
17398
+ await new Promise((resolve3, reject) => {
17399
+ socket.once("open", resolve3);
17400
+ socket.once("error", reject);
17401
+ socket.once("close", () => reject(new Error("Codex control socket closed before initialization")));
17402
+ });
17403
+ const initialized = await this.call("initialize", {
17404
+ clientInfo: { name: "dashup", title: "Dashup", version },
17405
+ capabilities: { experimentalApi: false, requestAttestation: false }
17406
+ });
17407
+ socket.send(JSON.stringify({ method: "initialized", params: {} }));
17408
+ const version2 = typeof initialized.userAgent === "string" ? /\b0\.(\d+)\.\d+\b/.exec(initialized.userAgent) : null;
17409
+ this.controls = !!version2 && Number(version2[1]) >= 140;
17410
+ })();
17411
+ try {
17412
+ await this.connecting;
17413
+ } catch (error) {
17414
+ this.socket?.terminate();
17415
+ throw error;
17416
+ } finally {
17417
+ this.connecting = void 0;
17220
17418
  }
17221
- const init = message.type === "system" && message.subtype === "init";
17222
- if (init) this.saved = true;
17223
- else if (message.type === "assistant") {
17224
- const text3 = (Array.isArray(message.message?.content) ? message.message.content : []).filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n\n");
17225
- if (!text3) return false;
17226
- this.messages.push({ id: String(message.uuid ?? randomUUID3()), role: "assistant", text: text3 });
17227
- } else if (message.type === "control_request" && message.request?.subtype === "can_use_tool") {
17228
- const id = randomUUID3();
17229
- this.approvals.set(id, {
17230
- request: String(message.request_id),
17231
- input: message.request.input ?? {},
17232
- approval: approval2(id, message.request)
17419
+ }
17420
+ call(method, params) {
17421
+ const socket = this.socket;
17422
+ if (this.stopped || socket?.readyState !== WebSocket2.OPEN)
17423
+ return Promise.reject(new Error("Codex control socket is disconnected"));
17424
+ if (this.pending.size >= 32) return Promise.reject(new Error("Codex request limit reached"));
17425
+ const id = ++this.sequence;
17426
+ return new Promise((resolve3, reject) => {
17427
+ const timer = setTimeout(() => {
17428
+ this.pending.delete(id);
17429
+ reject(new Error("Codex native request timed out"));
17430
+ }, REQUEST_MS);
17431
+ timer.unref();
17432
+ this.pending.set(id, { resolve: resolve3, reject, timer });
17433
+ socket.send(JSON.stringify({ id, method, params }), (error) => {
17434
+ if (error) {
17435
+ clearTimeout(timer);
17436
+ this.pending.delete(id);
17437
+ reject(new Error("Codex native request could not be sent"));
17438
+ }
17233
17439
  });
17234
- this.status = "attention";
17235
- } else if (message.type === "result") {
17236
- if (--this.open <= 0) this.turn?.stdin.end();
17237
- } else if (message.type === "stream_event" && !message.parent_tool_use_id) {
17238
- const event = message.event;
17239
- if (event?.type === "content_block_start") this.typing = "";
17240
- else if (event?.type === "content_block_delta" && event.delta?.type === "text_delta")
17241
- this.typing += String(event.delta.text ?? "");
17242
- else return false;
17243
- this.changed();
17244
- return false;
17245
- } else return false;
17246
- this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
17440
+ });
17441
+ }
17442
+ once(id, action) {
17443
+ const known = this.requests.get(id);
17444
+ if (known) return known;
17445
+ if (this.requests.size >= 1e3)
17446
+ throw new Error("Codex command history is full; reconnect before sending");
17447
+ const result = action();
17448
+ this.requests.set(id, result);
17449
+ return result;
17450
+ }
17451
+ // Starts a thread in `cwd` with Codex's own settings, or loads a saved one, then a turn.
17452
+ async start(cwd, text3, saved) {
17453
+ await this.connect();
17454
+ if (!this.controls) throw new Error("Update Codex to start threads from Dashup");
17455
+ const id = saved ? (await this.call("thread/resume", { threadId: saved, excludeTurns: true })).thread?.id : (await this.call("thread/start", { cwd })).thread?.id;
17456
+ if (typeof id !== "string") throw new Error("Codex did not open the thread");
17457
+ await this.call("turn/start", { threadId: id, input: [{ type: "text", text: text3, text_elements: [] }] });
17247
17458
  this.changed();
17248
- return init;
17459
+ return id;
17249
17460
  }
17250
- live(models) {
17251
- return {
17252
- id: this.id,
17253
- cwd: this.cwd,
17254
- title: this.title,
17255
- harness: "claude",
17256
- updatedAt: this.updatedAt,
17257
- status: this.status,
17258
- messages: [...this.messages],
17259
- typing: this.typing,
17260
- approvals: [...this.approvals.values()].map((pending) => pending.approval),
17261
- send: (requestId, text3, now) => this.once(requestId, () => this.say(text3, now)),
17262
- now: true,
17263
- options: (recorded) => claudeOptions(models, recorded),
17264
- configure: (requestId, option2, value2) => this.once(requestId, async () => {
17265
- this.chosen[option2] = value2;
17266
- this.apply(option2, value2);
17267
- }),
17268
- stop: this.turn ? (requestId) => this.once(requestId, async () => {
17269
- if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
17270
- this.interrupt();
17271
- }) : void 0,
17272
- approve: this.approvals.size ? (requestId, approvalId, choice) => this.once(requestId, async () => {
17273
- const pending = this.approvals.get(approvalId);
17274
- if (!pending || !this.turn || !CHOICES.some((value2) => value2.id === choice))
17275
- throw new Error("Claude Code approval is no longer pending");
17276
- this.write({
17277
- type: "control_response",
17278
- response: {
17279
- subtype: "success",
17280
- request_id: pending.request,
17281
- response: choice === "accept" ? { behavior: "allow", updatedInput: pending.input } : { behavior: "deny", message: "The person rejected this from their phone." }
17282
- }
17461
+ async read() {
17462
+ await this.connect();
17463
+ const loaded = await this.call("thread/loaded/list", { limit: MAX_SESSIONS2 });
17464
+ if (!Array.isArray(loaded.data)) return [];
17465
+ const sessions = [];
17466
+ for (const id of loaded.data.slice(0, MAX_SESSIONS2)) {
17467
+ if (typeof id !== "string") continue;
17468
+ try {
17469
+ const read = await this.call("thread/read", { threadId: id, includeTurns: true });
17470
+ const thread = read.thread;
17471
+ if (thread?.id !== id || typeof thread.cwd !== "string" || !thread.cwd) continue;
17472
+ const approvals = [...this.approvals.values()].filter((value2) => value2.threadId === id).map((value2) => value2.approval);
17473
+ sessions.push({
17474
+ id,
17475
+ harness: "codex",
17476
+ cwd: thread.cwd,
17477
+ title: typeof thread.name === "string" && thread.name ? thread.name : typeof thread.preview === "string" && thread.preview ? thread.preview : "Codex thread",
17478
+ updatedAt: new Date(Number(thread.updatedAt ?? 0) * 1e3).toISOString(),
17479
+ status: approvals.length ? "attention" : status(thread),
17480
+ messages: messages2(thread),
17481
+ typing: this.typing.get(id)?.text,
17482
+ approvals,
17483
+ // An interrupt for a finished turn is never answered, so the active turn is read first.
17484
+ stop: this.controls ? (requestId) => this.once(requestId, async () => {
17485
+ const native = (await this.call("thread/read", { threadId: id, includeTurns: true })).thread;
17486
+ const turn = native?.status?.type === "active" && Array.isArray(native.turns) ? native.turns.at(-1) : void 0;
17487
+ if (turn?.status !== "inProgress" || typeof turn.id !== "string")
17488
+ throw new Error("Nothing is running in this Codex thread");
17489
+ await this.call("turn/interrupt", { threadId: id, turnId: turn.id });
17490
+ this.changed();
17491
+ }) : void 0,
17492
+ send: this.controls && thread.status?.type === "idle" ? (requestId, text3) => this.once(requestId, async () => {
17493
+ if (!text3.trim() || Buffer.byteLength(text3) > MAX_BYTES / 2)
17494
+ throw new Error("Message is empty or too large");
17495
+ const current = await this.call("thread/loaded/list", { limit: MAX_SESSIONS2 });
17496
+ if (!Array.isArray(current.data) || !current.data.includes(id))
17497
+ throw new Error("Codex thread is no longer loaded");
17498
+ const native = (await this.call("thread/read", { threadId: id, includeTurns: false })).thread;
17499
+ if (native?.id !== id || native.status?.type !== "idle")
17500
+ throw new Error("Codex thread is busy or unavailable");
17501
+ const chosen = this.chosen.get(id) ?? {};
17502
+ await this.call("turn/start", {
17503
+ threadId: id,
17504
+ clientUserMessageId: requestId,
17505
+ input: [{ type: "text", text: text3, text_elements: [] }],
17506
+ ..."model" in chosen ? { model: chosen.model } : {},
17507
+ ..."effort" in chosen ? { effort: chosen.effort } : {},
17508
+ ..."fast" in chosen ? { serviceTier: chosen.fast ? "priority" : null } : {}
17509
+ });
17510
+ this.changed();
17511
+ }) : void 0,
17512
+ options: codexOptions,
17513
+ configure: this.controls ? (requestId, option2, value2) => this.once(requestId, async () => {
17514
+ this.chosen.set(id, { ...this.chosen.get(id), [option2]: value2 });
17515
+ }) : void 0,
17516
+ approve: approvals.length ? (requestId, approvalId, reply) => this.once(requestId, async () => {
17517
+ const approval2 = this.approvals.get(approvalId);
17518
+ const choice = "choice" in reply ? reply.choice : void 0;
17519
+ const socket = this.socket;
17520
+ if (!approval2 || approval2.threadId !== id || !approval2.approval.choices.some((value2) => value2.id === choice) || socket?.readyState !== WebSocket2.OPEN)
17521
+ throw new Error("Codex approval is no longer pending");
17522
+ await new Promise((resolve3, reject) => socket.send(
17523
+ JSON.stringify({ id: approval2.requestId, result: { decision: choice } }),
17524
+ (error) => error ? reject(new Error("Codex approval could not be sent")) : resolve3()
17525
+ ));
17526
+ this.approvals.delete(approvalId);
17527
+ this.changed();
17528
+ }) : void 0
17283
17529
  });
17284
- this.approvals.delete(approvalId);
17285
- if (!this.approvals.size) this.status = "working";
17286
- this.changed();
17287
- }) : void 0
17288
- };
17530
+ } catch {
17531
+ }
17532
+ }
17533
+ return sessions;
17289
17534
  }
17290
- end() {
17291
- this.turn?.kill();
17535
+ stop() {
17536
+ this.stopped = true;
17537
+ clearTimeout(this.notify);
17538
+ this.socket?.terminate();
17539
+ for (const pending of this.pending.values()) {
17540
+ clearTimeout(pending.timer);
17541
+ pending.reject(new Error("Codex observation stopped"));
17542
+ }
17543
+ this.pending.clear();
17544
+ this.approvals.clear();
17292
17545
  }
17293
17546
  };
17294
- var Headless = class {
17547
+ var Codex = class {
17295
17548
  constructor(changed) {
17296
17549
  this.changed = changed;
17297
17550
  }
17298
17551
  changed;
17299
- sessions = /* @__PURE__ */ new Map();
17300
- notify;
17301
- soon = () => {
17302
- if (this.notify) return;
17303
- this.notify = setTimeout(() => {
17304
- this.notify = void 0;
17305
- this.changed();
17306
- }, NOTIFY_MS);
17307
- this.notify.unref();
17308
- };
17309
- // Starts a session in `cwd` with its first message, and resolves with the session's id.
17310
- async start(cwd, text3) {
17311
- const title2 = text3.trim().split("\n")[0].slice(0, 80);
17312
- const session = new Session(randomUUID3(), cwd, title2, this.soon);
17313
- this.sessions.set(session.id, session);
17314
- try {
17315
- await session.say(text3);
17316
- } catch (error) {
17317
- this.sessions.delete(session.id);
17318
- throw error;
17552
+ clients = /* @__PURE__ */ new Map();
17553
+ // Control sockets of daemons this connector started, which discovery may not look for.
17554
+ started = /* @__PURE__ */ new Set();
17555
+ // Codex's own daemon runs the threads Dashup starts or resumes, so they outlive this connector.
17556
+ async start(cwd, text3, saved) {
17557
+ const path = process.env.DASHUP_CODEX_SOCKET || join5(codexHome(), ...SOCKET_PARTS);
17558
+ await promisify2(execFile2)("codex", ["app-server", "daemon", "start"], {
17559
+ timeout: 6e4,
17560
+ shell: process.platform === "win32"
17561
+ });
17562
+ this.started.add(path);
17563
+ let client = this.clients.get(path);
17564
+ if (!client) {
17565
+ client = new Client(path, this.changed);
17566
+ this.clients.set(path, client);
17319
17567
  }
17320
- return session.id;
17568
+ return client.start(cwd, text3, saved);
17321
17569
  }
17322
- // Continues a saved session with a message; Claude Code resumes it from its own transcript.
17323
17570
  async resume(id, cwd, text3) {
17324
- const known = this.sessions.get(id);
17325
- const session = known ?? new Session(id, cwd, void 0, this.soon, true);
17326
- this.sessions.set(id, session);
17327
- try {
17328
- await session.say(text3);
17329
- } catch (error) {
17330
- if (!known) this.sessions.delete(id);
17331
- throw error;
17332
- }
17571
+ await this.start(cwd, text3, id);
17333
17572
  }
17334
17573
  async refresh() {
17335
- const models = claudeModels(this.changed);
17336
- return [...this.sessions.values()].map((session) => session.live(models));
17574
+ const paths = /* @__PURE__ */ new Set([...await sockets(), ...this.started]);
17575
+ for (const [path, client] of this.clients)
17576
+ if (!paths.has(path)) {
17577
+ client.stop();
17578
+ this.clients.delete(path);
17579
+ }
17580
+ const results = await Promise.allSettled(
17581
+ [...paths].map(async (path) => {
17582
+ let client = this.clients.get(path);
17583
+ if (!client) {
17584
+ client = new Client(path, this.changed);
17585
+ this.clients.set(path, client);
17586
+ }
17587
+ return client.read();
17588
+ })
17589
+ );
17590
+ return results.flatMap((result) => result.status === "fulfilled" ? result.value : []);
17337
17591
  }
17338
17592
  stop() {
17339
- clearTimeout(this.notify);
17340
- for (const session of this.sessions.values()) session.end();
17593
+ for (const client of this.clients.values()) client.stop();
17594
+ this.clients.clear();
17341
17595
  }
17342
17596
  };
17343
17597
 
@@ -17348,9 +17602,9 @@ import { connect as connect2 } from "node:net";
17348
17602
  import { promisify as promisify3 } from "node:util";
17349
17603
  import { fileURLToPath } from "node:url";
17350
17604
  import { execFile as execFile3 } from "node:child_process";
17351
- import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
17605
+ import { createHash as createHash3, randomUUID as randomUUID5 } from "node:crypto";
17352
17606
  import { stat as stat2, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
17353
- import { join as join6, dirname as dirname5, basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
17607
+ import { join as join6, dirname as dirname5, basename as basename3, isAbsolute as isAbsolute3 } from "node:path";
17354
17608
  import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws";
17355
17609
  import { AhpClient } from "@microsoft/agent-host-protocol/client";
17356
17610
  import { chatReducer, sessionReducer, SUPPORTED_PROTOCOL_VERSIONS } from "@microsoft/agent-host-protocol";
@@ -17368,7 +17622,7 @@ var CONFIRMATION_CHOICES = [
17368
17622
  function roots() {
17369
17623
  const override = process.env.DASHUP_AHP_DATA ?? process.env.DASHUP_VSCODE_DATA;
17370
17624
  if (override !== void 0)
17371
- return override ? [basename2(override) === "User" ? dirname5(override) : override] : [];
17625
+ return override ? [basename3(override) === "User" ? dirname5(override) : override] : [];
17372
17626
  if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17373
17627
  const base = process.platform === "darwin" ? join6(homedir6(), "Library/Application Support") : process.platform === "win32" ? process.env.APPDATA ?? join6(homedir6(), "AppData/Roaming") : process.env.XDG_CONFIG_HOME ?? join6(homedir6(), ".config");
17374
17628
  return ["Code", "Code - Insiders"].map((name) => join6(base, name));
@@ -17410,12 +17664,12 @@ async function entries() {
17410
17664
  try {
17411
17665
  if (!await privateFile(path, MAX_METADATA)) continue;
17412
17666
  const parsed = JSON.parse(await readFile3(path, "utf8"));
17413
- const rows = basename2(path) === "metadata.json" && Array.isArray(parsed) ? parsed : [parsed];
17667
+ const rows = basename3(path) === "metadata.json" && Array.isArray(parsed) ? parsed : [parsed];
17414
17668
  for (const row of rows.slice(0, MAX_HOSTS)) {
17415
17669
  if (row?.schemaVersion !== 2 || !["editor", "standalone"].includes(row.type) || !Number.isSafeInteger(row.pid) || row.pid <= 0 || typeof row.instanceId !== "string" || !row.instanceId || typeof row.connectionToken !== "string" || !row.connectionToken || row.connectionToken.length > 1024 || row.endpoint?.type !== "socket" || typeof row.endpoint.path !== "string" || !isAbsolute3(row.endpoint.path))
17416
17670
  continue;
17417
17671
  const identity2 = createHash3("sha256").update(`${row.type}\0${row.pid}\0${row.instanceId}`).digest("hex");
17418
- if (basename2(path) !== "metadata.json" && basename2(path) !== `${identity2}.json`) continue;
17672
+ if (basename3(path) !== "metadata.json" && basename3(path) !== `${identity2}.json`) continue;
17419
17673
  try {
17420
17674
  process.kill(row.pid, 0);
17421
17675
  } catch {
@@ -17541,7 +17795,7 @@ var Host = class {
17541
17795
  client;
17542
17796
  connecting;
17543
17797
  stopped = false;
17544
- clientId = `dashup-${randomUUID4()}`;
17798
+ clientId = `dashup-${randomUUID5()}`;
17545
17799
  resources = /* @__PURE__ */ new Map();
17546
17800
  subscriptions = /* @__PURE__ */ new Set();
17547
17801
  receipts = /* @__PURE__ */ new Map();
@@ -17700,14 +17954,14 @@ var Host = class {
17700
17954
  }
17701
17955
  async read() {
17702
17956
  await this.connect();
17703
- const catalog = await this.client.request("listSessions", {
17957
+ const catalog2 = await this.client.request("listSessions", {
17704
17958
  channel: ROOT_CHANNEL,
17705
17959
  limit: MAX_SESSIONS3
17706
17960
  });
17707
- if (!Array.isArray(catalog.items)) return [];
17961
+ if (!Array.isArray(catalog2.items)) return [];
17708
17962
  const result = [];
17709
17963
  const active = /* @__PURE__ */ new Set();
17710
- for (const summary of catalog.items.slice(0, MAX_SESSIONS3)) {
17964
+ for (const summary of catalog2.items.slice(0, MAX_SESSIONS3)) {
17711
17965
  try {
17712
17966
  const session = await this.subscribe(summary.resource);
17713
17967
  if (!session || session.lifecycle !== "ready" || !Array.isArray(session.chats)) continue;
@@ -17722,12 +17976,12 @@ var Host = class {
17722
17976
  for (const pending of Array.isArray(session.inputNeeded) ? session.inputNeeded : []) {
17723
17977
  if (pending.kind !== "toolConfirmation" || pending.chat !== item.resource || pending.toolCall?.status !== "pending-confirmation")
17724
17978
  continue;
17725
- const preview2 = confirmation(pending.toolCall);
17979
+ const preview3 = confirmation(pending.toolCall);
17726
17980
  approvals.push({
17727
17981
  id: pending.id,
17728
17982
  title: text2(pending.toolCall.confirmationTitle) || pending.toolCall.displayName || "Native agent approval",
17729
- detail: preview2.detail,
17730
- choices: preview2.choices.map(({ id: id2, label }) => ({ id: id2, label }))
17983
+ detail: preview3.detail,
17984
+ choices: preview3.choices.map(({ id: id2, label }) => ({ id: id2, label }))
17731
17985
  });
17732
17986
  }
17733
17987
  const id = `vscode-ahp:${createHash3("sha256").update(this.entry.root).digest("hex").slice(0, 16)}:${item.resource}`;
@@ -17769,7 +18023,8 @@ var Host = class {
17769
18023
  message: { text: value2, origin: { kind: "user" } }
17770
18024
  });
17771
18025
  } : void 0,
17772
- approve: approvals.some((approval3) => approval3.choices.length) ? (requestId, approvalId, choice) => {
18026
+ approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, reply) => {
18027
+ const choice = "choice" in reply ? reply.choice : void 0;
17773
18028
  const prior = this.requests.get(requestId);
17774
18029
  if (prior) return prior;
17775
18030
  const current = this.resources.get(summary.resource)?.state;
@@ -18142,21 +18397,14 @@ var Server = class {
18142
18397
  (question) => question?.sessionID === id && typeof question.id === "string"
18143
18398
  ) : [];
18144
18399
  const approvals = pending.map((permission) => permissionApproval(permission, history, cwd));
18145
- for (const question of asks) {
18146
- const first = question.questions?.[0];
18147
- const options = first?.multiple !== true && question.questions?.length === 1 && Array.isArray(first?.options) ? first.options.filter(
18148
- (option2) => typeof option2?.label === "string"
18149
- ) : [];
18150
- approvals.push({
18151
- id: `question:${question.id}`,
18152
- title: typeof first?.header === "string" ? first.header : "OpenCode needs an answer",
18153
- detail: typeof first?.question === "string" ? first.question : "Open this question in the computer\u2019s native interface.",
18154
- choices: options.map((option2, index) => ({
18155
- id: `answer:${index}`,
18156
- label: option2.label
18157
- }))
18158
- });
18159
- }
18400
+ for (const question of asks)
18401
+ approvals.push(
18402
+ questionApproval(
18403
+ `question:${question.id}`,
18404
+ Array.isArray(question.questions) ? question.questions : [],
18405
+ (item) => item.multiple === true
18406
+ )
18407
+ );
18160
18408
  result.push({
18161
18409
  id,
18162
18410
  cwd,
@@ -18192,23 +18440,21 @@ var Server = class {
18192
18440
  });
18193
18441
  this.changed();
18194
18442
  }),
18195
- approve: approvals.some((approval3) => approval3.choices.length) ? (requestId, approvalId, choice) => this.once(requestId, async () => {
18443
+ approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, reply) => this.once(requestId, async () => {
18196
18444
  if (this.stopped) throw new Error("OpenCode is disconnected");
18445
+ const choice = "choice" in reply ? reply.choice : void 0;
18197
18446
  if (approvalId.startsWith("question:")) {
18198
18447
  const requestID = approvalId.slice("question:".length);
18199
18448
  const current = await this.request("/question", cwd);
18200
- const question = Array.isArray(current) ? current.find(
18449
+ if (!Array.isArray(current) || !current.some(
18201
18450
  (value2) => value2?.id === requestID && value2.sessionID === id
18202
- ) : void 0;
18203
- const index = /^answer:(\d+)$/.exec(choice)?.[1];
18204
- const option2 = index !== void 0 ? question?.questions?.[0]?.options?.[Number(index)] : void 0;
18205
- if (question?.questions?.length !== 1 || question.questions[0]?.multiple === true || typeof option2?.label !== "string")
18206
- throw new Error(
18207
- "Question is no longer pending or requires the native interface"
18208
- );
18209
- await this.request(`/question/${encodeURIComponent(requestID)}/reply`, cwd, {
18210
- answers: [[option2.label]]
18211
- });
18451
+ ))
18452
+ throw new Error("Question is no longer pending");
18453
+ await this.request(
18454
+ `/question/${encodeURIComponent(requestID)}/${"answers" in reply ? "reply" : "reject"}`,
18455
+ cwd,
18456
+ "answers" in reply ? { answers: reply.answers } : {}
18457
+ );
18212
18458
  } else {
18213
18459
  const current = await this.request("/permission", cwd);
18214
18460
  const permission = Array.isArray(current) ? current.find(
@@ -18432,6 +18678,12 @@ var LiveHarnesses = class {
18432
18678
  });
18433
18679
  return this.refreshing;
18434
18680
  }
18681
+ // What a saved session last ran with, as its agent names its options.
18682
+ recorded(harness, settings) {
18683
+ const changed = () => this.changed();
18684
+ if (harness === "claude") return claudeOptions(claudeModels(changed), settings);
18685
+ return harness === "codex" ? codexOptions(settings) : [];
18686
+ }
18435
18687
  // The agents whose own command runs on this computer. Claude Code is asked its models again.
18436
18688
  async agents() {
18437
18689
  const harnesses = ["claude", "codex", "opencode"];
@@ -18493,11 +18745,11 @@ var LiveHarnesses = class {
18493
18745
  };
18494
18746
 
18495
18747
  // src/harness/queue.ts
18496
- import { randomUUID as randomUUID5 } from "node:crypto";
18748
+ import { randomUUID as randomUUID6 } from "node:crypto";
18497
18749
  import { spawn as spawn5 } from "node:child_process";
18498
18750
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
18499
- import { join as join8, dirname as dirname6, basename as basename3 } from "node:path";
18500
- import { statSync as statSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
18751
+ import { join as join8, dirname as dirname6, basename as basename4 } from "node:path";
18752
+ import { statSync as statSync3, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
18501
18753
  var CODEX_VERSION = "0.160.1";
18502
18754
  var REQUEST_MS4 = 1e4;
18503
18755
  var MAX_BYTES4 = 1024 * 1024;
@@ -18506,7 +18758,7 @@ var MAX_FILES = 4096;
18506
18758
  var THREAD_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
18507
18759
  function identity(path) {
18508
18760
  const canonical2 = realpathSync2.native(path);
18509
- const file = statSync2(canonical2);
18761
+ const file = statSync3(canonical2);
18510
18762
  if (!file.isFile() || process.getuid && file.uid !== process.getuid())
18511
18763
  throw new Error("Native queue files are not owned by this user");
18512
18764
  return { path: canonical2, device: file.dev, inode: file.ino };
@@ -18519,17 +18771,17 @@ function packageVersion(executable) {
18519
18771
  for (let depth = 0; depth < 8; depth++, directory2 = dirname6(directory2)) {
18520
18772
  const path = join8(directory2, "package.json");
18521
18773
  if (!existsSync4(path)) continue;
18522
- const value2 = JSON.parse(readFileSync4(path, "utf8"));
18774
+ const value2 = JSON.parse(readFileSync5(path, "utf8"));
18523
18775
  if (value2.name?.startsWith("@openai/codex-")) return value2.version;
18524
18776
  }
18525
18777
  return void 0;
18526
18778
  }
18527
18779
  function processOwners(pid) {
18528
18780
  const root = `/proc/${pid}`;
18529
- if (process.getuid && statSync2(root).uid !== process.getuid()) return [];
18781
+ if (process.getuid && statSync3(root).uid !== process.getuid()) return [];
18530
18782
  const executable = realpathSync2.native(join8(root, "exe"));
18531
- if (basename3(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
18532
- const status2 = readFileSync4(join8(root, "stat"), "utf8");
18783
+ if (basename4(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
18784
+ const status2 = readFileSync5(join8(root, "stat"), "utf8");
18533
18785
  const start2 = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
18534
18786
  if (!start2 || !/^\d+$/.test(start2)) return [];
18535
18787
  const fds = readdirSync3(join8(root, "fd")).slice(0, MAX_FILES);
@@ -18541,14 +18793,14 @@ function processOwners(pid) {
18541
18793
  }
18542
18794
  });
18543
18795
  return files2.flatMap(({ fd, path }) => {
18544
- if (basename3(dirname6(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
18545
- const thread = basename3(path, ".lock");
18796
+ if (basename4(dirname6(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
18797
+ const thread = basename4(path, ".lock");
18546
18798
  if (!THREAD_ID.test(thread)) return [];
18547
18799
  try {
18548
- const lock = readFileSync4(join8(root, "fdinfo", fd), "utf8");
18800
+ const lock = readFileSync5(join8(root, "fdinfo", fd), "utf8");
18549
18801
  if (!/FLOCK\s+ADVISORY\s+WRITE/.test(lock)) return [];
18550
18802
  const home = realpathSync2.native(dirname6(dirname6(path)));
18551
- const directory2 = statSync2(home);
18803
+ const directory2 = statSync3(home);
18552
18804
  if (process.getuid && directory2.uid !== process.getuid()) return [];
18553
18805
  const queue = identity(join8(home, "queue_1.sqlite"));
18554
18806
  if (!files2.some((file) => {
@@ -18616,7 +18868,7 @@ function validateOwner(owner) {
18616
18868
  throw new Error("The original native conversation owner is no longer available");
18617
18869
  }
18618
18870
  function validateEvidence(owner) {
18619
- if (realpathSync2.native(owner.home) !== owner.home || process.getuid && statSync2(owner.home).uid !== process.getuid() || !sameFile(identity(owner.queue.path), owner.queue) || !sameFile(identity(owner.rollout.path), owner.rollout))
18871
+ if (realpathSync2.native(owner.home) !== owner.home || process.getuid && statSync3(owner.home).uid !== process.getuid() || !sameFile(identity(owner.queue.path), owner.queue) || !sameFile(identity(owner.rollout.path), owner.rollout))
18620
18872
  throw new Error("The original native history storage changed");
18621
18873
  const path = join8(owner.home, "thread_history_1.sqlite");
18622
18874
  if (existsSync4(path) && identity(path).path !== path)
@@ -18778,7 +19030,7 @@ var NativeQueue = class {
18778
19030
  if (existing) return existing;
18779
19031
  if (this.stopped) throw new Error("The connector is stopping");
18780
19032
  validateOwner(scope2.owner);
18781
- const clientUserMessageId = randomUUID5();
19033
+ const clientUserMessageId = randomUUID6();
18782
19034
  const request2 = { ...scope2, receipt: { state: "unknown", clientUserMessageId } };
18783
19035
  let writer;
18784
19036
  try {
@@ -18852,6 +19104,7 @@ var EPHEMERAL_TTL_MS = 6e4;
18852
19104
  var HOME_RECENT = 20;
18853
19105
  var FOLLOW_MS = 2 * 6e4;
18854
19106
  var RESCAN_MS = 1e3;
19107
+ var answers = (approval2, reply) => "answers" in reply ? reply.answers.length === approval2.questions?.length : approval2.choices.some((choice) => choice.id === reply.choice);
18855
19108
  var Command = z2.object({
18856
19109
  type: z2.literal("command"),
18857
19110
  requestId: z2.string().min(1),
@@ -18873,7 +19126,15 @@ var Command = z2.object({
18873
19126
  option: z2.string().min(1).max(100),
18874
19127
  value: z2.union([z2.string().min(1).max(200), z2.boolean()])
18875
19128
  }).strict(),
18876
- z2.object({ kind: z2.literal("approval.respond"), approvalId: z2.string(), choice: z2.string() }).strict(),
19129
+ z2.object({
19130
+ kind: z2.literal("approval.respond"),
19131
+ approvalId: z2.string(),
19132
+ choice: z2.string().optional(),
19133
+ text: z2.string().max(1e5).optional(),
19134
+ answers: z2.array(z2.array(z2.string().min(1).max(1e5)).min(1)).min(1).optional()
19135
+ }).strict().refine(
19136
+ (action) => action.choice === void 0 !== (action.answers === void 0) && (action.text === void 0 || action.choice !== void 0)
19137
+ ),
18877
19138
  z2.object({ kind: z2.literal("attachment.read"), id: z2.string().regex(/^[a-f0-9]{64}$/) }).strict(),
18878
19139
  z2.object({
18879
19140
  kind: z2.literal("session.start"),
@@ -18989,15 +19250,15 @@ var Connector = class {
18989
19250
  for (const project of this.discovery.projects.values())
18990
19251
  for (const thread of project.threads) {
18991
19252
  const native = this.discovery.locations.get(thread.id)?.live;
18992
- const approval3 = native?.approvals[0];
18993
- if (!approval3) continue;
19253
+ const approval2 = native?.approvals[0];
19254
+ if (!approval2) continue;
18994
19255
  await this.thread(pair, project, thread);
18995
19256
  await this.send(pair, {
18996
19257
  type: "attention",
18997
19258
  projectId: project.id,
18998
19259
  threadId: thread.id,
18999
- title: approval3.title,
19000
- body: approval3.detail,
19260
+ title: approval2.title,
19261
+ body: approval2.detail,
19001
19262
  reason: "approval"
19002
19263
  });
19003
19264
  }
@@ -19145,7 +19406,7 @@ var Connector = class {
19145
19406
  const started = threads.filter((thread) => !thread.parent);
19146
19407
  this.listedProjects.set(project.id, {
19147
19408
  ...project,
19148
- active: started.reduce((newest, thread) => thread.updatedAt > newest ? thread.updatedAt : newest, ""),
19409
+ active: started.reduce((newest2, thread) => thread.updatedAt > newest2 ? thread.updatedAt : newest2, ""),
19149
19410
  started: started.length
19150
19411
  });
19151
19412
  }
@@ -19251,9 +19512,9 @@ var Connector = class {
19251
19512
  if (JSON.stringify(prior) === JSON.stringify(snapshot)) continue;
19252
19513
  this.nativeSnapshots.set(thread.id, structuredClone(snapshot));
19253
19514
  for (const pair of this.activePairs()) await this.thread(pair, project, thread);
19254
- for (const approval3 of session.approvals)
19255
- if (!prior?.approvals.some((item) => item.id === approval3.id))
19256
- await this.attention(project.id, thread.id, approval3.title, approval3.detail, "approval");
19515
+ for (const approval2 of session.approvals)
19516
+ if (!prior?.approvals.some((item) => item.id === approval2.id))
19517
+ await this.attention(project.id, thread.id, approval2.title, approval2.detail, "approval");
19257
19518
  if (prior?.thread.status === "working" && thread.status === "idle")
19258
19519
  await this.attention(
19259
19520
  project.id,
@@ -19441,7 +19702,7 @@ var Connector = class {
19441
19702
  }
19442
19703
  }
19443
19704
  attach(socket) {
19444
- const sessions = bridgeAcp(socket, (session, approval3) => {
19705
+ const sessions = bridgeAcp(socket, (session, approval2) => {
19445
19706
  this.agents.set(session.id, session);
19446
19707
  const { project, thread } = this.adopt(session);
19447
19708
  if (!this.snapshots.has(session.id)) {
@@ -19456,15 +19717,15 @@ var Connector = class {
19456
19717
  }
19457
19718
  }).catch((error) => this.report(error));
19458
19719
  }
19459
- if (approval3)
19720
+ if (approval2)
19460
19721
  this.work = this.work.then(async () => {
19461
- if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(approval3))
19722
+ if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(approval2))
19462
19723
  return;
19463
19724
  await this.attention(
19464
19725
  project.id,
19465
19726
  thread.id,
19466
- approval3.title,
19467
- approval3.detail,
19727
+ approval2.title,
19728
+ approval2.detail,
19468
19729
  "approval"
19469
19730
  );
19470
19731
  }).catch((error) => this.report(error));
@@ -19526,11 +19787,22 @@ var Connector = class {
19526
19787
  };
19527
19788
  }
19528
19789
  thread(pair, project, thread, history) {
19790
+ const state = this.threadState(project, thread);
19529
19791
  return this.send(pair, {
19530
- ...this.threadState(project, thread),
19792
+ ...state,
19793
+ thread: this.recorded(state.thread),
19531
19794
  ...history ? { history } : {}
19532
19795
  });
19533
19796
  }
19797
+ // A thread no live session holds shows what its record says it last ran with, without the
19798
+ // choices only a live session takes. Only its own state carries them: a project's snapshot
19799
+ // holds every thread.
19800
+ recorded(thread) {
19801
+ const settings = thread.options || !thread.agent ? void 0 : this.discovery.locations.get(thread.id)?.history?.settings?.();
19802
+ if (!settings) return thread;
19803
+ const options = this.harnesses.recorded(thread.agent.harness, settings).map((option2) => option2.choices ? { ...option2, choices: option2.choices.filter((choice) => choice.value === option2.value) } : option2);
19804
+ return options.length ? { ...thread, options } : thread;
19805
+ }
19534
19806
  async command(pair, command2) {
19535
19807
  if (command2.action.kind === "projects.refresh") {
19536
19808
  await this.findAgents();
@@ -19630,24 +19902,24 @@ var Connector = class {
19630
19902
  }
19631
19903
  case "approval.respond": {
19632
19904
  if (!thread) throw new Error("Thread not found");
19633
- const action = command2.action;
19905
+ const { kind, approvalId, ...reply } = command2.action;
19634
19906
  const native = this.discovery.locations.get(thread.id)?.live;
19635
19907
  if (native?.approve) {
19636
- const approval4 = native.approvals.find((item) => item.id === action.approvalId);
19637
- if (!approval4?.choices.some((choice) => choice.id === action.choice))
19908
+ const approval3 = native.approvals.find((item) => item.id === approvalId);
19909
+ if (!approval3 || !answers(approval3, reply))
19638
19910
  throw new Error("Approval is no longer available");
19639
- return native.approve(command2.requestId, approval4.id, action.choice);
19911
+ return native.approve(command2.requestId, approval3.id, reply);
19640
19912
  }
19641
19913
  const agent = this.attached(thread);
19642
19914
  if (!agent)
19643
19915
  throw new Error("Approval requires an attached ACP session. Answer it on your computer.");
19644
- const [request2, approval3] = [...agent.approvals].find(([, item]) => item.id === action.approvalId) ?? [];
19645
- if (!approval3?.choices.some((choice) => choice.id === action.choice))
19916
+ const [request2, approval2] = [...agent.approvals].find(([, item]) => item.id === approvalId) ?? [];
19917
+ if (!approval2 || !("choice" in reply) || !answers(approval2, reply))
19646
19918
  throw new Error("Approval is no longer available");
19647
19919
  return commandAcp(agent, command2.requestId, {
19648
19920
  kind: "approval",
19649
19921
  id: request2,
19650
- choice: action.choice
19922
+ choice: reply.choice
19651
19923
  });
19652
19924
  }
19653
19925
  }