@dashup/connector 0.1.16 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +1116 -992
  3. package/package.json +4 -4
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.17";
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
- }
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) => {
16743
+ try {
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" }];
16749
+ } catch {
16750
+ return [];
16751
+ }
16752
+ })
16753
+ );
16754
+ return found.flat();
16755
+ }
16756
+ var Claude = class {
16757
+ constructor(changed, socket = join4(dirname4(configPath()), "claude.sock")) {
16758
+ this.changed = changed;
16759
+ this.socket = socket;
16660
16760
  }
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
- }
16761
+ changed;
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();
16680
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);
16681
16819
  });
16682
16820
  }
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;
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;
16709
16832
  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
- });
16833
+ asked = JSON.parse(Buffer.concat(chunks).toString("utf8"));
16763
16834
  } catch {
16764
16835
  }
16836
+ if (typeof asked?.tool !== "string" || !asked.input || typeof asked.input !== "object")
16837
+ return void response.writeHead(400).end();
16838
+ const id = randomUUID2();
16839
+ const prompted = {
16840
+ tool: asked.tool,
16841
+ input: asked.input,
16842
+ ...Array.isArray(asked.suggestions) ? { suggestions: asked.suggestions } : {}
16843
+ };
16844
+ this.prompts.set(id, {
16845
+ session,
16846
+ request: prompted,
16847
+ approval: ask(id, prompted, false),
16848
+ answer: (decision) => {
16849
+ this.prompts.delete(id);
16850
+ response.end(JSON.stringify(decision));
16851
+ this.changed();
16852
+ }
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 {
16765
16893
  }
16766
- return sessions;
16767
16894
  }
16768
16895
  stop() {
16769
- this.stopped = true;
16770
16896
  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();
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;
16778
16903
  }
16779
16904
  };
