@dashup/connector 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +1197 -1008
  3. package/package.json +1 -1
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;
@@ -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.16";
4051
+ var version = "0.1.18";
4052
4052
 
4053
4053
  // src/pair.ts
4054
4054
  var Invitation = z.object({
@@ -4240,8 +4240,11 @@ var READ_BYTES = 64 * 1024;
4240
4240
  var TITLE_LIMIT = 120;
4241
4241
  var LATEST_BYTES = 8 * 1024 * 1024;
4242
4242
  var JOURNALS = 16;
4243
+ var SEARCH_BYTES = 1024 * 1024;
4244
+ var MARKED = 512;
4243
4245
  var journals = /* @__PURE__ */ new Map();
4244
4246
  var metadata = /* @__PURE__ */ new Map();
4247
+ var marked = /* @__PURE__ */ new Map();
4245
4248
  function object(value2) {
4246
4249
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : void 0;
4247
4250
  }
@@ -4306,6 +4309,40 @@ function described(path, until, describe2) {
4306
4309
  metadata.set(path, { stamp, value: value2 });
4307
4310
  return value2;
4308
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
+ }
4309
4346
  var windowed = (path, messages5) => journals.get(path)?.start ? [EARLIER, ...messages5] : messages5;
4310
4347
  function journal(path, latest = false) {
4311
4348
  const stamp = statSync(path);
@@ -4562,20 +4599,18 @@ function claudeConversation(conversation, rows) {
4562
4599
  }
4563
4600
  function claudeSettings(path) {
4564
4601
  const rows = journal(path, true);
4565
- const settings = {};
4602
+ const marker = object(newest(path, '"ultra_effort_')?.attachment)?.type;
4566
4603
  for (let index = rows.length - 1; index >= 0; index--) {
4567
4604
  const row = rows[index];
4568
4605
  const message = object(row.message);
4569
- const marker = row.type === "attachment" ? object(row.attachment)?.type : void 0;
4570
- if (settings.model === void 0 && row.type === "assistant" && !row.isSidechain && typeof message?.model === "string" && message.model !== "<synthetic>") {
4571
- settings.model = message.model;
4572
- settings.fast = object(message.usage)?.speed === "fast";
4573
- }
4574
- if (settings.ultracode === void 0 && (marker === "ultra_effort_enter" || marker === "ultra_effort_exit"))
4575
- settings.ultracode = marker === "ultra_effort_enter";
4576
- 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
+ };
4577
4612
  }
4578
- return { ultracode: false, ...settings };
4613
+ return { ultracode: marker === "ultra_effort_enter" };
4579
4614
  }
4580
4615
  function claude(root, scan) {
4581
4616
  for (const path of files(join2(root, "projects"), scan, 1).filter((file) => file.endsWith(".jsonl"))) {
@@ -4632,6 +4667,21 @@ function applyPatch(patch) {
4632
4667
  +++ ${kind === "Delete" ? "/dev/null" : path}`
4633
4668
  ).replace(/^\*\*\* .*\n?/gm, "");
4634
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
+ }
4635
4685
  function codexMessages(path) {
4636
4686
  const rows = journal(path, true);
4637
4687
  const outputs = /* @__PURE__ */ new Map();
@@ -4724,7 +4774,8 @@ function codex(root, scan) {
4724
4774
  "Codex thread"
4725
4775
  ),
4726
4776
  files: [path, ...indexed ? [index] : []],
4727
- read: () => codexMessages(path)
4777
+ read: () => codexMessages(path),
4778
+ settings: () => codexSettings(path)
4728
4779
  });
4729
4780
  } catch {
4730
4781
  scan.warnings.push(`Cannot read Codex session: ${path}`);
@@ -4900,7 +4951,7 @@ function openCodeLegacy(root, scan) {
4900
4951
  continue;
4901
4952
  scan.paths.add(root);
4902
4953
  const messages5 = join2(root, "message", row.id);
4903
- 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);
4904
4955
  scan.sessions.push({
4905
4956
  harness: "opencode",
4906
4957
  id: row.id,
@@ -4908,7 +4959,7 @@ function openCodeLegacy(root, scan) {
4908
4959
  title: title(row.title, "OpenCode thread"),
4909
4960
  parent: typeof row.parentID === "string" ? row.parentID : void 0,
4910
4961
  updatedAt: date(
4911
- newest ? jsonFile(newest)?.time?.created : row.time?.created,
4962
+ newest2 ? jsonFile(newest2)?.time?.created : row.time?.created,
4912
4963
  statSync(path).mtimeMs
4913
4964
  ),
4914
4965
  files: [path, messages5, join2(root, "part")],
@@ -13264,22 +13315,22 @@ var FrontendAdapter = class {
13264
13315
  if (options.subscribe === false) requestData.__subscribe = false;
13265
13316
  const result = await this.transport.get(path, requestData);
13266
13317
  const items = result?.[(0, import_pluralize.default)(modelClass.type)] ?? result?.[modelClass.type + "s"] ?? result?.items ?? [];
13267
- const models = items.map(
13318
+ const models2 = items.map(
13268
13319
  (row) => modelClass.hydrate(this, row)
13269
13320
  );
13270
13321
  if (result?.__queryHash) {
13271
- Object.defineProperty(models, "__queryHash", {
13322
+ Object.defineProperty(models2, "__queryHash", {
13272
13323
  value: result.__queryHash,
13273
13324
  enumerable: false
13274
13325
  });
13275
13326
  }
13276
13327
  if (typeof result?.totalCount === "number") {
13277
- Object.defineProperty(models, "__totalCount", {
13328
+ Object.defineProperty(models2, "__totalCount", {
13278
13329
  value: result.totalCount,
13279
13330
  enumerable: false
13280
13331
  });
13281
13332
  }
13282
- return models;
13333
+ return models2;
13283
13334
  };
13284
13335
  chain.first = async () => {
13285
13336
  const all = await chain.find();
@@ -16451,1040 +16502,1096 @@ async function shareAttachment(config2, pair, project, thread, messages5, id) {
16451
16502
  import { promisify as promisify5 } from "node:util";
16452
16503
  import { execFile as execFile5 } from "node:child_process";
16453
16504
 
16454
- // src/harness/codex.ts
16455
- 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
16456
16511
  import { homedir as homedir4 } from "node:os";
16457
- import { connect } from "node:net";
16458
- import { promisify as promisify2 } from "node:util";
16459
16512
  import { randomUUID as randomUUID2 } from "node:crypto";
16460
- import { execFile as execFile2 } from "node:child_process";
16461
- import { join as join4, isAbsolute as isAbsolute2 } from "node:path";
16462
- import { stat, readdir, readFile } from "node:fs/promises";
16463
- var REQUEST_MS = 4e3;
16464
- var MAX_BYTES = 8 * 1024 * 1024;
16465
- var MAX_SESSIONS = 100;
16466
- var MAX_SOCKETS = 32;
16467
- var MAX_PROCESSES = 4096;
16468
- var SOCKET_PARTS = ["app-server-control", "app-server-control.sock"];
16469
- var DECISIONS = [
16470
- { id: "accept", label: "Allow once" },
16471
- { id: "decline", label: "Reject" }
16472
- ];
16473
- function approval(id, method, params) {
16474
- const command2 = method === "item/commandExecution/requestApproval";
16475
- let detail = [
16476
- command2 && typeof params.command === "string" ? params.command : "",
16477
- typeof params.cwd === "string" ? `Working directory: ${params.cwd}` : "",
16478
- typeof params.grantRoot === "string" ? `Write access root: ${params.grantRoot}` : "",
16479
- typeof params.reason === "string" ? params.reason : ""
16480
- ].filter(Boolean).join("\n\n");
16481
- const oversized = Buffer.byteLength(detail) > MAX_PREVIEW;
16482
- const unavailable = !command2 || params.kind !== void 0 && params.kind !== "command" || typeof params.command !== "string" || !params.command.trim() || oversized;
16483
- if (oversized) detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
16484
- if (unavailable) detail = [detail, NO_PREVIEW].filter(Boolean).join("\n\n");
16485
- return {
16486
- id,
16487
- title: command2 ? "Allow command?" : "Allow file changes?",
16488
- detail,
16489
- choices: DECISIONS.filter((choice) => !unavailable || choice.id === "decline").map((choice) => ({ ...choice }))
16490
- };
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) ?? [];
16491
16540
  }
16492
- var codexHome = () => process.env.DASHUP_CODEX_HOME ?? process.env.CODEX_HOME ?? join4(homedir4(), ".codex");
16493
- async function sockets() {
16494
- const explicit = process.env.DASHUP_CODEX_SOCKET;
16495
- if (explicit !== void 0) return explicit && isAbsolute2(explicit) ? [explicit] : [];
16496
- if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
16497
- const homes = /* @__PURE__ */ new Set([codexHome()]);
16498
- if (process.platform === "linux") {
16499
- const entries2 = await readdir("/proc").catch(() => []);
16500
- for (const pid of entries2.filter((value2) => /^\d+$/.test(value2)).slice(0, MAX_PROCESSES)) {
16501
- if (homes.size >= MAX_SOCKETS) break;
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) => {
16502
16557
  try {
16503
- const directory2 = `/proc/${pid}`;
16504
- const owner = await stat(directory2);
16505
- if (process.getuid && owner.uid !== process.getuid()) continue;
16506
- const command2 = (await readFile(join4(directory2, "cmdline"), "utf8")).slice(0, 4096);
16507
- if (!/(?:^|[\/\0 ])codex(?:[\0 .]|$)/.test(command2)) continue;
16508
- const environment = await readFile(join4(directory2, "environ"), "utf8");
16509
- const home = environment.split("\0").find((value2) => value2.startsWith("CODEX_HOME="))?.slice("CODEX_HOME=".length);
16510
- if (home && isAbsolute2(home)) homes.add(home);
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();
16511
16568
  } catch {
16512
16569
  }
16513
- }
16514
- }
16515
- return [...homes].map((home) => join4(home, ...SOCKET_PARTS));
16570
+ });
16571
+ child.stdin.write(
16572
+ `${JSON.stringify({ type: "control_request", request_id: "models", request: { subtype: "initialize" } })}
16573
+ `
16574
+ );
16575
+ });
16516
16576
  }
16517
- function messages2(thread) {
16518
- if (!Array.isArray(thread.turns)) return [];
16519
- return thread.turns.flatMap((turn) => Array.isArray(turn?.items) ? turn.items.flatMap((item) => {
16520
- if (typeof item?.id !== "string") return [];
16521
- if (item.type === "userMessage" && Array.isArray(item.content)) {
16522
- const text3 = item.content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
16523
- return text3 ? [{ id: item.id, role: "user", text: text3 }] : [];
16524
- }
16525
- if (item.type === "agentMessage" && typeof item.text === "string")
16526
- return [{ id: item.id, role: "assistant", text: item.text }];
16527
- if (item.type === "commandExecution" && typeof item.command === "string")
16528
- return [
16529
- {
16530
- id: item.id,
16531
- role: "tool",
16532
- text: [item.command, item.aggregatedOutput].filter((value2) => typeof value2 === "string").join("\n\n")
16533
- }
16534
- ];
16535
- return [];
16536
- }) : []);
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
+ ];
16537
16588
  }
16538
- function status(thread) {
16539
- if (thread.status?.type === "idle") return "idle";
16540
- if (thread.status?.type === "active") {
16541
- return thread.status.activeFlags?.some(
16542
- (flag) => flag === "waitingOnApproval" || flag === "waitingOnUserInput"
16543
- ) ? "attention" : "working";
16544
- }
16545
- return "unknown";
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
+ };
16546
16613
  }
16547
- var Client = class {
16548
- constructor(path, changed) {
16549
- this.path = path;
16550
- this.changed = changed;
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 {
16551
16621
  }
16552
- path;
16553
- changed;
16554
- socket;
16555
- connecting;
16556
- stopped = false;
16557
- sequence = 0;
16558
- controls = false;
16559
- notify;
16560
- pending = /* @__PURE__ */ new Map();
16561
- approvals = /* @__PURE__ */ new Map();
16562
- requests = /* @__PURE__ */ new Map();
16563
- // The reply each thread is writing, as far as it has streamed, and the item it belongs to.
16564
- typing = /* @__PURE__ */ new Map();
16565
- async connect() {
16566
- if (this.socket?.readyState === WebSocket2.OPEN) return;
16567
- if (this.connecting) return this.connecting;
16568
- this.connecting = (async () => {
16569
- const file = await stat(this.path);
16570
- if (!file.isSocket() || process.getuid && file.uid !== process.getuid())
16571
- throw new Error("Codex control socket is not owned by this user");
16572
- const socket = this.socket = new WebSocket2("ws://localhost/rpc", {
16573
- createConnection: () => connect(this.path),
16574
- maxPayload: MAX_BYTES,
16575
- handshakeTimeout: REQUEST_MS
16576
- });
16577
- socket.on("error", () => {
16578
- });
16579
- socket.on("message", (bytes) => {
16580
- let message;
16581
- try {
16582
- message = JSON.parse(bytes.toString());
16583
- } catch {
16584
- return;
16585
- }
16586
- if (typeof message.id === "number" && !message.method) {
16587
- const pending = this.pending.get(message.id);
16588
- if (!pending) return;
16589
- this.pending.delete(message.id);
16590
- clearTimeout(pending.timer);
16591
- if (message.error)
16592
- pending.reject(
16593
- new Error(`Codex native request failed (${String(message.error.code ?? "unknown")})`)
16594
- );
16595
- else pending.resolve(message.result ?? {});
16596
- return;
16597
- }
16598
- if (typeof message.method !== "string") return;
16599
- const params = message.params;
16600
- if (message.method === "item/agentMessage/delta" && typeof params?.threadId === "string" && typeof params.delta === "string") {
16601
- const known = this.typing.get(params.threadId);
16602
- this.typing.set(params.threadId, {
16603
- item: params.itemId,
16604
- text: (known && known.item === params.itemId ? known.text : "") + params.delta
16605
- });
16606
- }
16607
- if (message.method === "serverRequest/resolved") {
16608
- for (const [id, pending] of this.approvals)
16609
- if (pending.requestId === message.params?.requestId) this.approvals.delete(id);
16610
- }
16611
- if ((message.method === "item/commandExecution/requestApproval" || message.method === "item/fileChange/requestApproval") && (typeof message.id === "string" || typeof message.id === "number") && typeof message.params?.threadId === "string") {
16612
- const id = randomUUID2();
16613
- this.approvals.set(id, {
16614
- requestId: message.id,
16615
- threadId: params.threadId,
16616
- approval: approval(id, message.method, params)
16617
- });
16618
- }
16619
- if (/^(thread|turn|item|serverRequest)\//.test(message.method) && !this.notify) {
16620
- this.notify = setTimeout(() => {
16621
- this.notify = void 0;
16622
- this.changed();
16623
- }, 100);
16624
- this.notify.unref();
16625
- }
16626
- });
16627
- socket.on("close", () => {
16628
- if (this.socket !== socket) return;
16629
- this.socket = void 0;
16630
- this.controls = false;
16631
- this.approvals.clear();
16632
- for (const pending of this.pending.values()) {
16633
- clearTimeout(pending.timer);
16634
- pending.reject(new Error("Codex control socket disconnected"));
16635
- }
16636
- this.pending.clear();
16637
- if (!this.stopped) this.changed();
16638
- });
16639
- await new Promise((resolve3, reject) => {
16640
- socket.once("open", resolve3);
16641
- socket.once("error", reject);
16642
- socket.once("close", () => reject(new Error("Codex control socket closed before initialization")));
16643
- });
16644
- const initialized = await this.call("initialize", {
16645
- clientInfo: { name: "dashup", title: "Dashup", version },
16646
- capabilities: { experimentalApi: false, requestAttestation: false }
16647
- });
16648
- socket.send(JSON.stringify({ method: "initialized", params: {} }));
16649
- const version2 = typeof initialized.userAgent === "string" ? /\b0\.(\d+)\.\d+\b/.exec(initialized.userAgent) : null;
16650
- this.controls = !!version2 && Number(version2[1]) >= 140;
16651
- })();
16652
- try {
16653
- await this.connecting;
16654
- } catch (error) {
16655
- this.socket?.terminate();
16656
- throw error;
16657
- } finally {
16658
- this.connecting = void 0;
16659
- }
16660
- }
16661
- call(method, params) {
16662
- const socket = this.socket;
16663
- if (this.stopped || socket?.readyState !== WebSocket2.OPEN)
16664
- return Promise.reject(new Error("Codex control socket is disconnected"));
16665
- if (this.pending.size >= 32) return Promise.reject(new Error("Codex request limit reached"));
16666
- const id = ++this.sequence;
16667
- return new Promise((resolve3, reject) => {
16668
- const timer = setTimeout(() => {
16669
- this.pending.delete(id);
16670
- reject(new Error("Codex native request timed out"));
16671
- }, REQUEST_MS);
16672
- timer.unref();
16673
- this.pending.set(id, { resolve: resolve3, reject, timer });
16674
- socket.send(JSON.stringify({ id, method, params }), (error) => {
16675
- if (error) {
16676
- clearTimeout(timer);
16677
- this.pending.delete(id);
16678
- reject(new Error("Codex native request could not be sent"));
16679
- }
16680
- });
16681
- });
16682
- }
16683
- once(id, action) {
16684
- const known = this.requests.get(id);
16685
- if (known) return known;
16686
- if (this.requests.size >= 1e3)
16687
- throw new Error("Codex command history is full; reconnect before sending");
16688
- const result = action();
16689
- this.requests.set(id, result);
16690
- return result;
16691
- }
16692
- // Starts a thread in `cwd` with Codex's own settings, or loads a saved one, then a turn.
16693
- async start(cwd, text3, saved) {
16694
- await this.connect();
16695
- if (!this.controls) throw new Error("Update Codex to start threads from Dashup");
16696
- const id = saved ? (await this.call("thread/resume", { threadId: saved, excludeTurns: true })).thread?.id : (await this.call("thread/start", { cwd })).thread?.id;
16697
- if (typeof id !== "string") throw new Error("Codex did not open the thread");
16698
- await this.call("turn/start", { threadId: id, input: [{ type: "text", text: text3, text_elements: [] }] });
16699
- this.changed();
16700
- return id;
16701
- }
16702
- async read() {
16703
- await this.connect();
16704
- const loaded = await this.call("thread/loaded/list", { limit: MAX_SESSIONS });
16705
- if (!Array.isArray(loaded.data)) return [];
16706
- const sessions = [];
16707
- for (const id of loaded.data.slice(0, MAX_SESSIONS)) {
16708
- if (typeof id !== "string") continue;
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);
16676
+ return {
16677
+ id,
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
+ ]
16691
+ };
16692
+ }
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() {
16738
+ if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
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) => {
16709
16743
  try {
16710
- const read = await this.call("thread/read", { threadId: id, includeTurns: true });
16711
- const thread = read.thread;
16712
- if (thread?.id !== id || typeof thread.cwd !== "string" || !thread.cwd) continue;
16713
- const approvals = [...this.approvals.values()].filter((value2) => value2.threadId === id).map((value2) => value2.approval);
16714
- sessions.push({
16715
- id,
16716
- harness: "codex",
16717
- cwd: thread.cwd,
16718
- title: typeof thread.name === "string" && thread.name ? thread.name : typeof thread.preview === "string" && thread.preview ? thread.preview : "Codex thread",
16719
- updatedAt: new Date(Number(thread.updatedAt ?? 0) * 1e3).toISOString(),
16720
- status: approvals.length ? "attention" : status(thread),
16721
- messages: messages2(thread),
16722
- typing: this.typing.get(id)?.text,
16723
- approvals,
16724
- // An interrupt for a finished turn is never answered, so the active turn is read first.
16725
- stop: this.controls ? (requestId) => this.once(requestId, async () => {
16726
- const native = (await this.call("thread/read", { threadId: id, includeTurns: true })).thread;
16727
- const turn = native?.status?.type === "active" && Array.isArray(native.turns) ? native.turns.at(-1) : void 0;
16728
- if (turn?.status !== "inProgress" || typeof turn.id !== "string")
16729
- throw new Error("Nothing is running in this Codex thread");
16730
- await this.call("turn/interrupt", { threadId: id, turnId: turn.id });
16731
- this.changed();
16732
- }) : void 0,
16733
- send: this.controls && thread.status?.type === "idle" ? (requestId, text3) => this.once(requestId, async () => {
16734
- if (!text3.trim() || Buffer.byteLength(text3) > MAX_BYTES / 2)
16735
- throw new Error("Message is empty or too large");
16736
- const current = await this.call("thread/loaded/list", { limit: MAX_SESSIONS });
16737
- if (!Array.isArray(current.data) || !current.data.includes(id))
16738
- throw new Error("Codex thread is no longer loaded");
16739
- const native = (await this.call("thread/read", { threadId: id, includeTurns: false })).thread;
16740
- if (native?.id !== id || native.status?.type !== "idle")
16741
- throw new Error("Codex thread is busy or unavailable");
16742
- await this.call("turn/start", {
16743
- threadId: id,
16744
- clientUserMessageId: requestId,
16745
- input: [{ type: "text", text: text3, text_elements: [] }]
16746
- });
16747
- this.changed();
16748
- }) : void 0,
16749
- approve: approvals.length ? (requestId, approvalId, reply) => this.once(requestId, async () => {
16750
- const approval2 = this.approvals.get(approvalId);
16751
- const choice = "choice" in reply ? reply.choice : void 0;
16752
- const socket = this.socket;
16753
- if (!approval2 || approval2.threadId !== id || !approval2.approval.choices.some((value2) => value2.id === choice) || socket?.readyState !== WebSocket2.OPEN)
16754
- throw new Error("Codex approval is no longer pending");
16755
- await new Promise((resolve3, reject) => socket.send(
16756
- JSON.stringify({ id: approval2.requestId, result: { decision: choice } }),
16757
- (error) => error ? reject(new Error("Codex approval could not be sent")) : resolve3()
16758
- ));
16759
- this.approvals.delete(approvalId);
16760
- this.changed();
16761
- }) : void 0
16762
- });
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" }];
16763
16749
  } catch {
16750
+ return [];
16764
16751
  }
16765
- }
16766
- return sessions;
16767
- }
16768
- stop() {
16769
- this.stopped = true;
16770
- clearTimeout(this.notify);
16771
- this.socket?.terminate();
16772
- for (const pending of this.pending.values()) {
16773
- clearTimeout(pending.timer);
16774
- pending.reject(new Error("Codex observation stopped"));
16775
- }
16776
- this.pending.clear();
16777
- this.approvals.clear();
16778
- }
16779
- };
16780
- var Codex = class {
16781
- constructor(changed) {
16752
+ })
16753
+ );
16754
+ return found.flat();
16755
+ }
16756
+ var Claude = class {
16757
+ constructor(changed, socket = join4(dirname4(configPath()), "claude.sock")) {
16782
16758
  this.changed = changed;
16759
+ this.socket = socket;
16783
16760
  }
16784
16761
  changed;
16785
- clients = /* @__PURE__ */ new Map();
16786
- // Control sockets of daemons this connector started, which discovery may not look for.
16787
- started = /* @__PURE__ */ new Set();
16788
- // Codex's own daemon runs the threads Dashup starts or resumes, so they outlive this connector.
16789
- async start(cwd, text3, saved) {
16790
- const path = process.env.DASHUP_CODEX_SOCKET || join4(codexHome(), ...SOCKET_PARTS);
16791
- await promisify2(execFile2)("codex", ["app-server", "daemon", "start"], {
16792
- timeout: 6e4,
16793
- shell: process.platform === "win32"
16762
+ socket;
16763
+ watcher;
16764
+ notify;
16765
+ server;
16766
+ streams = /* @__PURE__ */ new Map();
16767
+ prompts = /* @__PURE__ */ new Map();
16768
+ requests = /* @__PURE__ */ new Map();
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();
16809
+ });
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);
16794
16819
  });
16795
- this.started.add(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.start(cwd, text3, saved);
16802
- }
16803
- async resume(id, cwd, text3) {
16804
- await this.start(cwd, text3, id);
16805
16820
  }
16806
- async refresh() {
16807
- const paths = /* @__PURE__ */ new Set([...await sockets(), ...this.started]);
16808
- for (const [path, client] of this.clients)
16809
- if (!paths.has(path)) {
16810
- client.stop();
16811
- this.clients.delete(path);
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 {
16812
16835
  }
16813
- const results = await Promise.allSettled(
16814
- [...paths].map(async (path) => {
16815
- let client = this.clients.get(path);
16816
- if (!client) {
16817
- client = new Client(path, this.changed);
16818
- this.clients.set(path, client);
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();
16819
16852
  }
16820
- return client.read();
16821
- })
16822
- );
16823
- return results.flatMap((result) => result.status === "fulfilled" ? result.value : []);
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();
16859
+ });
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.
16868
+ once(id, action) {
16869
+ const known = this.requests.get(id);
16870
+ if (known) return known;
16871
+ if (this.requests.size >= MAX_REQUESTS)
16872
+ this.requests.delete(this.requests.keys().next().value);
16873
+ const result = action();
16874
+ this.requests.set(id, result);
16875
+ return result;
16876
+ }
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
+ }
16824
16894
  }
16825
16895
  stop() {
16826
- for (const client of this.clients.values()) client.stop();
16827
- this.clients.clear();
16896
+ clearTimeout(this.notify);
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;
16828
16903
  }
16829
16904
  };
16830
16905
 
16831
16906
  // src/harness/headless.ts
16832
- import { randomUUID as randomUUID4 } from "node:crypto";
16833
- import { createInterface as createInterface3 } from "node:readline";
16834
- import { spawn as spawn3 } from "node:child_process";
16835
-
16836
- // src/harness/claude.ts
16837
- import { homedir as homedir5 } from "node:os";
16838
- import { randomUUID as randomUUID3 } from "node:crypto";
16839
- import { dirname as dirname4, join as join5, basename as basename2 } from "node:path";
16840
- import { spawn as spawn2 } from "node:child_process";
16841
- import { createInterface as createInterface2 } from "node:readline";
16842
- import { watch as watch2, readFileSync as readFileSync3 } from "node:fs";
16843
- import { mkdir, readdir as readdir2, readFile as readFile2, readlink, rm } from "node:fs/promises";
16844
- import { createServer as createServer2 } from "node:http";
16845
- var MODELS_MS = 3e4;
16846
- var COMMANDS = {
16847
- model: (value2) => `/model ${value2}`,
16848
- fast: (value2) => `/fast ${value2 ? "on" : "off"}`,
16849
- ultracode: (value2) => `/effort ultracode ${value2 ? "on" : "off"}`
16850
- };
16851
- var offered = /* @__PURE__ */ new Map();
16852
- var asking = /* @__PURE__ */ new Set();
16853
- var forgetModels = () => offered.clear();
16854
- function claudeModels(changed, command2 = "claude", version2 = "") {
16855
- const key = `${command2}\0${version2}`;
16856
- if (!offered.has(key) && !asking.has(key)) {
16857
- asking.add(key);
16858
- void listModels(command2).then((models) => {
16859
- asking.delete(key);
16860
- if (!models.length) return;
16861
- offered.set(key, models);
16862
- changed();
16863
- });
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;
16916
+ this.changed = changed;
16917
+ this.saved = saved;
16864
16918
  }
16865
- return offered.get(key) ?? [];
16866
- }
16867
- function listModels(command2) {
16868
- return new Promise((resolve3) => {
16869
- const child = spawn2(
16870
- command2,
16871
- ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"],
16872
- { cwd: homedir5(), stdio: "pipe", shell: process.platform === "win32" }
16873
- );
16874
- const timer = setTimeout(() => child.kill(), MODELS_MS);
16875
- child.stdin.on("error", () => {
16876
- });
16877
- child.once("error", () => resolve3([]));
16878
- child.once("exit", () => {
16879
- clearTimeout(timer);
16880
- resolve3([]);
16919
+ id;
16920
+ cwd;
16921
+ title;
16922
+ changed;
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();
16933
+ requests = /* @__PURE__ */ new Map();
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
+ `);
16939
+ }
16940
+ interrupt() {
16941
+ this.write({
16942
+ type: "control_request",
16943
+ request_id: randomUUID3(),
16944
+ request: { subtype: "interrupt" }
16881
16945
  });
16882
- createInterface2({ input: child.stdout }).on("line", (line) => {
16883
- try {
16884
- const message = JSON.parse(line);
16885
- if (message?.type !== "control_response" || message.response?.request_id !== "models")
16886
- return;
16887
- const models = message.response.response?.models;
16888
- resolve3(
16889
- (Array.isArray(models) ? models : []).filter(
16890
- (model) => typeof model?.value === "string" && typeof model.displayName === "string"
16891
- )
16892
- );
16893
- child.stdin.end();
16894
- } catch {
16895
- }
16946
+ }
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 } }
16896
16952
  });
16897
- child.stdin.write(
16898
- `${JSON.stringify({ type: "control_request", request_id: "models", request: { subtype: "initialize" } })}
16899
- `
16900
- );
16901
- });
16902
- }
16903
- function claudeOptions(models, recorded) {
16904
- const ran = typeof recorded.model === "string" ? recorded.model : "";
16905
- const current = models.find((model) => model.value !== "default" && model.resolvedModel === ran) ?? models.find((model) => model.value === ran);
16906
- const choices = models.map((model) => ({ value: model.value, name: model.displayName }));
16907
- if (ran && !current) choices.push({ value: ran, name: ran });
16908
- if (!choices.length) return [];
16909
- return [
16910
- { id: "model", name: "Model", value: current?.value ?? ran, choices },
16911
- ...current?.supportsFastMode ? [{ id: "fast", name: "Fast", value: recorded.fast === true }] : [],
16912
- ...current?.supportedEffortLevels?.includes("xhigh") ? [{ id: "ultracode", name: "Ultracode", value: recorded.ultracode === true }] : []
16913
- ];
16914
- }
16915
- var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
16916
- var SCOPE = 32;
16917
- var preview2 = (text3) => Buffer.byteLength(text3) > MAX_PREVIEW ? Buffer.from(text3).subarray(0, MAX_PREVIEW).toString("utf8") : text3;
16918
- function hunk(before, after, offset = 0) {
16919
- const old = before ? before.replace(/\n$/, "").split("\n") : [];
16920
- const next = after ? after.replace(/\n$/, "").split("\n") : [];
16921
- let start2 = 0;
16922
- while (start2 < old.length && start2 < next.length && old[start2] === next[start2]) start2++;
16923
- let end = 0;
16924
- while (end < old.length - start2 && end < next.length - start2 && old[old.length - 1 - end] === next[next.length - 1 - end])
16925
- end++;
16926
- const removed = old.slice(start2, old.length - end);
16927
- const added2 = next.slice(start2, next.length - end);
16928
- return {
16929
- lines: [
16930
- `@@ -${offset + start2 + 1},${removed.length} +${offset + start2 + 1},${added2.length} @@`,
16931
- ...old.slice(Math.max(0, start2 - 3), start2).map((line) => ` ${line}`),
16932
- ...removed.map((line) => `-${line}`),
16933
- ...added2.map((line) => `+${line}`),
16934
- ...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
16935
- ],
16936
- added: added2.length,
16937
- deleted: removed.length
16938
- };
16939
- }
16940
- function change(tool, input) {
16941
- const path = input.file_path;
16942
- if (typeof path !== "string") return void 0;
16943
- let current = "";
16944
- try {
16945
- current = readFileSync3(path, "utf8");
16946
- } catch {
16947
16953
  }
16948
- const edit = (value2) => {
16949
- const before = String(value2?.old_string ?? "");
16950
- const at = before ? current.indexOf(before) : -1;
16951
- return hunk(
16952
- before,
16953
- String(value2?.new_string ?? ""),
16954
- at < 0 ? 0 : current.slice(0, at).split("\n").length - 1
16955
- );
16956
- };
16957
- 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) : [];
16958
- if (!hunks.length) return void 0;
16959
- const diff = [`--- ${path}`, `+++ ${path}`, ...hunks.flatMap((item) => item.lines)].join("\n");
16960
- if (Buffer.byteLength(diff) > MAX_PREVIEW) return void 0;
16961
- return {
16962
- path,
16963
- diff,
16964
- added: hunks.reduce((sum, item) => sum + item.added, 0),
16965
- deleted: hunks.reduce((sum, item) => sum + item.deleted, 0)
16966
- };
16967
- }
16968
- function remember(suggestions = []) {
16969
- const updates = suggestions.filter(
16970
- (update) => update?.type === "addRules" && update.behavior === "allow" || update?.type === "addDirectories"
16971
- );
16972
- const rule = updates.flatMap((update) => update.rules ?? [])[0];
16973
- const folder = updates.flatMap((update) => update.directories ?? [])[0];
16974
- const scope2 = rule ? String(rule.ruleContent ?? rule.toolName) : folder ? `access to ${basename2(String(folder))}` : "";
16975
- if (scope2)
16976
- return {
16977
- label: `Always allow ${scope2.length > SCOPE ? `${scope2.slice(0, SCOPE - 1)}\u2026` : scope2}`,
16978
- updates
16979
- };
16980
- const edits = suggestions.find(
16981
- (update) => update?.type === "setMode" && update.mode === "acceptEdits"
16982
- );
16983
- return edits ? { label: "Allow all edits", updates: [edits] } : void 0;
16984
- }
16985
- function ask(id, request2, host) {
16986
- const { tool, input } = request2;
16987
- if (tool === "AskUserQuestion" && Array.isArray(input.questions))
16988
- return questionApproval(id, input.questions, (item) => item.multiSelect === true);
16989
- if (tool === "ExitPlanMode")
16990
- return {
16991
- id,
16992
- title: "Approve this plan?",
16993
- detail: host ? "" : "Approve it on your computer, or tell Claude what to change.",
16994
- choices: [
16995
- ...host ? [{ id: "accept", label: "Approve" }] : [],
16996
- { id: "revise", label: "Keep planning", text: true }
16997
- ],
16998
- plan: preview2(String(input.plan ?? ""))
16999
- };
17000
- const diff = change(tool, input);
17001
- const always = remember(request2.suggestions);
17002
- return {
17003
- id,
17004
- title: tool === "Bash" ? "Allow command?" : EDITS.has(tool) ? "Allow file changes?" : `Allow ${request2.label ?? tool}?`,
17005
- detail: preview2(
16954
+ once(id, action) {
16955
+ const known = this.requests.get(id);
16956
+ if (known) return known;
16957
+ if (this.requests.size >= MAX_REQUESTS2)
16958
+ throw new Error("Claude Code command history is full; restart Dashup before sending");
16959
+ const result = action();
16960
+ this.requests.set(id, result);
16961
+ return result;
16962
+ }
16963
+ // Resolves once Claude Code takes the message, or says why it could not start. `now` stops the
16964
+ // turn under way, so Claude Code takes the message at once.
16965
+ say(text3, now = false) {
16966
+ this.messages.push({ id: randomUUID3(), role: "user", text: text3 });
16967
+ this.typing = "";
16968
+ this.status = "working";
16969
+ this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16970
+ const message = { type: "user", message: { role: "user", content: text3 } };
16971
+ if (this.turn) {
16972
+ if (now) this.interrupt();
16973
+ this.open++;
16974
+ this.write(message);
16975
+ this.changed();
16976
+ return Promise.resolve();
16977
+ }
16978
+ const turn = this.turn = spawn3(
16979
+ "claude",
17006
16980
  [
17007
- request2.description ?? (typeof input.description === "string" ? input.description : ""),
17008
- typeof input.command === "string" ? input.command : diff ? "" : JSON.stringify(input, null, 2)
17009
- ].filter(Boolean).join("\n\n")
17010
- ),
17011
- ...diff ? { change: diff } : {},
17012
- choices: [
17013
- { id: "accept", label: "Allow once" },
17014
- ...always ? [{ id: "always", label: always.label }] : [],
17015
- { id: "decline", label: "Reject" }
17016
- ]
17017
- };
17018
- }
17019
- function decide(request2, approval2, reply) {
17020
- const input = request2.input;
17021
- if ("answers" in reply)
17022
- return {
17023
- behavior: "allow",
17024
- updatedInput: {
17025
- ...input,
17026
- answers: Object.fromEntries(
17027
- (approval2.questions ?? []).map((item, index) => [
17028
- item.question,
17029
- reply.answers[index].join(", ")
17030
- ])
17031
- )
17032
- }
17033
- };
17034
- if (reply.choice === "accept")
17035
- return {
17036
- behavior: "allow",
17037
- updatedInput: input,
17038
- ...approval2.plan === void 0 ? {} : { updatedPermissions: [{ type: "setMode", mode: "default", destination: "session" }] }
17039
- };
17040
- if (reply.choice === "always")
16981
+ "-p",
16982
+ "--input-format",
16983
+ "stream-json",
16984
+ "--output-format",
16985
+ "stream-json",
16986
+ "--verbose",
16987
+ "--include-partial-messages",
16988
+ "--permission-prompt-tool",
16989
+ "stdio",
16990
+ ...this.saved ? ["--resume", this.id] : ["--session-id", this.id]
16991
+ ],
16992
+ { cwd: this.cwd, stdio: "pipe", shell: process.platform === "win32" }
16993
+ );
16994
+ this.open = 1;
16995
+ let errors = "";
16996
+ turn.stdin.on("error", () => {
16997
+ });
16998
+ turn.stderr.on("data", (chunk) => {
16999
+ errors = (errors + String(chunk)).slice(-2e3);
17000
+ });
17001
+ const started = new Promise((resolve3, reject) => {
17002
+ createInterface3({ input: turn.stdout }).on("line", (line) => {
17003
+ if (this.receive(line)) resolve3();
17004
+ });
17005
+ const end = (reason) => {
17006
+ reject(new Error(reason));
17007
+ if (this.turn !== turn) return;
17008
+ this.turn = void 0;
17009
+ this.open = 0;
17010
+ this.approvals.clear();
17011
+ this.status = "idle";
17012
+ this.changed();
17013
+ };
17014
+ turn.once("error", () => end("Claude Code is not installed on this computer"));
17015
+ turn.once("exit", () => end(errors.trim() || "Claude Code stopped before starting"));
17016
+ });
17017
+ for (const [option2, value2] of Object.entries(this.chosen)) this.apply(option2, value2);
17018
+ this.write(message);
17019
+ this.changed();
17020
+ return started;
17021
+ }
17022
+ // Handles one line of Claude Code's output, and says whether it started the session.
17023
+ receive(line) {
17024
+ let message;
17025
+ try {
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) {
17041
17072
  return {
17042
- behavior: "allow",
17043
- updatedInput: input,
17044
- updatedPermissions: remember(request2.suggestions)?.updates ?? []
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
17045
17109
  };
17046
- if (reply.choice === "revise")
17047
- return { behavior: "deny", message: reply.text?.trim() || "Keep planning." };
17048
- return {
17049
- behavior: "deny",
17050
- message: reply.choice === "skip" ? "The person skipped these questions." : "The person rejected this from their phone."
17051
- };
17052
- }
17053
- var MAX_SESSIONS2 = 100;
17054
- var MAX_REQUESTS = 1e3;
17055
- var MAX_PROMPT = 4 * 1024 * 1024;
17056
- var SESSION = /^\/sessions\/([\w-]{1,100})(\/prompts)?$/;
17057
- var STATUS = {
17058
- busy: "working",
17059
- idle: "idle",
17060
- shell: "idle",
17061
- waiting: "attention"
17110
+ }
17111
+ end() {
17112
+ this.turn?.kill();
17113
+ }
17062
17114
  };
