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