@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.
- package/README.md +20 -23
- package/dist/index.js +782 -170
- 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
|
|
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
|
}
|
|
@@ -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
|
|
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();
|
|
@@ -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.
|
|
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
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
16737
|
+
const approval3 = this.approvals.get(approvalId);
|
|
16644
16738
|
const socket = this.socket;
|
|
16645
|
-
if (!
|
|
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:
|
|
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
|
|
16707
|
-
import { promisify as
|
|
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
|
|
16710
|
-
import { createHash as createHash3, randomUUID as
|
|
16711
|
-
import { stat as stat2, readdir as
|
|
16712
|
-
import { join as
|
|
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
|
|
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
|
|
17165
|
+
var MAX_SESSIONS3 = 100;
|
|
16721
17166
|
var ROOT_CHANNEL = "ahp-root://";
|
|
16722
|
-
var run2 =
|
|
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" ?
|
|
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" ?
|
|
16733
|
-
return ["Code", "Code - Insiders"].map((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:
|
|
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 =
|
|
16763
|
-
const files2 = (await
|
|
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) =>
|
|
16766
|
-
|
|
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
|
|
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(
|
|
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-${
|
|
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: () =>
|
|
17401
|
+
createConnection: () => connect3(this.entry.path),
|
|
16957
17402
|
maxPayload: MAX_BYTES2,
|
|
16958
|
-
handshakeTimeout:
|
|
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:
|
|
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
|
-
},
|
|
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:
|
|
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,
|
|
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((
|
|
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
|
|
17204
|
-
import { promisify as
|
|
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
|
|
17207
|
-
import { stat as stat3, readlink, readdir as
|
|
17208
|
-
var run3 =
|
|
17209
|
-
var
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
17262
|
-
const target = await readlink(
|
|
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:
|
|
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:
|
|
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(
|
|
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=${
|
|
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,
|
|
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((
|
|
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(),
|
|
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
|
|
17750
|
-
import { spawn as
|
|
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
|
|
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
|
|
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 =
|
|
17772
|
-
for (let depth = 0; depth < 8; depth++, directory2 =
|
|
17773
|
-
const path =
|
|
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(
|
|
18309
|
+
const executable = realpathSync2.native(join8(root, "exe"));
|
|
17784
18310
|
if (basename3(executable) !== "codex" || packageVersion(executable) !== CODEX_VERSION) return [];
|
|
17785
|
-
const status2 = readFileSync3(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
18332
|
+
const queue = identity(join8(home, "queue_1.sqlite"));
|
|
17807
18333
|
if (!files2.some((file) => {
|
|
17808
18334
|
try {
|
|
17809
|
-
return sameFile(identity(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
},
|
|
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 =
|
|
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
|
|
18233
|
-
if (!
|
|
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:
|
|
18240
|
-
body:
|
|
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
|
|
18481
|
-
if (!prior?.approvals.some((item) => item.id ===
|
|
18482
|
-
await this.attention(project.id, thread.id,
|
|
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(
|
|
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
|
-
|
|
19112
|
+
const outcome = await this.command(pair, content);
|
|
18567
19113
|
delete this.config.unconfirmed[content.requestId];
|
|
18568
|
-
if (
|
|
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,
|
|
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 (
|
|
19172
|
+
if (approval3)
|
|
18626
19173
|
this.work = this.work.then(async () => {
|
|
18627
|
-
if (this.agents.get(session.id) !== session || ![...session.approvals.values()].includes(
|
|
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
|
-
|
|
18633
|
-
|
|
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
|
|
18762
|
-
if (!
|
|
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,
|
|
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,
|
|
18769
|
-
if (!
|
|
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
|
|
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
|
|
18797
|
-
import { cp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
18798
|
-
import { dirname as
|
|
18799
|
-
var run4 =
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
18801
19382
|
var NODE_FLAGS = ["--disable-warning=ExperimentalWarning"];
|
|
18802
|
-
var serviceFile = () => process.platform === "darwin" ?
|
|
18803
|
-
process.env.APPDATA ??
|
|
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
|
-
) :
|
|
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 =
|
|
19422
|
+
const app = join10(dirname6(configPath()), "app");
|
|
18842
19423
|
await rm(app, { recursive: true, force: true });
|
|
18843
|
-
await cp(
|
|
19424
|
+
await cp(join10(root, "node_modules"), join10(app, "node_modules"), {
|
|
18844
19425
|
recursive: true,
|
|
18845
19426
|
verbatimSymlinks: true
|
|
18846
19427
|
});
|
|
18847
|
-
return
|
|
19428
|
+
return join10(app, relative(root, entry));
|
|
18848
19429
|
}
|
|
18849
19430
|
async function installStartup() {
|
|
18850
19431
|
const path = serviceFile();
|
|
18851
|
-
await
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
19046
|
-
|
|
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(
|
|
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 =
|
|
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);
|