17063
- async function registrations() {
17064
- if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17065
- const directory2 = join5(claudeHome(), "sessions");
17066
- const names = await readdir2(directory2).catch(() => []);
17067
- const found = await Promise.all(
17068
- names.filter((name) => /^\d+\.json$/.test(name)).slice(0, MAX_SESSIONS2).map(async (name) => {
17069
- try {
17070
- const value2 = JSON.parse(await readFile2(join5(directory2, name), "utf8"));
17071
- if (typeof value2.sessionId !== "string" || typeof value2.cwd !== "string") return [];
17072
- process.kill(value2.pid, 0);
17073
- const binary2 = await readlink(`/proc/${value2.pid}/exe`).catch(() => "");
17074
- return [{ ...value2, command: /[\\/]claude(?:\.exe)?$/.test(binary2) ? binary2 : "claude" }];
17075
- } catch {
17076
- return [];
17077
- }
17078
- })
17079
- );
17080
- return found.flat();
17081
- }
17082
- var Claude = class {
17083
- constructor(changed, socket = join5(dirname4(configPath()), "claude.sock")) {
17115
+ var Headless = class {
17116
+ constructor(changed) {
17084
17117
  this.changed = changed;
17085
- this.socket = socket;
17086
17118
  }
17087
17119
  changed;
17088
- socket;
17089
- watcher;
17120
+ sessions = /* @__PURE__ */ new Map();
17090
17121
  notify;
17091
- server;
17092
- streams = /* @__PURE__ */ new Map();
17093
- prompts = /* @__PURE__ */ new Map();
17094
- requests = /* @__PURE__ */ new Map();
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
+ }
17095
17155
  async refresh() {
17096
- this.watch();
17097
- return (await registrations()).map((session) => {
17098
- const plugin = this.streams.has(session.sessionId);
17099
- const models = claudeModels(this.changed, session.command, session.version);
17100
- const approvals = [...this.prompts.values()].filter((prompt) => prompt.session === session.sessionId).map((prompt) => prompt.approval);
17101
- return {
17102
- id: session.sessionId,
17103
- harness: "claude",
17104
- cwd: session.cwd,
17105
- updatedAt: new Date(session.updatedAt ?? 0).toISOString(),
17106
- status: approvals.length ? "attention" : STATUS[session.status ?? ""] ?? "unknown",
17107
- messages: [],
17108
- approvals,
17109
- approve: approvals.length ? (requestId, approvalId, reply) => this.once(requestId, async () => {
17110
- const prompt = this.prompts.get(approvalId);
17111
- if (prompt?.session !== session.sessionId)
17112
- throw new Error("Claude Code approval is no longer pending");
17113
- prompt.answer(decide(prompt.request, prompt.approval, reply));
17114
- }) : void 0,
17115
- send: plugin ? (requestId, text3, now) => this.once(requestId, () => this.deliver(session.sessionId, now ? { text: text3, now } : { text: text3 })) : void 0,
17116
- stop: plugin ? (requestId) => this.once(requestId, () => this.deliver(session.sessionId, { now: true })) : void 0,
17117
- now: plugin,
17118
- options: (recorded) => claudeOptions(models, recorded),
17119
- configure: plugin ? (requestId, option2, value2) => this.once(requestId, () => this.deliver(session.sessionId, { text: COMMANDS[option2](value2) })) : void 0
17120
- };
17121
- });
17156
+ const models2 = claudeModels(this.changed);
17157
+ return [...this.sessions.values()].map((session) => session.live(models2));
17122
17158
  }
17123
- // Only the connector serves the plugin; one-off commands such as `dashup doctor` read alone.
17124
- async listen() {
17125
- if (this.server || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
17126
- const server = this.server = createServer2((request2, response) => {
17127
- const [, id, prompt] = SESSION.exec(request2.url ?? "") ?? [];
17128
- if (id && prompt && request2.method === "POST") return this.prompt(id, request2, response);
17129
- if (!id || prompt || request2.method !== "GET") return void response.writeHead(404).end();
17130
- this.streams.get(id)?.end();
17131
- this.streams.set(id, response);
17132
- response.once("close", () => {
17133
- if (this.streams.get(id) === response) this.streams.delete(id);
17134
- this.changed();
17135
- });
17136
- response.writeHead(200, { "Content-Type": "application/x-ndjson" }).flushHeaders();
17137
- this.changed();
17138
- });
17139
- await mkdir(dirname4(this.socket), { recursive: true, mode: 448 });
17140
- await rm(this.socket, { force: true });
17141
- if (this.server !== server) return;
17142
- await new Promise((resolve3, reject) => {
17143
- server.once("error", reject);
17144
- server.listen(this.socket, resolve3);
17145
- });
17159
+ stop() {
17160
+ clearTimeout(this.notify);
17161
+ for (const session of this.sessions.values()) session.end();
17146
17162
  }
17147
- // Asks the phone what the session's permission request asks the person.
17148
- prompt(session, request2, response) {
17149
- const chunks = [];
17150
- let bytes = 0;
17151
- request2.on("data", (chunk) => {
17152
- bytes += chunk.length;
17153
- if (bytes > MAX_PROMPT) request2.destroy();
17154
- else chunks.push(chunk);
17155
- });
17156
- request2.once("end", () => {
17157
- let asked;
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;
17158
17259
  try {
17159
- asked = JSON.parse(Buffer.concat(chunks).toString("utf8"));
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);
17160
17268
  } catch {
17161
17269
  }
17162
- if (typeof asked?.tool !== "string" || !asked.input || typeof asked.input !== "object")
17163
- return void response.writeHead(400).end();
17164
- const id = randomUUID3();
17165
- const prompted = {
17166
- tool: asked.tool,
17167
- input: asked.input,
17168
- ...Array.isArray(asked.suggestions) ? { suggestions: asked.suggestions } : {}
17169
- };
17170
- this.prompts.set(id, {
17171
- session,
17172
- request: prompted,
17173
- approval: ask(id, prompted, false),
17174
- answer: (decision) => {
17175
- this.prompts.delete(id);
17176
- response.end(JSON.stringify(decision));
17177
- this.changed();
17178
- }
17179
- });
17180
- response.once("close", () => {
17181
- if (this.prompts.delete(id)) this.changed();
17182
- });
17183
- response.writeHead(200, { "Content-Type": "application/json" }).flushHeaders();
17184
- this.changed();
17185
- });
17186
- }
17187
- deliver(id, delivery) {
17188
- const stream = this.streams.get(id);
17189
- if (!stream) return Promise.reject(new Error("Claude Code is not taking messages"));
17190
- return new Promise((resolve3, reject) => stream.write(`${JSON.stringify(delivery)}
17191
- `, (error) => error ? reject(error) : resolve3()));
17192
- }
17193
- // Each request delivers at most once.
17194
- once(id, action) {
17195
- const known = this.requests.get(id);
17196
- if (known) return known;
17197
- if (this.requests.size >= MAX_REQUESTS)
17198
- this.requests.delete(this.requests.keys().next().value);
17199
- const result = action();
17200
- this.requests.set(id, result);
17201
- return result;
17202
- }
17203
- watch() {
17204
- if (this.watcher || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
17205
- try {
17206
- const watcher = this.watcher = watch2(join5(claudeHome(), "sessions"), () => {
17207
- if (this.notify) return;
17208
- this.notify = setTimeout(() => {
17209
- this.notify = void 0;
17210
- this.changed();
17211
- }, 100);
17212
- this.notify.unref();
17213
- });
17214
- watcher.on("error", () => {
17215
- watcher.close();
17216
- if (this.watcher === watcher) this.watcher = void 0;
17217
- });
17218
- } catch {
17219
17270
  }
17220
17271
  }
17221
- stop() {
17222
- clearTimeout(this.notify);
17223
- this.watcher?.close();
17224
- this.watcher = void 0;
17225
- this.prompts.clear();
17226
- this.server?.closeAllConnections();
17227
- this.server?.close();
17228
- this.server = void 0;
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";
17229
17301
  }
17230
- };
17231
-
17232
- // src/harness/headless.ts
17233
- var MAX_REQUESTS2 = 1e3;
17234
- var NOTIFY_MS = 100;
17235
- var SWITCHES = { fast: "fastMode", ultracode: "ultracode" };
17236
- var Session = class {
17237
- // A resumed session is `saved` and keeps the title its history gives.
17238
- constructor(id, cwd, title2, changed, saved = false) {
17239
- this.id = id;
17240
- this.cwd = cwd;
17241
- this.title = title2;
17302
+ return "unknown";
17303
+ }
17304
+ var Client = class {
17305
+ constructor(path, changed) {
17306
+ this.path = path;
17242
17307
  this.changed = changed;
17243
- this.saved = saved;
17244
17308
  }
17245
- id;
17246
- cwd;
17247
- title;
17309
+ path;
17248
17310
  changed;
17249
- saved;
17250
- messages = [];
17251
- status = "idle";
17252
- updatedAt = (/* @__PURE__ */ new Date()).toISOString();
17253
- turn;
17254
- // Messages written to this turn's process that Claude Code has not finished answering.
17255
- open = 0;
17256
- // The text Claude Code is writing, as far as it has streamed.
17257
- typing = "";
17311
+ socket;
17312
+ connecting;
17313
+ stopped = false;
17314
+ sequence = 0;
17315
+ controls = false;
17316
+ notify;
17317
+ pending = /* @__PURE__ */ new Map();
17258
17318
  approvals = /* @__PURE__ */ new Map();
17259
17319
  requests = /* @__PURE__ */ new Map();
17260
- // What the person chose for this session, applied to each turn as it starts.
17261
- chosen = {};
17262
- write(value2) {
17263
- this.turn?.stdin.write(`${JSON.stringify(value2)}
17264
- `);
17265
- }
17266
- interrupt() {
17267
- this.write({
17268
- type: "control_request",
17269
- request_id: randomUUID4(),
17270
- request: { subtype: "interrupt" }
17271
- });
17272
- }
17273
- apply(option2, value2) {
17274
- this.write({
17275
- type: "control_request",
17276
- request_id: randomUUID4(),
17277
- request: option2 === "model" ? { subtype: "set_model", model: value2 } : { subtype: "apply_flag_settings", settings: { [SWITCHES[option2]]: value2 } }
17278
- });
17279
- }
17280
- once(id, action) {
17281
- const known = this.requests.get(id);
17282
- if (known) return known;
17283
- if (this.requests.size >= MAX_REQUESTS2)
17284
- throw new Error("Claude Code command history is full; restart Dashup before sending");
17285
- const result = action();
17286
- this.requests.set(id, result);
17287
- return result;
17288
- }
17289
- // Resolves once Claude Code takes the message, or says why it could not start. `now` stops the
17290
- // turn under way, so Claude Code takes the message at once.
17291
- say(text3, now = false) {
17292
- this.messages.push({ id: randomUUID4(), role: "user", text: text3 });
17293
- this.typing = "";
17294
- this.status = "working";
17295
- this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
17296
- const message = { type: "user", message: { role: "user", content: text3 } };
17297
- if (this.turn) {
17298
- if (now) this.interrupt();
17299
- this.open++;
17300
- this.write(message);
17301
- this.changed();
17302
- return Promise.resolve();
17303
- }
17304
- const turn = this.turn = spawn3(
17305
- "claude",
17306
- [
17307
- "-p",
17308
- "--input-format",
17309
- "stream-json",
17310
- "--output-format",
17311
- "stream-json",
17312
- "--verbose",
17313
- "--include-partial-messages",
17314
- "--permission-prompt-tool",
17315
- "stdio",
17316
- ...this.saved ? ["--resume", this.id] : ["--session-id", this.id]
17317
- ],
17318
- { cwd: this.cwd, stdio: "pipe", shell: process.platform === "win32" }
17319
- );
17320
- this.open = 1;
17321
- let errors = "";
17322
- turn.stdin.on("error", () => {
17323
- });
17324
- turn.stderr.on("data", (chunk) => {
17325
- errors = (errors + String(chunk)).slice(-2e3);
17326
- });
17327
- const started = new Promise((resolve3, reject) => {
17328
- createInterface3({ input: turn.stdout }).on("line", (line) => {
17329
- if (this.receive(line)) resolve3();
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
17330
17335
  });
17331
- const end = (reason) => {
17332
- reject(new Error(reason));
17333
- if (this.turn !== turn) return;
17334
- this.turn = void 0;
17335
- this.open = 0;
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;
17336
17390
  this.approvals.clear();
17337
- this.status = "idle";
17338
- this.changed();
17339
- };
17340
- turn.once("error", () => end("Claude Code is not installed on this computer"));
17341
- turn.once("exit", () => end(errors.trim() || "Claude Code stopped before starting"));
17342
- });
17343
- for (const [option2, value2] of Object.entries(this.chosen)) this.apply(option2, value2);
17344
- this.write(message);
17345
- this.changed();
17346
- return started;
17347
- }
17348
- // Handles one line of Claude Code's output, and says whether it started the session.
17349
- receive(line) {
17350
- let message;
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
+ })();
17351
17411
  try {
17352
- message = JSON.parse(line);
17353
- } catch {
17354
- return false;
17355
- }
17356
- const init = message.type === "system" && message.subtype === "init";
17357
- if (init) this.saved = true;
17358
- else if (message.type === "assistant") {
17359
- 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");
17360
- if (!text3) return false;
17361
- this.messages.push({ id: String(message.uuid ?? randomUUID4()), role: "assistant", text: text3 });
17362
- } else if (message.type === "control_request" && message.request?.subtype === "can_use_tool") {
17363
- const id = randomUUID4();
17364
- const asked = message.request;
17365
- const request2 = {
17366
- tool: String(asked.tool_name),
17367
- input: asked.input ?? {},
17368
- label: typeof asked.display_name === "string" ? asked.display_name : void 0,
17369
- description: typeof asked.description === "string" ? asked.description : void 0,
17370
- suggestions: Array.isArray(asked.permission_suggestions) ? asked.permission_suggestions : void 0
17371
- };
17372
- this.approvals.set(id, {
17373
- control: String(message.request_id),
17374
- request: request2,
17375
- approval: ask(id, request2, true)
17412
+ await this.connecting;
17413
+ } catch (error) {
17414
+ this.socket?.terminate();
17415
+ throw error;
17416
+ } finally {
17417
+ this.connecting = void 0;
17418
+ }
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
+ }
17376
17439
  });
17377
- this.status = "attention";
17378
- } else if (message.type === "control_cancel_request") {
17379
- for (const [id, pending] of this.approvals)
17380
- if (pending.control === message.request_id) this.approvals.delete(id);
17381
- if (this.status === "attention" && !this.approvals.size) this.status = "working";
17382
- } else if (message.type === "result") {
17383
- if (--this.open <= 0) this.turn?.stdin.end();
17384
- } else if (message.type === "stream_event" && !message.parent_tool_use_id) {
17385
- const event = message.event;
17386
- if (event?.type === "content_block_start") this.typing = "";
17387
- else if (event?.type === "content_block_delta" && event.delta?.type === "text_delta")
17388
- this.typing += String(event.delta.text ?? "");
17389
- else return false;
17390
- this.changed();
17391
- return false;
17392
- } else return false;
17393
- 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: [] }] });
17394
17458
  this.changed();
