@dashup/connector 0.1.4 → 0.1.5

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 +8 -4
  2. package/dist/index.js +683 -132
  3. package/package.json +3 -3
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.5";
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,6 +4642,7 @@ 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
4647
  function claudeChange(change) {
4642
4648
  if (typeof change?.filePath !== "string") return void 0;
@@ -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, then its first turn.
16681
+ async start(cwd, text3) {
16682
+ await this.connect();
16683
+ if (!this.controls) throw new Error("Update Codex to start threads from Dashup");
16684
+ const id = (await this.call("thread/start", { cwd })).thread?.id;
16685
+ if (typeof id !== "string") throw new Error("Codex did not start a 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,25 @@ 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, so they outlive this connector.
16775
+ async start(cwd, text3) {
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);
16788
+ }
16708
16789
  async refresh() {
16709
- const paths = new Set(await sockets());
16790
+ const paths = /* @__PURE__ */ new Set([...await sockets(), ...this.started]);
16710
16791
  for (const [path, client] of this.clients)
16711
16792
  if (!paths.has(path)) {
16712
16793
  client.stop();
@@ -16730,26 +16811,360 @@ var Codex = class {
16730
16811
  }
16731
16812
  };
16732
16813
 
16814
+ // src/harness/headless.ts
16815
+ import { randomUUID as randomUUID3 } from "node:crypto";
16816
+ import { createInterface as createInterface3 } from "node:readline";
16817
+ import { spawn as spawn3 } from "node:child_process";
16818
+ var CHOICES = [
16819
+ { id: "accept", label: "Allow once" },
16820
+ { id: "decline", label: "Reject" }
16821
+ ];
16822
+ var EDITS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "NotebookEdit", "Write"]);
16823
+ var MAX_REQUESTS = 1e3;
16824
+ var NOTIFY_MS = 100;
16825
+ function approval2(id, request2) {
16826
+ const input = request2.input ?? {};
16827
+ const tool = String(request2.display_name ?? request2.tool_name ?? "a tool");
16828
+ let detail = [
16829
+ typeof request2.description === "string" ? request2.description : "",
16830
+ typeof input.command === "string" ? input.command : JSON.stringify(input, null, 2)
16831
+ ].filter(Boolean).join("\n\n");
16832
+ if (Buffer.byteLength(detail) > MAX_PREVIEW)
16833
+ detail = Buffer.from(detail).subarray(0, MAX_PREVIEW).toString("utf8");
16834
+ return {
16835
+ id,
16836
+ title: request2.tool_name === "Bash" ? "Allow command?" : EDITS.has(request2.tool_name) ? "Allow file changes?" : `Allow ${tool}?`,
16837
+ detail,
16838
+ choices: CHOICES.map((choice) => ({ ...choice }))
16839
+ };
16840
+ }
16841
+ var Session = class {
16842
+ constructor(id, cwd, title2, changed) {
16843
+ this.id = id;
16844
+ this.cwd = cwd;
16845
+ this.title = title2;
16846
+ this.changed = changed;
16847
+ }
16848
+ id;
16849
+ cwd;
16850
+ title;
16851
+ changed;
16852
+ messages = [];
16853
+ status = "idle";
16854
+ updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16855
+ turn;
16856
+ // Messages written to this turn's process that Claude Code has not finished answering.
16857
+ open = 0;
16858
+ // Once Claude Code has saved the session, later turns resume it.
16859
+ saved = false;
16860
+ approvals = /* @__PURE__ */ new Map();
16861
+ requests = /* @__PURE__ */ new Map();
16862
+ write(value2) {
16863
+ this.turn?.stdin.write(`${JSON.stringify(value2)}
16864
+ `);
16865
+ }
16866
+ once(id, action) {
16867
+ const known = this.requests.get(id);
16868
+ if (known) return known;
16869
+ if (this.requests.size >= MAX_REQUESTS)
16870
+ throw new Error("Claude Code command history is full; restart Dashup before sending");
16871
+ const result = action();
16872
+ this.requests.set(id, result);
16873
+ return result;
16874
+ }
16875
+ // Resolves once Claude Code takes the message, or says why it could not start.
16876
+ say(text3) {
16877
+ this.messages.push({ id: randomUUID3(), role: "user", text: text3 });
16878
+ this.status = "working";
16879
+ this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16880
+ const message = { type: "user", message: { role: "user", content: text3 } };
16881
+ if (this.turn) {
16882
+ this.open++;
16883
+ this.write(message);
16884
+ this.changed();
16885
+ return Promise.resolve();
16886
+ }
16887
+ const turn = this.turn = spawn3(
16888
+ "claude",
16889
+ [
16890
+ "-p",
16891
+ "--input-format",
16892
+ "stream-json",
16893
+ "--output-format",
16894
+ "stream-json",
16895
+ "--verbose",
16896
+ "--permission-prompt-tool",
16897
+ "stdio",
16898
+ ...this.saved ? ["--resume", this.id] : ["--session-id", this.id]
16899
+ ],
16900
+ { cwd: this.cwd, stdio: "pipe", shell: process.platform === "win32" }
16901
+ );
16902
+ this.open = 1;
16903
+ let errors = "";
16904
+ turn.stdin.on("error", () => {
16905
+ });
16906
+ turn.stderr.on("data", (chunk) => {
16907
+ errors = (errors + String(chunk)).slice(-2e3);
16908
+ });
16909
+ const started = new Promise((resolve3, reject) => {
16910
+ createInterface3({ input: turn.stdout }).on("line", (line) => {
16911
+ if (this.receive(line)) resolve3();
16912
+ });
16913
+ const end = (reason) => {
16914
+ reject(new Error(reason));
16915
+ if (this.turn !== turn) return;
16916
+ this.turn = void 0;
16917
+ this.open = 0;
16918
+ this.approvals.clear();
16919
+ this.status = "idle";
16920
+ this.changed();
16921
+ };
16922
+ turn.once("error", () => end("Claude Code is not installed on this computer"));
16923
+ turn.once("exit", () => end(errors.trim() || "Claude Code stopped before starting"));
16924
+ });
16925
+ this.write(message);
16926
+ this.changed();
16927
+ return started;
16928
+ }
16929
+ // Handles one line of Claude Code's output, and says whether it started the session.
16930
+ receive(line) {
16931
+ let message;
16932
+ try {
16933
+ message = JSON.parse(line);
16934
+ } catch {
16935
+ return false;
16936
+ }
16937
+ const init = message.type === "system" && message.subtype === "init";
16938
+ if (init) this.saved = true;
16939
+ else if (message.type === "assistant") {
16940
+ 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");
16941
+ if (!text3) return false;
16942
+ this.messages.push({ id: String(message.message?.id ?? randomUUID3()), role: "assistant", text: text3 });
16943
+ } else if (message.type === "control_request" && message.request?.subtype === "can_use_tool") {
16944
+ const id = randomUUID3();
16945
+ this.approvals.set(id, {
16946
+ request: String(message.request_id),
16947
+ input: message.request.input ?? {},
16948
+ approval: approval2(id, message.request)
16949
+ });
16950
+ this.status = "attention";
16951
+ } else if (message.type === "result") {
16952
+ if (--this.open <= 0) this.turn?.stdin.end();
16953
+ } else return false;
16954
+ this.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
16955
+ this.changed();
16956
+ return init;
16957
+ }
16958
+ live() {
16959
+ return {
16960
+ id: this.id,
16961
+ cwd: this.cwd,
16962
+ title: this.title,
16963
+ harness: "claude",
16964
+ updatedAt: this.updatedAt,
16965
+ status: this.status,
16966
+ messages: [...this.messages],
16967
+ approvals: [...this.approvals.values()].map((pending) => pending.approval),
16968
+ send: (requestId, text3) => this.once(requestId, () => this.say(text3)),
16969
+ stop: this.turn ? (requestId) => this.once(requestId, async () => {
16970
+ if (!this.turn) throw new Error("Nothing is running in this Claude Code session");
16971
+ this.write({
16972
+ type: "control_request",
16973
+ request_id: randomUUID3(),
16974
+ request: { subtype: "interrupt" }
16975
+ });
16976
+ }) : void 0,
16977
+ approve: this.approvals.size ? (requestId, approvalId, choice) => this.once(requestId, async () => {
16978
+ const pending = this.approvals.get(approvalId);
16979
+ if (!pending || !this.turn || !CHOICES.some((value2) => value2.id === choice))
16980
+ throw new Error("Claude Code approval is no longer pending");
16981
+ this.write({
16982
+ type: "control_response",
16983
+ response: {
16984
+ subtype: "success",
16985
+ request_id: pending.request,
16986
+ response: choice === "accept" ? { behavior: "allow", updatedInput: pending.input } : { behavior: "deny", message: "The person rejected this from their phone." }
16987
+ }
16988
+ });
16989
+ this.approvals.delete(approvalId);
16990
+ if (!this.approvals.size) this.status = "working";
16991
+ this.changed();
16992
+ }) : void 0
16993
+ };
16994
+ }
16995
+ end() {
16996
+ this.turn?.kill();
16997
+ }
16998
+ };
16999
+ var Headless = class {
17000
+ constructor(changed) {
17001
+ this.changed = changed;
17002
+ }
17003
+ changed;
17004
+ sessions = /* @__PURE__ */ new Map();
17005
+ notify;
17006
+ soon = () => {
17007
+ if (this.notify) return;
17008
+ this.notify = setTimeout(() => {
17009
+ this.notify = void 0;
17010
+ this.changed();
17011
+ }, NOTIFY_MS);
17012
+ this.notify.unref();
17013
+ };
17014
+ // Starts a session in `cwd` with its first message, and resolves with the session's id.
17015
+ async start(cwd, text3) {
17016
+ const title2 = text3.trim().split("\n")[0].slice(0, 80);
17017
+ const session = new Session(randomUUID3(), cwd, title2, this.soon);
17018
+ this.sessions.set(session.id, session);
17019
+ try {
17020
+ await session.say(text3);
17021
+ } catch (error) {
17022
+ this.sessions.delete(session.id);
17023
+ throw error;
17024
+ }
17025
+ return session.id;
17026
+ }
17027
+ async refresh() {
17028
+ return [...this.sessions.values()].map((session) => session.live());
17029
+ }
17030
+ stop() {
17031
+ clearTimeout(this.notify);
17032
+ for (const session of this.sessions.values()) session.end();
17033
+ }
17034
+ };
17035
+
17036
+ // src/harness/claude.ts
17037
+ import { join as join5 } from "node:path";
17038
+ import { connect as connect2 } from "node:net";
17039
+ import { watch as watch2 } from "node:fs";
17040
+ import { lstat, readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
17041
+ var PEER_PROTOCOL = 1;
17042
+ var MAX_SESSIONS2 = 100;
17043
+ var REQUEST_MS2 = 4e3;
17044
+ var MAX_REQUESTS2 = 1e3;
17045
+ var ENVELOPE = "cross-session-message";
17046
+ var STATUS = {
17047
+ busy: "working",
17048
+ idle: "idle",
17049
+ shell: "idle",
17050
+ waiting: "attention"
17051
+ };
17052
+ async function registrations() {
17053
+ if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
17054
+ const directory2 = join5(claudeHome(), "sessions");
17055
+ const names = await readdir2(directory2).catch(() => []);
17056
+ const found = await Promise.all(
17057
+ names.filter((name) => /^\d+\.json$/.test(name)).slice(0, MAX_SESSIONS2).map(async (name) => {
17058
+ try {
17059
+ const value2 = JSON.parse(await readFile2(join5(directory2, name), "utf8"));
17060
+ if (typeof value2.sessionId !== "string" || typeof value2.cwd !== "string") return [];
17061
+ process.kill(value2.pid, 0);
17062
+ return [value2];
17063
+ } catch {
17064
+ return [];
17065
+ }
17066
+ })
17067
+ );
17068
+ return found.flat();
17069
+ }
17070
+ async function post(session, path, text3) {
17071
+ const file = await lstat(path);
17072
+ if (!file.isSocket() || process.getuid && file.uid !== process.getuid())
17073
+ throw new Error("Claude Code inbox is not owned by this user");
17074
+ const body2 = text3.replace(new RegExp(`<(?=/?${ENVELOPE})`, "gi"), "<\\");
17075
+ const line = JSON.stringify({
17076
+ type: "user",
17077
+ session_id: session.sessionId,
17078
+ priority: "next",
17079
+ message: { role: "user", content: `<${ENVELOPE} from-name="Dashup">
17080
+ ${body2}
17081
+ </${ENVELOPE}>` }
17082
+ });
17083
+ await new Promise((resolve3, reject) => {
17084
+ const socket = connect2(path);
17085
+ socket.setTimeout(REQUEST_MS2, () => socket.destroy(new Error("timed out")));
17086
+ socket.once("error", () => reject(new Error("Claude Code is not accepting messages")));
17087
+ socket.end(`${line}
17088
+ `, resolve3);
17089
+ });
17090
+ }
17091
+ var Claude = class {
17092
+ constructor(changed) {
17093
+ this.changed = changed;
17094
+ }
17095
+ changed;
17096
+ watcher;
17097
+ notify;
17098
+ requests = /* @__PURE__ */ new Map();
17099
+ async refresh() {
17100
+ this.watch();
17101
+ return (await registrations()).map((session) => {
17102
+ const path = session.peerProtocol === PEER_PROTOCOL ? session.messagingSocketPath : void 0;
17103
+ return {
17104
+ id: session.sessionId,
17105
+ harness: "claude",
17106
+ cwd: session.cwd,
17107
+ updatedAt: new Date(session.updatedAt ?? 0).toISOString(),
17108
+ status: STATUS[session.status ?? ""] ?? "unknown",
17109
+ messages: [],
17110
+ approvals: [],
17111
+ send: path ? (requestId, text3) => this.once(requestId, () => post(session, path, text3)) : void 0
17112
+ };
17113
+ });
17114
+ }
17115
+ once(id, action) {
17116
+ const known = this.requests.get(id);
17117
+ if (known) return known;
17118
+ if (this.requests.size >= MAX_REQUESTS2) this.requests.delete(this.requests.keys().next().value);
17119
+ const result = action();
17120
+ this.requests.set(id, result);
17121
+ return result;
17122
+ }
17123
+ watch() {
17124
+ if (this.watcher || process.env.DASHUP_LIVE_DISCOVERY === "0") return;
17125
+ try {
17126
+ const watcher = this.watcher = watch2(join5(claudeHome(), "sessions"), () => {
17127
+ if (this.notify) return;
17128
+ this.notify = setTimeout(() => {
17129
+ this.notify = void 0;
17130
+ this.changed();
17131
+ }, 100);
17132
+ this.notify.unref();
17133
+ });
17134
+ watcher.on("error", () => {
17135
+ watcher.close();
17136
+ if (this.watcher === watcher) this.watcher = void 0;
17137
+ });
17138
+ } catch {
17139
+ }
17140
+ }
17141
+ stop() {
17142
+ clearTimeout(this.notify);
17143
+ this.watcher?.close();
17144
+ this.watcher = void 0;
17145
+ }
17146
+ };
17147
+
16733
17148
  // src/harness/vscode.ts
16734
17149
  import Socket3 from "ws";
16735
17150
  import { homedir as homedir5 } from "node:os";
16736
- import { connect as connect2 } from "node:net";
16737
- import { promisify as promisify3 } from "node:util";
17151
+ import { connect as connect3 } from "node:net";
17152
+ import { promisify as promisify4 } from "node:util";
16738
17153
  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";
17154
+ import { execFile as execFile4 } from "node:child_process";
17155
+ import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
17156
+ import { stat as stat2, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
17157
+ import { join as join6, dirname as dirname4, basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
16743
17158
  import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws";
16744
17159
  import { AhpClient } from "@microsoft/agent-host-protocol/client";
16745
17160
  import { chatReducer, sessionReducer, SUPPORTED_PROTOCOL_VERSIONS } from "@microsoft/agent-host-protocol";
16746
- var REQUEST_MS2 = 4e3;
17161
+ var REQUEST_MS3 = 4e3;
16747
17162
  var MAX_BYTES2 = 8 * 1024 * 1024;
16748
17163
  var MAX_METADATA = 64 * 1024;
16749
17164
  var MAX_HOSTS = 16;
16750
- var MAX_SESSIONS2 = 100;
17165
+ var MAX_SESSIONS3 = 100;
16751
17166
  var ROOT_CHANNEL = "ahp-root://";
16752
- var run2 = promisify3(execFile3);
17167
+ var run2 = promisify4(execFile4);
16753
17168
  var CONFIRMATION_CHOICES = [
16754
17169
  { id: "approve", label: "Allow once", kind: "approve" },
16755
17170
  { id: "deny", label: "Reject", kind: "deny" }
@@ -16757,10 +17172,10 @@ var CONFIRMATION_CHOICES = [
16757
17172
  function roots() {
16758
17173
  const override = process.env.DASHUP_AHP_DATA ?? process.env.DASHUP_VSCODE_DATA;
16759
17174
  if (override !== void 0)
16760
- return override ? [basename2(override) === "User" ? dirname3(override) : override] : [];
17175
+ return override ? [basename2(override) === "User" ? dirname4(override) : override] : [];
16761
17176
  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));
17177
+ 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");
17178
+ return ["Code", "Code - Insiders"].map((name) => join6(base, name));
16764
17179
  }
16765
17180
  async function privateFile(path, limit) {
16766
17181
  const file = await stat2(path).catch(() => void 0);
@@ -16777,7 +17192,7 @@ async function privateFile(path, limit) {
16777
17192
  "-EncodedCommand",
16778
17193
  Buffer.from(script, "utf16le").toString("base64")
16779
17194
  ],
16780
- { timeout: REQUEST_MS2, maxBuffer: MAX_METADATA }
17195
+ { timeout: REQUEST_MS3, maxBuffer: MAX_METADATA }
16781
17196
  );
16782
17197
  return stdout.trim().toLowerCase() === "true";
16783
17198
  } catch {
@@ -16789,16 +17204,16 @@ async function privateFile(path, limit) {
16789
17204
  async function entries() {
16790
17205
  const result = /* @__PURE__ */ new Map();
16791
17206
  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);
17207
+ const registry2 = join6(root, "agent-host/local-endpoint");
17208
+ const files2 = (await readdir3(join6(registry2, "entries")).catch(() => [])).filter((name) => name.endsWith(".json")).slice(0, MAX_HOSTS);
16794
17209
  const sources = [
16795
- ...files2.map((name) => join5(registry2, "entries", name)),
16796
- join5(registry2, "metadata.json")
17210
+ ...files2.map((name) => join6(registry2, "entries", name)),
17211
+ join6(registry2, "metadata.json")
16797
17212
  ];
16798
17213
  for (const path of sources) {
16799
17214
  try {
16800
17215
  if (!await privateFile(path, MAX_METADATA)) continue;
16801
- const parsed = JSON.parse(await readFile2(path, "utf8"));
17216
+ const parsed = JSON.parse(await readFile3(path, "utf8"));
16802
17217
  const rows = basename2(path) === "metadata.json" && Array.isArray(parsed) ? parsed : [parsed];
16803
17218
  for (const row of rows.slice(0, MAX_HOSTS)) {
16804
17219
  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 +17233,7 @@ async function entries() {
16818
17233
  if (!socket?.isSocket() || process.getuid && socket.uid !== process.getuid())
16819
17234
  continue;
16820
17235
  }
16821
- if (!await privateFile(dirname3(path), MAX_BYTES2)) continue;
17236
+ if (!await privateFile(dirname4(path), MAX_BYTES2)) continue;
16822
17237
  const key = `${root}\0${identity2}`;
16823
17238
  if (!result.has(key))
16824
17239
  result.set(key, {
@@ -16931,7 +17346,7 @@ var Host = class {
16931
17346
  client;
16932
17347
  connecting;
16933
17348
  stopped = false;
16934
- clientId = `dashup-${randomUUID3()}`;
17349
+ clientId = `dashup-${randomUUID4()}`;
16935
17350
  resources = /* @__PURE__ */ new Map();
16936
17351
  subscriptions = /* @__PURE__ */ new Set();
16937
17352
  receipts = /* @__PURE__ */ new Map();
@@ -16983,9 +17398,9 @@ var Host = class {
16983
17398
  if (this.connecting) return this.connecting;
16984
17399
  this.connecting = (async () => {
16985
17400
  const socket = new Socket3(`ws://localhost/?tkn=${encodeURIComponent(this.entry.token)}`, {
16986
- createConnection: () => connect2(this.entry.path),
17401
+ createConnection: () => connect3(this.entry.path),
16987
17402
  maxPayload: MAX_BYTES2,
16988
- handshakeTimeout: REQUEST_MS2
17403
+ handshakeTimeout: REQUEST_MS3
16989
17404
  });
16990
17405
  socket.on("error", () => {
16991
17406
  });
@@ -17018,7 +17433,7 @@ var Host = class {
17018
17433
  }
17019
17434
  };
17020
17435
  const client = this.client = new AhpClient(transport, {
17021
- requestTimeoutMs: REQUEST_MS2,
17436
+ requestTimeoutMs: REQUEST_MS3,
17022
17437
  subscriptionBuffer: 1024
17023
17438
  });
17024
17439
  this.resources.clear();
@@ -17082,7 +17497,7 @@ var Host = class {
17082
17497
  const timer = setTimeout(() => {
17083
17498
  this.receipts.delete(handle.clientSeq);
17084
17499
  reject(new Error("Native AHP action was not acknowledged; delivery is unknown"));
17085
- }, REQUEST_MS2);
17500
+ }, REQUEST_MS3);
17086
17501
  this.receipts.set(handle.clientSeq, { resolve: resolve3, reject, timer });
17087
17502
  });
17088
17503
  this.requests.set(requestId, pending);
@@ -17092,12 +17507,12 @@ var Host = class {
17092
17507
  await this.connect();
17093
17508
  const catalog = await this.client.request("listSessions", {
17094
17509
  channel: ROOT_CHANNEL,
17095
- limit: MAX_SESSIONS2
17510
+ limit: MAX_SESSIONS3
17096
17511
  });
17097
17512
  if (!Array.isArray(catalog.items)) return [];
17098
17513
  const result = [];
17099
17514
  const active = /* @__PURE__ */ new Set();
17100
- for (const summary of catalog.items.slice(0, MAX_SESSIONS2)) {
17515
+ for (const summary of catalog.items.slice(0, MAX_SESSIONS3)) {
17101
17516
  try {
17102
17517
  const session = await this.subscribe(summary.resource);
17103
17518
  if (!session || session.lifecycle !== "ready" || !Array.isArray(session.chats)) continue;
@@ -17159,7 +17574,7 @@ var Host = class {
17159
17574
  message: { text: value2, origin: { kind: "user" } }
17160
17575
  });
17161
17576
  } : void 0,
17162
- approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, choice) => {
17577
+ approve: approvals.some((approval3) => approval3.choices.length) ? (requestId, approvalId, choice) => {
17163
17578
  const prior = this.requests.get(requestId);
17164
17579
  if (prior) return prior;
17165
17580
  const current = this.resources.get(summary.resource)?.state;
@@ -17230,15 +17645,15 @@ var VSCode = class {
17230
17645
  };
17231
17646
 
17232
17647
  // src/harness/opencode.ts
17233
- import { join as join6 } from "node:path";
17234
- import { promisify as promisify4 } from "node:util";
17648
+ import { join as join7 } from "node:path";
17649
+ import { promisify as promisify5 } from "node:util";
17235
17650
  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;
17651
+ import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
17652
+ import { stat as stat3, readlink, readdir as readdir4, readFile as readFile4 } from "node:fs/promises";
17653
+ var run3 = promisify5(execFile5);
17654
+ var REQUEST_MS4 = 4e3;
17240
17655
  var MAX_BYTES3 = 8 * 1024 * 1024;
17241
- var MAX_SESSIONS3 = 100;
17656
+ var MAX_SESSIONS4 = 100;
17242
17657
  var MAX_PROCESSES2 = 4096;
17243
17658
  var MAX_ENDPOINTS = 32;
17244
17659
  var EVENT_DELAY_MS = 100;
@@ -17261,10 +17676,10 @@ function authorization(environment) {
17261
17676
  }
17262
17677
  async function linuxEndpoints() {
17263
17678
  const result = [];
17264
- const processes = (await readdir3("/proc")).filter((id) => /^\d+$/.test(id)).slice(0, MAX_PROCESSES2);
17679
+ const processes = (await readdir4("/proc")).filter((id) => /^\d+$/.test(id)).slice(0, MAX_PROCESSES2);
17265
17680
  const listeners = /* @__PURE__ */ new Map();
17266
17681
  for (const name of ["tcp", "tcp6"]) {
17267
- const table = await readFile3(`/proc/net/${name}`, "utf8").catch(() => "");
17682
+ const table = await readFile4(`/proc/net/${name}`, "utf8").catch(() => "");
17268
17683
  for (const line of table.split("\n").slice(1)) {
17269
17684
  const columns = line.trim().split(/\s+/);
17270
17685
  if (columns[3] !== "0A") continue;
@@ -17280,16 +17695,16 @@ async function linuxEndpoints() {
17280
17695
  const directory2 = `/proc/${pid}`;
17281
17696
  const owner = await stat3(directory2);
17282
17697
  if (process.getuid && owner.uid !== process.getuid()) continue;
17283
- const command2 = (await readFile3(join6(directory2, "cmdline"), "utf8")).slice(0, 4096);
17698
+ const command2 = (await readFile4(join7(directory2, "cmdline"), "utf8")).slice(0, 4096);
17284
17699
  if (!/(?:^|[\/\0 ])opencode(?:[\0 .]|$)/.test(command2)) continue;
17285
17700
  const environment = Object.fromEntries(
17286
- (await readFile3(join6(directory2, "environ"), "utf8")).split("\0").map((entry) => {
17701
+ (await readFile4(join7(directory2, "environ"), "utf8")).split("\0").map((entry) => {
17287
17702
  const split = entry.indexOf("=");
17288
17703
  return [entry.slice(0, split), entry.slice(split + 1)];
17289
17704
  })
17290
17705
  );
17291
- for (const fd of (await readdir3(join6(directory2, "fd"))).slice(0, 1024)) {
17292
- const target = await readlink(join6(directory2, "fd", fd)).catch(() => "");
17706
+ for (const fd of (await readdir4(join7(directory2, "fd"))).slice(0, 1024)) {
17707
+ const target = await readlink(join7(directory2, "fd", fd)).catch(() => "");
17293
17708
  const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1];
17294
17709
  const url2 = inode ? listeners.get(inode) : void 0;
17295
17710
  if (url2) result.push({ url: url2, authorization: authorization(environment) });
@@ -17318,7 +17733,7 @@ async function endpoints() {
17318
17733
  const { stdout } = await run3(
17319
17734
  "lsof",
17320
17735
  ["-nP", "-a", "-c", "opencode", "-iTCP", "-sTCP:LISTEN", "-Fn"],
17321
- { timeout: REQUEST_MS3, maxBuffer: MAX_BYTES3 }
17736
+ { timeout: REQUEST_MS4, maxBuffer: MAX_BYTES3 }
17322
17737
  );
17323
17738
  return stdout.split("\n").flatMap((line) => {
17324
17739
  const port = /^n(?:127\.0\.0\.1|\[::1\]|\*):(\d+)$/.exec(line)?.[1];
@@ -17335,7 +17750,7 @@ async function endpoints() {
17335
17750
  const { stdout } = await run3(
17336
17751
  "powershell.exe",
17337
17752
  ["-NoProfile", "-NonInteractive", "-Command", script],
17338
- { timeout: REQUEST_MS3, maxBuffer: MAX_BYTES3 }
17753
+ { timeout: REQUEST_MS4, maxBuffer: MAX_BYTES3 }
17339
17754
  );
17340
17755
  return stdout.split(/\r?\n/).filter((port) => /^\d+$/.test(port)).slice(0, MAX_ENDPOINTS).map((port) => ({
17341
17756
  url: localEndpoint(`http://127.0.0.1:${port}`),
@@ -17455,7 +17870,7 @@ var Server = class {
17455
17870
  };
17456
17871
  }
17457
17872
  async request(path, directory2, payload) {
17458
- const signal = AbortSignal.timeout(REQUEST_MS3);
17873
+ const signal = AbortSignal.timeout(REQUEST_MS4);
17459
17874
  const response = await fetch(this.endpoint.url + path, {
17460
17875
  method: payload === void 0 ? "GET" : "POST",
17461
17876
  headers: {
@@ -17482,15 +17897,25 @@ var Server = class {
17482
17897
  this.requests.set(id, pending);
17483
17898
  return pending;
17484
17899
  }
17900
+ // A session Dashup starts takes OpenCode's default agent and model for its folder.
17901
+ async start(cwd, text3) {
17902
+ const id = (await this.request("/session", cwd, {}))?.id;
17903
+ if (typeof id !== "string") throw new Error("OpenCode did not start a session");
17904
+ await this.request(`/session/${encodeURIComponent(id)}/prompt_async`, cwd, {
17905
+ parts: [{ type: "text", text: text3 }]
17906
+ });
17907
+ this.changed();
17908
+ return id;
17909
+ }
17485
17910
  async read() {
17486
17911
  const health = await this.request("/global/health");
17487
17912
  if (health?.healthy !== true || typeof health.version !== "string") return [];
17488
17913
  this.listen();
17489
- const sessions = await this.request(`/session?limit=${MAX_SESSIONS3}`);
17914
+ const sessions = await this.request(`/session?limit=${MAX_SESSIONS4}`);
17490
17915
  if (!Array.isArray(sessions)) return [];
17491
17916
  const result = [];
17492
17917
  const states = /* @__PURE__ */ new Map();
17493
- for (const session of sessions.slice(0, MAX_SESSIONS3)) {
17918
+ for (const session of sessions.slice(0, MAX_SESSIONS4)) {
17494
17919
  if (typeof session?.id !== "string" || typeof session.directory !== "string" || !session.directory)
17495
17920
  continue;
17496
17921
  try {
@@ -17566,7 +17991,7 @@ var Server = class {
17566
17991
  });
17567
17992
  this.changed();
17568
17993
  }),
17569
- approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, choice) => this.once(requestId, async () => {
17994
+ approve: approvals.some((approval3) => approval3.choices.length) ? (requestId, approvalId, choice) => this.once(requestId, async () => {
17570
17995
  if (this.stopped) throw new Error("OpenCode is disconnected");
17571
17996
  if (approvalId.startsWith("question:")) {
17572
17997
  const requestID = approvalId.slice("question:".length);
@@ -17615,7 +18040,7 @@ var Server = class {
17615
18040
  listen() {
17616
18041
  if (this.stream || this.retry || this.stopped) return;
17617
18042
  const controller = this.stream = new AbortController();
17618
- const connectTimer = setTimeout(() => controller.abort(), REQUEST_MS3);
18043
+ const connectTimer = setTimeout(() => controller.abort(), REQUEST_MS4);
17619
18044
  connectTimer.unref();
17620
18045
  void (async () => {
17621
18046
  const response = await fetch(this.endpoint.url + "/global/event", {
@@ -17684,15 +18109,57 @@ var Server = class {
17684
18109
  clearTimeout(this.notify);
17685
18110
  }
17686
18111
  };
18112
+ function serve(cwd) {
18113
+ const child = spawn4("opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
18114
+ cwd,
18115
+ stdio: ["ignore", "pipe", "ignore"],
18116
+ shell: process.platform === "win32"
18117
+ });
18118
+ const endpoint = new Promise((resolve3, reject) => {
18119
+ let output = "";
18120
+ child.stdout.on("data", (chunk) => {
18121
+ output = (output + String(chunk)).slice(-4096);
18122
+ const port = /https?:\/\/(?:127\.0\.0\.1|localhost):(\d+)/.exec(output)?.[1];
18123
+ if (port)
18124
+ resolve3({
18125
+ url: localEndpoint(`http://127.0.0.1:${port}`),
18126
+ authorization: authorization(process.env)
18127
+ });
18128
+ });
18129
+ child.once("error", () => reject(new Error("OpenCode is not installed on this computer")));
18130
+ child.once("exit", () => reject(new Error("OpenCode stopped before it started serving")));
18131
+ });
18132
+ return { process: child, endpoint };
18133
+ }
17687
18134
  var OpenCode = class {
17688
18135
  constructor(changed) {
17689
18136
  this.changed = changed;
17690
18137
  }
17691
18138
  changed;
17692
18139
  servers = /* @__PURE__ */ new Map();
18140
+ // Servers this connector started, by folder.
18141
+ started = /* @__PURE__ */ new Map();
18142
+ async start(cwd, text3) {
18143
+ let served = this.started.get(cwd);
18144
+ if (!served) this.started.set(cwd, served = serve(cwd));
18145
+ const endpoint = await served.endpoint.catch((error) => {
18146
+ this.started.delete(cwd);
18147
+ throw error;
18148
+ });
18149
+ let server = this.servers.get(endpoint.url);
18150
+ if (!server) {
18151
+ server = new Server(endpoint, this.changed);
18152
+ this.servers.set(endpoint.url, server);
18153
+ }
18154
+ return server.start(cwd, text3);
18155
+ }
17693
18156
  async refresh() {
17694
18157
  const discovered = await endpoints().catch(() => []);
17695
18158
  const current = new Map(discovered.map((endpoint) => [endpoint.url, endpoint]));
18159
+ for (const served of this.started.values()) {
18160
+ const endpoint = await served.endpoint.catch(() => void 0);
18161
+ if (endpoint) current.set(endpoint.url, endpoint);
18162
+ }
17696
18163
  for (const [url2, server] of this.servers)
17697
18164
  if (!current.has(url2)) {
17698
18165
  server.stop();
@@ -17713,6 +18180,8 @@ var OpenCode = class {
17713
18180
  stop() {
17714
18181
  for (const server of this.servers.values()) server.stop();
17715
18182
  this.servers.clear();
18183
+ for (const served of this.started.values()) served.process.kill();
18184
+ this.started.clear();
17716
18185
  }
17717
18186
  };
17718
18187
 
@@ -17721,6 +18190,8 @@ var DISCOVERY_MS = 1e4;
17721
18190
  var LiveHarnesses = class {
17722
18191
  listeners = /* @__PURE__ */ new Set();
17723
18192
  codex = new Codex(() => this.changed());
18193
+ headless = new Headless(() => this.changed());
18194
+ claude = new Claude(() => this.changed());
17724
18195
  vscode = new VSCode(() => this.changed());
17725
18196
  opencode = new OpenCode(() => this.changed());
17726
18197
  timer;
@@ -17734,8 +18205,10 @@ var LiveHarnesses = class {
17734
18205
  if (this.refreshing) return this.refreshing;
17735
18206
  this.refreshing = (async () => {
17736
18207
  const results = await Promise.allSettled([
18208
+ this.headless.refresh(),
17737
18209
  this.opencode.refresh(),
17738
18210
  this.codex.refresh(),
18211
+ this.claude.refresh(),
17739
18212
  this.vscode.refresh()
17740
18213
  ]);
17741
18214
  const sessions = /* @__PURE__ */ new Map();
@@ -17751,6 +18224,27 @@ var LiveHarnesses = class {
17751
18224
  });
17752
18225
  return this.refreshing;
17753
18226
  }
18227
+ // The agents whose own command runs on this computer.
18228
+ async agents() {
18229
+ const harnesses = ["claude", "codex", "opencode"];
18230
+ const runs = await Promise.all(
18231
+ harnesses.map((harness) => promisify6(execFile6)(harness, ["--version"], {
18232
+ timeout: 1e4,
18233
+ shell: process.platform === "win32"
18234
+ }).then(
18235
+ () => true,
18236
+ () => false
18237
+ ))
18238
+ );
18239
+ return harnesses.filter((_, index) => runs[index]);
18240
+ }
18241
+ // Starts a session in `cwd` with its first message, and resolves with the session's native id.
18242
+ start(harness, cwd, text3) {
18243
+ return { claude: this.headless, codex: this.codex, opencode: this.opencode }[harness].start(
18244
+ cwd,
18245
+ text3
18246
+ );
18247
+ }
17754
18248
  watch(onChange) {
17755
18249
  this.listeners.add(onChange);
17756
18250
  if (!this.timer && process.env.DASHUP_LIVE_DISCOVERY !== "0") {
@@ -17769,20 +18263,22 @@ var LiveHarnesses = class {
17769
18263
  this.stopped = true;
17770
18264
  clearInterval(this.timer);
17771
18265
  this.listeners.clear();
18266
+ this.headless.stop();
17772
18267
  this.opencode.stop();
17773
18268
  this.codex.stop();
18269
+ this.claude.stop();
17774
18270
  this.vscode.stop();
17775
18271
  }
17776
18272
  };
17777
18273
 
17778
18274
  // src/harness/queue.ts
17779
- import { randomUUID as randomUUID4 } from "node:crypto";
17780
- import { spawn as spawn3 } from "node:child_process";
18275
+ import { randomUUID as randomUUID5 } from "node:crypto";
18276
+ import { spawn as spawn5 } from "node:child_process";
17781
18277
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
17782
- import { join as join7, dirname as dirname4, basename as basename3 } from "node:path";
18278
+ import { join as join8, dirname as dirname5, basename as basename3 } from "node:path";
17783
18279
  import { statSync as statSync2, existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync3, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
17784
18280
  var CODEX_VERSION = "0.160.1";
17785
- var REQUEST_MS4 = 1e4;
18281
+ var REQUEST_MS5 = 1e4;
17786
18282
  var MAX_BYTES4 = 1024 * 1024;
17787
18283
  var MAX_PROCESSES3 = 4096;
17788
18284
  var MAX_FILES = 4096;
@@ -17798,9 +18294,9 @@ function sameFile(left, right) {
17798
18294
  return left.path === right.path && left.device === right.device && left.inode === right.inode;
17799
18295
  }
17800
18296
  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");
18297
+ let directory2 = dirname5(executable);
18298
+ for (let depth = 0; depth < 8; depth++, directory2 = dirname5(directory2)) {
18299
+ const path = join8(directory2, "package.json");
17804
18300
  if (!existsSync5(path)) continue;
17805
18301
  const value2 = JSON.parse(readFileSync3(path, "utf8"));
17806
18302
  if (value2.name?.startsWith("@openai/codex-")) return value2.version;
@@ -17810,40 +18306,40 @@ function packageVersion(executable) {
17810
18306
  function processOwners(pid) {
17811
18307
  const root = `/proc/${pid}`;
17812
18308
  if (process.getuid && statSync2(root).uid !== process.getuid()) return [];
17813
- const executable = realpathSync2.native(join7(root, "exe"));
18309
+ const executable = realpathSync2.native(join8(root, "exe"));
17814
18310
  if (basename3(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
17815
- const status2 = readFileSync3(join7(root, "stat"), "utf8");
18311
+ const status2 = readFileSync3(join8(root, "stat"), "utf8");
17816
18312
  const start = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
17817
18313
  if (!start || !/^\d+$/.test(start)) return [];
17818
- const fds = readdirSync3(join7(root, "fd")).slice(0, MAX_FILES);
18314
+ const fds = readdirSync3(join8(root, "fd")).slice(0, MAX_FILES);
17819
18315
  const files2 = fds.flatMap((fd) => {
17820
18316
  try {
17821
- return [{ fd, path: readlinkSync(join7(root, "fd", fd)) }];
18317
+ return [{ fd, path: readlinkSync(join8(root, "fd", fd)) }];
17822
18318
  } catch {
17823
18319
  return [];
17824
18320
  }
17825
18321
  });
17826
18322
  return files2.flatMap(({ fd, path }) => {
17827
- if (basename3(dirname4(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
18323
+ if (basename3(dirname5(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
17828
18324
  const thread = basename3(path, ".lock");
17829
18325
  if (!THREAD_ID.test(thread)) return [];
17830
18326
  try {
17831
- const lock = readFileSync3(join7(root, "fdinfo", fd), "utf8");
18327
+ const lock = readFileSync3(join8(root, "fdinfo", fd), "utf8");
17832
18328
  if (!/FLOCK\s+ADVISORY\s+WRITE/.test(lock)) return [];
17833
- const home = realpathSync2.native(dirname4(dirname4(path)));
18329
+ const home = realpathSync2.native(dirname5(dirname5(path)));
17834
18330
  const directory2 = statSync2(home);
17835
18331
  if (process.getuid && directory2.uid !== process.getuid()) return [];
17836
- const queue = identity(join7(home, "queue_1.sqlite"));
18332
+ const queue = identity(join8(home, "queue_1.sqlite"));
17837
18333
  if (!files2.some((file) => {
17838
18334
  try {
17839
- return sameFile(identity(join7(root, "fd", file.fd)), queue);
18335
+ return sameFile(identity(join8(root, "fd", file.fd)), queue);
17840
18336
  } catch {
17841
18337
  return false;
17842
18338
  }
17843
18339
  }))
17844
18340
  return [];
17845
18341
  const rollout = files2.find(
17846
- (file) => file.path.startsWith(join7(home, "sessions") + "/") && file.path.endsWith(`-${thread}.jsonl`)
18342
+ (file) => file.path.startsWith(join8(home, "sessions") + "/") && file.path.endsWith(`-${thread}.jsonl`)
17847
18343
  );
17848
18344
  if (!rollout) return [];
17849
18345
  return [
@@ -17855,7 +18351,7 @@ function processOwners(pid) {
17855
18351
  thread,
17856
18352
  queue,
17857
18353
  version: CODEX_VERSION,
17858
- cwd: realpathSync2.native(join7(root, "cwd")),
18354
+ cwd: realpathSync2.native(join8(root, "cwd")),
17859
18355
  rollout: identity(rollout.path)
17860
18356
  }
17861
18357
  ];
@@ -17901,7 +18397,7 @@ function validateOwner(owner) {
17901
18397
  function validateEvidence(owner) {
17902
18398
  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
18399
  throw new Error("The original native history storage changed");
17904
- const path = join7(owner.home, "thread_history_1.sqlite");
18400
+ const path = join8(owner.home, "thread_history_1.sqlite");
17905
18401
  if (existsSync5(path) && identity(path).path !== path)
17906
18402
  throw new Error("Native history is outside the original home");
17907
18403
  }
@@ -17918,7 +18414,7 @@ function nativeReceipt(owner, clientId) {
17918
18414
  } finally {
17919
18415
  queue.close();
17920
18416
  }
17921
- const path = join7(owner.home, "thread_history_1.sqlite");
18417
+ const path = join8(owner.home, "thread_history_1.sqlite");
17922
18418
  if (!existsSync5(path)) return { queueId };
17923
18419
  const history = new DatabaseSync3(path, { readOnly: true });
17924
18420
  try {
@@ -17974,7 +18470,7 @@ var Writer = class {
17974
18470
  tail = "";
17975
18471
  pending = /* @__PURE__ */ new Map();
17976
18472
  constructor(owner) {
17977
- this.process = spawn3(owner.executable, ["app-server", "--listen", "stdio://"], {
18473
+ this.process = spawn5(owner.executable, ["app-server", "--listen", "stdio://"], {
17978
18474
  cwd: owner.cwd,
17979
18475
  env: { ...process.env, CODEX_HOME: owner.home },
17980
18476
  stdio: ["pipe", "pipe", "ignore"]
@@ -18014,7 +18510,7 @@ var Writer = class {
18014
18510
  this.pending.delete(id);
18015
18511
  reject(new Error("Native queue acknowledgement was not received"));
18016
18512
  this.close();
18017
- }, REQUEST_MS4);
18513
+ }, REQUEST_MS5);
18018
18514
  this.pending.set(id, { resolve: resolve3, reject, timer });
18019
18515
  this.process.stdin.write(JSON.stringify({ id, method, params }) + "\n");
18020
18516
  });
@@ -18061,7 +18557,7 @@ var NativeQueue = class {
18061
18557
  if (existing) return existing;
18062
18558
  if (this.stopped) throw new Error("The connector is stopping");
18063
18559
  validateOwner(scope2.owner);
18064
- const clientUserMessageId = randomUUID4();
18560
+ const clientUserMessageId = randomUUID5();
18065
18561
  const request2 = { ...scope2, receipt: { state: "unknown", clientUserMessageId } };
18066
18562
  let writer;
18067
18563
  try {
@@ -18146,7 +18642,13 @@ var Command = z2.object({
18146
18642
  z2.object({ kind: z2.literal("thread.stop") }).strict(),
18147
18643
  z2.object({ kind: z2.literal("approval.respond"), approvalId: z2.string(), choice: z2.string() }).strict(),
18148
18644
  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()
18645
+ z2.object({ kind: z2.literal("screen.stop"), sessionId: z2.string().min(1).max(128) }).strict(),
18646
+ z2.object({
18647
+ kind: z2.literal("session.start"),
18648
+ harness: z2.enum(["claude", "codex", "opencode"]),
18649
+ text: z2.string().min(1).max(1e5),
18650
+ folder: z2.object({ parent: z2.string().min(1), name: z2.string().min(1).max(255) }).strict().optional()
18651
+ }).strict()
18150
18652
  ])
18151
18653
  }).strict();
18152
18654
  var Connector = class {
@@ -18208,6 +18710,8 @@ var Connector = class {
18208
18710
  listed = /* @__PURE__ */ new Map();
18209
18711
  listedProjects = /* @__PURE__ */ new Map();
18210
18712
  overviewQueued = false;
18713
+ // The agents that can start sessions here.
18714
+ startable = [];
18211
18715
  snapshots = /* @__PURE__ */ new Set();
18212
18716
  nativeSnapshots = /* @__PURE__ */ new Map();
18213
18717
  queueReceipts = /* @__PURE__ */ new Map();
@@ -18221,6 +18725,7 @@ var Connector = class {
18221
18725
  this.nativeSnapshots.set(thread.id, structuredClone(this.threadState(project, thread)));
18222
18726
  }
18223
18727
  this.refreshQueues();
18728
+ void this.findAgents();
18224
18729
  this.stopWatching = this.discovery.watch(this.rescan);
18225
18730
  this.releases.push(this.harnesses.watch(this.rescan));
18226
18731
  this.releases.push(
@@ -18259,15 +18764,15 @@ var Connector = class {
18259
18764
  for (const project of this.discovery.projects.values())
18260
18765
  for (const thread of project.threads) {
18261
18766
  const native = this.discovery.locations.get(thread.id)?.live;
18262
- const approval2 = native?.approvals[0];
18263
- if (!approval2) continue;
18767
+ const approval3 = native?.approvals[0];
18768
+ if (!approval3) continue;
18264
18769
  await this.thread(pair, project, thread);
18265
18770
  await this.send(pair, {
18266
18771
  type: "attention",
18267
18772
  projectId: project.id,
18268
18773
  threadId: thread.id,
18269
- title: approval2.title,
18270
- body: approval2.detail,
18774
+ title: approval3.title,
18775
+ body: approval3.detail,
18271
18776
  reason: "approval"
18272
18777
  });
18273
18778
  }
@@ -18435,6 +18940,10 @@ var Connector = class {
18435
18940
  }
18436
18941
  return {
18437
18942
  type: "projects",
18943
+ version,
18944
+ screen: this.discovery.capabilities.screen,
18945
+ agents: this.startable,
18946
+ folders: this.discovery.folders(),
18438
18947
  projects: [...this.listedProjects.values()],
18439
18948
  sessions: [
18440
18949
  ...sessions.filter(busy),
@@ -18442,6 +18951,11 @@ var Connector = class {
18442
18951
  ]
18443
18952
  };
18444
18953
  }
18954
+ // Agents change only when someone installs or removes one, so a refresh asks again.
18955
+ async findAgents() {
18956
+ this.startable = await this.harnesses.agents();
18957
+ this.refreshOverview();
18958
+ }
18445
18959
  async listProjects(pair) {
18446
18960
  const overview = this.overview();
18447
18961
  const text3 = JSON.stringify(overview);
@@ -18507,9 +19021,9 @@ var Connector = class {
18507
19021
  if (JSON.stringify(prior) === JSON.stringify(snapshot)) continue;
18508
19022
  this.nativeSnapshots.set(thread.id, structuredClone(snapshot));
18509
19023
  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");
19024
+ for (const approval3 of session.approvals)
19025
+ if (!prior?.approvals.some((item) => item.id === approval3.id))
19026
+ await this.attention(project.id, thread.id, approval3.title, approval3.detail, "approval");
18513
19027
  if (prior?.thread.status === "working" && thread.status === "idle")
18514
19028
  await this.attention(
18515
19029
  project.id,
@@ -18591,18 +19105,21 @@ var Connector = class {
18591
19105
  async execute(pair, content, envelope) {
18592
19106
  let queue = void 0;
18593
19107
  try {
18594
- if (["screen.start", "approval.respond", "thread.stop"].includes(content.action.kind) && Math.abs(Date.now() - Date.parse(envelope.sentAt)) > EPHEMERAL_TTL_MS)
19108
+ if (["screen.start", "approval.respond", "thread.stop", "session.start"].includes(
19109
+ content.action.kind
19110
+ ) && Math.abs(Date.now() - Date.parse(envelope.sentAt)) > EPHEMERAL_TTL_MS)
18595
19111
  throw new Error("Command expired. Try again while this computer is online.");
18596
- queue = await this.command(pair, content);
19112
+ const outcome = await this.command(pair, content);
18597
19113
  delete this.config.unconfirmed[content.requestId];
18598
- if (queue) {
19114
+ if (outcome && "state" in outcome) {
19115
+ queue = outcome;
18599
19116
  await this.queueResult(pair, content.requestId, content.projectId, queue);
18600
19117
  this.queueReceipts.set(content.requestId, JSON.stringify(queue));
18601
19118
  return;
18602
19119
  }
18603
19120
  await this.send(
18604
19121
  pair,
18605
- { type: "result", requestId: content.requestId, ok: true },
19122
+ { type: "result", requestId: content.requestId, ok: true, ...outcome ? { started: outcome } : {} },
18606
19123
  content.projectId
18607
19124
  );
18608
19125
  } catch (error) {
@@ -18637,7 +19154,7 @@ var Connector = class {
18637
19154
  }
18638
19155
  }
18639
19156
  attach(socket) {
18640
- const sessions = bridgeAcp(socket, (session, approval2) => {
19157
+ const sessions = bridgeAcp(socket, (session, approval3) => {
18641
19158
  this.agents.set(session.id, session);
18642
19159
  const { project, thread } = this.adopt(session);
18643
19160
  if (!this.snapshots.has(session.id)) {
@@ -18652,15 +19169,15 @@ var Connector = class {
18652
19169
  }
18653
19170
  }).catch((error) => this.report(error));
18654
19171
  }
18655
- if (approval2)
19172
+ if (approval3)
18656
19173
  this.work = this.work.then(async () => {
18657
- if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(approval2))
19174
+ if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(approval3))
18658
19175
  return;
18659
19176
  await this.attention(
18660
19177
  project.id,
18661
19178
  thread.id,
18662
- approval2.title,
18663
- approval2.detail,
19179
+ approval3.title,
19180
+ approval3.detail,
18664
19181
  "approval"
18665
19182
  );
18666
19183
  }).catch((error) => this.report(error));
@@ -18729,9 +19246,12 @@ var Connector = class {
18729
19246
  }
18730
19247
  async command(pair, command2) {
18731
19248
  if (command2.action.kind === "projects.refresh") {
19249
+ await this.findAgents();
18732
19250
  await this.changed();
18733
19251
  return this.publishProjects(pair);
18734
19252
  }
19253
+ if (command2.action.kind === "session.start")
19254
+ return this.startSession(command2.projectId, command2.action);
18735
19255
  const project = this.discovery.projects.get(command2.projectId);
18736
19256
  if (!project) throw new Error("Project is no longer open on this computer");
18737
19257
  const thread = project.threads.find((item) => item.id === command2.threadId);
@@ -18788,15 +19308,15 @@ var Connector = class {
18788
19308
  const action = command2.action;
18789
19309
  const native = this.discovery.locations.get(thread.id)?.live;
18790
19310
  if (native?.approve) {
18791
- const approval3 = native.approvals.find((item) => item.id === action.approvalId);
18792
- if (!approval3?.choices.some((choice) => choice.id === action.choice))
19311
+ const approval4 = native.approvals.find((item) => item.id === action.approvalId);
19312
+ if (!approval4?.choices.some((choice) => choice.id === action.choice))
18793
19313
  throw new Error("Approval is no longer available");
18794
- return native.approve(command2.requestId, approval3.id, action.choice);
19314
+ return native.approve(command2.requestId, approval4.id, action.choice);
18795
19315
  }
18796
19316
  const agent = this.attached(thread);
18797
19317
  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))
19318
+ const [request2, approval3] = [...agent.approvals].find(([, item]) => item.id === action.approvalId) ?? [];
19319
+ if (!approval3?.choices.some((choice) => choice.id === action.choice))
18800
19320
  throw new Error("Approval is no longer available");
18801
19321
  return commandAcp(agent, command2.requestId, {
18802
19322
  kind: "approval",
@@ -18812,6 +19332,37 @@ var Connector = class {
18812
19332
  return;
18813
19333
  }
18814
19334
  }
19335
+ // Starts an agent in a project, or in a new folder inside one this computer offers. Commands run
19336
+ // in the work queue, so this refreshes directly rather than queueing a change behind itself.
19337
+ async startSession(projectId, { harness, text: text3, folder }) {
19338
+ if (!this.startable.includes(harness))
19339
+ throw new Error(`${HARNESSES[harness]} doesn't run on this computer`);
19340
+ let cwd;
19341
+ if (folder) {
19342
+ const name = folder.name.trim();
19343
+ if (!this.discovery.folders().includes(folder.parent))
19344
+ throw new Error("That folder is no longer offered for new projects");
19345
+ if (!name || name === "." || name === ".." || /[\\/:*?"<>|\x00-\x1f]/.test(name))
19346
+ throw new Error("Name the folder without slashes or special characters");
19347
+ cwd = join9(folder.parent, name);
19348
+ await mkdir(cwd).catch((error) => {
19349
+ throw new Error(error.code === "EEXIST" ? `${name} already exists` : `Couldn't create ${name}`);
19350
+ });
19351
+ } else {
19352
+ const project = this.discovery.projects.get(projectId);
19353
+ if (!project) throw new Error("Project is no longer open on this computer");
19354
+ if (project.remote)
19355
+ throw new Error(`Install -up on ${project.remote.host} to start sessions there`);
19356
+ cwd = project.path;
19357
+ }
19358
+ const session = await this.harnesses.start(harness, cwd, text3);
19359
+ await this.changed();
19360
+ for (const project of this.discovery.projects.values())
19361
+ for (const thread of project.threads)
19362
+ if (thread.agent?.harness === harness && thread.agent.sessionId === session)
19363
+ return { projectId: project.id, threadId: thread.id };
19364
+ throw new Error(`${HARNESSES[harness]} started, but this computer hasn't listed it yet`);
19365
+ }
18815
19366
  report(error) {
18816
19367
  process.stderr.write(`${error instanceof Error ? error.message : "Dashup operation failed"}
18817
19368
  `);
@@ -18821,18 +19372,18 @@ var Connector = class {
18821
19372
  // src/install.ts
18822
19373
  import { existsSync as existsSync6 } from "node:fs";
18823
19374
  import { homedir as homedir6, userInfo } from "node:os";
18824
- import { promisify as promisify5 } from "node:util";
19375
+ import { promisify as promisify7 } from "node:util";
18825
19376
  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);
19377
+ import { execFile as execFile7, spawn as spawn6 } from "node:child_process";
19378
+ import { cp, mkdir as mkdir2, rm, writeFile } from "node:fs/promises";
19379
+ import { dirname as dirname6, join as join10, relative, sep } from "node:path";
19380
+ var run4 = promisify7(execFile7);
18830
19381
  var xml = (value2) => value2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
18831
19382
  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"),
19383
+ var serviceFile = () => process.platform === "darwin" ? join10(homedir6(), "Library/LaunchAgents/com.dashup.connector.plist") : process.platform === "win32" ? join10(
19384
+ process.env.APPDATA ?? join10(homedir6(), "AppData/Roaming"),
18834
19385
  "Microsoft/Windows/Start Menu/Programs/Startup/Dashup.vbs"
18835
- ) : join8(homedir6(), ".config/systemd/user/dashup.service");
19386
+ ) : join10(homedir6(), ".config/systemd/user/dashup.service");
18836
19387
  var startupInstalled = () => existsSync6(serviceFile());
18837
19388
  function service(platform, node, entry, config2) {
18838
19389
  const command2 = [node, ...NODE_FLAGS, entry];
@@ -18868,17 +19419,17 @@ async function durableEntry() {
18868
19419
  );
18869
19420
  if (!["_npx", "dlx"].some((cache2) => entry.includes(`${sep}${cache2}${sep}`))) return entry;
18870
19421
  const root = entry.slice(0, entry.indexOf(`${sep}node_modules${sep}`));
18871
- const app = join8(dirname5(configPath()), "app");
19422
+ const app = join10(dirname6(configPath()), "app");
18872
19423
  await rm(app, { recursive: true, force: true });
18873
- await cp(join8(root, "node_modules"), join8(app, "node_modules"), {
19424
+ await cp(join10(root, "node_modules"), join10(app, "node_modules"), {
18874
19425
  recursive: true,
18875
19426
  verbatimSymlinks: true
18876
19427
  });
18877
- return join8(app, relative(root, entry));
19428
+ return join10(app, relative(root, entry));
18878
19429
  }
18879
19430
  async function installStartup() {
18880
19431
  const path = serviceFile();
18881
- await mkdir(dirname5(path), { recursive: true });
19432
+ await mkdir2(dirname6(path), { recursive: true });
18882
19433
  await writeFile(
18883
19434
  path,
18884
19435
  service(process.platform, process.execPath, await durableEntry(), configPath())
@@ -18888,7 +19439,7 @@ async function installStartup() {
18888
19439
  });
18889
19440
  await run4("launchctl", ["load", path]);
18890
19441
  } else if (process.platform === "win32") {
18891
- spawn4("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
19442
+ spawn6("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
18892
19443
  } else {
18893
19444
  await run4("systemctl", ["--user", "daemon-reload"]);
18894
19445
  await run4("systemctl", ["--user", "enable", "dashup.service"]);
@@ -18908,7 +19459,7 @@ async function uninstallStartup() {
18908
19459
  await run4("systemctl", ["--user", "disable", "dashup.service"]).catch(() => {
18909
19460
  });
18910
19461
  await rm(path, { force: true });
18911
- await rm(join8(dirname5(configPath()), "app"), { recursive: true, force: true });
19462
+ await rm(join10(dirname6(configPath()), "app"), { recursive: true, force: true });
18912
19463
  }
18913
19464
 
18914
19465
  // src/index.ts