16780
- var Codex = class {
16781
- constructor(changed) {
16905
+
16906
+ // src/harness/headless.ts
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;
16782
16916
  this.changed = changed;
16917
+ this.saved = saved;
16783
16918
  }
16919
+ id;
16920
+ cwd;
16921
+ title;
16784
16922
  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"
16794
- });
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);
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
+ `);
16802
16939
  }
16803
- async resume(id, cwd, text3) {
16804
- await this.start(cwd, text3, id);
16940
+ interrupt() {
16941
+ this.write({
16942
+ type: "control_request",
16943
+ request_id: randomUUID3(),
16944
+ request: { subtype: "interrupt" }
16945
+ });
16805
16946
  }
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);
16812
- }
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);
16819
- }
16820
- return client.read();
16821
- })
16822
- );
16823
- return results.flatMap((result) => result.status === "fulfilled" ? result.value : []);
16947
+ apply(option2, value2) {
16948
+ this.write({
16949
+ type: "control_request",
16950
+ request_id: randomUUID3(),
16951
+ request: option2 === "model" ? { subtype: "set_model", model: value2 } : { subtype: "apply_flag_settings", settings: { [SWITCHES[option2]]: value2 } }
16952
+ });
16824
16953
  }
16825
- stop() {
16826
- for (const client of this.clients.values()) client.stop();
16827
- this.clients.clear();
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;
16828
16962
  }
16829
- };
16830
-
16831
- // 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
- });
16864
- }
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" }
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",
16980
+ [
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" }
16873
16993
  );
16874
- const timer = setTimeout(() => child.kill(), MODELS_MS);
16875
- child.stdin.on("error", () => {
16994
+ this.open = 1;
16995
+ let errors = "";
16996
+ turn.stdin.on("error", () => {
16876
16997
  });
16877
- child.once("error", () => resolve3([]));
16878
- child.once("exit", () => {
16879
- clearTimeout(timer);
16880
- resolve3([]);
16998
+ turn.stderr.on("data", (chunk) => {
16999
+ errors = (errors + String(chunk)).slice(-2e3);
16881
17000
  });
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
- }
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"));
16896
17016
  });
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 {
17017
+ for (const [option2, value2] of Object.entries(this.chosen)) this.apply(option2, value2);
17018
+ this.write(message);
17019
+ this.changed();
17020
+ return started;
16947
17021
  }
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(
17006
- [
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")
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 ?? []
17045
- };
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"
17073
+ id: this.id,
17074
+ cwd: this.cwd,
17075
+ title: this.title,
17076
+ harness: "claude",
17077
+ updatedAt: this.updatedAt,
17078
+ status: this.status,
17079
+ messages: [...this.messages],
17080
+ typing: this.typing,
17081
+ approvals: [...this.approvals.values()].map((pending) => pending.approval),
17082
+ send: (requestId, text3, now) => this.once(requestId, () => this.say(text3, now)),
17083
+ now: true,
17084
+ options: (recorded) => claudeOptions(models2, recorded),
17085
+ configure: (requestId, option2, value2) => this.once(requestId, async () => {
17086
+ this.chosen[option2] = value2;
17087
+ this.apply(option2, value2);
17088
+ }),
17089
+ stop: this.turn ? (requestId) => this.once(requestId, async () => {
17090
+ if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
17091
+ this.interrupt();
17092
+ }) : void 0,
17093
+ approve: this.approvals.size ? (requestId, approvalId, reply) => this.once(requestId, async () => {
17094
+ const pending = this.approvals.get(approvalId);
17095
+ if (!pending || !this.turn)
17096
+ throw new Error("Claude Code approval is no longer pending");
17097
+ this.write({
17098
+ type: "control_response",
17099
+ response: {
17100
+ subtype: "success",
17101
+ request_id: pending.control,
17102
+ response: decide(pending.request, pending.approval, reply)
17103
+ }
17104
+ });
17105
+ this.approvals.delete(approvalId);
17106
+ if (!this.approvals.size) this.status = "working";
17107
+ this.changed();
17108
+ }) : void 0
17109
+ };
17110
+ }
17111
+ end() {
17112
+ this.turn?.kill();
17113
+ }
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();
17095
- 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
- });
17122
- }
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();
17122
+ soon = () => {
17123
+ if (this.notify) return;
17124
+ this.notify = setTimeout(() => {
17125
+ this.notify = void 0;
17137
17126
  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
- });
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;
17146
17142
  }
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;
17158
- try {
17159
- asked = JSON.parse(Buffer.concat(chunks).toString("utf8"));
17160
- } catch {
17161
- }
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
- });
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
+ }
17186
17154
  }
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()));
17155
+ async refresh() {
17156
+ const models2 = claudeModels(this.changed);
17157
+ return [...this.sessions.values()].map((session) => session.live(models2));
17192
17158
  }
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;
17159
+ stop() {
17160
+ clearTimeout(this.notify);
17161
+ for (const session of this.sessions.values()) session.end();
17202
17162
  }
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 {
17163
+ };
17164
+
17165
+ // src/harness/codex.ts
17166
+ import WebSocket2 from "ws";
17167
+ import { homedir as homedir5 } from "node:os";
17168
+ import { connect } from "node:net";
17169
+ import { promisify as promisify2 } from "node:util";
17170
+ import { randomUUID as randomUUID4 } from "node:crypto";
17171
+ import { execFile as execFile2 } from "node:child_process";
17172
+ import { join as join5, isAbsolute as isAbsolute2 } from "node:path";
17173
+ import { statSync as statSync2, readFileSync as readFileSync4 } from "node:fs";
17174
+ import { stat, readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
17175
+ var REQUEST_MS = 4e3;
17176
+ var MAX_BYTES = 8 * 1024 * 1024;
17177
+ var MAX_SESSIONS2 = 100;
17178
+ var MAX_SOCKETS = 32;
17179
+ var MAX_PROCESSES = 4096;
17180
+ var SOCKET_PARTS = ["app-server-control", "app-server-control.sock"];
17181
+ var DECISIONS = [
17182
+ { id: "accept", label: "Allow once" },
17183
+ { id: "decline", label: "Reject" }
17184
+ ];
17185
+ function approval(id, method, params) {
17186
+ const command2 = method === "item/commandExecution/requestApproval";
17187
+ let detail = [
17188
+ command2 && typeof params.command === "string" ? params.command : "",
17189
+ typeof params.cwd === "string" ? `Working directory: ${params.cwd}` : "",
17190
+ typeof params.grantRoot === "string" ? `Write access root: ${params.grantRoot}` : "",
17191
+ typeof params.reason === "string" ? params.reason : ""
17192
+ ].filter(Boolean).join("\n\n");
17193
+ const oversized = Buffer.byteLength(detail) > MAX_PREVIEW;
17194
+ const unavailable = !command2 || params.kind !== void 0 && params.kind !== "command" || typeof params.command !== "string" || !params.command.trim() || oversized;
17195
+ if (oversized) detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
17196
+ if (unavailable) detail = [detail, NO_PREVIEW].filter(Boolean).join("\n\n");
17197
+ return {
17198
+ id,
17199
+ title: command2 ? "Allow command?" : "Allow file changes?",
17200
+ detail,
17201
+ choices: DECISIONS.filter((choice) => !unavailable || choice.id === "decline").map((choice) => ({ ...choice }))
17202
+ };
17203
+ }
17204
+ var codexHome = () => process.env.DASHUP_CODEX_HOME ?? process.env.CODEX_HOME ?? join5(homedir5(), ".codex");
17205
+ var catalog;
17206
+ function models() {
17207
+ const path = join5(codexHome(), "models_cache.json");
17208
+ try {
17209
+ const { mtimeMs } = statSync2(path);
17210
+ if (catalog?.path !== path || catalog.mtimeMs !== mtimeMs) {
17211
+ const listed = JSON.parse(readFileSync4(path, "utf8"))?.models;
17212
+ catalog = {
17213
+ path,
17214
+ mtimeMs,
17215
+ models: (Array.isArray(listed) ? listed : []).filter(
17216
+ (model) => typeof model?.slug === "string"
17217
+ )
17218
+ };
17219
+ }
17220
+ return catalog.models;
17221
+ } catch {
17222
+ return [];
17223
+ }
17224
+ }
17225
+ function codexOptions(recorded) {
17226
+ const listed = models();
17227
+ const ran = typeof recorded.model === "string" ? recorded.model : "";
17228
+ const current = listed.find((model) => model.slug === ran);
17229
+ const choices = listed.filter((model) => model.visibility !== "hide").map((model) => ({ value: model.slug, name: model.display_name ?? model.slug }));
17230
+ if (!ran) return [];
17231
+ if (!choices.some((choice) => choice.value === ran))
17232
+ choices.push({ value: ran, name: current?.display_name ?? ran });
17233
+ const effort = typeof recorded.effort === "string" ? recorded.effort : current?.default_reasoning_level;
17234
+ const efforts = [...(current?.supported_reasoning_levels ?? []).map((level) => level.effort)];
17235
+ if (effort && !efforts.includes(effort)) efforts.push(effort);
17236
+ const fast = current?.service_tiers?.find((tier) => tier.id === "priority");
17237
+ return [
17238
+ { id: "model", name: "Model", value: ran, choices },
17239
+ ...effort ? [
17240
+ {
17241
+ id: "effort",
17242
+ name: "Effort",
17243
+ value: effort,
17244
+ choices: efforts.map((value2) => ({ value: value2, name: `${value2} effort` }))
17245
+ }
17246
+ ] : [],
17247
+ ...fast ? [{ id: "fast", name: fast.name ?? "Fast", value: recorded.fast === true }] : []
17248
+ ];
17249
+ }
17250
+ async function sockets() {
17251
+ const explicit = process.env.DASHUP_CODEX_SOCKET;
17252
+ if (explicit !== void 0) return explicit && isAbsolute2(explicit) ? [explicit] : [];
17253
+ if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17254
+ const homes = /* @__PURE__ */ new Set([codexHome()]);
17255
+ if (process.platform === "linux") {
17256
+ const entries2 = await readdir2("/proc").catch(() => []);
17257
+ for (const pid of entries2.filter((value2) => /^\d+$/.test(value2)).slice(0, MAX_PROCESSES)) {
17258
+ if (homes.size >= MAX_SOCKETS) break;
17259
+ try {
17260
+ const directory2 = `/proc/${pid}`;
17261
+ const owner = await stat(directory2);
17262
+ if (process.getuid && owner.uid !== process.getuid()) continue;
17263
+ const command2 = (await readFile2(join5(directory2, "cmdline"), "utf8")).slice(0, 4096);
17264
+ if (!/(?:^|[\/\0 ])codex(?:[\0 .]|$)/.test(command2)) continue;
17265
+ const environment = await readFile2(join5(directory2, "environ"), "utf8");
17266
+ const home = environment.split("\0").find((value2) => value2.startsWith("CODEX_HOME="))?.slice("CODEX_HOME=".length);
17267
+ if (home && isAbsolute2(home)) homes.add(home);
17268
+ } catch {
17269
+ }
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;
17412
+ await this.connecting;
17413
+ } catch (error) {
17414
+ this.socket?.terminate();
17415
+ throw error;
17416
+ } finally {
17417
+ this.connecting = void 0;
17355
17418
  }
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)
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;
@@ -18571,6 +18678,12 @@ var LiveHarnesses = class {
18571
18678
  });
18572
18679
  return this.refreshing;
18573
18680
  }
18681
+ // What a saved session last ran with, as its agent names its options.
18682
+ recorded(harness, settings) {
18683
+ const changed = () => this.changed();
18684
+ if (harness === "claude") return claudeOptions(claudeModels(changed), settings);
18685
+ return harness === "codex" ? codexOptions(settings) : [];
18686
+ }
18574
18687
  // The agents whose own command runs on this computer. Claude Code is asked its models again.
18575
18688
  async agents() {
18576
18689
  const harnesses = ["claude", "codex", "opencode"];
@@ -18636,7 +18749,7 @@ import { randomUUID as randomUUID6 } from "node:crypto";
18636
18749
  import { spawn as spawn5 } from "node:child_process";
18637
18750
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
18638
18751
  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";
18752
+ import { statSync as statSync3, existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
18640
18753
  var CODEX_VERSION = "0.160.1";
18641
18754
  var REQUEST_MS4 = 1e4;
18642
18755
  var MAX_BYTES4 = 1024 * 1024;
@@ -18645,7 +18758,7 @@ var MAX_FILES = 4096;
18645
18758
  var THREAD_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
18646
18759
  function identity(path) {
18647
18760
  const canonical2 = realpathSync2.native(path);
18648
- const file = statSync2(canonical2);
18761
+ const file = statSync3(canonical2);
18649
18762
  if (!file.isFile() || process.getuid && file.uid !== process.getuid())
18650
18763
  throw new Error("Native queue files are not owned by this user");
18651
18764
  return { path: canonical2, device: file.dev, inode: file.ino };
@@ -18658,17 +18771,17 @@ function packageVersion(executable) {
18658
18771
  for (let depth = 0; depth < 8; depth++, directory2 = dirname6(directory2)) {
18659
18772
  const path = join8(directory2, "package.json");
18660
18773
  if (!existsSync4(path)) continue;
18661
- const value2 = JSON.parse(readFileSync4(path, "utf8"));
18774
+ const value2 = JSON.parse(readFileSync5(path, "utf8"));
18662
18775
  if (value2.name?.startsWith("@openai/codex-")) return value2.version;
18663
18776
  }
18664
18777
  return void 0;
18665
18778
  }
18666
18779
  function processOwners(pid) {
18667
18780
  const root = `/proc/${pid}`;
18668
- if (process.getuid && statSync2(root).uid !== process.getuid()) return [];
18781
+ if (process.getuid && statSync3(root).uid !== process.getuid()) return [];
18669
18782
  const executable = realpathSync2.native(join8(root, "exe"));
18670
18783
  if (basename4(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
18671
- const status2 = readFileSync4(join8(root, "stat"), "utf8");
18784
+ const status2 = readFileSync5(join8(root, "stat"), "utf8");
18672
18785
  const start2 = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
18673
18786
  if (!start2 || !/^\d+$/.test(start2)) return [];
18674
18787
  const fds = readdirSync3(join8(root, "fd")).slice(0, MAX_FILES);
@@ -18684,10 +18797,10 @@ function processOwners(pid) {
18684
18797
  const thread = basename4(path, ".lock");
18685
18798
  if (!THREAD_ID.test(thread)) return [];
18686
18799
  try {
18687
- const lock = readFileSync4(join8(root, "fdinfo", fd), "utf8");
18800
+ const lock = readFileSync5(join8(root, "fdinfo", fd), "utf8");
18688
18801
  if (!/FLOCK\s+ADVISORY\s+WRITE/.test(lock)) return [];
18689
18802
  const home = realpathSync2.native(dirname6(dirname6(path)));
18690
- const directory2 = statSync2(home);
18803
+ const directory2 = statSync3(home);
18691
18804
  if (process.getuid && directory2.uid !== process.getuid()) return [];
18692
18805
  const queue = identity(join8(home, "queue_1.sqlite"));
18693
18806
  if (!files2.some((file) => {
@@ -18755,7 +18868,7 @@ function validateOwner(owner) {
18755
18868
  throw new Error("The original native conversation owner is no longer available");
18756
18869
  }
18757
18870
  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))
18871
+ if (realpathSync2.native(owner.home) !== owner.home || process.getuid && statSync3(owner.home).uid !== process.getuid() || !sameFile(identity(owner.queue.path), owner.queue) || !sameFile(identity(owner.rollout.path), owner.rollout))
18759
18872
  throw new Error("The original native history storage changed");
18760
18873
  const path = join8(owner.home, "thread_history_1.sqlite");
18761
18874
  if (existsSync4(path) && identity(path).path !== path)
@@ -19293,7 +19406,7 @@ var Connector = class {
19293
19406
  const started = threads.filter((thread) => !thread.parent);
19294
19407
  this.listedProjects.set(project.id, {
19295
19408
  ...project,
19296
- active: started.reduce((newest, thread) => thread.updatedAt > newest ? thread.updatedAt : newest, ""),
19409
+ active: started.reduce((newest2, thread) => thread.updatedAt > newest2 ? thread.updatedAt : newest2, ""),
19297
19410
  started: started.length
19298
19411
  });
19299
19412
  }
@@ -19674,11 +19787,22 @@ var Connector = class {
19674
19787
  };
19675
19788
  }
19676
19789
  thread(pair, project, thread, history) {
19790
+ const state = this.threadState(project, thread);
19677
19791
  return this.send(pair, {
19678
- ...this.threadState(project, thread),
19792
+ ...state,
19793
+ thread: this.recorded(state.thread),
19679
19794
  ...history ? { history } : {}
19680
19795
  });
19681
19796
  }
19797
+ // A thread no live session holds shows what its record says it last ran with, without the
19798
+ // choices only a live session takes. Only its own state carries them: a project's snapshot
19799
+ // holds every thread.
19800
+ recorded(thread) {
19801
+ const settings = thread.options || !thread.agent ? void 0 : this.discovery.locations.get(thread.id)?.history?.settings?.();
19802
+ if (!settings) return thread;
19803
+ const options = this.harnesses.recorded(thread.agent.harness, settings).map((option2) => option2.choices ? { ...option2, choices: option2.choices.filter((choice) => choice.value === option2.value) } : option2);
19804
+ return options.length ? { ...thread, options } : thread;
19805
+ }
19682
19806
  async command(pair, command2) {
19683
19807
  if (command2.action.kind === "projects.refresh") {
19684
19808
  await this.findAgents();