17395
- return init;
17459
+ return id;
17396
17460
  }
17397
- live(models) {
17398
- return {
17399
- id: this.id,
17400
- cwd: this.cwd,
17401
- title: this.title,
17402
- harness: "claude",
17403
- updatedAt: this.updatedAt,
17404
- status: this.status,
17405
- messages: [...this.messages],
17406
- typing: this.typing,
17407
- approvals: [...this.approvals.values()].map((pending) => pending.approval),
17408
- send: (requestId, text3, now) => this.once(requestId, () => this.say(text3, now)),
17409
- now: true,
17410
- options: (recorded) => claudeOptions(models, recorded),
17411
- configure: (requestId, option2, value2) => this.once(requestId, async () => {
17412
- this.chosen[option2] = value2;
17413
- this.apply(option2, value2);
17414
- }),
17415
- stop: this.turn ? (requestId) => this.once(requestId, async () => {
17416
- if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
17417
- this.interrupt();
17418
- }) : void 0,
17419
- approve: this.approvals.size ? (requestId, approvalId, reply) => this.once(requestId, async () => {
17420
- const pending = this.approvals.get(approvalId);
17421
- if (!pending || !this.turn)
17422
- throw new Error("Claude Code approval is no longer pending");
17423
- this.write({
17424
- type: "control_response",
17425
- response: {
17426
- subtype: "success",
17427
- request_id: pending.control,
17428
- response: decide(pending.request, pending.approval, reply)
17429
- }
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
17430
17529
  });
17431
- this.approvals.delete(approvalId);
17432
- if (!this.approvals.size) this.status = "working";
17433
- this.changed();
17434
- }) : void 0
17435
- };
17530
+ } catch {
17531
+ }
17532
+ }
17533
+ return sessions;
17436
17534
  }
