@dashup/connector 0.1.4 → 0.1.6

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 +10 -4
  2. package/dist/index.js +809 -150
  3. package/package.json +5 -5
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ var require_XMLHttpRequest = __commonJS({
42
42
  "use strict";
43
43
  var fs = __require("fs");
44
44
  var Url = __require("url");
45
- var spawn5 = __require("child_process").spawn;
45
+ var spawn7 = __require("child_process").spawn;
46
46
  module.exports = XMLHttpRequest3;
47
47
  XMLHttpRequest3.XMLHttpRequest = XMLHttpRequest3;
48
48
  function XMLHttpRequest3(opts) {
@@ -338,7 +338,7 @@ var require_XMLHttpRequest = __commonJS({
338
338
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
339
339
  fs.writeFileSync(syncFile, "", "utf8");
340
340
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
341
- var syncProc = spawn5(process.argv[0], ["-e", execString]);
341
+ var syncProc = spawn7(process.argv[0], ["-e", execString]);
342
342
  var statusText;
343
343
  while (fs.existsSync(syncFile)) {
344
344
  }
@@ -2714,10 +2714,14 @@ import { z as z3 } from "zod";
2714
2714
  var PRODUCT = {
2715
2715
  name: "Dashup",
2716
2716
  shorthand: "-up",
2717
+ app: "com.dashup.mobile",
2718
+ site: "https://dashup.com",
2717
2719
  api: "https://api.dashup.com",
2720
+ support: "support@dashup.com",
2718
2721
  currency: "USD",
2719
2722
  trialDays: 14,
2720
- monthlyPrice: 10
2723
+ monthlyPrice: 10,
2724
+ storeProduct: "dashup.monthly"
2721
2725
  };
2722
2726
  var PAIRING_TTL_MS = 5 * 6e4;
2723
2727
  var PROTOCOL_VERSION = 1;
@@ -3767,7 +3771,7 @@ function bridgeAcp(socket, onUpdate) {
3767
3771
  if (session && direction === "agent" && message.method === methods.client.session.requestPermission) {
3768
3772
  const tool = message.params.toolCall ?? {};
3769
3773
  const diff = (tool.content ?? []).find((part) => part.type === "diff");
3770
- const approval2 = {
3774
+ const approval3 = {
3771
3775
  id: randomUUID(),
3772
3776
  title: String(message.params.title ?? tool.title ?? "Agent needs approval"),
3773
3777
  detail: String(message.params.description ?? ""),
@@ -3785,10 +3789,10 @@ ${String(diff.newText).split("\n").map((line) => "+" + line).join("\n")}`
3785
3789
  }
3786
3790
  } : {}
3787
3791
  };
3788
- session.approvals.set(String(message.id), approval2);
3792
+ session.approvals.set(String(message.id), approval3);
3789
3793
  permissions.set(String(message.id), session);
3790
3794
  session.status = "attention";
3791
- publish(session, approval2);
3795
+ publish(session, approval3);
3792
3796
  }
3793
3797
  if (direction === "editor" && !message.method) {
3794
3798
  const pending = permissions.get(String(message.id));
@@ -3836,16 +3840,16 @@ function attachAcp(socket, onEvent) {
3836
3840
  async function proxyAcp(config2, executable, args2) {
3837
3841
  if (!config2.localSecret) throw new Error("Start dashup watch before using the ACP proxy");
3838
3842
  const socket = await new Promise((resolve3, reject) => {
3839
- const connect3 = request({
3843
+ const connect4 = request({
3840
3844
  hostname: "127.0.0.1",
3841
3845
  port: connectorPort(),
3842
3846
  method: "CONNECT",
3843
3847
  path: "/acp",
3844
3848
  headers: { Authorization: `Bearer ${config2.localSecret}` }
3845
3849
  });
3846
- connect3.on("connect", (response, stream) => response.statusCode === 200 ? resolve3(stream) : reject(new Error("Dashup ACP connection was rejected")));
3847
- connect3.on("error", reject);
3848
- connect3.end();
3850
+ connect4.on("connect", (response, stream) => response.statusCode === 200 ? resolve3(stream) : reject(new Error("Dashup ACP connection was rejected")));
3851
+ connect4.on("error", reject);
3852
+ connect4.end();
3849
3853
  });
3850
3854
  const child = spawn(executable, args2, { stdio: ["pipe", "pipe", "inherit"], windowsHide: true });
3851
3855
  const local = /* @__PURE__ */ new Set();
@@ -4013,7 +4017,7 @@ import * as ui from "@clack/prompts";
4013
4017
  import { z } from "zod";
4014
4018
 
4015
4019
  // package.json
4016
- var version = "0.1.4";
4020
+ var version = "0.1.6";
4017
4021
 
4018
4022
  // src/pair.ts
4019
4023
  var Invitation = z.object({
@@ -4409,7 +4413,7 @@ var Viewer = class _Viewer {
4409
4413
  // src/discovery.ts
4410
4414
  import { homedir as homedir3 } from "node:os";
4411
4415
  import { createHmac } from "node:crypto";
4412
- import { basename, join as join3 } from "node:path";
4416
+ import { basename, dirname as dirname3, join as join3 } from "node:path";
4413
4417
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
4414
4418
  import { zstdDecompressSync } from "node:zlib";
4415
4419
  import { watch, existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
@@ -4569,6 +4573,7 @@ function title(value2, fallback) {
4569
4573
  const text3 = typeof value2 === "string" ? value2.trim().split("\n")[0].trim() : "";
4570
4574
  return text3 ? text3.slice(0, TITLE_LIMIT) : fallback;
4571
4575
  }
4576
+ var injected = (content) => /^\s*(# AGENTS\.md instructions for |<[a-z_][\w -]*>)/i.test(text(content));
4572
4577
  function text(value2) {
4573
4578
  if (typeof value2 === "string") return value2;
4574
4579
  if (Array.isArray(value2)) return value2.map(text).filter(Boolean).join("\n");
@@ -4637,18 +4642,19 @@ function step(id, name, input, result) {
4637
4642
  }
4638
4643
  };
4639
4644
  }
4645
+ var claudeHome = () => process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
4640
4646
  var claudeMessage = (row) => (row.type === "user" || row.type === "assistant") && !row.isSidechain;
4641
- function claudeChange(change) {
4642
- if (typeof change?.filePath !== "string") return void 0;
4643
- const hunks = list(change.structuredPatch);
4647
+ function claudeChange(change2) {
4648
+ if (typeof change2?.filePath !== "string") return void 0;
4649
+ const hunks = list(change2.structuredPatch);
4644
4650
  if (!hunks.length)
4645
- return change.type === "create" && typeof change.content === "string" ? added(change.filePath, change.content) : void 0;
4651
+ return change2.type === "create" && typeof change2.content === "string" ? added(change2.filePath, change2.content) : void 0;
4646
4652
  return [
4647
- `--- ${change.filePath}`,
4648
- `+++ ${change.filePath}`,
4649
- ...hunks.flatMap((hunk) => [
4650
- `@@ -${hunk?.oldStart},${hunk?.oldLines} +${hunk?.newStart},${hunk?.newLines} @@`,
4651
- ...list(hunk?.lines)
4653
+ `--- ${change2.filePath}`,
4654
+ `+++ ${change2.filePath}`,
4655
+ ...hunks.flatMap((hunk2) => [
4656
+ `@@ -${hunk2?.oldStart},${hunk2?.oldLines} +${hunk2?.newStart},${hunk2?.newLines} @@`,
4657
+ ...list(hunk2?.lines)
4652
4658
  ])
4653
4659
  ].join("\n");
4654
4660
  }
@@ -4714,7 +4720,9 @@ function claude(root, scan) {
4714
4720
  continue;
4715
4721
  }
4716
4722
  const named = latest.find((row) => row.type === "custom-title" || row.type === "ai-title");
4717
- const first = rows.find((row) => row.type === "user" && !row.isMeta && !row.isSidechain);
4723
+ const first = rows.find(
4724
+ (row) => row.type === "user" && !row.isMeta && !row.isSidechain && !injected(row.message?.content)
4725
+ );
4718
4726
  scan.sessions.push({
4719
4727
  harness: "claude",
4720
4728
  id: owner.sessionId,
@@ -4762,7 +4770,7 @@ function codexMessages(path) {
4762
4770
  if (!item) return [];
4763
4771
  const id = String(item.id ?? row.ordinal ?? `${index}`);
4764
4772
  if (item.type === "message" && ["user", "assistant"].includes(item.role))
4765
- return prose(id, item.role, item.content);
4773
+ return item.role === "user" && injected(item.content) ? [] : prose(id, item.role, item.content);
4766
4774
  if (!["function_call", "custom_tool_call"].includes(item.type)) return [];
4767
4775
  const input = item.type === "function_call" ? parse(String(item.arguments)) ?? item.arguments : item.input;
4768
4776
  const output = outputs.get(item.call_id);
@@ -4803,6 +4811,7 @@ function codex(root, scan) {
4803
4811
  scan.warnings.push(`Cannot read Codex session index: ${index}`);
4804
4812
  }
4805
4813
  }
4814
+ const titles = codexTitles(root);
4806
4815
  for (const path of [
4807
4816
  ...files(join2(root, "sessions"), scan),
4808
4817
  ...files(join2(root, "archived_sessions"), scan)
@@ -4818,13 +4827,16 @@ function codex(root, scan) {
4818
4827
  }
4819
4828
  const named = names.get(owner.id);
4820
4829
  const first = rows.find(
4821
- (row) => row.type === "response_item" && row.payload?.type === "message" && row.payload.role === "user"
4830
+ (row) => row.type === "response_item" && row.payload?.type === "message" && row.payload.role === "user" && !injected(row.payload.content)
4822
4831
  );
4823
4832
  scan.sessions.push({
4824
4833
  harness: "codex",
4825
4834
  id: owner.id,
4826
4835
  cwd,
4827
- title: title(named?.thread_name ?? text(first?.payload?.content), "Codex thread"),
4836
+ title: title(
4837
+ named?.thread_name ?? titles.get(owner.id) ?? text(first?.payload?.content),
4838
+ "Codex thread"
4839
+ ),
4828
4840
  parent: typeof owner.parent_thread_id === "string" ? owner.parent_thread_id : void 0,
4829
4841
  updatedAt: date(latest.find(codexItem)?.timestamp, statSync(path).mtimeMs),
4830
4842
  files: [path, ...existsSync3(index) ? [index] : []],
@@ -4893,6 +4905,30 @@ function databaseMessages(path, session) {
4893
4905
  db.close();
4894
4906
  }
4895
4907
  }
4908
+ function codexSubagent(row) {
4909
+ const spawn7 = object(object(parse(String(row.source ?? "")))?.subagent)?.thread_spawn;
4910
+ const task = typeof spawn7?.agent_path === "string" ? spawn7.agent_path.split("/").pop() : "";
4911
+ return [row.agent_nickname, task?.replace(/_/g, " ")].filter(Boolean).join(" \xB7 ");
4912
+ }
4913
+ function codexTitles(root) {
4914
+ const titles = /* @__PURE__ */ new Map();
4915
+ const state = (existsSync3(root) ? readdirSync(root) : []).filter((name) => /^state_\d+\.sqlite$/.test(name)).sort((a, b) => Number(b.match(/\d+/)[0]) - Number(a.match(/\d+/)[0]))[0];
4916
+ if (!state) return titles;
4917
+ let db;
4918
+ try {
4919
+ db = openDatabase(join2(root, state));
4920
+ for (const row of db.prepare("SELECT id, title, first_user_message, source, agent_nickname FROM threads").all()) {
4921
+ const name = [row.title, row.first_user_message].find(
4922
+ (value2) => typeof value2 === "string" && !!value2.trim() && !injected(value2)
4923
+ );
4924
+ if (typeof row.id === "string") titles.set(row.id, name ?? codexSubagent(row));
4925
+ }
4926
+ } catch {
4927
+ } finally {
4928
+ db?.close();
4929
+ }
4930
+ return titles;
4931
+ }
4896
4932
  function openCodeDatabase(path, scan) {
4897
4933
  if (!existsSync3(path)) {
4898
4934
  watchParent(path, scan);
@@ -4991,7 +5027,7 @@ function openCodeLegacy(root, scan) {
4991
5027
  function scanHistories() {
4992
5028
  const home = homedir2();
4993
5029
  const scan = { sessions: [], paths: /* @__PURE__ */ new Set(), warnings: [] };
4994
- claude(process.env.CLAUDE_CONFIG_DIR ?? join2(home, ".claude"), scan);
5030
+ claude(claudeHome(), scan);
4995
5031
  codex(process.env.CODEX_HOME ?? join2(home, ".codex"), scan);
4996
5032
  const opencode = join2(process.env.XDG_DATA_HOME ?? join2(home, ".local/share"), "opencode");
4997
5033
  openCodeDatabase(process.env.OPENCODE_DB ?? join2(opencode, "opencode.db"), scan);
@@ -5133,7 +5169,7 @@ var Discovery = class {
5133
5169
  const project = this.project(session.cwd);
5134
5170
  const thread = {
5135
5171
  id: agentThread(session.harness, session.id, this.secret),
5136
- title: session.title,
5172
+ title: session.title ?? "",
5137
5173
  editor: "cli",
5138
5174
  status: "unknown",
5139
5175
  updatedAt: session.updatedAt,
@@ -5152,13 +5188,24 @@ var Discovery = class {
5152
5188
  thread.updatedAt = session.updatedAt;
5153
5189
  if (session.parent) thread.parent = agentThread(session.harness, session.parent, this.secret);
5154
5190
  }
5191
+ // Where new projects go: the folders holding this computer's projects, most used first.
5192
+ folders() {
5193
+ const counts = /* @__PURE__ */ new Map();
5194
+ for (const project of this.projects.values())
5195
+ if (!project.remote) {
5196
+ const parent = dirname3(project.path);
5197
+ counts.set(parent, (counts.get(parent) ?? 0) + 1);
5198
+ }
5199
+ const folders = [...counts].sort((a, b) => b[1] - a[1]).map(([folder]) => folder).filter((folder) => existsSync4(folder)).slice(0, 3);
5200
+ return folders.length ? folders : [homedir3()];
5201
+ }
5155
5202
  observe(session) {
5156
5203
  const found = this.native(session);
5157
5204
  const location2 = this.locations.get(found.thread.id);
5158
5205
  location2.live = session;
5159
5206
  Object.assign(found.thread, {
5160
5207
  editor: session.editor ?? found.thread.editor,
5161
- title: session.title,
5208
+ title: session.title ?? found.thread.title,
5162
5209
  status: session.status,
5163
5210
  updatedAt: location2.history?.updatedAt ?? session.updatedAt,
5164
5211
  capabilities: {
@@ -5377,6 +5424,8 @@ var Discovery = class {
5377
5424
 
5378
5425
  // src/connector.ts
5379
5426
  import { z as z2 } from "zod";
5427
+ import { join as join9 } from "node:path";
5428
+ import { mkdir } from "node:fs/promises";
5380
5429
  import { randomBytes as randomBytes4 } from "node:crypto";
5381
5430
 
5382
5431
  // ../../node_modules/.pnpm/engine.io-client@6.6.7/node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
@@ -16395,11 +16444,17 @@ async function publishHistory(config2, pair, project, thread, messages5) {
16395
16444
  return summary;
16396
16445
  }
16397
16446
 
16447
+ // src/harness/live.ts
16448
+ import { promisify as promisify6 } from "node:util";
16449
+ import { execFile as execFile6 } from "node:child_process";
16450
+
16398
16451
  // src/harness/codex.ts
16399
16452
  import WebSocket2 from "ws";
16400
16453
  import { homedir as homedir4 } from "node:os";
16401
16454
  import { connect } from "node:net";
16455
+ import { promisify as promisify3 } from "node:util";
16402
16456
  import { randomUUID as randomUUID2 } from "node:crypto";
16457
+ import { execFile as execFile3 } from "node:child_process";
16403
16458
  import { join as join4, isAbsolute as isAbsolute2 } from "node:path";
16404
16459
  import { stat, readdir, readFile } from "node:fs/promises";
16405
16460
  var REQUEST_MS = 4e3;
@@ -16431,13 +16486,12 @@ function approval(id, method, params) {
16431
16486
  choices: DECISIONS.filter((choice) => !unavailable || choice.id === "decline").map((choice) => ({ ...choice }))
16432
16487
  };
16433
16488
  }
16489
+ var codexHome = () => process.env.DASHUP_CODEX_HOME ?? process.env.CODEX_HOME ?? join4(homedir4(), ".codex");
16434
16490
  async function sockets() {
16435
16491
  const explicit = process.env.DASHUP_CODEX_SOCKET;
16436
16492
  if (explicit !== void 0) return explicit && isAbsolute2(explicit) ? [explicit] : [];
16437
16493
  if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
16438
- const homes = /* @__PURE__ */ new Set([
16439
- process.env.DASHUP_CODEX_HOME ?? process.env.CODEX_HOME ?? join4(homedir4(), ".codex")
16440
- ]);
16494
+ const homes = /* @__PURE__ */ new Set([codexHome()]);
16441
16495
  if (process.platform === "linux") {
16442
16496
  const entries2 = await readdir("/proc").catch(() => []);
16443
16497
  for (const pid of entries2.filter((value2) => /^\d+$/.test(value2)).slice(0, MAX_PROCESSES)) {
@@ -16623,6 +16677,16 @@ var Client = class {
16623
16677
  this.requests.set(id, result);
16624
16678
  return result;
16625
16679
  }
16680
+ // Starts a thread in `cwd` with Codex's own settings, or loads a saved one, then a turn.
16681
+ async start(cwd, text3, saved) {
16682
+ await this.connect();
16683
+ if (!this.controls) throw new Error("Update Codex to start threads from Dashup");
16684
+ const id = saved ? (await this.call("thread/resume", { threadId: saved, excludeTurns: true })).thread?.id : (await this.call("thread/start", { cwd })).thread?.id;
16685
+ if (typeof id !== "string") throw new Error("Codex did not open the thread");
16686
+ await this.call("turn/start", { threadId: id, input: [{ type: "text", text: text3, text_elements: [] }] });
16687
+ this.changed();
16688
+ return id;
16689
+ }
16626
16690
  async read() {
16627
16691
  await this.connect();
16628
16692
  const loaded = await this.call("thread/loaded/list", { limit: MAX_SESSIONS });
@@ -16670,12 +16734,12 @@ var Client = class {
16670
16734
  this.changed();
16671
16735
  }) : void 0,
16672
16736
  approve: approvals.length ? (requestId, approvalId, choice) => this.once(requestId, async () => {
16673
- const approval2 = this.approvals.get(approvalId);
16737
+ const approval3 = this.approvals.get(approvalId);
16674
16738
  const socket = this.socket;
16675
- if (!approval2 || approval2.threadId !== id || !approval2.approval.choices.some((value2) => value2.id === choice) || socket?.readyState !== WebSocket2.OPEN)
16739
+ if (!approval3 || approval3.threadId !== id || !approval3.approval.choices.some((value2) => value2.id === choice) || socket?.readyState !== WebSocket2.OPEN)
16676
16740
  throw new Error("Codex approval is no longer pending");
16677
16741
  await new Promise((resolve3, reject) => socket.send(
16678
- JSON.stringify({ id: approval2.requestId, result: { decision: choice } }),
16742
+ JSON.stringify({ id: approval3.requestId, result: { decision: choice } }),
16679
16743
  (error) => error ? reject(new Error("Codex approval could not be sent")) : resolve3()
16680
16744
  ));
16681
16745
  this.approvals.delete(approvalId);
@@ -16705,8 +16769,28 @@ var Codex = class {
16705
16769
  }
16706
16770
  changed;
16707
16771
  clients = /* @__PURE__ */ new Map();
16772
+ // Control sockets of daemons this connector started, which discovery may not look for.
16773
+ started = /* @__PURE__ */ new Set();
16774
+ // Codex's own daemon runs the threads Dashup starts or resumes, so they outlive this connector.
16775
+ async start(cwd, text3, saved) {
16776
+ const path = process.env.DASHUP_CODEX_SOCKET || join4(codexHome(), ...SOCKET_PARTS);
16777
+ await promisify3(execFile3)("codex", ["app-server", "daemon", "start"], {
16778
+ timeout: 6e4,
16779
+ shell: process.platform === "win32"
16780
+ });
16781
+ this.started.add(path);
16782
+ let client = this.clients.get(path);
16783
+ if (!client) {
16784
+ client = new Client(path, this.changed);
16785
+ this.clients.set(path, client);
16786
+ }
16787
+ return client.start(cwd, text3, saved);
16788
+ }
16789
+ async resume(id, cwd, text3) {
16790
+ await this.start(cwd, text3, id);
16791
+ }
16708
16792
  async refresh() {
16709
- const paths = new Set(await sockets());
16793
+ const paths = /* @__PURE__ */ new Set([...await sockets(), ...this.started]);
16710
16794
  for (const [path, client] of this.clients)
16711
16795
  if (!paths.has(path)) {
16712
16796
  client.stop();
@@ -16730,26 +16814,426 @@ var Codex = class {
16730
16814
  }
16731
16815
  };
16732
16816
 
16817
+ // src/harness/headless.ts
16818
+ import { randomUUID as randomUUID3 } from "node:crypto";
16819
+ import { readFileSync as readFileSync3 } from "node:fs";
16820
+ import { createInterface as createInterface3 } from "node:readline";
16821
+ import { spawn as spawn3 } from "node:child_process";
16822
+ var CHOICES = [
16823
+ { id: "accept", label: "Allow once" },
16824
+ { id: "decline", label: "Reject" }
16825
+ ];
16826
+ var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
16827
+ var MAX_REQUESTS = 1e3;
16828
+ var NOTIFY_MS = 100;
16829
+ function hunk(before, after, offset = 0) {
16830
+ const old = before ? before.replace(/\n$/, "").split("\n") : [];
16831
+ const next = after ? after.replace(/\n$/, "").split("\n") : [];
16832
+ let start = 0;
16833
+ while (start < old.length && start < next.length && old[start] === next[start]) start++;
16834
+ let end = 0;
16835
+ while (end < old.length - start && end < next.length - start && old[old.length - 1 - end] === next[next.length - 1 - end])
16836
+ end++;
16837
+ const removed = old.slice(start, old.length - end);
16838
+ const added2 = next.slice(start, next.length - end);
16839
+ return {
16840
+ lines: [
16841
+ `@@ -${offset + start + 1},${removed.length} +${offset + start + 1},${added2.length} @@`,
16842
+ ...old.slice(Math.max(0, start - 3), start).map((line) => ` ${line}`),
16843
+ ...removed.map((line) => `-${line}`),
16844
+ ...added2.map((line) => `+${line}`),
16845
+ ...old.slice(old.length - end, old.length - end + 3).map((line) => ` ${line}`)
16846
+ ],
16847
+ added: added2.length,
16848
+ deleted: removed.length
16849
+ };
16850
+ }
16851
+ function change(tool, input) {
16852
+ const path = input.file_path;
16853
+ if (typeof path !== "string") return void 0;
16854
+ let current = "";
16855
+ try {
16856
+ current = readFileSync3(path, "utf8");
16857
+ } catch {
16858
+ }
16859
+ const edit = (value2) => {
16860
+ const before = String(value2?.old_string ?? "");
16861
+ const at = before ? current.indexOf(before) : -1;
16862
+ return hunk(
16863
+ before,
16864
+ String(value2?.new_string ?? ""),
16865
+ at < 0 ? 0 : current.slice(0, at).split("\n").length - 1
16866
+ );
16867
+ };
16868
+ 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) : [];
16869
+ if (!hunks.length) return void 0;
16870
+ const diff = [`--- ${path}`, `+++ ${path}`, ...hunks.flatMap((item) => item.lines)].join("\n");
16871
+ if (Buffer.byteLength(diff) > MAX_PREVIEW) return void 0;
16872
+ return {
16873
+ path,
16874
+ diff,
16875
+ added: hunks.reduce((sum, item) => sum + item.added, 0),
16876
+ deleted: hunks.reduce((sum, item) => sum + item.deleted, 0)
16877
+ };
16878
+ }
16879
+ function approval2(id, request2) {
16880
+ const input = request2.input ?? {};
16881
+ const tool = String(request2.display_name ?? request2.tool_name ?? "a tool");
16882
+ const diff = change(request2.tool_name, input);
16883
+ let detail = [
16884
+ typeof request2.description === "string" ? request2.description : "",
16885
+ typeof input.command === "string" ? input.command : diff ? "" : JSON.stringify(input, null, 2)
16886
+ ].filter(Boolean).join("\n\n");
16887
+ if (Buffer.byteLength(detail) > MAX_PREVIEW)
16888
+ detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
16889
+ return {
16890
+ id,
16891
+ title: request2.tool_name === "Bash" ? "Allow command?" : EDITS.has(request2.tool_name) ? "Allow file changes?" : `Allow ${tool}?`,
16892
+ detail,
16893
+ ...diff ? { change: diff } : {},
16894
+ choices: CHOICES.map((choice) => ({ ...choice }))
16895
+ };
16896
+ }
16897
+ var Session = class {
16898
+ // A resumed session is `saved` and keeps the title its history gives.
16899
+ constructor(id, cwd, title2, changed, saved = false) {
16900
+ this.id = id;
16901
+ this.cwd = cwd;
16902
+ this.title = title2;
16903
+ this.changed = changed;
16904
+ this.saved = saved;
16905
+ }
16906
+ id;
16907
+ cwd;
16908
+ title;
16909
+ changed;
16910
+ saved;
16911
+ messages = [];
16912
+ status = "idle";
16913
+ updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16914
+ turn;
16915
+ // Messages written to this turn's process that Claude Code has not finished answering.
16916
+ open = 0;
16917
+ approvals = /* @__PURE__ */ new Map();
16918
+ requests = /* @__PURE__ */ new Map();
16919
+ write(value2) {
16920
+ this.turn?.stdin.write(`${JSON.stringify(value2)}
16921
+ `);
16922
+ }
16923
+ once(id, action) {
16924
+ const known = this.requests.get(id);
16925
+ if (known) return known;
16926
+ if (this.requests.size >= MAX_REQUESTS)
16927
+ throw new Error("Claude Code command history is full; restart Dashup before sending");
16928
+ const result = action();
16929
+ this.requests.set(id, result);
16930
+ return result;
16931
+ }
16932
+ // Resolves once Claude Code takes the message, or says why it could not start.
16933
+ say(text3) {
16934
+ this.messages.push({ id: randomUUID3(), role: "user", text: text3 });
16935
+ this.status = "working";
16936
+ this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16937
+ const message = { type: "user", message: { role: "user", content: text3 } };
16938
+ if (this.turn) {
16939
+ this.open++;
16940
+ this.write(message);
16941
+ this.changed();
16942
+ return Promise.resolve();
16943
+ }
16944
+ const turn = this.turn = spawn3(
16945
+ "claude",
16946
+ [
16947
+ "-p",
16948
+ "--input-format",
16949
+ "stream-json",
16950
+ "--output-format",
16951
+ "stream-json",
16952
+ "--verbose",
16953
+ "--permission-prompt-tool",
16954
+ "stdio",
16955
+ ...this.saved ? ["--resume", this.id] : ["--session-id", this.id]
16956
+ ],
16957
+ { cwd: this.cwd, stdio: "pipe", shell: process.platform === "win32" }
16958
+ );
16959
+ this.open = 1;
16960
+ let errors = "";
16961
+ turn.stdin.on("error", () => {
16962
+ });
16963
+ turn.stderr.on("data", (chunk) => {
16964
+ errors = (errors + String(chunk)).slice(-2e3);
16965
+ });
16966
+ const started = new Promise((resolve3, reject) => {
16967
+ createInterface3({ input: turn.stdout }).on("line", (line) => {
16968
+ if (this.receive(line)) resolve3();
16969
+ });
16970
+ const end = (reason) => {
16971
+ reject(new Error(reason));
16972
+ if (this.turn !== turn) return;
16973
+ this.turn = void 0;
16974
+ this.open = 0;
16975
+ this.approvals.clear();
16976
+ this.status = "idle";
16977
+ this.changed();
16978
+ };
16979
+ turn.once("error", () => end("Claude Code is not installed on this computer"));
16980
+ turn.once("exit", () => end(errors.trim() || "Claude Code stopped before starting"));
16981
+ });
16982
+ this.write(message);
16983
+ this.changed();
16984
+ return started;
16985
+ }
16986
+ // Handles one line of Claude Code's output, and says whether it started the session.
16987
+ receive(line) {
16988
+ let message;
16989
+ try {
16990
+ message = JSON.parse(line);
16991
+ } catch {
16992
+ return false;
16993
+ }
16994
+ const init = message.type === "system" && message.subtype === "init";
16995
+ if (init) this.saved = true;
16996
+ else if (message.type === "assistant") {
16997
+ 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");
16998
+ if (!text3) return false;
16999
+ this.messages.push({ id: String(message.message?.id ?? randomUUID3()), role: "assistant", text: text3 });
17000
+ } else if (message.type === "control_request" && message.request?.subtype === "can_use_tool") {
17001
+ const id = randomUUID3();
17002
+ this.approvals.set(id, {
17003
+ request: String(message.request_id),
17004
+ input: message.request.input ?? {},
17005
+ approval: approval2(id, message.request)
17006
+ });
17007
+ this.status = "attention";
17008
+ } else if (message.type === "result") {
17009
+ if (--this.open <= 0) this.turn?.stdin.end();
17010
+ } else return false;
17011
+ this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
17012
+ this.changed();
17013
+ return init;
17014
+ }
17015
+ live() {
17016
+ return {
17017
+ id: this.id,
17018
+ cwd: this.cwd,
17019
+ title: this.title,
17020
+ harness: "claude",
17021
+ updatedAt: this.updatedAt,
17022
+ status: this.status,
17023
+ messages: [...this.messages],
17024
+ approvals: [...this.approvals.values()].map((pending) => pending.approval),
17025
+ send: (requestId, text3) => this.once(requestId, () => this.say(text3)),
17026
+ stop: this.turn ? (requestId) => this.once(requestId, async () => {
17027
+ if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
17028
+ this.write({
17029
+ type: "control_request",
17030
+ request_id: randomUUID3(),
17031
+ request: { subtype: "interrupt" }
17032
+ });
17033
+ }) : void 0,
17034
+ approve: this.approvals.size ? (requestId, approvalId, choice) => this.once(requestId, async () => {
17035
+ const pending = this.approvals.get(approvalId);
17036
+ if (!pending || !this.turn || !CHOICES.some((value2) => value2.id === choice))
17037
+ throw new Error("Claude Code approval is no longer pending");
17038
+ this.write({
17039
+ type: "control_response",
17040
+ response: {
17041
+ subtype: "success",
17042
+ request_id: pending.request,
17043
+ response: choice === "accept" ? { behavior: "allow", updatedInput: pending.input } : { behavior: "deny", message: "The person rejected this from their phone." }
17044
+ }
17045
+ });
17046
+ this.approvals.delete(approvalId);
17047
+ if (!this.approvals.size) this.status = "working";
17048
+ this.changed();
17049
+ }) : void 0
17050
+ };
17051
+ }
17052
+ end() {
17053
+ this.turn?.kill();
17054
+ }
17055
+ };
17056
+ var Headless = class {
17057
+ constructor(changed) {
17058
+ this.changed = changed;
17059
+ }
17060
+ changed;
17061
+ sessions = /* @__PURE__ */ new Map();
17062
+ notify;
17063
+ soon = () => {
17064
+ if (this.notify) return;
17065
+ this.notify = setTimeout(() => {
17066
+ this.notify = void 0;
17067
+ this.changed();
17068
+ }, NOTIFY_MS);
17069
+ this.notify.unref();
17070
+ };
17071
+ // Starts a session in `cwd` with its first message, and resolves with the session's id.
17072
+ async start(cwd, text3) {
17073
+ const title2 = text3.trim().split("\n")[0].slice(0, 80);
17074
+ const session = new Session(randomUUID3(), cwd, title2, this.soon);
17075
+ this.sessions.set(session.id, session);
17076
+ try {
17077
+ await session.say(text3);
17078
+ } catch (error) {
17079
+ this.sessions.delete(session.id);
17080
+ throw error;
17081
+ }
17082
+ return session.id;
17083
+ }
17084
+ // Continues a saved session with a message; Claude Code resumes it from its own transcript.
17085
+ async resume(id, cwd, text3) {
17086
+ const known = this.sessions.get(id);
17087
+ const session = known ?? new Session(id, cwd, void 0, this.soon, true);
17088
+ this.sessions.set(id, session);
17089
+ try {
17090
+ await session.say(text3);
17091
+ } catch (error) {
17092
+ if (!known) this.sessions.delete(id);
17093
+ throw error;
17094
+ }
17095
+ }
17096
+ async refresh() {
17097
+ return [...this.sessions.values()].map((session) => session.live());
17098
+ }
17099
+ stop() {
17100
+ clearTimeout(this.notify);
17101
+ for (const session of this.sessions.values()) session.end();
17102
+ }
17103
+ };
17104
+
17105
+ // src/harness/claude.ts
17106
+ import { join as join5 } from "node:path";
17107
+ import { connect as connect2 } from "node:net";
17108
+ import { watch as watch2 } from "node:fs";
17109
+ import { lstat, readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
17110
+ var PEER_PROTOCOL = 1;
17111
+ var MAX_SESSIONS2 = 100;
17112
+ var REQUEST_MS2 = 4e3;
17113
+ var MAX_REQUESTS2 = 1e3;
17114
+ var ENVELOPE = "cross-session-message";
17115
+ var STATUS = {
17116
+ busy: "working",
17117
+ idle: "idle",
17118
+ shell: "idle",
17119
+ waiting: "attention"
17120
+ };
17121
+ async function registrations() {
17122
+ if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17123
+ const directory2 = join5(claudeHome(), "sessions");
17124
+ const names = await readdir2(directory2).catch(() => []);
17125
+ const found = await Promise.all(
17126
+ names.filter((name) => /^\d+\.json$/.test(name)).slice(0, MAX_SESSIONS2).map(async (name) => {
17127
+ try {
17128
+ const value2 = JSON.parse(await readFile2(join5(directory2, name), "utf8"));
17129
+ if (typeof value2.sessionId !== "string" || typeof value2.cwd !== "string") return [];
17130
+ process.kill(value2.pid, 0);
17131
+ return [value2];
17132
+ } catch {
17133
+ return [];
17134
+ }
17135
+ })
17136
+ );
17137
+ return found.flat();
17138
+ }
17139
+ async function post(session, path, text3) {
17140
+ const file = await lstat(path);
17141
+ if (!file.isSocket() || process.getuid && file.uid !== process.getuid())
17142
+ throw new Error("Claude Code inbox is not owned by this user");
17143
+ const body2 = text3.replace(new RegExp(`<(?=/?${ENVELOPE})`, "gi"), "<\\");
17144
+ const line = JSON.stringify({
17145
+ type: "user",
17146
+ session_id: session.sessionId,
17147
+ priority: "next",
17148
+ message: { role: "user", content: `<${ENVELOPE} from-name="Dashup">
17149
+ ${body2}
17150
+ </${ENVELOPE}>` }
17151
+ });
17152
+ await new Promise((resolve3, reject) => {
17153
+ const socket = connect2(path);
17154
+ socket.setTimeout(REQUEST_MS2, () => socket.destroy(new Error("timed out")));
17155
+ socket.once("error", () => reject(new Error("Claude Code is not accepting messages")));
17156
+ socket.end(`${line}
17157
+ `, resolve3);
17158
+ });
17159
+ }
17160
+ var Claude = class {
17161
+ constructor(changed) {
17162
+ this.changed = changed;
17163
+ }
17164
+ changed;
17165
+ watcher;
17166
+ notify;
17167
+ requests = /* @__PURE__ */ new Map();
17168
+ async refresh() {
17169
+ this.watch();
17170
+ return (await registrations()).map((session) => {
17171
+ const path = session.peerProtocol === PEER_PROTOCOL ? session.messagingSocketPath : void 0;
17172
+ return {
17173
+ id: session.sessionId,
17174
+ harness: "claude",
17175
+ cwd: session.cwd,
17176
+ updatedAt: new Date(session.updatedAt ?? 0).toISOString(),
17177
+ status: STATUS[session.status ?? ""] ?? "unknown",
17178
+ messages: [],
17179
+ approvals: [],
17180
+ send: path ? (requestId, text3) => this.once(requestId, () => post(session, path, text3)) : void 0
17181
+ };
17182
+ });
17183
+ }
17184
+ once(id, action) {
17185
+ const known = this.requests.get(id);
17186
+ if (known) return known;
17187
+ if (this.requests.size >= MAX_REQUESTS2) this.requests.delete(this.requests.keys().next().value);
17188
+ const result = action();
17189
+ this.requests.set(id, result);
17190
+ return result;
17191
+ }
17192
+ watch() {
17193
+ if (this.watcher || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
17194
+ try {
17195
+ const watcher = this.watcher = watch2(join5(claudeHome(), "sessions"), () => {
17196
+ if (this.notify) return;
17197
+ this.notify = setTimeout(() => {
17198
+ this.notify = void 0;
17199
+ this.changed();
17200
+ }, 100);
17201
+ this.notify.unref();
17202
+ });
17203
+ watcher.on("error", () => {
17204
+ watcher.close();
17205
+ if (this.watcher === watcher) this.watcher = void 0;
17206
+ });
17207
+ } catch {
17208
+ }
17209
+ }
17210
+ stop() {
17211
+ clearTimeout(this.notify);
17212
+ this.watcher?.close();
17213
+ this.watcher = void 0;
17214
+ }
17215
+ };
17216
+
16733
17217
  // src/harness/vscode.ts
16734
17218
  import Socket3 from "ws";
16735
17219
  import { homedir as homedir5 } from "node:os";
16736
- import { connect as connect2 } from "node:net";
16737
- import { promisify as promisify3 } from "node:util";
17220
+ import { connect as connect3 } from "node:net";
17221
+ import { promisify as promisify4 } from "node:util";
16738
17222
  import { fileURLToPath as fileURLToPath2 } from "node:url";
16739
- import { execFile as execFile3 } from "node:child_process";
16740
- import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
16741
- import { stat as stat2, readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
16742
- import { join as join5, dirname as dirname3, basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
17223
+ import { execFile as execFile4 } from "node:child_process";
17224
+ import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
17225
+ import { stat as stat2, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
17226
+ import { join as join6, dirname as dirname4, basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
16743
17227
  import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws";
16744
17228
  import { AhpClient } from "@microsoft/agent-host-protocol/client";
16745
17229
  import { chatReducer, sessionReducer, SUPPORTED_PROTOCOL_VERSIONS } from "@microsoft/agent-host-protocol";
16746
- var REQUEST_MS2 = 4e3;
17230
+ var REQUEST_MS3 = 4e3;
16747
17231
  var MAX_BYTES2 = 8 * 1024 * 1024;
16748
17232
  var MAX_METADATA = 64 * 1024;
16749
17233
  var MAX_HOSTS = 16;
16750
- var MAX_SESSIONS2 = 100;
17234
+ var MAX_SESSIONS3 = 100;
16751
17235
  var ROOT_CHANNEL = "ahp-root://";
16752
- var run2 = promisify3(execFile3);
17236
+ var run2 = promisify4(execFile4);
16753
17237
  var CONFIRMATION_CHOICES = [
16754
17238
  { id: "approve", label: "Allow once", kind: "approve" },
16755
17239
  { id: "deny", label: "Reject", kind: "deny" }
@@ -16757,10 +17241,10 @@ var CONFIRMATION_CHOICES = [
16757
17241
  function roots() {
16758
17242
  const override = process.env.DASHUP_AHP_DATA ?? process.env.DASHUP_VSCODE_DATA;
16759
17243
  if (override !== void 0)
16760
- return override ? [basename2(override) === "User" ? dirname3(override) : override] : [];
17244
+ return override ? [basename2(override) === "User" ? dirname4(override) : override] : [];
16761
17245
  if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
16762
- const base = process.platform === "darwin" ? join5(homedir5(), "Library/Application Support") : process.platform === "win32" ? process.env.APPDATA ?? join5(homedir5(), "AppData/Roaming") : process.env.XDG_CONFIG_HOME ?? join5(homedir5(), ".config");
16763
- return ["Code", "Code - Insiders"].map((name) => join5(base, name));
17246
+ const base = process.platform === "darwin" ? join6(homedir5(), "Library/Application Support") : process.platform === "win32" ? process.env.APPDATA ?? join6(homedir5(), "AppData/Roaming") : process.env.XDG_CONFIG_HOME ?? join6(homedir5(), ".config");
17247
+ return ["Code", "Code - Insiders"].map((name) => join6(base, name));
16764
17248
  }
16765
17249
  async function privateFile(path, limit) {
16766
17250
  const file = await stat2(path).catch(() => void 0);
@@ -16777,7 +17261,7 @@ async function privateFile(path, limit) {
16777
17261
  "-EncodedCommand",
16778
17262
  Buffer.from(script, "utf16le").toString("base64")
16779
17263
  ],
16780
- { timeout: REQUEST_MS2, maxBuffer: MAX_METADATA }
17264
+ { timeout: REQUEST_MS3, maxBuffer: MAX_METADATA }
16781
17265
  );
16782
17266
  return stdout.trim().toLowerCase() === "true";
16783
17267
  } catch {
@@ -16789,16 +17273,16 @@ async function privateFile(path, limit) {
16789
17273
  async function entries() {
16790
17274
  const result = /* @__PURE__ */ new Map();
16791
17275
  for (const root of roots()) {
16792
- const registry2 = join5(root, "agent-host/local-endpoint");
16793
- const files2 = (await readdir2(join5(registry2, "entries")).catch(() => [])).filter((name) => name.endsWith(".json")).slice(0, MAX_HOSTS);
17276
+ const registry2 = join6(root, "agent-host/local-endpoint");
17277
+ const files2 = (await readdir3(join6(registry2, "entries")).catch(() => [])).filter((name) => name.endsWith(".json")).slice(0, MAX_HOSTS);
16794
17278
  const sources = [
16795
- ...files2.map((name) => join5(registry2, "entries", name)),
16796
- join5(registry2, "metadata.json")
17279
+ ...files2.map((name) => join6(registry2, "entries", name)),
17280
+ join6(registry2, "metadata.json")
16797
17281
  ];
16798
17282
  for (const path of sources) {
16799
17283
  try {
16800
17284
  if (!await privateFile(path, MAX_METADATA)) continue;
16801
- const parsed = JSON.parse(await readFile2(path, "utf8"));
17285
+ const parsed = JSON.parse(await readFile3(path, "utf8"));
16802
17286
  const rows = basename2(path) === "metadata.json" && Array.isArray(parsed) ? parsed : [parsed];
16803
17287
  for (const row of rows.slice(0, MAX_HOSTS)) {
16804
17288
  if (row?.schemaVersion !== 2 || !["editor", "standalone"].includes(row.type) || !Number.isSafeInteger(row.pid) || row.pid <= 0 || typeof row.instanceId !== "string" || !row.instanceId || typeof row.connectionToken !== "string" || !row.connectionToken || row.connectionToken.length > 1024 || row.endpoint?.type !== "socket" || typeof row.endpoint.path !== "string" || !isAbsolute3(row.endpoint.path))
@@ -16818,7 +17302,7 @@ async function entries() {
16818
17302
  if (!socket?.isSocket() || process.getuid && socket.uid !== process.getuid())
16819
17303
  continue;
16820
17304
  }
16821
- if (!await privateFile(dirname3(path), MAX_BYTES2)) continue;
17305
+ if (!await privateFile(dirname4(path), MAX_BYTES2)) continue;
16822
17306
  const key = `${root}\0${identity2}`;
16823
17307
  if (!result.has(key))
16824
17308
  result.set(key, {
@@ -16931,7 +17415,7 @@ var Host = class {
16931
17415
  client;
16932
17416
  connecting;
16933
17417
  stopped = false;
16934
- clientId = `dashup-${randomUUID3()}`;
17418
+ clientId = `dashup-${randomUUID4()}`;
16935
17419
  resources = /* @__PURE__ */ new Map();
16936
17420
  subscriptions = /* @__PURE__ */ new Set();
16937
17421
  receipts = /* @__PURE__ */ new Map();
@@ -16983,9 +17467,9 @@ var Host = class {
16983
17467
  if (this.connecting) return this.connecting;
16984
17468
  this.connecting = (async () => {
16985
17469
  const socket = new Socket3(`ws://localhost/?tkn=${encodeURIComponent(this.entry.token)}`, {
16986
- createConnection: () => connect2(this.entry.path),
17470
+ createConnection: () => connect3(this.entry.path),
16987
17471
  maxPayload: MAX_BYTES2,
16988
- handshakeTimeout: REQUEST_MS2
17472
+ handshakeTimeout: REQUEST_MS3
16989
17473
  });
16990
17474
  socket.on("error", () => {
16991
17475
  });
@@ -17018,7 +17502,7 @@ var Host = class {
17018
17502
  }
17019
17503
  };
17020
17504
  const client = this.client = new AhpClient(transport, {
17021
- requestTimeoutMs: REQUEST_MS2,
17505
+ requestTimeoutMs: REQUEST_MS3,
17022
17506
  subscriptionBuffer: 1024
17023
17507
  });
17024
17508
  this.resources.clear();
@@ -17082,7 +17566,7 @@ var Host = class {
17082
17566
  const timer = setTimeout(() => {
17083
17567
  this.receipts.delete(handle.clientSeq);
17084
17568
  reject(new Error("Native AHP action was not acknowledged; delivery is unknown"));
17085
- }, REQUEST_MS2);
17569
+ }, REQUEST_MS3);
17086
17570
  this.receipts.set(handle.clientSeq, { resolve: resolve3, reject, timer });
17087
17571
  });
17088
17572
  this.requests.set(requestId, pending);
@@ -17092,12 +17576,12 @@ var Host = class {
17092
17576
  await this.connect();
17093
17577
  const catalog = await this.client.request("listSessions", {
17094
17578
  channel: ROOT_CHANNEL,
17095
- limit: MAX_SESSIONS2
17579
+ limit: MAX_SESSIONS3
17096
17580
  });
17097
17581
  if (!Array.isArray(catalog.items)) return [];
17098
17582
  const result = [];
17099
17583
  const active = /* @__PURE__ */ new Set();
17100
- for (const summary of catalog.items.slice(0, MAX_SESSIONS2)) {
17584
+ for (const summary of catalog.items.slice(0, MAX_SESSIONS3)) {
17101
17585
  try {
17102
17586
  const session = await this.subscribe(summary.resource);
17103
17587
  if (!session || session.lifecycle !== "ready" || !Array.isArray(session.chats)) continue;
@@ -17159,7 +17643,7 @@ var Host = class {
17159
17643
  message: { text: value2, origin: { kind: "user" } }
17160
17644
  });
17161
17645
  } : void 0,
17162
- approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, choice) => {
17646
+ approve: approvals.some((approval3) => approval3.choices.length) ? (requestId, approvalId, choice) => {
17163
17647
  const prior = this.requests.get(requestId);
17164
17648
  if (prior) return prior;
17165
17649
  const current = this.resources.get(summary.resource)?.state;
@@ -17230,15 +17714,15 @@ var VSCode = class {
17230
17714
  };
17231
17715
 
17232
17716
  // src/harness/opencode.ts
17233
- import { join as join6 } from "node:path";
17234
- import { promisify as promisify4 } from "node:util";
17717
+ import { join as join7 } from "node:path";
17718
+ import { promisify as promisify5 } from "node:util";
17235
17719
  import { createHash as createHash4 } from "node:crypto";
17236
- import { execFile as execFile4 } from "node:child_process";
17237
- import { stat as stat3, readlink, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
17238
- var run3 = promisify4(execFile4);
17239
- var REQUEST_MS3 = 4e3;
17720
+ import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
17721
+ import { stat as stat3, readlink, readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
17722
+ var run3 = promisify5(execFile5);
17723
+ var REQUEST_MS4 = 4e3;
17240
17724
  var MAX_BYTES3 = 8 * 1024 * 1024;
17241
- var MAX_SESSIONS3 = 100;
17725
+ var MAX_SESSIONS4 = 100;
17242
17726
  var MAX_PROCESSES2 = 4096;
17243
17727
  var MAX_ENDPOINTS = 32;
17244
17728
  var EVENT_DELAY_MS = 100;
@@ -17261,10 +17745,10 @@ function authorization(environment) {
17261
17745
  }
17262
17746
  async function linuxEndpoints() {
17263
17747
  const result = [];
17264
- const processes = (await readdir3("/proc")).filter((id) => /^\d+$/.test(id)).slice(0, MAX_PROCESSES2);
17748
+ const processes = (await readdir4("/proc")).filter((id) => /^\d+$/.test(id)).slice(0, MAX_PROCESSES2);
17265
17749
  const listeners = /* @__PURE__ */ new Map();
17266
17750
  for (const name of ["tcp", "tcp6"]) {
17267
- const table = await readFile3(`/proc/net/${name}`, "utf8").catch(() => "");
17751
+ const table = await readFile4(`/proc/net/${name}`, "utf8").catch(() => "");
17268
17752
  for (const line of table.split("\n").slice(1)) {
17269
17753
  const columns = line.trim().split(/\s+/);
17270
17754
  if (columns[3] !== "0A") continue;
@@ -17280,16 +17764,16 @@ async function linuxEndpoints() {
17280
17764
  const directory2 = `/proc/${pid}`;
17281
17765
  const owner = await stat3(directory2);
17282
17766
  if (process.getuid && owner.uid !== process.getuid()) continue;
17283
- const command2 = (await readFile3(join6(directory2, "cmdline"), "utf8")).slice(0, 4096);
17767
+ const command2 = (await readFile4(join7(directory2, "cmdline"), "utf8")).slice(0, 4096);
17284
17768
  if (!/(?:^|[\/\0 ])opencode(?:[\0 .]|$)/.test(command2)) continue;
17285
17769
  const environment = Object.fromEntries(
17286
- (await readFile3(join6(directory2, "environ"), "utf8")).split("\0").map((entry) => {
17770
+ (await readFile4(join7(directory2, "environ"), "utf8")).split("\0").map((entry) => {
17287
17771
  const split = entry.indexOf("=");
17288
17772
  return [entry.slice(0, split), entry.slice(split + 1)];
17289
17773
  })
17290
17774
  );
17291
- for (const fd of (await readdir3(join6(directory2, "fd"))).slice(0, 1024)) {
17292
- const target = await readlink(join6(directory2, "fd", fd)).catch(() => "");
17775
+ for (const fd of (await readdir4(join7(directory2, "fd"))).slice(0, 1024)) {
17776
+ const target = await readlink(join7(directory2, "fd", fd)).catch(() => "");
17293
17777
  const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1];
17294
17778
  const url2 = inode ? listeners.get(inode) : void 0;
17295
17779
  if (url2) result.push({ url: url2, authorization: authorization(environment) });
@@ -17318,7 +17802,7 @@ async function endpoints() {
17318
17802
  const { stdout } = await run3(
17319
17803
  "lsof",
17320
17804
  ["-nP", "-a", "-c", "opencode", "-iTCP", "-sTCP:LISTEN", "-Fn"],
17321
- { timeout: REQUEST_MS3, maxBuffer: MAX_BYTES3 }
17805
+ { timeout: REQUEST_MS4, maxBuffer: MAX_BYTES3 }
17322
17806
  );
17323
17807
  return stdout.split("\n").flatMap((line) => {
17324
17808
  const port = /^n(?:127\.0\.0\.1|\[::1\]|\*):(\d+)$/.exec(line)?.[1];
@@ -17335,7 +17819,7 @@ async function endpoints() {
17335
17819
  const { stdout } = await run3(
17336
17820
  "powershell.exe",
17337
17821
  ["-NoProfile", "-NonInteractive", "-Command", script],
17338
- { timeout: REQUEST_MS3, maxBuffer: MAX_BYTES3 }
17822
+ { timeout: REQUEST_MS4, maxBuffer: MAX_BYTES3 }
17339
17823
  );
17340
17824
  return stdout.split(/\r?\n/).filter((port) => /^\d+$/.test(port)).slice(0, MAX_ENDPOINTS).map((port) => ({
17341
17825
  url: localEndpoint(`http://127.0.0.1:${port}`),
@@ -17389,7 +17873,7 @@ function permissionApproval(permission, history, cwd) {
17389
17873
  const command2 = typeof input?.command === "string" ? input.command : metadata2?.command;
17390
17874
  const path = typeof metadata2?.filepath === "string" ? metadata2.filepath : input?.filePath;
17391
17875
  const diff = typeof metadata2?.diff === "string" ? metadata2.diff : "";
17392
- const change = typeof path === "string" && path.trim() && diff.trim() ? { path, diff } : void 0;
17876
+ const change2 = typeof path === "string" && path.trim() && diff.trim() ? { path, diff } : void 0;
17393
17877
  const patterns = Array.isArray(permission.patterns) ? permission.patterns.filter((value2) => typeof value2 === "string") : [];
17394
17878
  const always = Array.isArray(permission.always) ? permission.always.filter((value2) => typeof value2 === "string") : [];
17395
17879
  let detail = [
@@ -17397,7 +17881,7 @@ function permissionApproval(permission, history, cwd) {
17397
17881
  ${command2}` : "",
17398
17882
  hasInput ? `Tool input:
17399
17883
  ${JSON.stringify(input, null, 2)}` : "",
17400
- diff && !change ? `Proposed diff:
17884
+ diff && !change2 ? `Proposed diff:
17401
17885
  ${diff}` : "",
17402
17886
  `Working directory: ${cwd}`,
17403
17887
  patterns.length ? `Permission scope:
@@ -17406,10 +17890,10 @@ ${patterns.join("\n")}` : "",
17406
17890
  ${always.join("\n")}` : ""
17407
17891
  ].filter(Boolean).join("\n\n");
17408
17892
  const oversized = Buffer.byteLength(detail) > MAX_PREVIEW || Buffer.byteLength(diff) > MAX_PREVIEW;
17409
- const unavailable = !(hasInput || change || typeof command2 === "string" && command2.trim()) || oversized;
17893
+ const unavailable = !(hasInput || change2 || typeof command2 === "string" && command2.trim()) || oversized;
17410
17894
  if (oversized) {
17411
17895
  detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
17412
- if (change) change.diff = Buffer.from(diff).subarray(0, MAX_PREVIEW).toString("utf8");
17896
+ if (change2) change2.diff = Buffer.from(diff).subarray(0, MAX_PREVIEW).toString("utf8");
17413
17897
  }
17414
17898
  if (unavailable) detail += `
17415
17899
 
@@ -17419,7 +17903,7 @@ ${SCREEN_PREVIEW}`;
17419
17903
  title: `Allow ${String(permission.permission ?? "tool")}?`,
17420
17904
  detail,
17421
17905
  choices: PERMISSION_CHOICES.filter((choice) => !unavailable || choice.id === "reject").map((choice) => ({ ...choice })),
17422
- ...change ? { change } : {}
17906
+ ...change2 ? { change: change2 } : {}
17423
17907
  };
17424
17908
  }
17425
17909
  function promptContext(history) {
@@ -17455,7 +17939,7 @@ var Server = class {
17455
17939
  };
17456
17940
  }
17457
17941
  async request(path, directory2, payload) {
17458
- const signal = AbortSignal.timeout(REQUEST_MS3);
17942
+ const signal = AbortSignal.timeout(REQUEST_MS4);
17459
17943
  const response = await fetch(this.endpoint.url + path, {
17460
17944
  method: payload === void 0 ? "GET" : "POST",
17461
17945
  headers: {
@@ -17482,15 +17966,31 @@ var Server = class {
17482
17966
  this.requests.set(id, pending);
17483
17967
  return pending;
17484
17968
  }
17969
+ // A message to one of this folder's sessions, in the agent and model it last used; a new
17970
+ // session takes OpenCode's defaults.
17971
+ async prompt(id, cwd, text3) {
17972
+ const existing = await this.request(`/session/${encodeURIComponent(id)}/message?limit=200`, cwd);
17973
+ await this.request(`/session/${encodeURIComponent(id)}/prompt_async`, cwd, {
17974
+ ...promptContext(existing),
17975
+ parts: [{ type: "text", text: text3 }]
17976
+ });
17977
+ this.changed();
17978
+ }
17979
+ async start(cwd, text3) {
17980
+ const id = (await this.request("/session", cwd, {}))?.id;
17981
+ if (typeof id !== "string") throw new Error("OpenCode did not start a session");
17982
+ await this.prompt(id, cwd, text3);
17983
+ return id;
17984
+ }
17485
17985
  async read() {
17486
17986
  const health = await this.request("/global/health");
17487
17987
  if (health?.healthy !== true || typeof health.version !== "string") return [];
17488
17988
  this.listen();
17489
- const sessions = await this.request(`/session?limit=${MAX_SESSIONS3}`);
17989
+ const sessions = await this.request(`/session?limit=${MAX_SESSIONS4}`);
17490
17990
  if (!Array.isArray(sessions)) return [];
17491
17991
  const result = [];
17492
17992
  const states = /* @__PURE__ */ new Map();
17493
- for (const session of sessions.slice(0, MAX_SESSIONS3)) {
17993
+ for (const session of sessions.slice(0, MAX_SESSIONS4)) {
17494
17994
  if (typeof session?.id !== "string" || typeof session.directory !== "string" || !session.directory)
17495
17995
  continue;
17496
17996
  try {
@@ -17566,7 +18066,7 @@ var Server = class {
17566
18066
  });
17567
18067
  this.changed();
17568
18068
  }),
17569
- approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, choice) => this.once(requestId, async () => {
18069
+ approve: approvals.some((approval3) => approval3.choices.length) ? (requestId, approvalId, choice) => this.once(requestId, async () => {
17570
18070
  if (this.stopped) throw new Error("OpenCode is disconnected");
17571
18071
  if (approvalId.startsWith("question:")) {
17572
18072
  const requestID = approvalId.slice("question:".length);
@@ -17615,7 +18115,7 @@ var Server = class {
17615
18115
  listen() {
17616
18116
  if (this.stream || this.retry || this.stopped) return;
17617
18117
  const controller = this.stream = new AbortController();
17618
- const connectTimer = setTimeout(() => controller.abort(), REQUEST_MS3);
18118
+ const connectTimer = setTimeout(() => controller.abort(), REQUEST_MS4);
17619
18119
  connectTimer.unref();
17620
18120
  void (async () => {
17621
18121
  const response = await fetch(this.endpoint.url + "/global/event", {
@@ -17684,15 +18184,64 @@ var Server = class {
17684
18184
  clearTimeout(this.notify);
17685
18185
  }
17686
18186
  };
18187
+ function serve(cwd) {
18188
+ const child = spawn4("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
18189
+ cwd,
18190
+ stdio: ["ignore", "pipe", "ignore"],
18191
+ shell: process.platform === "win32"
18192
+ });
18193
+ const endpoint = new Promise((resolve3, reject) => {
18194
+ let output = "";
18195
+ child.stdout.on("data", (chunk) => {
18196
+ output = (output + String(chunk)).slice(-4096);
18197
+ const port = /https?:\/\/(?:127\.0\.0\.1|localhost):(\d+)/.exec(output)?.[1];
18198
+ if (port)
18199
+ resolve3({
18200
+ url: localEndpoint(`http://127.0.0.1:${port}`),
18201
+ authorization: authorization(process.env)
18202
+ });
18203
+ });
18204
+ child.once("error", () => reject(new Error("OpenCode is not installed on this computer")));
18205
+ child.once("exit", () => reject(new Error("OpenCode stopped before it started serving")));
18206
+ });
18207
+ return { process: child, endpoint };
18208
+ }
17687
18209
  var OpenCode = class {
17688
18210
  constructor(changed) {
17689
18211
  this.changed = changed;
17690
18212
  }
17691
18213
  changed;
17692
18214
  servers = /* @__PURE__ */ new Map();
18215
+ // Servers this connector started, by folder.
18216
+ started = /* @__PURE__ */ new Map();
18217
+ // The server this connector runs for a folder, started when first needed.
18218
+ async served(cwd) {
18219
+ let served = this.started.get(cwd);
18220
+ if (!served) this.started.set(cwd, served = serve(cwd));
18221
+ const endpoint = await served.endpoint.catch((error) => {
18222
+ this.started.delete(cwd);
18223
+ throw error;
18224
+ });
18225
+ let server = this.servers.get(endpoint.url);
18226
+ if (!server) {
18227
+ server = new Server(endpoint, this.changed);
18228
+ this.servers.set(endpoint.url, server);
18229
+ }
18230
+ return server;
18231
+ }
18232
+ async start(cwd, text3) {
18233
+ return (await this.served(cwd)).start(cwd, text3);
18234
+ }
18235
+ async resume(id, cwd, text3) {
18236
+ await (await this.served(cwd)).prompt(id, cwd, text3);
18237
+ }
17693
18238
  async refresh() {
17694
18239
  const discovered = await endpoints().catch(() => []);
17695
18240
  const current = new Map(discovered.map((endpoint) => [endpoint.url, endpoint]));
18241
+ for (const served of this.started.values()) {
18242
+ const endpoint = await served.endpoint.catch(() => void 0);
18243
+ if (endpoint) current.set(endpoint.url, endpoint);
18244
+ }
17696
18245
  for (const [url2, server] of this.servers)
17697
18246
  if (!current.has(url2)) {
17698
18247
  server.stop();
@@ -17713,6 +18262,8 @@ var OpenCode = class {
17713
18262
  stop() {
17714
18263
  for (const server of this.servers.values()) server.stop();
17715
18264
  this.servers.clear();
18265
+ for (const served of this.started.values()) served.process.kill();
18266
+ this.started.clear();
17716
18267
  }
17717
18268
  };
17718
18269
 
@@ -17721,6 +18272,8 @@ var DISCOVERY_MS = 1e4;
17721
18272
  var LiveHarnesses = class {
17722
18273
  listeners = /* @__PURE__ */ new Set();
17723
18274
  codex = new Codex(() => this.changed());
18275
+ headless = new Headless(() => this.changed());
18276
+ claude = new Claude(() => this.changed());
17724
18277
  vscode = new VSCode(() => this.changed());
17725
18278
  opencode = new OpenCode(() => this.changed());
17726
18279
  timer;
@@ -17734,8 +18287,10 @@ var LiveHarnesses = class {
17734
18287
  if (this.refreshing) return this.refreshing;
17735
18288
  this.refreshing = (async () => {
17736
18289
  const results = await Promise.allSettled([
18290
+ this.headless.refresh(),
17737
18291
  this.opencode.refresh(),
17738
18292
  this.codex.refresh(),
18293
+ this.claude.refresh(),
17739
18294
  this.vscode.refresh()
17740
18295
  ]);
17741
18296
  const sessions = /* @__PURE__ */ new Map();
@@ -17751,6 +18306,35 @@ var LiveHarnesses = class {
17751
18306
  });
17752
18307
  return this.refreshing;
17753
18308
  }
18309
+ // The agents whose own command runs on this computer.
18310
+ async agents() {
18311
+ const harnesses = ["claude", "codex", "opencode"];
18312
+ const runs = await Promise.all(
18313
+ harnesses.map((harness) => promisify6(execFile6)(harness, ["--version"], {
18314
+ timeout: 1e4,
18315
+ shell: process.platform === "win32"
18316
+ }).then(
18317
+ () => true,
18318
+ () => false
18319
+ ))
18320
+ );
18321
+ return harnesses.filter((_, index) => runs[index]);
18322
+ }
18323
+ // Starts a session in `cwd` with its first message, and resolves with the session's native id.
18324
+ start(harness, cwd, text3) {
18325
+ return { claude: this.headless, codex: this.codex, opencode: this.opencode }[harness].start(
18326
+ cwd,
18327
+ text3
18328
+ );
18329
+ }
18330
+ // Continues a saved session with a message, and resolves once its agent takes it.
18331
+ resume(harness, id, cwd, text3) {
18332
+ return { claude: this.headless, codex: this.codex, opencode: this.opencode }[harness].resume(
18333
+ id,
18334
+ cwd,
18335
+ text3
18336
+ );
18337
+ }
17754
18338
  watch(onChange) {
17755
18339
  this.listeners.add(onChange);
17756
18340
  if (!this.timer && process.env.DASHUP_LIVE_DISCOVERY !== "0") {
@@ -17769,20 +18353,22 @@ var LiveHarnesses = class {
17769
18353
  this.stopped = true;
17770
18354
  clearInterval(this.timer);
17771
18355
  this.listeners.clear();
18356
+ this.headless.stop();
17772
18357
  this.opencode.stop();
17773
18358
  this.codex.stop();
18359
+ this.claude.stop();
17774
18360
  this.vscode.stop();
17775
18361
  }
17776
18362
  };
17777
18363
 
17778
18364
  // src/harness/queue.ts
17779
- import { randomUUID as randomUUID4 } from "node:crypto";
17780
- import { spawn as spawn3 } from "node:child_process";
18365
+ import { randomUUID as randomUUID5 } from "node:crypto";
18366
+ import { spawn as spawn5 } from "node:child_process";
17781
18367
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
17782
- import { join as join7, dirname as dirname4, basename as basename3 } from "node:path";
17783
- import { statSync as statSync2, existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync3, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
18368
+ import { join as join8, dirname as dirname5, basename as basename3 } from "node:path";
18369
+ import { statSync as statSync2, existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
17784
18370
  var CODEX_VERSION = "0.160.1";
17785
- var REQUEST_MS4 = 1e4;
18371
+ var REQUEST_MS5 = 1e4;
17786
18372
  var MAX_BYTES4 = 1024 * 1024;
17787
18373
  var MAX_PROCESSES3 = 4096;
17788
18374
  var MAX_FILES = 4096;
@@ -17798,11 +18384,11 @@ function sameFile(left, right) {
17798
18384
  return left.path === right.path && left.device === right.device && left.inode === right.inode;
17799
18385
  }
17800
18386
  function packageVersion(executable) {
17801
- let directory2 = dirname4(executable);
17802
- for (let depth = 0; depth < 8; depth++, directory2 = dirname4(directory2)) {
17803
- const path = join7(directory2, "package.json");
18387
+ let directory2 = dirname5(executable);
18388
+ for (let depth = 0; depth < 8; depth++, directory2 = dirname5(directory2)) {
18389
+ const path = join8(directory2, "package.json");
17804
18390
  if (!existsSync5(path)) continue;
17805
- const value2 = JSON.parse(readFileSync3(path, "utf8"));
18391
+ const value2 = JSON.parse(readFileSync4(path, "utf8"));
17806
18392
  if (value2.name?.startsWith("@openai/codex-")) return value2.version;
17807
18393
  }
17808
18394
  return void 0;
@@ -17810,40 +18396,40 @@ function packageVersion(executable) {
17810
18396
  function processOwners(pid) {
17811
18397
  const root = `/proc/${pid}`;
17812
18398
  if (process.getuid && statSync2(root).uid !== process.getuid()) return [];
17813
- const executable = realpathSync2.native(join7(root, "exe"));
18399
+ const executable = realpathSync2.native(join8(root, "exe"));
17814
18400
  if (basename3(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
17815
- const status2 = readFileSync3(join7(root, "stat"), "utf8");
18401
+ const status2 = readFileSync4(join8(root, "stat"), "utf8");
17816
18402
  const start = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
17817
18403
  if (!start || !/^\d+$/.test(start)) return [];
17818
- const fds = readdirSync3(join7(root, "fd")).slice(0, MAX_FILES);
18404
+ const fds = readdirSync3(join8(root, "fd")).slice(0, MAX_FILES);
17819
18405
  const files2 = fds.flatMap((fd) => {
17820
18406
  try {
17821
- return [{ fd, path: readlinkSync(join7(root, "fd", fd)) }];
18407
+ return [{ fd, path: readlinkSync(join8(root, "fd", fd)) }];
17822
18408
  } catch {
17823
18409
  return [];
17824
18410
  }
17825
18411
  });
17826
18412
  return files2.flatMap(({ fd, path }) => {
17827
- if (basename3(dirname4(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
18413
+ if (basename3(dirname5(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
17828
18414
  const thread = basename3(path, ".lock");
17829
18415
  if (!THREAD_ID.test(thread)) return [];
17830
18416
  try {
17831
- const lock = readFileSync3(join7(root, "fdinfo", fd), "utf8");
18417
+ const lock = readFileSync4(join8(root, "fdinfo", fd), "utf8");
17832
18418
  if (!/FLOCK\s+ADVISORY\s+WRITE/.test(lock)) return [];
17833
- const home = realpathSync2.native(dirname4(dirname4(path)));
18419
+ const home = realpathSync2.native(dirname5(dirname5(path)));
17834
18420
  const directory2 = statSync2(home);
17835
18421
  if (process.getuid && directory2.uid !== process.getuid()) return [];
17836
- const queue = identity(join7(home, "queue_1.sqlite"));
18422
+ const queue = identity(join8(home, "queue_1.sqlite"));
17837
18423
  if (!files2.some((file) => {
17838
18424
  try {
17839
- return sameFile(identity(join7(root, "fd", file.fd)), queue);
18425
+ return sameFile(identity(join8(root, "fd", file.fd)), queue);
17840
18426
  } catch {
17841
18427
  return false;
17842
18428
  }
17843
18429
  }))
17844
18430
  return [];
17845
18431
  const rollout = files2.find(
17846
- (file) => file.path.startsWith(join7(home, "sessions") + "/") && file.path.endsWith(`-${thread}.jsonl`)
18432
+ (file) => file.path.startsWith(join8(home, "sessions") + "/") && file.path.endsWith(`-${thread}.jsonl`)
17847
18433
  );
17848
18434
  if (!rollout) return [];
17849
18435
  return [
@@ -17855,7 +18441,7 @@ function processOwners(pid) {
17855
18441
  thread,
17856
18442
  queue,
17857
18443
  version: CODEX_VERSION,
17858
- cwd: realpathSync2.native(join7(root, "cwd")),
18444
+ cwd: realpathSync2.native(join8(root, "cwd")),
17859
18445
  rollout: identity(rollout.path)
17860
18446
  }
17861
18447
  ];
@@ -17901,7 +18487,7 @@ function validateOwner(owner) {
17901
18487
  function validateEvidence(owner) {
17902
18488
  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))
17903
18489
  throw new Error("The original native history storage changed");
17904
- const path = join7(owner.home, "thread_history_1.sqlite");
18490
+ const path = join8(owner.home, "thread_history_1.sqlite");
17905
18491
  if (existsSync5(path) && identity(path).path !== path)
17906
18492
  throw new Error("Native history is outside the original home");
17907
18493
  }
@@ -17918,7 +18504,7 @@ function nativeReceipt(owner, clientId) {
17918
18504
  } finally {
17919
18505
  queue.close();
17920
18506
  }
17921
- const path = join7(owner.home, "thread_history_1.sqlite");
18507
+ const path = join8(owner.home, "thread_history_1.sqlite");
17922
18508
  if (!existsSync5(path)) return { queueId };
17923
18509
  const history = new DatabaseSync3(path, { readOnly: true });
17924
18510
  try {
@@ -17974,7 +18560,7 @@ var Writer = class {
17974
18560
  tail = "";
17975
18561
  pending = /* @__PURE__ */ new Map();
17976
18562
  constructor(owner) {
17977
- this.process = spawn3(owner.executable, ["app-server", "--listen", "stdio://"], {
18563
+ this.process = spawn5(owner.executable, ["app-server", "--listen", "stdio://"], {
17978
18564
  cwd: owner.cwd,
17979
18565
  env: { ...process.env, CODEX_HOME: owner.home },
17980
18566
  stdio: ["pipe", "pipe", "ignore"]
@@ -18014,7 +18600,7 @@ var Writer = class {
18014
18600
  this.pending.delete(id);
18015
18601
  reject(new Error("Native queue acknowledgement was not received"));
18016
18602
  this.close();
18017
- }, REQUEST_MS4);
18603
+ }, REQUEST_MS5);
18018
18604
  this.pending.set(id, { resolve: resolve3, reject, timer });
18019
18605
  this.process.stdin.write(JSON.stringify({ id, method, params }) + "\n");
18020
18606
  });
@@ -18061,7 +18647,7 @@ var NativeQueue = class {
18061
18647
  if (existing) return existing;
18062
18648
  if (this.stopped) throw new Error("The connector is stopping");
18063
18649
  validateOwner(scope2.owner);
18064
- const clientUserMessageId = randomUUID4();
18650
+ const clientUserMessageId = randomUUID5();
18065
18651
  const request2 = { ...scope2, receipt: { state: "unknown", clientUserMessageId } };
18066
18652
  let writer;
18067
18653
  try {
@@ -18146,7 +18732,13 @@ var Command = z2.object({
18146
18732
  z2.object({ kind: z2.literal("thread.stop") }).strict(),
18147
18733
  z2.object({ kind: z2.literal("approval.respond"), approvalId: z2.string(), choice: z2.string() }).strict(),
18148
18734
  z2.object({ kind: z2.literal("screen.start"), sessionId: z2.string().min(1).max(128) }).strict(),
18149
- z2.object({ kind: z2.literal("screen.stop"), sessionId: z2.string().min(1).max(128) }).strict()
18735
+ z2.object({ kind: z2.literal("screen.stop"), sessionId: z2.string().min(1).max(128) }).strict(),
18736
+ z2.object({
18737
+ kind: z2.literal("session.start"),
18738
+ harness: z2.enum(["claude", "codex", "opencode"]),
18739
+ text: z2.string().min(1).max(1e5),
18740
+ folder: z2.object({ parent: z2.string().min(1), name: z2.string().min(1).max(255) }).strict().optional()
18741
+ }).strict()
18150
18742
  ])
18151
18743
  }).strict();
18152
18744
  var Connector = class {
@@ -18208,6 +18800,8 @@ var Connector = class {
18208
18800
  listed = /* @__PURE__ */ new Map();
18209
18801
  listedProjects = /* @__PURE__ */ new Map();
18210
18802
  overviewQueued = false;
18803
+ // The agents that can start sessions here.
18804
+ startable = [];
18211
18805
  snapshots = /* @__PURE__ */ new Set();
18212
18806
  nativeSnapshots = /* @__PURE__ */ new Map();
18213
18807
  queueReceipts = /* @__PURE__ */ new Map();
@@ -18221,6 +18815,8 @@ var Connector = class {
18221
18815
  this.nativeSnapshots.set(thread.id, structuredClone(this.threadState(project, thread)));
18222
18816
  }
18223
18817
  this.refreshQueues();
18818
+ this.refreshResumes();
18819
+ void this.findAgents().then(() => this.queueChange());
18224
18820
  this.stopWatching = this.discovery.watch(this.rescan);
18225
18821
  this.releases.push(this.harnesses.watch(this.rescan));
18226
18822
  this.releases.push(
@@ -18259,15 +18855,15 @@ var Connector = class {
18259
18855
  for (const project of this.discovery.projects.values())
18260
18856
  for (const thread of project.threads) {
18261
18857
  const native = this.discovery.locations.get(thread.id)?.live;
18262
- const approval2 = native?.approvals[0];
18263
- if (!approval2) continue;
18858
+ const approval3 = native?.approvals[0];
18859
+ if (!approval3) continue;
18264
18860
  await this.thread(pair, project, thread);
18265
18861
  await this.send(pair, {
18266
18862
  type: "attention",
18267
18863
  projectId: project.id,
18268
18864
  threadId: thread.id,
18269
- title: approval2.title,
18270
- body: approval2.detail,
18865
+ title: approval3.title,
18866
+ body: approval3.detail,
18271
18867
  reason: "approval"
18272
18868
  });
18273
18869
  }
@@ -18435,6 +19031,10 @@ var Connector = class {
18435
19031
  }
18436
19032
  return {
18437
19033
  type: "projects",
19034
+ version,
19035
+ screen: this.discovery.capabilities.screen,
19036
+ agents: this.startable,
19037
+ folders: this.discovery.folders(),
18438
19038
  projects: [...this.listedProjects.values()],
18439
19039
  sessions: [
18440
19040
  ...sessions.filter(busy),
@@ -18442,6 +19042,10 @@ var Connector = class {
18442
19042
  ]
18443
19043
  };
18444
19044
  }
19045
+ // Agents change only when someone installs or removes one, so a refresh asks again.
19046
+ async findAgents() {
19047
+ this.startable = await this.harnesses.agents();
19048
+ }
18445
19049
  async listProjects(pair) {
18446
19050
  const overview = this.overview();
18447
19051
  const text3 = JSON.stringify(overview);
@@ -18471,6 +19075,7 @@ var Connector = class {
18471
19075
  const native = await this.harnesses.refresh();
18472
19076
  const live = native.map((session) => ({ ...this.discovery.observe(session), session }));
18473
19077
  this.refreshQueues();
19078
+ this.refreshResumes();
18474
19079
  for (const agent of this.agents.values()) if (!agent.socket.destroyed) this.adopt(agent);
18475
19080
  const current = new Set(live.map(({ thread }) => thread.id));
18476
19081
  for (const [id, snapshot] of this.nativeSnapshots) {
@@ -18507,9 +19112,9 @@ var Connector = class {
18507
19112
  if (JSON.stringify(prior) === JSON.stringify(snapshot)) continue;
18508
19113
  this.nativeSnapshots.set(thread.id, structuredClone(snapshot));
18509
19114
  for (const pair of this.activePairs()) await this.thread(pair, project, thread);
18510
- for (const approval2 of session.approvals)
18511
- if (!prior?.approvals.some((item) => item.id === approval2.id))
18512
- await this.attention(project.id, thread.id, approval2.title, approval2.detail, "approval");
19115
+ for (const approval3 of session.approvals)
19116
+ if (!prior?.approvals.some((item) => item.id === approval3.id))
19117
+ await this.attention(project.id, thread.id, approval3.title, approval3.detail, "approval");
18513
19118
  if (prior?.thread.status === "working" && thread.status === "idle")
18514
19119
  await this.attention(
18515
19120
  project.id,
@@ -18522,6 +19127,17 @@ var Connector = class {
18522
19127
  for (const pair of this.activePairs()) await this.listProjects(pair);
18523
19128
  await this.publishQueueReceipts();
18524
19129
  }
19130
+ // A saved session nothing else has open continues headless when its agent runs here. One a
19131
+ // native process owns takes queued messages instead.
19132
+ refreshResumes() {
19133
+ for (const project of this.discovery.projects.values())
19134
+ for (const thread of project.threads) {
19135
+ const location2 = this.discovery.locations.get(thread.id);
19136
+ const harness = thread.agent?.harness;
19137
+ if (location2?.history && !location2.live && !project.remote && !thread.capabilities.queue && harness && harness !== "other" && this.startable.includes(harness))
19138
+ thread.capabilities = { ...thread.capabilities, send: true };
19139
+ }
19140
+ }
18525
19141
  refreshQueues() {
18526
19142
  this.nativeQueue.refresh();
18527
19143
  for (const project of this.discovery.projects.values())
@@ -18591,18 +19207,21 @@ var Connector = class {
18591
19207
  async execute(pair, content, envelope) {
18592
19208
  let queue = void 0;
18593
19209
  try {
18594
- if (["screen.start", "approval.respond", "thread.stop"].includes(content.action.kind) && Math.abs(Date.now() - Date.parse(envelope.sentAt)) > EPHEMERAL_TTL_MS)
19210
+ if (["screen.start", "approval.respond", "thread.stop", "session.start"].includes(
19211
+ content.action.kind
19212
+ ) && Math.abs(Date.now() - Date.parse(envelope.sentAt)) > EPHEMERAL_TTL_MS)
18595
19213
  throw new Error("Command expired. Try again while this computer is online.");
18596
- queue = await this.command(pair, content);
19214
+ const outcome = await this.command(pair, content);
18597
19215
  delete this.config.unconfirmed[content.requestId];
18598
- if (queue) {
19216
+ if (outcome && "state" in outcome) {
19217
+ queue = outcome;
18599
19218
  await this.queueResult(pair, content.requestId, content.projectId, queue);
18600
19219
  this.queueReceipts.set(content.requestId, JSON.stringify(queue));
18601
19220
  return;
18602
19221
  }
18603
19222
  await this.send(
18604
19223
  pair,
18605
- { type: "result", requestId: content.requestId, ok: true },
19224
+ { type: "result", requestId: content.requestId, ok: true, ...outcome ? { started: outcome } : {} },
18606
19225
  content.projectId
18607
19226
  );
18608
19227
  } catch (error) {
@@ -18637,7 +19256,7 @@ var Connector = class {
18637
19256
  }
18638
19257
  }
18639
19258
  attach(socket) {
18640
- const sessions = bridgeAcp(socket, (session, approval2) => {
19259
+ const sessions = bridgeAcp(socket, (session, approval3) => {
18641
19260
  this.agents.set(session.id, session);
18642
19261
  const { project, thread } = this.adopt(session);
18643
19262
  if (!this.snapshots.has(session.id)) {
@@ -18652,15 +19271,15 @@ var Connector = class {
18652
19271
  }
18653
19272
  }).catch((error) => this.report(error));
18654
19273
  }
18655
- if (approval2)
19274
+ if (approval3)
18656
19275
  this.work = this.work.then(async () => {
18657
- if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(approval2))
19276
+ if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(approval3))
18658
19277
  return;
18659
19278
  await this.attention(
18660
19279
  project.id,
18661
19280
  thread.id,
18662
- approval2.title,
18663
- approval2.detail,
19281
+ approval3.title,
19282
+ approval3.detail,
18664
19283
  "approval"
18665
19284
  );
18666
19285
  }).catch((error) => this.report(error));
@@ -18729,9 +19348,12 @@ var Connector = class {
18729
19348
  }
18730
19349
  async command(pair, command2) {
18731
19350
  if (command2.action.kind === "projects.refresh") {
19351
+ await this.findAgents();
18732
19352
  await this.changed();
18733
19353
  return this.publishProjects(pair);
18734
19354
  }
19355
+ if (command2.action.kind === "session.start")
19356
+ return this.startSession(command2.projectId, command2.action);
18735
19357
  const project = this.discovery.projects.get(command2.projectId);
18736
19358
  if (!project) throw new Error("Project is no longer open on this computer");
18737
19359
  const thread = project.threads.find((item) => item.id === command2.threadId);
@@ -18747,9 +19369,15 @@ var Connector = class {
18747
19369
  throw new Error(
18748
19370
  "This session has no live reply connection. Open Screen to use the existing agent."
18749
19371
  );
18750
- const native = this.discovery.locations.get(thread.id)?.live;
18751
- if (native?.send) return native.send(command2.requestId, command2.action.text);
19372
+ const location2 = this.discovery.locations.get(thread.id);
19373
+ if (location2?.live?.send) return location2.live.send(command2.requestId, command2.action.text);
18752
19374
  const agent = this.attached(thread);
19375
+ const saved = thread.agent;
19376
+ if (!agent && location2?.history && saved && saved.harness !== "other") {
19377
+ await this.harnesses.resume(saved.harness, saved.sessionId, project.path, command2.action.text);
19378
+ await this.changed();
19379
+ return;
19380
+ }
18753
19381
  if (!agent || agent.socket.destroyed || agent.status === "working")
18754
19382
  throw new Error("Wait for the current ACP turn to finish");
18755
19383
  return commandAcp(agent, command2.requestId, { kind: "prompt", text: command2.action.text });
@@ -18788,15 +19416,15 @@ var Connector = class {
18788
19416
  const action = command2.action;
18789
19417
  const native = this.discovery.locations.get(thread.id)?.live;
18790
19418
  if (native?.approve) {
18791
- const approval3 = native.approvals.find((item) => item.id === action.approvalId);
18792
- if (!approval3?.choices.some((choice) => choice.id === action.choice))
19419
+ const approval4 = native.approvals.find((item) => item.id === action.approvalId);
19420
+ if (!approval4?.choices.some((choice) => choice.id === action.choice))
18793
19421
  throw new Error("Approval is no longer available");
18794
- return native.approve(command2.requestId, approval3.id, action.choice);
19422
+ return native.approve(command2.requestId, approval4.id, action.choice);
18795
19423
  }
18796
19424
  const agent = this.attached(thread);
18797
19425
  if (!agent) throw new Error("Approval requires an attached ACP session. Use Screen.");
18798
- const [request2, approval2] = [...agent.approvals].find(([, item]) => item.id === action.approvalId) ?? [];
18799
- if (!approval2?.choices.some((choice) => choice.id === action.choice))
19426
+ const [request2, approval3] = [...agent.approvals].find(([, item]) => item.id === action.approvalId) ?? [];
19427
+ if (!approval3?.choices.some((choice) => choice.id === action.choice))
18800
19428
  throw new Error("Approval is no longer available");
18801
19429
  return commandAcp(agent, command2.requestId, {
18802
19430
  kind: "approval",
@@ -18812,6 +19440,37 @@ var Connector = class {
18812
19440
  return;
18813
19441
  }
18814
19442
  }
19443
+ // Starts an agent in a project, or in a new folder inside one this computer offers. Commands run
19444
+ // in the work queue, so this refreshes directly rather than queueing a change behind itself.
19445
+ async startSession(projectId, { harness, text: text3, folder }) {
19446
+ if (!this.startable.includes(harness))
19447
+ throw new Error(`${HARNESSES[harness]} doesn't run on this computer`);
19448
+ let cwd;
19449
+ if (folder) {
19450
+ const name = folder.name.trim();
19451
+ if (!this.discovery.folders().includes(folder.parent))
19452
+ throw new Error("That folder is no longer offered for new projects");
19453
+ if (!name || name === "." || name === ".." || /[\\/:*?"<>|\x00-\x1f]/.test(name))
19454
+ throw new Error("Name the folder without slashes or special characters");
19455
+ cwd = join9(folder.parent, name);
19456
+ await mkdir(cwd).catch((error) => {
19457
+ throw new Error(error.code === "EEXIST" ? `${name} already exists` : `Couldn't create ${name}`);
19458
+ });
19459
+ } else {
19460
+ const project = this.discovery.projects.get(projectId);
19461
+ if (!project) throw new Error("Project is no longer open on this computer");
19462
+ if (project.remote)
19463
+ throw new Error(`Install -up on ${project.remote.host} to start sessions there`);
19464
+ cwd = project.path;
19465
+ }
19466
+ const session = await this.harnesses.start(harness, cwd, text3);
19467
+ await this.changed();
19468
+ for (const project of this.discovery.projects.values())
19469
+ for (const thread of project.threads)
19470
+ if (thread.agent?.harness === harness && thread.agent.sessionId === session)
19471
+ return { projectId: project.id, threadId: thread.id };
19472
+ throw new Error(`${HARNESSES[harness]} started, but this computer hasn't listed it yet`);
19473
+ }
18815
19474
  report(error) {
18816
19475
  process.stderr.write(`${error instanceof Error ? error.message : "Dashup operation failed"}
18817
19476
  `);
@@ -18821,18 +19480,18 @@ var Connector = class {
18821
19480
  // src/install.ts
18822
19481
  import { existsSync as existsSync6 } from "node:fs";
18823
19482
  import { homedir as homedir6, userInfo } from "node:os";
18824
- import { promisify as promisify5 } from "node:util";
19483
+ import { promisify as promisify7 } from "node:util";
18825
19484
  import { fileURLToPath as fileURLToPath3 } from "node:url";
18826
- import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
18827
- import { cp, mkdir, rm, writeFile } from "node:fs/promises";
18828
- import { dirname as dirname5, join as join8, relative, sep } from "node:path";
18829
- var run4 = promisify5(execFile5);
19485
+ import { execFile as execFile7, spawn as spawn6 } from "node:child_process";
19486
+ import { cp, mkdir as mkdir2, rm, writeFile } from "node:fs/promises";
19487
+ import { dirname as dirname6, join as join10, relative, sep } from "node:path";
19488
+ var run4 = promisify7(execFile7);
18830
19489
  var xml = (value2) => value2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
18831
19490
  var NODE_FLAGS = ["--disable-warning=ExperimentalWarning"];
18832
- var serviceFile = () => process.platform === "darwin" ? join8(homedir6(), "Library/LaunchAgents/com.dashup.connector.plist") : process.platform === "win32" ? join8(
18833
- process.env.APPDATA ?? join8(homedir6(), "AppData/Roaming"),
19491
+ var serviceFile = () => process.platform === "darwin" ? join10(homedir6(), "Library/LaunchAgents/com.dashup.connector.plist") : process.platform === "win32" ? join10(
19492
+ process.env.APPDATA ?? join10(homedir6(), "AppData/Roaming"),
18834
19493
  "Microsoft/Windows/Start Menu/Programs/Startup/Dashup.vbs"
18835
- ) : join8(homedir6(), ".config/systemd/user/dashup.service");
19494
+ ) : join10(homedir6(), ".config/systemd/user/dashup.service");
18836
19495
  var startupInstalled = () => existsSync6(serviceFile());
18837
19496
  function service(platform, node, entry, config2) {
18838
19497
  const command2 = [node, ...NODE_FLAGS, entry];
@@ -18868,17 +19527,17 @@ async function durableEntry() {
18868
19527
  );
18869
19528
  if (!["_npx", "dlx"].some((cache2) => entry.includes(`${sep}${cache2}${sep}`))) return entry;
18870
19529
  const root = entry.slice(0, entry.indexOf(`${sep}node_modules${sep}`));
18871
- const app = join8(dirname5(configPath()), "app");
19530
+ const app = join10(dirname6(configPath()), "app");
18872
19531
  await rm(app, { recursive: true, force: true });
18873
- await cp(join8(root, "node_modules"), join8(app, "node_modules"), {
19532
+ await cp(join10(root, "node_modules"), join10(app, "node_modules"), {
18874
19533
  recursive: true,
18875
19534
  verbatimSymlinks: true
18876
19535
  });
18877
- return join8(app, relative(root, entry));
19536
+ return join10(app, relative(root, entry));
18878
19537
  }
18879
19538
  async function installStartup() {
18880
19539
  const path = serviceFile();
18881
- await mkdir(dirname5(path), { recursive: true });
19540
+ await mkdir2(dirname6(path), { recursive: true });
18882
19541
  await writeFile(
18883
19542
  path,
18884
19543
  service(process.platform, process.execPath, await durableEntry(), configPath())
@@ -18888,7 +19547,7 @@ async function installStartup() {
18888
19547
  });
18889
19548
  await run4("launchctl", ["load", path]);
18890
19549
  } else if (process.platform === "win32") {
18891
- spawn4("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
19550
+ spawn6("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
18892
19551
  } else {
18893
19552
  await run4("systemctl", ["--user", "daemon-reload"]);
18894
19553
  await run4("systemctl", ["--user", "enable", "dashup.service"]);
@@ -18908,7 +19567,7 @@ async function uninstallStartup() {
18908
19567
  await run4("systemctl", ["--user", "disable", "dashup.service"]).catch(() => {
18909
19568
  });
18910
19569
  await rm(path, { force: true });
18911
- await rm(join8(dirname5(configPath()), "app"), { recursive: true, force: true });
19570
+ await rm(join10(dirname6(configPath()), "app"), { recursive: true, force: true });
18912
19571
  }
18913
19572
 
18914
19573
  // src/index.ts