@dashup/connector 0.1.3 → 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 +20 -23
  2. package/dist/index.js +782 -170
  3. package/package.json +1 -1
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
  }
@@ -2706,7 +2706,7 @@ var require_pluralize = __commonJS({
2706
2706
 
2707
2707
  // src/index.ts
2708
2708
  import { randomBytes as randomBytes5, timingSafeEqual } from "node:crypto";
2709
- import { createServer } from "node:http";
2709
+ import { createServer as createServer2 } from "node:http";
2710
2710
  import * as ui2 from "@clack/prompts";
2711
2711
  import { z as z3 } from "zod";
2712
2712
 
@@ -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();
@@ -4004,13 +4008,16 @@ async function proxyAcp(config2, executable, args2) {
4004
4008
 
4005
4009
  // src/pair.ts
4006
4010
  import { hostname } from "node:os";
4011
+ import { promisify } from "node:util";
4012
+ import { createServer } from "node:http";
4013
+ import { execFile } from "node:child_process";
4007
4014
  import { randomBytes as randomBytes2, createHash as createHash2 } from "node:crypto";
4008
4015
  import QRCode from "qrcode";
4009
4016
  import * as ui from "@clack/prompts";
4010
4017
  import { z } from "zod";
4011
4018
 
4012
4019
  // package.json
4013
- var version = "0.1.3";
4020
+ var version = "0.1.5";
4014
4021
 
4015
4022
  // src/pair.ts
4016
4023
  var Invitation = z.object({
@@ -4026,29 +4033,55 @@ function invitation(config2, secret = config2.secret) {
4026
4033
  const computer = createHash2("sha256").update(secret).digest("hex");
4027
4034
  return { code, key, text: formatPairingCode({ api: config2.api, computer, code, key }) };
4028
4035
  }
4029
- async function scanned(config2, { code, text: text3 }, done, secret = config2.secret) {
4030
- const qr = await QRCode.toString(text3, {
4031
- type: "terminal",
4032
- small: true,
4033
- errorCorrectionLevel: "L"
4036
+ var SCAN = "Scan this in the Dashup app under Pair a computer. It expires in 5 minutes.";
4037
+ var BROWSER = {
4038
+ darwin: ["open"],
4039
+ win32: ["rundll32", "url.dll,FileProtocolHandler"]
4040
+ };
4041
+ var NO_BROWSER = "No browser here can show the pairing code. Run dashup pair in a terminal on this computer.";
4042
+ async function show(text3) {
4043
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY)
4044
+ throw new Error(NO_BROWSER);
4045
+ const path = `/${randomBytes2(16).toString("hex")}`;
4046
+ const qr = await QRCode.toString(text3, { type: "svg", margin: 2, errorCorrectionLevel: "L" });
4047
+ const page = `<!doctype html><meta charset="utf-8"><title>Pair with Dashup</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#fff;color:#16181d;font:16px/1.5 system-ui,sans-serif}main{max-width:22rem;padding:2rem;text-align:center}svg{width:100%;margin:1rem 0}code{font-size:11px;word-break:break-all;color:#5d6470}</style><main><p>${SCAN}</p>${qr}<p>Can't scan? Paste this code instead:</p><code>${text3}</code></main>`;
4048
+ const server = createServer((request2, response) => {
4049
+ if (request2.url !== path) return void response.writeHead(404).end();
4050
+ response.writeHead(200, {
4051
+ "Content-Type": "text/html; charset=utf-8",
4052
+ "Cache-Control": "no-store",
4053
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
4054
+ "Referrer-Policy": "no-referrer"
4055
+ });
4056
+ response.end(page);
4034
4057
  });
4035
- const rows = qr.split("\n").filter((row) => row.replace(/\x1b\[\d+m/g, ""));
4058
+ await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
4059
+ const [command2, ...args2] = BROWSER[process.platform] ?? ["xdg-open"];
4060
+ const url2 = `http://127.0.0.1:${server.address().port}${path}`;
4061
+ try {
4062
+ await promisify(execFile)(command2, [...args2, url2]);
4063
+ } catch {
4064
+ server.close();
4065
+ throw new Error(NO_BROWSER);
4066
+ }
4067
+ process.stdout.write(`Opened the pairing code in a browser on this computer. ${SCAN}
4068
+ `);
4069
+ return () => server.close();
4070
+ }
4071
+ async function scanned(config2, { code, text: text3 }, done, secret = config2.secret) {
4036
4072
  if (interactive) {
4037
- ui.log.step("Scan this in the Dashup app under Pair a computer. It expires in 5 minutes.");
4073
+ const qr = await QRCode.toString(text3, {
4074
+ type: "terminal",
4075
+ small: true,
4076
+ errorCorrectionLevel: "L"
4077
+ });
4078
+ ui.log.step(SCAN);
4038
4079
  ui.log.message("Can't scan? Paste this code instead:", { spacing: 0 });
4039
4080
  process.stdout.write(` ${text3}
4040
4081
  `);
4041
- ui.log.message(rows);
4042
- } else {
4043
- process.stdout.write(
4044
- `
4045
- Pairing code: ${text3}
4046
-
4047
- ${rows.join("\n")}
4048
- Scan this in the Dashup app (Pair a computer). It expires in 5 minutes.
4049
- `
4050
- );
4082
+ ui.log.message(qr.split("\n").filter((row) => row.replace(/\x1b\[\d+m/g, "")));
4051
4083
  }
4084
+ const close = interactive ? void 0 : await show(text3);
4052
4085
  const waiting = interactive ? ui.spinner() : void 0;
4053
4086
  waiting?.start("Waiting for your phone");
4054
4087
  try {
@@ -4071,16 +4104,18 @@ Scan this in the Dashup app (Pair a computer). It expires in 5 minutes.
4071
4104
  if (!waiting) throw error;
4072
4105
  waiting.error(error instanceof Error ? error.message : "Pairing failed");
4073
4106
  throw Object.assign(error, { reported: true });
4107
+ } finally {
4108
+ close?.();
4074
4109
  }
4075
4110
  }
4076
4111
  var reported = (error) => error?.reported === true;
4077
- async function link(config2) {
4112
+ async function link(config2, account) {
4078
4113
  const secret = newSecret(randomBytes2);
4079
4114
  const invite = invitation(config2, secret);
4080
4115
  const { pairing, email } = await scanned(
4081
4116
  config2,
4082
4117
  invite,
4083
- (account) => `Scanned by ${account}`,
4118
+ (account2) => `Scanned by ${account2}`,
4084
4119
  secret
4085
4120
  );
4086
4121
  const linked = { ...config2, secret };
@@ -4091,14 +4126,13 @@ async function link(config2) {
4091
4126
  kind: "desktop",
4092
4127
  platform
4093
4128
  });
4094
- const confirmed = await ui.confirm({
4095
- message: `Pair this computer with ${email}?`,
4096
- initialValue: false
4097
- });
4129
+ const confirmed = account ? email.toLowerCase() === account.toLowerCase() : await ui.confirm({ message: `Pair this computer with ${email}?`, initialValue: false });
4098
4130
  if (confirmed !== true) {
4099
4131
  await api(linked, `/dashup/pairings/${pairing.id}`, void 0, "DELETE").catch(() => {
4100
4132
  });
4101
- ui.cancel("Pairing cancelled. Nothing was shared.");
4133
+ ui.cancel(
4134
+ account ? `Pairing cancelled: that phone uses ${email}, not ${account}. Nothing was shared.` : "Pairing cancelled. Nothing was shared."
4135
+ );
4102
4136
  return false;
4103
4137
  }
4104
4138
  Object.assign(config2, {
@@ -4120,9 +4154,9 @@ import { createInterface as createInterface2 } from "node:readline";
4120
4154
  import { spawn as spawn2 } from "node:child_process";
4121
4155
 
4122
4156
  // src/screen.ts
4123
- import { promisify } from "node:util";
4124
- import { execFile, spawnSync } from "node:child_process";
4125
- var run = promisify(execFile);
4157
+ import { promisify as promisify2 } from "node:util";
4158
+ import { execFile as execFile2, spawnSync } from "node:child_process";
4159
+ var run = promisify2(execFile2);
4126
4160
  var powershell = (source) => run("powershell.exe", [
4127
4161
  "-NoProfile",
4128
4162
  "-NonInteractive",
@@ -4379,7 +4413,7 @@ var Viewer = class _Viewer {
4379
4413
  // src/discovery.ts
4380
4414
  import { homedir as homedir3 } from "node:os";
4381
4415
  import { createHmac } from "node:crypto";
4382
- import { basename, join as join3 } from "node:path";
4416
+ import { basename, dirname as dirname3, join as join3 } from "node:path";
4383
4417
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
4384
4418
  import { zstdDecompressSync } from "node:zlib";
4385
4419
  import { watch, existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
@@ -4539,6 +4573,7 @@ function title(value2, fallback) {
4539
4573
  const text3 = typeof value2 === "string" ? value2.trim().split("\n")[0].trim() : "";
4540
4574
  return text3 ? text3.slice(0, TITLE_LIMIT) : fallback;
4541
4575
  }
4576
+ var injected = (content) => /^\s*(# AGENTS\.md instructions for |<[a-z_][\w -]*>)/i.test(text(content));
4542
4577
  function text(value2) {
4543
4578
  if (typeof value2 === "string") return value2;
4544
4579
  if (Array.isArray(value2)) return value2.map(text).filter(Boolean).join("\n");
@@ -4607,6 +4642,7 @@ function step(id, name, input, result) {
4607
4642
  }
4608
4643
  };
4609
4644
  }
4645
+ var claudeHome = () => process.env.CLAUDE_CONFIG_DIR ?? join2(homedir2(), ".claude");
4610
4646
  var claudeMessage = (row) => (row.type === "user" || row.type === "assistant") && !row.isSidechain;
4611
4647
  function claudeChange(change) {
4612
4648
  if (typeof change?.filePath !== "string") return void 0;
@@ -4684,7 +4720,9 @@ function claude(root, scan) {
4684
4720
  continue;
4685
4721
  }
4686
4722
  const named = latest.find((row) => row.type === "custom-title" || row.type === "ai-title");
4687
- 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
+ );
4688
4726
  scan.sessions.push({
4689
4727
  harness: "claude",
4690
4728
  id: owner.sessionId,
@@ -4732,7 +4770,7 @@ function codexMessages(path) {
4732
4770
  if (!item) return [];
4733
4771
  const id = String(item.id ?? row.ordinal ?? `${index}`);
4734
4772
  if (item.type === "message" && ["user", "assistant"].includes(item.role))
4735
- return prose(id, item.role, item.content);
4773
+ return item.role === "user" && injected(item.content) ? [] : prose(id, item.role, item.content);
4736
4774
  if (!["function_call", "custom_tool_call"].includes(item.type)) return [];
4737
4775
  const input = item.type === "function_call" ? parse(String(item.arguments)) ?? item.arguments : item.input;
4738
4776
  const output = outputs.get(item.call_id);
@@ -4773,6 +4811,7 @@ function codex(root, scan) {
4773
4811
  scan.warnings.push(`Cannot read Codex session index: ${index}`);
4774
4812
  }
4775
4813
  }
4814
+ const titles = codexTitles(root);
4776
4815
  for (const path of [
4777
4816
  ...files(join2(root, "sessions"), scan),
4778
4817
  ...files(join2(root, "archived_sessions"), scan)
@@ -4788,13 +4827,16 @@ function codex(root, scan) {
4788
4827
  }
4789
4828
  const named = names.get(owner.id);
4790
4829
  const first = rows.find(
4791
- (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)
4792
4831
  );
4793
4832
  scan.sessions.push({
4794
4833
  harness: "codex",
4795
4834
  id: owner.id,
4796
4835
  cwd,
4797
- 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
+ ),
4798
4840
  parent: typeof owner.parent_thread_id === "string" ? owner.parent_thread_id : void 0,
4799
4841
  updatedAt: date(latest.find(codexItem)?.timestamp, statSync(path).mtimeMs),
4800
4842
  files: [path, ...existsSync3(index) ? [index] : []],
@@ -4863,6 +4905,30 @@ function databaseMessages(path, session) {
4863
4905
  db.close();
4864
4906
  }
4865
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
+ }
4866
4932
  function openCodeDatabase(path, scan) {
4867
4933
  if (!existsSync3(path)) {
4868
4934
  watchParent(path, scan);
@@ -4961,7 +5027,7 @@ function openCodeLegacy(root, scan) {
4961
5027
  function scanHistories() {
4962
5028
  const home = homedir2();
4963
5029
  const scan = { sessions: [], paths: /* @__PURE__ */ new Set(), warnings: [] };
4964
- claude(process.env.CLAUDE_CONFIG_DIR ?? join2(home, ".claude"), scan);
5030
+ claude(claudeHome(), scan);
4965
5031
  codex(process.env.CODEX_HOME ?? join2(home, ".codex"), scan);
4966
5032
  const opencode = join2(process.env.XDG_DATA_HOME ?? join2(home, ".local/share"), "opencode");
4967
5033
  openCodeDatabase(process.env.OPENCODE_DB ?? join2(opencode, "opencode.db"), scan);
@@ -5103,7 +5169,7 @@ var Discovery = class {
5103
5169
  const project = this.project(session.cwd);
5104
5170
  const thread = {
5105
5171
  id: agentThread(session.harness, session.id, this.secret),
5106
- title: session.title,
5172
+ title: session.title ?? "",
5107
5173
  editor: "cli",
5108
5174
  status: "unknown",
5109
5175
  updatedAt: session.updatedAt,
@@ -5122,13 +5188,24 @@ var Discovery = class {
5122
5188
  thread.updatedAt = session.updatedAt;
5123
5189
  if (session.parent) thread.parent = agentThread(session.harness, session.parent, this.secret);
5124
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
+ }
5125
5202
  observe(session) {
5126
5203
  const found = this.native(session);
5127
5204
  const location2 = this.locations.get(found.thread.id);
5128
5205
  location2.live = session;
5129
5206
  Object.assign(found.thread, {
5130
5207
  editor: session.editor ?? found.thread.editor,
5131
- title: session.title,
5208
+ title: session.title ?? found.thread.title,
5132
5209
  status: session.status,
5133
5210
  updatedAt: location2.history?.updatedAt ?? session.updatedAt,
5134
5211
  capabilities: {
@@ -5347,6 +5424,8 @@ var Discovery = class {
5347
5424
 
5348
5425
  // src/connector.ts
5349
5426
  import { z as z2 } from "zod";
5427
+ import { join as join9 } from "node:path";
5428
+ import { mkdir } from "node:fs/promises";
5350
5429
  import { randomBytes as randomBytes4 } from "node:crypto";
5351
5430
 
5352
5431
  // ../../node_modules/.pnpm/engine.io-client@6.6.7/node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
@@ -16365,11 +16444,17 @@ async function publishHistory(config2, pair, project, thread, messages5) {
16365
16444
  return summary;
16366
16445
  }
16367
16446
 
16447
+ // src/harness/live.ts
16448
+ import { promisify as promisify6 } from "node:util";
16449
+ import { execFile as execFile6 } from "node:child_process";
16450
+
16368
16451
  // src/harness/codex.ts
16369
16452
  import WebSocket2 from "ws";
16370
16453
  import { homedir as homedir4 } from "node:os";
16371
16454
  import { connect } from "node:net";
16455
+ import { promisify as promisify3 } from "node:util";
16372
16456
  import { randomUUID as randomUUID2 } from "node:crypto";
16457
+ import { execFile as execFile3 } from "node:child_process";
16373
16458
  import { join as join4, isAbsolute as isAbsolute2 } from "node:path";
16374
16459
  import { stat, readdir, readFile } from "node:fs/promises";
16375
16460
  var REQUEST_MS = 4e3;
@@ -16401,13 +16486,12 @@ function approval(id, method, params) {
16401
16486
  choices: DECISIONS.filter((choice) => !unavailable || choice.id === "decline").map((choice) => ({ ...choice }))
16402
16487
  };
16403
16488
  }
16489
+ var codexHome = () => process.env.DASHUP_CODEX_HOME ?? process.env.CODEX_HOME ?? join4(homedir4(), ".codex");
16404
16490
  async function sockets() {
16405
16491
  const explicit = process.env.DASHUP_CODEX_SOCKET;
16406
16492
  if (explicit !== void 0) return explicit && isAbsolute2(explicit) ? [explicit] : [];
16407
16493
  if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
16408
- const homes = /* @__PURE__ */ new Set([
16409
- process.env.DASHUP_CODEX_HOME ?? process.env.CODEX_HOME ?? join4(homedir4(), ".codex")
16410
- ]);
16494
+ const homes = /* @__PURE__ */ new Set([codexHome()]);
16411
16495
  if (process.platform === "linux") {
16412
16496
  const entries2 = await readdir("/proc").catch(() => []);
16413
16497
  for (const pid of entries2.filter((value2) => /^\d+$/.test(value2)).slice(0, MAX_PROCESSES)) {
@@ -16593,6 +16677,16 @@ var Client = class {
16593
16677
  this.requests.set(id, result);
16594
16678
  return result;
16595
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
+ }
16596
16690
  async read() {
16597
16691
  await this.connect();
16598
16692
  const loaded = await this.call("thread/loaded/list", { limit: MAX_SESSIONS });
@@ -16640,12 +16734,12 @@ var Client = class {
16640
16734
  this.changed();
16641
16735
  }) : void 0,
16642
16736
  approve: approvals.length ? (requestId, approvalId, choice) => this.once(requestId, async () => {
16643
- const approval2 = this.approvals.get(approvalId);
16737
+ const approval3 = this.approvals.get(approvalId);
16644
16738
  const socket = this.socket;
16645
- 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)
16646
16740
  throw new Error("Codex approval is no longer pending");
16647
16741
  await new Promise((resolve3, reject) => socket.send(
16648
- JSON.stringify({ id: approval2.requestId, result: { decision: choice } }),
16742
+ JSON.stringify({ id: approval3.requestId, result: { decision: choice } }),
16649
16743
  (error) => error ? reject(new Error("Codex approval could not be sent")) : resolve3()
16650
16744
  ));
16651
16745
  this.approvals.delete(approvalId);
@@ -16675,8 +16769,25 @@ var Codex = class {
16675
16769
  }
16676
16770
  changed;
16677
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
+ }
16678
16789
  async refresh() {
16679
- const paths = new Set(await sockets());
16790
+ const paths = /* @__PURE__ */ new Set([...await sockets(), ...this.started]);
16680
16791
  for (const [path, client] of this.clients)
16681
16792
  if (!paths.has(path)) {
16682
16793
  client.stop();
@@ -16700,26 +16811,360 @@ var Codex = class {
16700
16811
  }
16701
16812
  };
16702
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
+
16703
17148
  // src/harness/vscode.ts
16704
17149
  import Socket3 from "ws";
16705
17150
  import { homedir as homedir5 } from "node:os";
16706
- import { connect as connect2 } from "node:net";
16707
- import { promisify as promisify2 } from "node:util";
17151
+ import { connect as connect3 } from "node:net";
17152
+ import { promisify as promisify4 } from "node:util";
16708
17153
  import { fileURLToPath as fileURLToPath2 } from "node:url";
16709
- import { execFile as execFile2 } from "node:child_process";
16710
- import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
16711
- import { stat as stat2, readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
16712
- 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";
16713
17158
  import { WebSocketTransport } from "@microsoft/agent-host-protocol/ws";
16714
17159
  import { AhpClient } from "@microsoft/agent-host-protocol/client";
16715
17160
  import { chatReducer, sessionReducer, SUPPORTED_PROTOCOL_VERSIONS } from "@microsoft/agent-host-protocol";
16716
- var REQUEST_MS2 = 4e3;
17161
+ var REQUEST_MS3 = 4e3;
16717
17162
  var MAX_BYTES2 = 8 * 1024 * 1024;
16718
17163
  var MAX_METADATA = 64 * 1024;
16719
17164
  var MAX_HOSTS = 16;
16720
- var MAX_SESSIONS2 = 100;
17165
+ var MAX_SESSIONS3 = 100;
16721
17166
  var ROOT_CHANNEL = "ahp-root://";
16722
- var run2 = promisify2(execFile2);
17167
+ var run2 = promisify4(execFile4);
16723
17168
  var CONFIRMATION_CHOICES = [
16724
17169
  { id: "approve", label: "Allow once", kind: "approve" },
16725
17170
  { id: "deny", label: "Reject", kind: "deny" }
@@ -16727,10 +17172,10 @@ var CONFIRMATION_CHOICES = [
16727
17172
  function roots() {
16728
17173
  const override = process.env.DASHUP_AHP_DATA ?? process.env.DASHUP_VSCODE_DATA;
16729
17174
  if (override !== void 0)
16730
- return override ? [basename2(override) === "User" ? dirname3(override) : override] : [];
17175
+ return override ? [basename2(override) === "User" ? dirname4(override) : override] : [];
16731
17176
  if (process.env.DASHUP_LIVE_DISCOVERY === "0") return [];
16732
- 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");
16733
- 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));
16734
17179
  }
16735
17180
  async function privateFile(path, limit) {
16736
17181
  const file = await stat2(path).catch(() => void 0);
@@ -16747,7 +17192,7 @@ async function privateFile(path, limit) {
16747
17192
  "-EncodedCommand",
16748
17193
  Buffer.from(script, "utf16le").toString("base64")
16749
17194
  ],
16750
- { timeout: REQUEST_MS2, maxBuffer: MAX_METADATA }
17195
+ { timeout: REQUEST_MS3, maxBuffer: MAX_METADATA }
16751
17196
  );
16752
17197
  return stdout.trim().toLowerCase() === "true";
16753
17198
  } catch {
@@ -16759,16 +17204,16 @@ async function privateFile(path, limit) {
16759
17204
  async function entries() {
16760
17205
  const result = /* @__PURE__ */ new Map();
16761
17206
  for (const root of roots()) {
16762
- const registry2 = join5(root, "agent-host/local-endpoint");
16763
- 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);
16764
17209
  const sources = [
16765
- ...files2.map((name) => join5(registry2, "entries", name)),
16766
- join5(registry2, "metadata.json")
17210
+ ...files2.map((name) => join6(registry2, "entries", name)),
17211
+ join6(registry2, "metadata.json")
16767
17212
  ];
16768
17213
  for (const path of sources) {
16769
17214
  try {
16770
17215
  if (!await privateFile(path, MAX_METADATA)) continue;
16771
- const parsed = JSON.parse(await readFile2(path, "utf8"));
17216
+ const parsed = JSON.parse(await readFile3(path, "utf8"));
16772
17217
  const rows = basename2(path) === "metadata.json" && Array.isArray(parsed) ? parsed : [parsed];
16773
17218
  for (const row of rows.slice(0, MAX_HOSTS)) {
16774
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))
@@ -16788,7 +17233,7 @@ async function entries() {
16788
17233
  if (!socket?.isSocket() || process.getuid && socket.uid !== process.getuid())
16789
17234
  continue;
16790
17235
  }
16791
- if (!await privateFile(dirname3(path), MAX_BYTES2)) continue;
17236
+ if (!await privateFile(dirname4(path), MAX_BYTES2)) continue;
16792
17237
  const key = `${root}\0${identity2}`;
16793
17238
  if (!result.has(key))
16794
17239
  result.set(key, {
@@ -16901,7 +17346,7 @@ var Host = class {
16901
17346
  client;
16902
17347
  connecting;
16903
17348
  stopped = false;
16904
- clientId = `dashup-${randomUUID3()}`;
17349
+ clientId = `dashup-${randomUUID4()}`;
16905
17350
  resources = /* @__PURE__ */ new Map();
16906
17351
  subscriptions = /* @__PURE__ */ new Set();
16907
17352
  receipts = /* @__PURE__ */ new Map();
@@ -16953,9 +17398,9 @@ var Host = class {
16953
17398
  if (this.connecting) return this.connecting;
16954
17399
  this.connecting = (async () => {
16955
17400
  const socket = new Socket3(`ws://localhost/?tkn=${encodeURIComponent(this.entry.token)}`, {
16956
- createConnection: () => connect2(this.entry.path),
17401
+ createConnection: () => connect3(this.entry.path),
16957
17402
  maxPayload: MAX_BYTES2,
16958
- handshakeTimeout: REQUEST_MS2
17403
+ handshakeTimeout: REQUEST_MS3
16959
17404
  });
16960
17405
  socket.on("error", () => {
16961
17406
  });
@@ -16988,7 +17433,7 @@ var Host = class {
16988
17433
  }
16989
17434
  };
16990
17435
  const client = this.client = new AhpClient(transport, {
16991
- requestTimeoutMs: REQUEST_MS2,
17436
+ requestTimeoutMs: REQUEST_MS3,
16992
17437
  subscriptionBuffer: 1024
16993
17438
  });
16994
17439
  this.resources.clear();
@@ -17052,7 +17497,7 @@ var Host = class {
17052
17497
  const timer = setTimeout(() => {
17053
17498
  this.receipts.delete(handle.clientSeq);
17054
17499
  reject(new Error("Native AHP action was not acknowledged; delivery is unknown"));
17055
- }, REQUEST_MS2);
17500
+ }, REQUEST_MS3);
17056
17501
  this.receipts.set(handle.clientSeq, { resolve: resolve3, reject, timer });
17057
17502
  });
17058
17503
  this.requests.set(requestId, pending);
@@ -17062,12 +17507,12 @@ var Host = class {
17062
17507
  await this.connect();
17063
17508
  const catalog = await this.client.request("listSessions", {
17064
17509
  channel: ROOT_CHANNEL,
17065
- limit: MAX_SESSIONS2
17510
+ limit: MAX_SESSIONS3
17066
17511
  });
17067
17512
  if (!Array.isArray(catalog.items)) return [];
17068
17513
  const result = [];
17069
17514
  const active = /* @__PURE__ */ new Set();
17070
- for (const summary of catalog.items.slice(0, MAX_SESSIONS2)) {
17515
+ for (const summary of catalog.items.slice(0, MAX_SESSIONS3)) {
17071
17516
  try {
17072
17517
  const session = await this.subscribe(summary.resource);
17073
17518
  if (!session || session.lifecycle !== "ready" || !Array.isArray(session.chats)) continue;
@@ -17129,7 +17574,7 @@ var Host = class {
17129
17574
  message: { text: value2, origin: { kind: "user" } }
17130
17575
  });
17131
17576
  } : void 0,
17132
- approve: approvals.some((approval2) => approval2.choices.length) ? (requestId, approvalId, choice) => {
17577
+ approve: approvals.some((approval3) => approval3.choices.length) ? (requestId, approvalId, choice) => {
17133
17578
  const prior = this.requests.get(requestId);
17134
17579
  if (prior) return prior;
17135
17580
  const current = this.resources.get(summary.resource)?.state;
@@ -17200,15 +17645,15 @@ var VSCode = class {
17200
17645
  };
17201
17646
 
17202
17647
  // src/harness/opencode.ts
17203
- import { join as join6 } from "node:path";
17204
- import { promisify as promisify3 } from "node:util";
17648
+ import { join as join7 } from "node:path";
17649
+ import { promisify as promisify5 } from "node:util";
17205
17650
  import { createHash as createHash4 } from "node:crypto";
17206
- import { execFile as execFile3 } from "node:child_process";
17207
- import { stat as stat3, readlink, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
17208
- var run3 = promisify3(execFile3);
17209
- 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;
17210
17655
  var MAX_BYTES3 = 8 * 1024 * 1024;
17211
- var MAX_SESSIONS3 = 100;
17656
+ var MAX_SESSIONS4 = 100;
17212
17657
  var MAX_PROCESSES2 = 4096;
17213
17658
  var MAX_ENDPOINTS = 32;
17214
17659
  var EVENT_DELAY_MS = 100;
@@ -17231,10 +17676,10 @@ function authorization(environment) {
17231
17676
  }
17232
17677
  async function linuxEndpoints() {
17233
17678
  const result = [];
17234
- 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);
17235
17680
  const listeners = /* @__PURE__ */ new Map();
17236
17681
  for (const name of ["tcp", "tcp6"]) {
17237
- const table = await readFile3(`/proc/net/${name}`, "utf8").catch(() => "");
17682
+ const table = await readFile4(`/proc/net/${name}`, "utf8").catch(() => "");
17238
17683
  for (const line of table.split("\n").slice(1)) {
17239
17684
  const columns = line.trim().split(/\s+/);
17240
17685
  if (columns[3] !== "0A") continue;
@@ -17250,16 +17695,16 @@ async function linuxEndpoints() {
17250
17695
  const directory2 = `/proc/${pid}`;
17251
17696
  const owner = await stat3(directory2);
17252
17697
  if (process.getuid && owner.uid !== process.getuid()) continue;
17253
- const command2 = (await readFile3(join6(directory2, "cmdline"), "utf8")).slice(0, 4096);
17698
+ const command2 = (await readFile4(join7(directory2, "cmdline"), "utf8")).slice(0, 4096);
17254
17699
  if (!/(?:^|[\/\0 ])opencode(?:[\0 .]|$)/.test(command2)) continue;
17255
17700
  const environment = Object.fromEntries(
17256
- (await readFile3(join6(directory2, "environ"), "utf8")).split("\0").map((entry) => {
17701
+ (await readFile4(join7(directory2, "environ"), "utf8")).split("\0").map((entry) => {
17257
17702
  const split = entry.indexOf("=");
17258
17703
  return [entry.slice(0, split), entry.slice(split + 1)];
17259
17704
  })
17260
17705
  );
17261
- for (const fd of (await readdir3(join6(directory2, "fd"))).slice(0, 1024)) {
17262
- 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(() => "");
17263
17708
  const inode = /^socket:\[(\d+)\]$/.exec(target)?.[1];
17264
17709
  const url2 = inode ? listeners.get(inode) : void 0;
17265
17710
  if (url2) result.push({ url: url2, authorization: authorization(environment) });
@@ -17288,7 +17733,7 @@ async function endpoints() {
17288
17733
  const { stdout } = await run3(
17289
17734
  "lsof",
17290
17735
  ["-nP", "-a", "-c", "opencode", "-iTCP", "-sTCP:LISTEN", "-Fn"],
17291
- { timeout: REQUEST_MS3, maxBuffer: MAX_BYTES3 }
17736
+ { timeout: REQUEST_MS4, maxBuffer: MAX_BYTES3 }
17292
17737
  );
17293
17738
  return stdout.split("\n").flatMap((line) => {
17294
17739
  const port = /^n(?:127\.0\.0\.1|\[::1\]|\*):(\d+)$/.exec(line)?.[1];
@@ -17305,7 +17750,7 @@ async function endpoints() {
17305
17750
  const { stdout } = await run3(
17306
17751
  "powershell.exe",
17307
17752
  ["-NoProfile", "-NonInteractive", "-Command", script],
17308
- { timeout: REQUEST_MS3, maxBuffer: MAX_BYTES3 }
17753
+ { timeout: REQUEST_MS4, maxBuffer: MAX_BYTES3 }
17309
17754
  );
17310
17755
  return stdout.split(/\r?\n/).filter((port) => /^\d+$/.test(port)).slice(0, MAX_ENDPOINTS).map((port) => ({
17311
17756
  url: localEndpoint(`http://127.0.0.1:${port}`),
@@ -17425,7 +17870,7 @@ var Server = class {
17425
17870
  };
17426
17871
  }
17427
17872
  async request(path, directory2, payload) {
17428
- const signal = AbortSignal.timeout(REQUEST_MS3);
17873
+ const signal = AbortSignal.timeout(REQUEST_MS4);
17429
17874
  const response = await fetch(this.endpoint.url + path, {
17430
17875
  method: payload === void 0 ? "GET" : "POST",
17431
17876
  headers: {
@@ -17452,15 +17897,25 @@ var Server = class {
17452
17897
  this.requests.set(id, pending);
17453
17898
  return pending;
17454
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
+ }
17455
17910
  async read() {
17456
17911
  const health = await this.request("/global/health");
17457
17912
  if (health?.healthy !== true || typeof health.version !== "string") return [];
17458
17913
  this.listen();
17459
- const sessions = await this.request(`/session?limit=${MAX_SESSIONS3}`);
17914
+ const sessions = await this.request(`/session?limit=${MAX_SESSIONS4}`);
17460
17915
  if (!Array.isArray(sessions)) return [];
17461
17916
  const result = [];
17462
17917
  const states = /* @__PURE__ */ new Map();
17463
- for (const session of sessions.slice(0, MAX_SESSIONS3)) {
17918
+ for (const session of sessions.slice(0, MAX_SESSIONS4)) {
17464
17919
  if (typeof session?.id !== "string" || typeof session.directory !== "string" || !session.directory)
17465
17920
  continue;
17466
17921
  try {
@@ -17536,7 +17991,7 @@ var Server = class {
17536
17991
  });
17537
17992
  this.changed();
17538
17993
  }),
17539
- 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 () => {
17540
17995
  if (this.stopped) throw new Error("OpenCode is disconnected");
17541
17996
  if (approvalId.startsWith("question:")) {
17542
17997
  const requestID = approvalId.slice("question:".length);
@@ -17585,7 +18040,7 @@ var Server = class {
17585
18040
  listen() {
17586
18041
  if (this.stream || this.retry || this.stopped) return;
17587
18042
  const controller = this.stream = new AbortController();
17588
- const connectTimer = setTimeout(() => controller.abort(), REQUEST_MS3);
18043
+ const connectTimer = setTimeout(() => controller.abort(), REQUEST_MS4);
17589
18044
  connectTimer.unref();
17590
18045
  void (async () => {
17591
18046
  const response = await fetch(this.endpoint.url + "/global/event", {
@@ -17654,15 +18109,57 @@ var Server = class {
17654
18109
  clearTimeout(this.notify);
17655
18110
  }
17656
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
+ }
17657
18134
  var OpenCode = class {
17658
18135
  constructor(changed) {
17659
18136
  this.changed = changed;
17660
18137
  }
17661
18138
  changed;
17662
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
+ }
17663
18156
  async refresh() {
17664
18157
  const discovered = await endpoints().catch(() => []);
17665
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
+ }
17666
18163
  for (const [url2, server] of this.servers)
17667
18164
  if (!current.has(url2)) {
17668
18165
  server.stop();
@@ -17683,6 +18180,8 @@ var OpenCode = class {
17683
18180
  stop() {
17684
18181
  for (const server of this.servers.values()) server.stop();
17685
18182
  this.servers.clear();
18183
+ for (const served of this.started.values()) served.process.kill();
18184
+ this.started.clear();
17686
18185
  }
17687
18186
  };
17688
18187
 
@@ -17691,6 +18190,8 @@ var DISCOVERY_MS = 1e4;
17691
18190
  var LiveHarnesses = class {
17692
18191
  listeners = /* @__PURE__ */ new Set();
17693
18192
  codex = new Codex(() => this.changed());
18193
+ headless = new Headless(() => this.changed());
18194
+ claude = new Claude(() => this.changed());
17694
18195
  vscode = new VSCode(() => this.changed());
17695
18196
  opencode = new OpenCode(() => this.changed());
17696
18197
  timer;
@@ -17704,8 +18205,10 @@ var LiveHarnesses = class {
17704
18205
  if (this.refreshing) return this.refreshing;
17705
18206
  this.refreshing = (async () => {
17706
18207
  const results = await Promise.allSettled([
18208
+ this.headless.refresh(),
17707
18209
  this.opencode.refresh(),
17708
18210
  this.codex.refresh(),
18211
+ this.claude.refresh(),
17709
18212
  this.vscode.refresh()
17710
18213
  ]);
17711
18214
  const sessions = /* @__PURE__ */ new Map();
@@ -17721,6 +18224,27 @@ var LiveHarnesses = class {
17721
18224
  });
17722
18225
  return this.refreshing;
17723
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
+ }
17724
18248
  watch(onChange) {
17725
18249
  this.listeners.add(onChange);
17726
18250
  if (!this.timer && process.env.DASHUP_LIVE_DISCOVERY !== "0") {
@@ -17739,20 +18263,22 @@ var LiveHarnesses = class {
17739
18263
  this.stopped = true;
17740
18264
  clearInterval(this.timer);
17741
18265
  this.listeners.clear();
18266
+ this.headless.stop();
17742
18267
  this.opencode.stop();
17743
18268
  this.codex.stop();
18269
+ this.claude.stop();
17744
18270
  this.vscode.stop();
17745
18271
  }
17746
18272
  };
17747
18273
 
17748
18274
  // src/harness/queue.ts
17749
- import { randomUUID as randomUUID4 } from "node:crypto";
17750
- 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";
17751
18277
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
17752
- 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";
17753
18279
  import { statSync as statSync2, existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync3, readlinkSync, realpathSync as realpathSync2 } from "node:fs";
17754
18280
  var CODEX_VERSION = "0.160.1";
17755
- var REQUEST_MS4 = 1e4;
18281
+ var REQUEST_MS5 = 1e4;
17756
18282
  var MAX_BYTES4 = 1024 * 1024;
17757
18283
  var MAX_PROCESSES3 = 4096;
17758
18284
  var MAX_FILES = 4096;
@@ -17768,9 +18294,9 @@ function sameFile(left, right) {
17768
18294
  return left.path === right.path && left.device === right.device && left.inode === right.inode;
17769
18295
  }
17770
18296
  function packageVersion(executable) {
17771
- let directory2 = dirname4(executable);
17772
- for (let depth = 0; depth < 8; depth++, directory2 = dirname4(directory2)) {
17773
- 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");
17774
18300
  if (!existsSync5(path)) continue;
17775
18301
  const value2 = JSON.parse(readFileSync3(path, "utf8"));
17776
18302
  if (value2.name?.startsWith("@openai/codex-")) return value2.version;
@@ -17780,40 +18306,40 @@ function packageVersion(executable) {
17780
18306
  function processOwners(pid) {
17781
18307
  const root = `/proc/${pid}`;
17782
18308
  if (process.getuid && statSync2(root).uid !== process.getuid()) return [];
17783
- const executable = realpathSync2.native(join7(root, "exe"));
18309
+ const executable = realpathSync2.native(join8(root, "exe"));
17784
18310
  if (basename3(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
17785
- const status2 = readFileSync3(join7(root, "stat"), "utf8");
18311
+ const status2 = readFileSync3(join8(root, "stat"), "utf8");
17786
18312
  const start = status2.slice(status2.lastIndexOf(")") + 2).split(" ")[19];
17787
18313
  if (!start || !/^\d+$/.test(start)) return [];
17788
- const fds = readdirSync3(join7(root, "fd")).slice(0, MAX_FILES);
18314
+ const fds = readdirSync3(join8(root, "fd")).slice(0, MAX_FILES);
17789
18315
  const files2 = fds.flatMap((fd) => {
17790
18316
  try {
17791
- return [{ fd, path: readlinkSync(join7(root, "fd", fd)) }];
18317
+ return [{ fd, path: readlinkSync(join8(root, "fd", fd)) }];
17792
18318
  } catch {
17793
18319
  return [];
17794
18320
  }
17795
18321
  });
17796
18322
  return files2.flatMap(({ fd, path }) => {
17797
- if (basename3(dirname4(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
18323
+ if (basename3(dirname5(path)) !== "thread-writer-locks" || !path.endsWith(".lock")) return [];
17798
18324
  const thread = basename3(path, ".lock");
17799
18325
  if (!THREAD_ID.test(thread)) return [];
17800
18326
  try {
17801
- const lock = readFileSync3(join7(root, "fdinfo", fd), "utf8");
18327
+ const lock = readFileSync3(join8(root, "fdinfo", fd), "utf8");
17802
18328
  if (!/FLOCK\s+ADVISORY\s+WRITE/.test(lock)) return [];
17803
- const home = realpathSync2.native(dirname4(dirname4(path)));
18329
+ const home = realpathSync2.native(dirname5(dirname5(path)));
17804
18330
  const directory2 = statSync2(home);
17805
18331
  if (process.getuid && directory2.uid !== process.getuid()) return [];
17806
- const queue = identity(join7(home, "queue_1.sqlite"));
18332
+ const queue = identity(join8(home, "queue_1.sqlite"));
17807
18333
  if (!files2.some((file) => {
17808
18334
  try {
17809
- return sameFile(identity(join7(root, "fd", file.fd)), queue);
18335
+ return sameFile(identity(join8(root, "fd", file.fd)), queue);
17810
18336
  } catch {
17811
18337
  return false;
17812
18338
  }
17813
18339
  }))
17814
18340
  return [];
17815
18341
  const rollout = files2.find(
17816
- (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`)
17817
18343
  );
17818
18344
  if (!rollout) return [];
17819
18345
  return [
@@ -17825,7 +18351,7 @@ function processOwners(pid) {
17825
18351
  thread,
17826
18352
  queue,
17827
18353
  version: CODEX_VERSION,
17828
- cwd: realpathSync2.native(join7(root, "cwd")),
18354
+ cwd: realpathSync2.native(join8(root, "cwd")),
17829
18355
  rollout: identity(rollout.path)
17830
18356
  }
17831
18357
  ];
@@ -17871,7 +18397,7 @@ function validateOwner(owner) {
17871
18397
  function validateEvidence(owner) {
17872
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))
17873
18399
  throw new Error("The original native history storage changed");
17874
- const path = join7(owner.home, "thread_history_1.sqlite");
18400
+ const path = join8(owner.home, "thread_history_1.sqlite");
17875
18401
  if (existsSync5(path) && identity(path).path !== path)
17876
18402
  throw new Error("Native history is outside the original home");
17877
18403
  }
@@ -17888,7 +18414,7 @@ function nativeReceipt(owner, clientId) {
17888
18414
  } finally {
17889
18415
  queue.close();
17890
18416
  }
17891
- const path = join7(owner.home, "thread_history_1.sqlite");
18417
+ const path = join8(owner.home, "thread_history_1.sqlite");
17892
18418
  if (!existsSync5(path)) return { queueId };
17893
18419
  const history = new DatabaseSync3(path, { readOnly: true });
17894
18420
  try {
@@ -17944,7 +18470,7 @@ var Writer = class {
17944
18470
  tail = "";
17945
18471
  pending = /* @__PURE__ */ new Map();
17946
18472
  constructor(owner) {
17947
- this.process = spawn3(owner.executable, ["app-server", "--listen", "stdio://"], {
18473
+ this.process = spawn5(owner.executable, ["app-server", "--listen", "stdio://"], {
17948
18474
  cwd: owner.cwd,
17949
18475
  env: { ...process.env, CODEX_HOME: owner.home },
17950
18476
  stdio: ["pipe", "pipe", "ignore"]
@@ -17984,7 +18510,7 @@ var Writer = class {
17984
18510
  this.pending.delete(id);
17985
18511
  reject(new Error("Native queue acknowledgement was not received"));
17986
18512
  this.close();
17987
- }, REQUEST_MS4);
18513
+ }, REQUEST_MS5);
17988
18514
  this.pending.set(id, { resolve: resolve3, reject, timer });
17989
18515
  this.process.stdin.write(JSON.stringify({ id, method, params }) + "\n");
17990
18516
  });
@@ -18031,7 +18557,7 @@ var NativeQueue = class {
18031
18557
  if (existing) return existing;
18032
18558
  if (this.stopped) throw new Error("The connector is stopping");
18033
18559
  validateOwner(scope2.owner);
18034
- const clientUserMessageId = randomUUID4();
18560
+ const clientUserMessageId = randomUUID5();
18035
18561
  const request2 = { ...scope2, receipt: { state: "unknown", clientUserMessageId } };
18036
18562
  let writer;
18037
18563
  try {
@@ -18116,7 +18642,13 @@ var Command = z2.object({
18116
18642
  z2.object({ kind: z2.literal("thread.stop") }).strict(),
18117
18643
  z2.object({ kind: z2.literal("approval.respond"), approvalId: z2.string(), choice: z2.string() }).strict(),
18118
18644
  z2.object({ kind: z2.literal("screen.start"), sessionId: z2.string().min(1).max(128) }).strict(),
18119
- 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()
18120
18652
  ])
18121
18653
  }).strict();
18122
18654
  var Connector = class {
@@ -18178,6 +18710,8 @@ var Connector = class {
18178
18710
  listed = /* @__PURE__ */ new Map();
18179
18711
  listedProjects = /* @__PURE__ */ new Map();
18180
18712
  overviewQueued = false;
18713
+ // The agents that can start sessions here.
18714
+ startable = [];
18181
18715
  snapshots = /* @__PURE__ */ new Set();
18182
18716
  nativeSnapshots = /* @__PURE__ */ new Map();
18183
18717
  queueReceipts = /* @__PURE__ */ new Map();
@@ -18191,6 +18725,7 @@ var Connector = class {
18191
18725
  this.nativeSnapshots.set(thread.id, structuredClone(this.threadState(project, thread)));
18192
18726
  }
18193
18727
  this.refreshQueues();
18728
+ void this.findAgents();
18194
18729
  this.stopWatching = this.discovery.watch(this.rescan);
18195
18730
  this.releases.push(this.harnesses.watch(this.rescan));
18196
18731
  this.releases.push(
@@ -18229,15 +18764,15 @@ var Connector = class {
18229
18764
  for (const project of this.discovery.projects.values())
18230
18765
  for (const thread of project.threads) {
18231
18766
  const native = this.discovery.locations.get(thread.id)?.live;
18232
- const approval2 = native?.approvals[0];
18233
- if (!approval2) continue;
18767
+ const approval3 = native?.approvals[0];
18768
+ if (!approval3) continue;
18234
18769
  await this.thread(pair, project, thread);
18235
18770
  await this.send(pair, {
18236
18771
  type: "attention",
18237
18772
  projectId: project.id,
18238
18773
  threadId: thread.id,
18239
- title: approval2.title,
18240
- body: approval2.detail,
18774
+ title: approval3.title,
18775
+ body: approval3.detail,
18241
18776
  reason: "approval"
18242
18777
  });
18243
18778
  }
@@ -18405,6 +18940,10 @@ var Connector = class {
18405
18940
  }
18406
18941
  return {
18407
18942
  type: "projects",
18943
+ version,
18944
+ screen: this.discovery.capabilities.screen,
18945
+ agents: this.startable,
18946
+ folders: this.discovery.folders(),
18408
18947
  projects: [...this.listedProjects.values()],
18409
18948
  sessions: [
18410
18949
  ...sessions.filter(busy),
@@ -18412,6 +18951,11 @@ var Connector = class {
18412
18951
  ]
18413
18952
  };
18414
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
+ }
18415
18959
  async listProjects(pair) {
18416
18960
  const overview = this.overview();
18417
18961
  const text3 = JSON.stringify(overview);
@@ -18477,9 +19021,9 @@ var Connector = class {
18477
19021
  if (JSON.stringify(prior) === JSON.stringify(snapshot)) continue;
18478
19022
  this.nativeSnapshots.set(thread.id, structuredClone(snapshot));
18479
19023
  for (const pair of this.activePairs()) await this.thread(pair, project, thread);
18480
- for (const approval2 of session.approvals)
18481
- if (!prior?.approvals.some((item) => item.id === approval2.id))
18482
- 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");
18483
19027
  if (prior?.thread.status === "working" && thread.status === "idle")
18484
19028
  await this.attention(
18485
19029
  project.id,
@@ -18561,18 +19105,21 @@ var Connector = class {
18561
19105
  async execute(pair, content, envelope) {
18562
19106
  let queue = void 0;
18563
19107
  try {
18564
- 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)
18565
19111
  throw new Error("Command expired. Try again while this computer is online.");
18566
- queue = await this.command(pair, content);
19112
+ const outcome = await this.command(pair, content);
18567
19113
  delete this.config.unconfirmed[content.requestId];
18568
- if (queue) {
19114
+ if (outcome && "state" in outcome) {
19115
+ queue = outcome;
18569
19116
  await this.queueResult(pair, content.requestId, content.projectId, queue);
18570
19117
  this.queueReceipts.set(content.requestId, JSON.stringify(queue));
18571
19118
  return;
18572
19119
  }
18573
19120
  await this.send(
18574
19121
  pair,
18575
- { type: "result", requestId: content.requestId, ok: true },
19122
+ { type: "result", requestId: content.requestId, ok: true, ...outcome ? { started: outcome } : {} },
18576
19123
  content.projectId
18577
19124
  );
18578
19125
  } catch (error) {
@@ -18607,7 +19154,7 @@ var Connector = class {
18607
19154
  }
18608
19155
  }
18609
19156
  attach(socket) {
18610
- const sessions = bridgeAcp(socket, (session, approval2) => {
19157
+ const sessions = bridgeAcp(socket, (session, approval3) => {
18611
19158
  this.agents.set(session.id, session);
18612
19159
  const { project, thread } = this.adopt(session);
18613
19160
  if (!this.snapshots.has(session.id)) {
@@ -18622,15 +19169,15 @@ var Connector = class {
18622
19169
  }
18623
19170
  }).catch((error) => this.report(error));
18624
19171
  }
18625
- if (approval2)
19172
+ if (approval3)
18626
19173
  this.work = this.work.then(async () => {
18627
- 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))
18628
19175
  return;
18629
19176
  await this.attention(
18630
19177
  project.id,
18631
19178
  thread.id,
18632
- approval2.title,
18633
- approval2.detail,
19179
+ approval3.title,
19180
+ approval3.detail,
18634
19181
  "approval"
18635
19182
  );
18636
19183
  }).catch((error) => this.report(error));
@@ -18699,9 +19246,12 @@ var Connector = class {
18699
19246
  }
18700
19247
  async command(pair, command2) {
18701
19248
  if (command2.action.kind === "projects.refresh") {
19249
+ await this.findAgents();
18702
19250
  await this.changed();
18703
19251
  return this.publishProjects(pair);
18704
19252
  }
19253
+ if (command2.action.kind === "session.start")
19254
+ return this.startSession(command2.projectId, command2.action);
18705
19255
  const project = this.discovery.projects.get(command2.projectId);
18706
19256
  if (!project) throw new Error("Project is no longer open on this computer");
18707
19257
  const thread = project.threads.find((item) => item.id === command2.threadId);
@@ -18758,15 +19308,15 @@ var Connector = class {
18758
19308
  const action = command2.action;
18759
19309
  const native = this.discovery.locations.get(thread.id)?.live;
18760
19310
  if (native?.approve) {
18761
- const approval3 = native.approvals.find((item) => item.id === action.approvalId);
18762
- 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))
18763
19313
  throw new Error("Approval is no longer available");
18764
- return native.approve(command2.requestId, approval3.id, action.choice);
19314
+ return native.approve(command2.requestId, approval4.id, action.choice);
18765
19315
  }
18766
19316
  const agent = this.attached(thread);
18767
19317
  if (!agent) throw new Error("Approval requires an attached ACP session. Use Screen.");
18768
- const [request2, approval2] = [...agent.approvals].find(([, item]) => item.id === action.approvalId) ?? [];
18769
- 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))
18770
19320
  throw new Error("Approval is no longer available");
18771
19321
  return commandAcp(agent, command2.requestId, {
18772
19322
  kind: "approval",
@@ -18782,6 +19332,37 @@ var Connector = class {
18782
19332
  return;
18783
19333
  }
18784
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
+ }
18785
19366
  report(error) {
18786
19367
  process.stderr.write(`${error instanceof Error ? error.message : "Dashup operation failed"}
18787
19368
  `);
@@ -18790,19 +19371,19 @@ var Connector = class {
18790
19371
 
18791
19372
  // src/install.ts
18792
19373
  import { existsSync as existsSync6 } from "node:fs";
18793
- import { homedir as homedir6 } from "node:os";
18794
- import { promisify as promisify4 } from "node:util";
19374
+ import { homedir as homedir6, userInfo } from "node:os";
19375
+ import { promisify as promisify7 } from "node:util";
18795
19376
  import { fileURLToPath as fileURLToPath3 } from "node:url";
18796
- import { execFile as execFile4, spawn as spawn4 } from "node:child_process";
18797
- import { cp, mkdir, rm, writeFile } from "node:fs/promises";
18798
- import { dirname as dirname5, join as join8, relative, sep } from "node:path";
18799
- var run4 = promisify4(execFile4);
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);
18800
19381
  var xml = (value2) => value2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
18801
19382
  var NODE_FLAGS = ["--disable-warning=ExperimentalWarning"];
18802
- var serviceFile = () => process.platform === "darwin" ? join8(homedir6(), "Library/LaunchAgents/com.dashup.connector.plist") : process.platform === "win32" ? join8(
18803
- 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"),
18804
19385
  "Microsoft/Windows/Start Menu/Programs/Startup/Dashup.vbs"
18805
- ) : join8(homedir6(), ".config/systemd/user/dashup.service");
19386
+ ) : join10(homedir6(), ".config/systemd/user/dashup.service");
18806
19387
  var startupInstalled = () => existsSync6(serviceFile());
18807
19388
  function service(platform, node, entry, config2) {
18808
19389
  const command2 = [node, ...NODE_FLAGS, entry];
@@ -18838,17 +19419,17 @@ async function durableEntry() {
18838
19419
  );
18839
19420
  if (!["_npx", "dlx"].some((cache2) => entry.includes(`${sep}${cache2}${sep}`))) return entry;
18840
19421
  const root = entry.slice(0, entry.indexOf(`${sep}node_modules${sep}`));
18841
- const app = join8(dirname5(configPath()), "app");
19422
+ const app = join10(dirname6(configPath()), "app");
18842
19423
  await rm(app, { recursive: true, force: true });
18843
- await cp(join8(root, "node_modules"), join8(app, "node_modules"), {
19424
+ await cp(join10(root, "node_modules"), join10(app, "node_modules"), {
18844
19425
  recursive: true,
18845
19426
  verbatimSymlinks: true
18846
19427
  });
18847
- return join8(app, relative(root, entry));
19428
+ return join10(app, relative(root, entry));
18848
19429
  }
18849
19430
  async function installStartup() {
18850
19431
  const path = serviceFile();
18851
- await mkdir(dirname5(path), { recursive: true });
19432
+ await mkdir2(dirname6(path), { recursive: true });
18852
19433
  await writeFile(
18853
19434
  path,
18854
19435
  service(process.platform, process.execPath, await durableEntry(), configPath())
@@ -18858,13 +19439,28 @@ async function installStartup() {
18858
19439
  });
18859
19440
  await run4("launchctl", ["load", path]);
18860
19441
  } else if (process.platform === "win32") {
18861
- spawn4("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
19442
+ spawn6("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
18862
19443
  } else {
18863
19444
  await run4("systemctl", ["--user", "daemon-reload"]);
18864
19445
  await run4("systemctl", ["--user", "enable", "dashup.service"]);
18865
19446
  await run4("systemctl", ["--user", "restart", "dashup.service"]);
19447
+ if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY)
19448
+ return run4("loginctl", ["enable-linger"]).then(
19449
+ () => void 0,
19450
+ () => `It stops when you log out. To keep it running, run sudo loginctl enable-linger ${userInfo().username}.`
19451
+ );
18866
19452
  }
18867
19453
  }
19454
+ async function uninstallStartup() {
19455
+ const path = serviceFile();
19456
+ if (process.platform === "darwin") await run4("launchctl", ["unload", path]).catch(() => {
19457
+ });
19458
+ else if (process.platform !== "win32")
19459
+ await run4("systemctl", ["--user", "disable", "dashup.service"]).catch(() => {
19460
+ });
19461
+ await rm(path, { force: true });
19462
+ await rm(join10(dirname6(configPath()), "app"), { recursive: true, force: true });
19463
+ }
18868
19464
 
18869
19465
  // src/index.ts
18870
19466
  var config = loadConfig();
@@ -18902,9 +19498,9 @@ async function background() {
18902
19498
  try {
18903
19499
  await running("/stop", { method: "POST" }).catch(() => null);
18904
19500
  await answering(false);
18905
- await installStartup();
19501
+ const note = await installStartup();
18906
19502
  if (await answering(true)) {
18907
- const message = "Dashup runs in the background and starts when you sign in.";
19503
+ const message = `Dashup runs in the background and starts when you sign in.${note ? ` ${note}` : ""}`;
18908
19504
  if (waiting) waiting.stop(message);
18909
19505
  else process.stdout.write(`${message}
18910
19506
  `);
@@ -18941,11 +19537,21 @@ async function main() {
18941
19537
  throw new Error("Use a separate DASHUP_CONFIG file to connect a different server");
18942
19538
  config.api = url2.origin;
18943
19539
  }
19540
+ const account = option("--account");
19541
+ if (account !== void 0 && !/^[^\s@]+@[^\s@]+$/.test(account))
19542
+ throw new Error("Use --account with the email you sign in to the Dashup app with");
18944
19543
  if (command === "install") {
18945
19544
  if (!config.secret) throw new Error("Pair this computer first: run dashup pair.");
18946
19545
  if (!await background()) process.exitCode = 1;
18947
19546
  return;
18948
19547
  }
19548
+ if (command === "uninstall") {
19549
+ await running("/stop", { method: "POST" }).catch(() => null);
19550
+ await answering(false);
19551
+ await uninstallStartup();
19552
+ process.stdout.write("Dashup no longer starts when you sign in. This computer stays paired.\n");
19553
+ return;
19554
+ }
18949
19555
  if (command === "stop") {
18950
19556
  const response = await running("/stop", { method: "POST" }).catch(refused);
18951
19557
  if (!response) throw new Error("Dashup is not running on this computer");
@@ -19036,20 +19642,24 @@ async function main() {
19036
19642
  }
19037
19643
  if (!["pair", "watch"].includes(command)) {
19038
19644
  process.stdout.write(
19039
- "Dashup: pair | watch | projects | alert --project <id> | doctor | permissions --screen --control | stop | acp -- <original-agent-command> | install\n"
19645
+ "Dashup: pair [--account <email>] | watch | projects | alert --project <id> | doctor | permissions --screen --control | stop | acp -- <original-agent-command> | install | uninstall\n"
19040
19646
  );
19041
19647
  return;
19042
19648
  }
19043
19649
  const pairPhone = (invite) => scanned(config, invite, (email) => `Paired a phone signed in as ${email}`);
19044
19650
  const keepRunning = async (stopHere) => {
19045
- if (!interactive) return;
19046
- const answer = startupInstalled() || await ui2.confirm({ message: BACKGROUND });
19047
- if (answer === true) {
19651
+ const answer = startupInstalled() || interactive && await ui2.confirm({ message: BACKGROUND }) === true;
19652
+ if (answer) {
19048
19653
  stopHere();
19049
19654
  const started = await background();
19050
19655
  ui2.outro(started ? DONE2 : "Run dashup to keep Dashup running in this terminal.");
19051
19656
  process.exit(started ? 0 : 1);
19052
19657
  }
19658
+ if (!interactive) {
19659
+ stopHere();
19660
+ process.stdout.write("Paired. Run dashup install to keep Dashup running in the background.\n");
19661
+ process.exit(0);
19662
+ }
19053
19663
  ui2.log.info("Dashup runs in this terminal. Keep it open; Ctrl+C stops it.");
19054
19664
  ui2.outro(DONE2);
19055
19665
  };
@@ -19075,10 +19685,12 @@ async function main() {
19075
19685
  if (interactive) ui2.outro(DONE2);
19076
19686
  process.exit();
19077
19687
  }
19078
- if (!config.secret && !interactive)
19079
- throw new Error("Run dashup pair in a terminal to pair this computer.");
19688
+ if (!config.secret && !interactive && !account)
19689
+ throw new Error(
19690
+ "To pair this computer, run dashup pair in a terminal, or add --account with your Dashup email."
19691
+ );
19080
19692
  let connector;
19081
- const server = createServer(async (request2, response) => {
19693
+ const server = createServer2(async (request2, response) => {
19082
19694
  if (!authorized(request2)) {
19083
19695
  response.writeHead(403).end();
19084
19696
  return;
@@ -19157,7 +19769,7 @@ async function main() {
19157
19769
  const linking = !config.secret;
19158
19770
  if (linking) {
19159
19771
  intro2();
19160
- if (!await link(config)) process.exit(1);
19772
+ if (!await link(config, account)) process.exit(1);
19161
19773
  await keepRunning(() => server.close());
19162
19774
  }
19163
19775
  connector = new Connector(config);