17437
- end() {
17438
- 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();
17439
17545
  }
17440
17546
  };
17441
- var Headless = class {
17547
+ var Codex = class {
17442
17548
  constructor(changed) {
17443
17549
  this.changed = changed;
17444
17550
  }
17445
17551
  changed;
17446
- sessions = /* @__PURE__ */ new Map();
17447
- notify;
17448
- soon = () => {
17449
- if (this.notify) return;
17450
- this.notify = setTimeout(() => {
17451
- this.notify = void 0;
17452
- this.changed();
17453
- }, NOTIFY_MS);
17454
- this.notify.unref();
17455
- };
17456
- // Starts a session in `cwd` with its first message, and resolves with the session's id.
17457
- async start(cwd, text3) {
17458
- const title2 = text3.trim().split("\n")[0].slice(0, 80);
17459
- const session = new Session(randomUUID4(), cwd, title2, this.soon);
17460
- this.sessions.set(session.id, session);
17461
- try {
17462
- await session.say(text3);
17463
- } catch (error) {
17464
- this.sessions.delete(session.id);
17465
- 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);
17466
17567
  }
17467
- return session.id;
17568
+ return client.start(cwd, text3, saved);
17468
17569
  }
17469
- // Continues a saved session with a message; Claude Code resumes it from its own transcript.
17470
17570
  async resume(id, cwd, text3) {
17471
- const known = this.sessions.get(id);
17472
- const session = known ?? new Session(id, cwd, void 0, this.soon, true);
17473
- this.sessions.set(id, session);
17474
- try {
17475
- await session.say(text3);
17476
- } catch (error) {
17477
- if (!known) this.sessions.delete(id);
17478
- throw error;
17479
- }
17571
+ await this.start(cwd, text3, id);
17480
17572
  }
17481
17573
  async refresh() {
17482
- const models = claudeModels(this.changed);
17483
- 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 : []);
17484
17591
  }
17485
17592
  stop() {
17486
- clearTimeout(this.notify);
17487
- for (const session of this.sessions.values()) session.end();
17593
+ for (const client of this.clients.values()) client.stop();
17594
+ this.clients.clear();
17488
17595
  }
17489
17596
  };
17490
17597
 
@@ -17847,14 +17954,14 @@ var Host = class {
17847
17954
  }
17848
17955
  async read() {
17849
17956
  await this.connect();
17850
- const catalog = await this.client.request("listSessions", {
17957
+ const catalog2 = await this.client.request("listSessions", {
17851
17958
  channel: ROOT_CHANNEL,
17852
17959
  limit: MAX_SESSIONS3
17853
17960
  });
17854
- if (!Array.isArray(catalog.items)) return [];
17961
+ if (!Array.isArray(catalog2.items)) return [];
17855
17962
  const result = [];
17856
17963
  const active = /* @__PURE__ */ new Set();
17857
- for (const summary of catalog.items.slice(0, MAX_SESSIONS3)) {
17964
+ for (const summary of catalog2.items.slice(0, MAX_SESSIONS3)) {
17858
17965
  try {
17859
17966
  const session = await this.subscribe(summary.resource);
17860
17967
  if (!session || session.lifecycle !== "ready" || !Array.isArray(session.chats)) continue;
@@ -18180,7 +18287,14 @@ ${NO_PREVIEW}`;
18180
18287
  ...change2 ? { change: change2 } : {}
18181
18288
  };
18182
18289
  }
18183
- function promptContext(history) {
18290
+ function promptContext(history, session) {
18291
+ const current = session?.model;
18292
+ if (typeof session?.agent === "string" && typeof current?.providerID === "string" && typeof current.id === "string")
18293
+ return {
18294
+ agent: session.agent,
18295
+ model: { providerID: current.providerID, modelID: current.id },
18296
+ ...typeof current.variant === "string" ? { variant: current.variant } : {}
18297
+ };
18184
18298
  if (!Array.isArray(history)) throw new Error("OpenCode session context is unavailable");
18185
18299
  const user = [...history].reverse().find((entry) => entry?.info?.role === "user")?.info;
18186
18300
  if (!user) return {};
@@ -18194,6 +18308,26 @@ function promptContext(history) {
18194
18308
  ...variant === void 0 ? {} : { variant }
18195
18309
  };
18196
18310
  }
18311
+ function sessionOptions(models2, agents, session) {
18312
+ const current = session?.model;
18313
+ if (typeof current?.providerID !== "string" || typeof current.id !== "string") return [];
18314
+ const value2 = `${current.providerID}/${current.id}`;
18315
+ const agent = typeof session?.agent === "string" ? session.agent : void 0;
18316
+ const listed = (Array.isArray(models2) ? models2 : []).filter((model) => typeof model?.providerID === "string" && typeof model.id === "string").map((model) => ({
18317
+ value: `${model.providerID}/${model.id}`,
18318
+ name: String(model.name ?? model.id)
18319
+ }));
18320
+ const primary = (Array.isArray(agents) ? agents : []).filter(
18321
+ (item) => typeof item?.id === "string" && item.mode !== "subagent" && !item.hidden
18322
+ ).map((item) => ({ value: String(item.id), name: String(item.name ?? item.id) }));
18323
+ if (!listed.some((choice) => choice.value === value2)) listed.push({ value: value2, name: current.id });
18324
+ if (agent && !primary.some((choice) => choice.value === agent))
18325
+ primary.push({ value: agent, name: agent });
18326
+ return [
18327
+ { id: "model", name: "Model", value: value2, choices: listed },
18328
+ ...agent ? [{ id: "agent", name: "Agent", value: agent, choices: primary }] : []
18329
+ ];
18330
+ }
18197
18331
  var Server = class {
18198
18332
  constructor(endpoint, changed) {
18199
18333
  this.endpoint = endpoint;
@@ -18206,6 +18340,8 @@ var Server = class {
18206
18340
  notify;
18207
18341
  stopped = false;
18208
18342
  requests = /* @__PURE__ */ new Map();
18343
+ // The models and agents OpenCode lists for each folder, asked once while it answers.
18344
+ catalogs = /* @__PURE__ */ new Map();
18209
18345
  headers(directory2) {
18210
18346
  return {
18211
18347
  ...this.endpoint.authorization ? { Authorization: this.endpoint.authorization } : {},
@@ -18240,12 +18376,29 @@ var Server = class {
18240
18376
  this.requests.set(id, pending);
18241
18377
  return pending;
18242
18378
  }
18243
- // A message to one of this folder's sessions, in the agent and model it last used; a new
18379
+ catalog(cwd) {
18380
+ let known = this.catalogs.get(cwd);
18381
+ if (!known) {
18382
+ known = Promise.all([
18383
+ this.request("/api/model", cwd).then((listed) => listed?.data),
18384
+ this.request("/api/agent", cwd).then((listed) => listed?.data)
18385
+ ]).catch(() => {
18386
+ this.catalogs.delete(cwd);
18387
+ return [[], []];
18388
+ });
18389
+ this.catalogs.set(cwd, known);
18390
+ }
18391
+ return known;
18392
+ }
18393
+ // A message to one of this folder's sessions, in the agent and model it runs with; a new
18244
18394
  // session takes OpenCode's defaults.
18245
18395
  async prompt(id, cwd, text3) {
18246
- const existing = await this.request(`/session/${encodeURIComponent(id)}/message?limit=200`, cwd);
18396
+ const [existing, session] = await Promise.all([
18397
+ this.request(`/session/${encodeURIComponent(id)}/message?limit=200`, cwd),
18398
+ this.request(`/api/session/${encodeURIComponent(id)}`, cwd).catch(() => void 0)
18399
+ ]);
18247
18400
  await this.request(`/session/${encodeURIComponent(id)}/prompt_async`, cwd, {
18248
- ...promptContext(existing),
18401
+ ...promptContext(existing, session?.data),
18249
18402
  parts: [{ type: "text", text: text3 }]
18250
18403
  });
18251
18404
  this.changed();
@@ -18275,11 +18428,16 @@ var Server = class {
18275
18428
  state = Promise.all([
18276
18429
  this.request("/session/status", cwd),
18277
18430
  this.request("/permission", cwd).catch(() => []),
18278
- this.request("/question", cwd).catch(() => [])
18431
+ this.request("/question", cwd).catch(() => []),
18432
+ this.request(`/api/session?limit=${MAX_SESSIONS4}`, cwd).then(
18433
+ (listed) => Array.isArray(listed?.data) ? listed.data : [],
18434
+ () => []
18435
+ ),
18436
+ this.catalog(cwd)
18279
18437
  ]);
18280
18438
  states.set(cwd, state);
18281
18439
  }
18282
- const [history, [status2, permissions, questions]] = await Promise.all([
18440
+ const [history, [status2, permissions, questions, settings, [models2, agents]]] = await Promise.all([
18283
18441
  this.request(`/session/${encodeURIComponent(id)}/message?limit=200`, cwd),
18284
18442
  state
18285
18443
  ]);
@@ -18289,6 +18447,7 @@ var Server = class {
18289
18447
  const asks = Array.isArray(questions) ? questions.filter(
18290
18448
  (question) => question?.sessionID === id && typeof question.id === "string"
18291
18449
  ) : [];
18450
+ const current = settings.find((item) => item?.id === id);
18292
18451
  const approvals = pending.map((permission) => permissionApproval(permission, history, cwd));
18293
18452
  for (const question of asks)
18294
18453
  approvals.push(
@@ -18309,8 +18468,8 @@ var Server = class {
18309
18468
  approvals,
18310
18469
  stop: (requestId) => this.once(requestId, async () => {
18311
18470
  if (this.stopped) throw new Error("OpenCode is disconnected");
18312
- const current = await this.request("/session/status", cwd);
18313
- if (!["busy", "retry"].includes(current?.[id]?.type))
18471
+ const current2 = await this.request("/session/status", cwd);
18472
+ if (!["busy", "retry"].includes(current2?.[id]?.type))
18314
18473
  throw new Error("Nothing is running in this OpenCode session");
18315
18474
  await this.request(`/session/${encodeURIComponent(id)}/abort`, cwd, {});
18316
18475
  this.changed();
@@ -18327,19 +18486,32 @@ var Server = class {
18327
18486
  if (Array.isArray(existing) && existing.some((entry) => entry?.info?.id === messageID))
18328
18487
  return;
18329
18488
  await this.request(`/session/${encodeURIComponent(id)}/prompt_async`, cwd, {
18330
- ...promptContext(existing),
18489
+ ...promptContext(existing, current),
18331
18490
  messageID,
18332
18491
  parts: [{ type: "text", text: text3 }]
18333
18492
  });
18334
18493
  this.changed();
18335
18494
  }),
18495
+ options: () => sessionOptions(models2, agents, current),
18496
+ // OpenCode keeps the model, as `provider/model`, and the agent on the session.
18497
+ configure: (requestId, option2, value2) => this.once(requestId, async () => {
18498
+ if (this.stopped) throw new Error("OpenCode is disconnected");
18499
+ const chosen = String(value2);
18500
+ const split = chosen.indexOf("/");
18501
+ await this.request(
18502
+ `/api/session/${encodeURIComponent(id)}/${option2}`,
18503
+ cwd,
18504
+ option2 === "model" ? { model: { providerID: chosen.slice(0, split), id: chosen.slice(split + 1) } } : { agent: chosen }
18505
+ );
18506
+ this.changed();
18507
+ }),
18336
18508
  approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, reply) => this.once(requestId, async () => {
18337
18509
  if (this.stopped) throw new Error("OpenCode is disconnected");
18338
18510
  const choice = "choice" in reply ? reply.choice : void 0;
18339
18511
  if (approvalId.startsWith("question:")) {
18340
18512
  const requestID = approvalId.slice("question:".length);
18341
- const current = await this.request("/question", cwd);
18342
- if (!Array.isArray(current) || !current.some(
18513
+ const current2 = await this.request("/question", cwd);
18514
+ if (!Array.isArray(current2) || !current2.some(
18343
18515
  (value2) => value2?.id === requestID && value2.sessionID === id
18344
18516
  ))
18345
18517
  throw new Error("Question is no longer pending");
@@ -18349,8 +18521,8 @@ var Server = class {
18349
18521
  "answers" in reply ? { answers: reply.answers } : {}
18350
18522
  );
18351
18523
  } else {
18352
- const current = await this.request("/permission", cwd);
18353
- const permission = Array.isArray(current) ? current.find(
18524
+ const current2 = await this.request("/permission", cwd);
18525
+ const permission = Array.isArray(current2) ? current2.find(
18354
18526
  (value2) => value2?.id === approvalId && value2.sessionID === id
18355
18527
  ) : void 0;
18356
18528
  if (!PERMISSION_CHOICES.some((value2) => value2.id === choice) || !permission)
@@ -18571,6 +18743,12 @@ var LiveHarnesses = class {
18571
18743
  });
18572
18744
  return this.refreshing;
18573
18745
  }
18746
+ // What a saved session last ran with, as its agent names its options.
18747
+ recorded(harness, settings) {
18748
+ const changed = () => this.changed();
18749
+ if (harness === "claude") return claudeOptions(claudeModels(changed), settings);
18750
+ return harness === "codex" ? codexOptions(settings) : [];
18751
+ }
18574
18752
  // The agents whose own command runs on this computer. Claude Code is asked its models again.
18575
18753
  async agents() {
18576
18754
  const harnesses = ["claude", "codex", "opencode"];
@@ -18636,7 +18814,7 @@ import { randomUUID as randomUUID6 } from "node:crypto";
18636
18814
  import { spawn as spawn5 } from "node:child_process";
18637
18815
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
18638
18816
  import { join as join8, dirname as dirname6, basename as basename4 } from "node:path";
18639
- import { statSync as statSync2, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
18817
+ import { statSync as statSync3, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
18640
18818
  var CODEX_VERSION = "0.160.1";
18641
18819
  var REQUEST_MS4 = 1e4;
18642
18820
  var MAX_BYTES4 = 1024 * 1024;
@@ -18645,7 +18823,7 @@ var MAX_FILES = 4096;
18645
18823
  var THREAD_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
18646
18824
  function identity(path) {
18647
18825
  const canonical2 = realpathSync2.native(path);
18648
- const file = statSync2(canonical2);
18826
+ const file = statSync3(canonical2);
18649
18827
  if (!file.isFile() || process.getuid && file.uid !== process.getuid())
18650
18828
  throw new Error("Native queue files are not owned by this user");
18651
18829
  return { path: canonical2, device: file.dev, inode: file.ino };
@@ -18658,17 +18836,17 @@ function packageVersion(executable) {
18658
18836
  for (let depth = 0; depth < 8; depth++, directory2 = dirname6(directory2)) {
18659
18837
  const path = join8(directory2, "package.json");
18660
18838
  if (!existsSync4(path)) continue;
18661
- const value2 = JSON.parse(readFileSync4(path, "utf8"));
18839
+ const value2 = JSON.parse(readFileSync5(path, "utf8"));
18662
18840
  if (value2.name?.startsWith("@openai/codex-")) return value2.version;
18663
18841
  }
18664
18842
  return void 0;
18665
18843
  }
18666
18844
  function processOwners(pid) {
18667
18845
  const root = `/proc/${pid}`;
18668
- if (process.getuid && statSync2(root).uid !== process.getuid()) return [];
18846
+ if (process.getuid && statSync3(root).uid !== process.getuid()) return [];
18669
18847
  const executable = realpathSync2.native(join8(root, "exe"));
18670
18848
  if (basename4(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
18671
- const status2 = readFileSync4(join8(root, "stat"), "utf8");
18849
+ const status2 = readFileSync5(join8(root, "stat"), "utf8");
18672
18850
  const start2 = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
18673
18851
  if (!start2 || !/^\d+$/.test(start2)) return [];
18674
18852
  const fds = readdirSync3(join8(root, "fd")).slice(0, MAX_FILES);
@@ -18684,10 +18862,10 @@ function processOwners(pid) {
18684
18862
  const thread = basename4(path, ".lock");
18685
18863
  if (!THREAD_ID.test(thread)) return [];
18686
18864
  try {
18687
- const lock = readFileSync4(join8(root, "fdinfo", fd), "utf8");
18865
+ const lock = readFileSync5(join8(root, "fdinfo", fd), "utf8");
18688
18866
  if (!/FLOCK\s+ADVISORY\s+WRITE/.test(lock)) return [];
18689
18867
  const home = realpathSync2.native(dirname6(dirname6(path)));
18690
- const directory2 = statSync2(home);
18868
+ const directory2 = statSync3(home);
18691
18869
  if (process.getuid && directory2.uid !== process.getuid()) return [];
18692
18870
  const queue = identity(join8(home, "queue_1.sqlite"));
18693
18871
  if (!files2.some((file) => {
@@ -18755,7 +18933,7 @@ function validateOwner(owner) {
18755
18933
  throw new Error("The original native conversation owner is no longer available");
18756
18934
  }
18757
18935
  function validateEvidence(owner) {
18758
- 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))
18936
+ 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))
18759
18937
  throw new Error("The original native history storage changed");
18760
18938
  const path = join8(owner.home, "thread_history_1.sqlite");
18761
18939
  if (existsSync4(path) && identity(path).path !== path)
@@ -19293,7 +19471,7 @@ var Connector = class {
19293
19471
  const started = threads.filter((thread) => !thread.parent);
19294
19472
  this.listedProjects.set(project.id, {
19295
19473
  ...project,
19296
- active: started.reduce((newest, thread) => thread.updatedAt > newest ? thread.updatedAt : newest, ""),
19474
+ active: started.reduce((newest2, thread) => thread.updatedAt > newest2 ? thread.updatedAt : newest2, ""),
19297
19475
  started: started.length
19298
19476
  });
19299
19477
  }
@@ -19674,11 +19852,22 @@ var Connector = class {
19674
19852
  };
19675
19853
  }
19676
19854
  thread(pair, project, thread, history) {
19855
+ const state = this.threadState(project, thread);
19677
19856
  return this.send(pair, {
19678
- ...this.threadState(project, thread),
19857
+ ...state,
19858
+ thread: this.recorded(state.thread),
19679
19859
  ...history ? { history } : {}
19680
19860
  });
19681
19861
  }
19862
+ // A thread no live session holds shows what its record says it last ran with, without the
19863
+ // choices only a live session takes. Only its own state carries them: a project's snapshot
19864
+ // holds every thread.
19865
+ recorded(thread) {
19866
+ const settings = thread.options || !thread.agent ? void 0 : this.discovery.locations.get(thread.id)?.history?.settings?.();
19867
+ if (!settings) return thread;
19868
+ const options = this.harnesses.recorded(thread.agent.harness, settings).map((option2) => option2.choices ? { ...option2, choices: option2.choices.filter((choice) => choice.value === option2.value) } : option2);
19869
+ return options.length ? { ...thread, options } : thread;
19870
+ }
19682
19871
  async command(pair, command2) {
19683
19872
  if (command2.action.kind === "projects.refresh") {
19684
19873
  await this.findAgents();