@paradow/ledge 0.8.1 → 0.9.1
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/dist/cli/{attach-KTZOG7E5.js → attach-AZFKVVKL.js} +7 -6
- package/dist/cli/{bridge-QKQW7UD6.js → bridge-XHPCIYE4.js} +9 -3
- package/dist/cli/{chunk-7AKUPFMS.js → chunk-65XGYBHL.js} +15 -6
- package/dist/cli/{chunk-L6IGH4LF.js → chunk-FGOFRWIB.js} +2 -13
- package/dist/cli/{chunk-Y5TCZTFL.js → chunk-I4C5U6AI.js} +97 -5
- package/dist/cli/{chunk-6ZKNEJND.js → chunk-IWJAJAPK.js} +14 -4
- package/dist/cli/{chunk-KJWMK65Y.js → chunk-KDYAHPYN.js} +167 -7
- package/dist/cli/{chunk-ZPANTZKC.js → chunk-LYTRXJAQ.js} +38 -5
- package/dist/cli/{chunk-Q5CLZPYJ.js → chunk-Q7RCYJ4X.js} +2 -2
- package/dist/cli/{chunk-3TNPEAMR.js → chunk-TO4XO2ML.js} +4 -2
- package/dist/cli/chunk-YUSY3E47.js +17 -0
- package/dist/cli/chunk-YXXXSRQX.js +6 -0
- package/dist/cli/{daemon-MSDX3HWZ.js → daemon-NDXT3ZDF.js} +391 -172
- package/dist/cli/{hook-BBVUY4RK.js → hook-5HXY2UGM.js} +48 -12
- package/dist/cli/index.js +18 -10
- package/dist/cli/{launch-RWT4EWWI.js → launch-HEJKGMYS.js} +7 -6
- package/dist/cli/{setup-RMPIUCPH.js → setup-7AEH5DC3.js} +4 -3
- package/dist/cli/{status-L2GROM4J.js → status-PDNC6IC2.js} +3 -2
- package/dist/cli/{uninstall-GI4WT3UQ.js → uninstall-7U7ENKOS.js} +4 -3
- package/dist/cli/{version-VOKMNNLW.js → version-AQN6JNH7.js} +1 -1
- package/dist/cli/who-LIMQJMNI.js +43 -0
- package/package.json +1 -1
- package/dist/cli/chunk-CCB5DTOS.js +0 -6
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
attachEnv
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-65XGYBHL.js";
|
|
4
|
+
import "./chunk-LYTRXJAQ.js";
|
|
5
5
|
import {
|
|
6
6
|
realTmux
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-TO4XO2ML.js";
|
|
8
|
+
import "./chunk-YUSY3E47.js";
|
|
9
9
|
import {
|
|
10
10
|
connect
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-IWJAJAPK.js";
|
|
12
12
|
import {
|
|
13
13
|
PROTOCOL_PORT
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-KDYAHPYN.js";
|
|
15
|
+
import "./chunk-FGOFRWIB.js";
|
|
15
16
|
|
|
16
17
|
// src/cli/attach.ts
|
|
17
18
|
import { spawn } from "child_process";
|
|
@@ -5,14 +5,20 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
PROTOCOL_PORT,
|
|
7
7
|
parseClientMessage,
|
|
8
|
-
parseServerMessage
|
|
9
|
-
|
|
8
|
+
parseServerMessage,
|
|
9
|
+
readDaemonToken
|
|
10
|
+
} from "./chunk-KDYAHPYN.js";
|
|
11
|
+
import "./chunk-FGOFRWIB.js";
|
|
10
12
|
|
|
11
13
|
// src/cli/bridge.ts
|
|
12
14
|
import WebSocket from "ws";
|
|
15
|
+
function bearerHeaders() {
|
|
16
|
+
const token = readDaemonToken();
|
|
17
|
+
return token ? { authorization: `Bearer ${token}` } : {};
|
|
18
|
+
}
|
|
13
19
|
function bridge(opts = {}) {
|
|
14
20
|
const input = opts.input ?? process.stdin, output = opts.output ?? process.stdout;
|
|
15
|
-
const ws = new WebSocket(`ws://127.0.0.1:${opts.port ?? PROTOCOL_PORT}`, { maxPayload: MAX_FRAME, handshakeTimeout: 1e4 });
|
|
21
|
+
const ws = new WebSocket(`ws://127.0.0.1:${opts.port ?? PROTOCOL_PORT}`, { maxPayload: MAX_FRAME, handshakeTimeout: 1e4, headers: bearerHeaders() });
|
|
16
22
|
return new Promise((resolve, reject) => {
|
|
17
23
|
let settled = false, off = () => {
|
|
18
24
|
};
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
exitFile,
|
|
3
3
|
spawnSession
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-LYTRXJAQ.js";
|
|
5
5
|
import {
|
|
6
6
|
realTmux
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-TO4XO2ML.js";
|
|
8
8
|
import {
|
|
9
|
-
ensureLedgeDir,
|
|
10
9
|
log
|
|
11
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-YUSY3E47.js";
|
|
12
11
|
import {
|
|
12
|
+
DaemonRefusedError,
|
|
13
13
|
connect
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-IWJAJAPK.js";
|
|
15
15
|
import {
|
|
16
16
|
PROTOCOL_PORT
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-KDYAHPYN.js";
|
|
18
|
+
import {
|
|
19
|
+
ensureLedgeDir
|
|
20
|
+
} from "./chunk-FGOFRWIB.js";
|
|
18
21
|
|
|
19
22
|
// src/cli/launch.ts
|
|
20
23
|
import { spawn } from "child_process";
|
|
@@ -59,6 +62,7 @@ async function probe(port, timeoutMs) {
|
|
|
59
62
|
try {
|
|
60
63
|
client = await connect({ port, timeoutMs });
|
|
61
64
|
} catch (e) {
|
|
65
|
+
if (e instanceof DaemonRefusedError) return { client: null, state: "unauthorized" };
|
|
62
66
|
if (e.message === "connect timeout") return { client: null, state: "timeout" };
|
|
63
67
|
return { client: null, state: await portOpen(port) ? "foreign" : "refused" };
|
|
64
68
|
}
|
|
@@ -80,6 +84,10 @@ async function ensureDaemon(opts = {}) {
|
|
|
80
84
|
log(`port ${port} is open but does not speak the Ledge protocol`);
|
|
81
85
|
return { client: null, reason: "foreign" };
|
|
82
86
|
}
|
|
87
|
+
if (first.state === "unauthorized") {
|
|
88
|
+
log(`the daemon on port ${port} refused this client's token`);
|
|
89
|
+
return { client: null, reason: "unauthorized" };
|
|
90
|
+
}
|
|
83
91
|
if (first.state === "timeout") {
|
|
84
92
|
log(`port ${port} accepted a connection but did not answer within ${timeoutMs} ms`);
|
|
85
93
|
return { client: null, reason: "unresponsive" };
|
|
@@ -142,6 +150,7 @@ async function readExitCode(id) {
|
|
|
142
150
|
}
|
|
143
151
|
var UNTRACKED = {
|
|
144
152
|
foreign: (port) => `ledge: port ${port} is used by another program, session not tracked`,
|
|
153
|
+
unauthorized: () => "ledge: the daemon refused this client's token (missing or changed), session not tracked; restart the daemon: systemctl --user restart ledge-daemon",
|
|
145
154
|
unresponsive: () => "ledge: daemon not responding, session not tracked",
|
|
146
155
|
unreachable: () => "ledge: daemon not running, session not tracked"
|
|
147
156
|
};
|
|
@@ -17,7 +17,7 @@ function ledgeDir() {
|
|
|
17
17
|
}
|
|
18
18
|
function paths() {
|
|
19
19
|
const dir = ledgeDir();
|
|
20
|
-
return { dir, tmuxConf: join(dir, "tmux.conf"), journal: join(dir, "sessions.json"), log: join(dir, "daemon.log"), pid: join(dir, "daemon.pid"), history: join(dir, "history"), machines: join(dir, "machines.json"), projects: join(dir, "projects.json"), island: join(dir, "island.json") };
|
|
20
|
+
return { dir, tmuxConf: join(dir, "tmux.conf"), journal: join(dir, "sessions.json"), log: join(dir, "daemon.log"), pid: join(dir, "daemon.pid"), history: join(dir, "history"), machines: join(dir, "machines.json"), projects: join(dir, "projects.json"), island: join(dir, "island.json"), token: join(dir, "token") };
|
|
21
21
|
}
|
|
22
22
|
async function ensureLedgeDir() {
|
|
23
23
|
const p = paths();
|
|
@@ -32,18 +32,7 @@ async function ensureLedgeDir() {
|
|
|
32
32
|
${TITLE_LINES}`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// src/log.ts
|
|
36
|
-
import { appendFileSync } from "fs";
|
|
37
|
-
function log(line) {
|
|
38
|
-
try {
|
|
39
|
-
appendFileSync(paths().log, `${(/* @__PURE__ */ new Date()).toISOString()} | ${line}
|
|
40
|
-
`);
|
|
41
|
-
} catch {
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
35
|
export {
|
|
46
36
|
paths,
|
|
47
|
-
ensureLedgeDir
|
|
48
|
-
log
|
|
37
|
+
ensureLedgeDir
|
|
49
38
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cleanTopic,
|
|
3
3
|
sessionTitle
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KDYAHPYN.js";
|
|
5
5
|
|
|
6
6
|
// src/daemon/crossSession.ts
|
|
7
7
|
var FIRST_LINE = /^Another Claude session sent a message:\s*\n/;
|
|
@@ -19,6 +19,44 @@ function crossSession(text) {
|
|
|
19
19
|
}
|
|
20
20
|
return out.length ? out : void 0;
|
|
21
21
|
}
|
|
22
|
+
var NOTICE_HEAD = /^\[Cross-session ([\w -]+?) notice\]/;
|
|
23
|
+
var NOTICE_SPLIT = /\n\s*(?=\[Cross-session [\w -]+? notice\])/;
|
|
24
|
+
var DELIVERY = [
|
|
25
|
+
[/\bheld by that session\b/, "held"],
|
|
26
|
+
[/\bdenied by the recipient\b/, "denied"],
|
|
27
|
+
[/\bnot approved before expiry\b/, "expired"],
|
|
28
|
+
[/\bapproved and released\b/, "delivered"],
|
|
29
|
+
[/\bdropped at that session's inbox\b/, "dropped"],
|
|
30
|
+
[/\b(?:was|were) refused\b/, "refused"]
|
|
31
|
+
];
|
|
32
|
+
function crossSessionNotices(text) {
|
|
33
|
+
const body = text.trim();
|
|
34
|
+
if (!NOTICE_HEAD.test(body)) return void 0;
|
|
35
|
+
return body.split(NOTICE_SPLIT).map((part) => notice(part.trim()));
|
|
36
|
+
}
|
|
37
|
+
function notice(text) {
|
|
38
|
+
const kind = NOTICE_HEAD.exec(text)[1];
|
|
39
|
+
if (kind === "idle") {
|
|
40
|
+
const peer = /"([^"]*)"/.exec(text)?.[1] ?? "";
|
|
41
|
+
if (/^\[[^\]]*\]\s*No idle signal arrived from "/.test(text)) return { kind: "expired", peer, text };
|
|
42
|
+
const exited = /\bhas exited(?: \(at ([^)]+)\))?/.exec(text);
|
|
43
|
+
if (exited) return { kind: "exited", peer, ...exited[1] ? { time: exited[1] } : {}, text };
|
|
44
|
+
const unavailable = /is not holding your idle subscription(?: \(([^)]*)\))?/.exec(text);
|
|
45
|
+
if (unavailable) return { kind: "unavailable", peer, ...unavailable[1] ? { reason: unavailable[1] } : {}, text };
|
|
46
|
+
const time = /\bfinished a turn at (.+?)\.(?=\s|$)/.exec(text)?.[1];
|
|
47
|
+
const summary = /Its harness reports:\s*«([\s\S]*?)»/.exec(text)?.[1]?.trim();
|
|
48
|
+
return { kind: "idle", peer, ...time ? { time } : {}, ...summary ? { summary } : {}, text };
|
|
49
|
+
}
|
|
50
|
+
if (kind === "delivery") {
|
|
51
|
+
const status = DELIVERY.find(([re]) => re.test(text))?.[1] ?? "unknown";
|
|
52
|
+
const many = /\b(\d+) of your messages\b/.exec(text);
|
|
53
|
+
const to = /\brecipients?:\s*([^;)]*)/.exec(text)?.[1];
|
|
54
|
+
const recipients = to ? to.split(",").map((r) => r.trim()).filter((r) => r && r !== "\u2026") : [];
|
|
55
|
+
const reason = status === "dropped" ? /NOT delivered \(([^)]*)\)/.exec(text)?.[1] : void 0;
|
|
56
|
+
return { kind: "delivery", status, count: many ? Number(many[1]) : 1, recipients, ...reason ? { reason } : {}, text };
|
|
57
|
+
}
|
|
58
|
+
return { kind: "other", text };
|
|
59
|
+
}
|
|
22
60
|
|
|
23
61
|
// src/daemon/state.ts
|
|
24
62
|
var HOOK_PRIORITY_MS = 2e3;
|
|
@@ -63,6 +101,11 @@ function pendingTool(p) {
|
|
|
63
101
|
if (p.kind === "plan") return "ExitPlanMode";
|
|
64
102
|
return p.tool;
|
|
65
103
|
}
|
|
104
|
+
function answeredBy(p, ev) {
|
|
105
|
+
if (p.source !== "hook" || ev.name !== "PostToolUse" && ev.name !== "PostToolUseFailure") return false;
|
|
106
|
+
const tool = pendingTool(p);
|
|
107
|
+
return tool !== void 0 && ev.tool === tool && ev.helperId === p.helperId;
|
|
108
|
+
}
|
|
66
109
|
function clearedByToolRun(p, tool) {
|
|
67
110
|
const t = pendingTool(p);
|
|
68
111
|
return p.source === "pane" || t === void 0 || t === tool;
|
|
@@ -235,16 +278,17 @@ function reduceEvent(s, ev) {
|
|
|
235
278
|
case "PreToolUse":
|
|
236
279
|
case "PermissionRequest":
|
|
237
280
|
case "Notification": {
|
|
238
|
-
|
|
281
|
+
const notice2 = ev.name === "UserPromptSubmit" && !!ev.text && !!crossSessionNotices(ev.text);
|
|
282
|
+
if (ev.name === "UserPromptSubmit" && ev.text && !s.topic && !notice2 && !crossSession(ev.text)) s = withTopic(s, ev.text, "prompt");
|
|
239
283
|
const incoming = pendingFor(ev);
|
|
240
284
|
if (incoming) {
|
|
241
|
-
if (ev.name === "Notification" && s.pending
|
|
285
|
+
if (ev.name === "Notification" && s.pending) return { ...s, lastHookAt: at, lastActivityAt: at };
|
|
242
286
|
const pending = mergePending(s.pending, incoming);
|
|
243
287
|
const state = pending.kind === "question" ? "waiting_input" : "waiting_permission";
|
|
244
288
|
return { ...s, state, pending, lastHookAt: at, lastActivityAt: at };
|
|
245
289
|
}
|
|
246
290
|
if (ev.name === "Notification" || ev.name === "SessionStart") return { ...s, lastHookAt: at, lastActivityAt: at };
|
|
247
|
-
const prompted = ev.name === "UserPromptSubmit" && s.lastReply ? withoutReply(s) : s;
|
|
291
|
+
const prompted = ev.name === "UserPromptSubmit" && s.lastReply && !notice2 ? withoutReply(s) : s;
|
|
248
292
|
if (prompted.pending?.helperId) return underHelper(prompted, "working", at);
|
|
249
293
|
if (prompted === s && s.state === "working" && s.lastActivityAt === at && !s.pending) return s;
|
|
250
294
|
return { ...prompted, state: "working", pending: void 0, lastHookAt: at, lastActivityAt: at };
|
|
@@ -252,8 +296,56 @@ function reduceEvent(s, ev) {
|
|
|
252
296
|
}
|
|
253
297
|
}
|
|
254
298
|
|
|
299
|
+
// src/daemon/git.ts
|
|
300
|
+
import { execFile } from "child_process";
|
|
301
|
+
import { basename, dirname, resolve } from "path";
|
|
302
|
+
function repositoryId(remote) {
|
|
303
|
+
const value = remote.trim();
|
|
304
|
+
try {
|
|
305
|
+
const url = new URL(value);
|
|
306
|
+
if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return void 0;
|
|
307
|
+
return `${url.hostname.toLowerCase()}${url.pathname.replace(/\.git\/?$/, "").replace(/\/$/, "")}`;
|
|
308
|
+
} catch {
|
|
309
|
+
const scp = value.match(/^(?:[^@\s]+@)?([a-zA-Z0-9.-]+):([^\s]+)$/);
|
|
310
|
+
return scp ? `${scp[1].toLowerCase()}/${scp[2].replace(/\.git$/, "").replace(/\/$/, "")}` : void 0;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
var execGit = (file, args, cwd) => new Promise((res) => {
|
|
314
|
+
execFile(file, args, { cwd, timeout: 3e3, maxBuffer: 1024 * 1024 }, (err, stdout) => res({ code: err ? 1 : 0, stdout: String(stdout) }));
|
|
315
|
+
});
|
|
316
|
+
async function gitInfo(cwd, exec = execGit) {
|
|
317
|
+
const r = await exec("git", ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-common-dir", "--abbrev-ref", "HEAD"], cwd);
|
|
318
|
+
if (r.code !== 0) return null;
|
|
319
|
+
const [toplevel = "", common = "", abbrev = ""] = r.stdout.split("\n").map((l) => l.trim());
|
|
320
|
+
if (!toplevel || !common || !abbrev) return null;
|
|
321
|
+
const worktree = resolve(common) !== resolve(toplevel, ".git");
|
|
322
|
+
const repoPath = worktree ? dirname(resolve(common)) : resolve(toplevel);
|
|
323
|
+
let branch = abbrev;
|
|
324
|
+
if (abbrev === "HEAD") {
|
|
325
|
+
const sha = await exec("git", ["rev-parse", "--short", "HEAD"], cwd);
|
|
326
|
+
if (sha.code !== 0 || !sha.stdout.trim()) return null;
|
|
327
|
+
branch = sha.stdout.trim();
|
|
328
|
+
}
|
|
329
|
+
const remote = await exec("git", ["config", "--get", "remote.origin.url"], cwd);
|
|
330
|
+
const repoId = remote.code === 0 ? repositoryId(remote.stdout) : void 0;
|
|
331
|
+
return { repo: basename(repoPath), repoPath, branch, worktree, ...repoId ? { repoId } : {} };
|
|
332
|
+
}
|
|
333
|
+
async function gitSummary(cwd, exec = execGit) {
|
|
334
|
+
const status = await exec("git", ["status", "--short", "--branch"], cwd);
|
|
335
|
+
if (status.code !== 0) return null;
|
|
336
|
+
const log = await exec("git", ["log", "--oneline", "-10"], cwd);
|
|
337
|
+
const diff = await exec("git", ["diff", "--stat"], cwd);
|
|
338
|
+
return [`# git status --short --branch`, status.stdout.trimEnd(), "", "# git log --oneline -10", log.stdout.trimEnd(), "", "# git diff --stat", diff.stdout.trimEnd() || "(clean)"].join("\n");
|
|
339
|
+
}
|
|
340
|
+
|
|
255
341
|
export {
|
|
256
342
|
crossSession,
|
|
343
|
+
crossSessionNotices,
|
|
257
344
|
QUESTION_TOOLS,
|
|
258
|
-
|
|
345
|
+
pendingTool,
|
|
346
|
+
answeredBy,
|
|
347
|
+
reduce,
|
|
348
|
+
repositoryId,
|
|
349
|
+
gitInfo,
|
|
350
|
+
gitSummary
|
|
259
351
|
};
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROTOCOL_PORT,
|
|
3
|
-
parseServerMessage
|
|
4
|
-
|
|
3
|
+
parseServerMessage,
|
|
4
|
+
readDaemonToken
|
|
5
|
+
} from "./chunk-KDYAHPYN.js";
|
|
5
6
|
|
|
6
7
|
// src/client.ts
|
|
7
8
|
import WebSocket from "ws";
|
|
9
|
+
var DaemonRefusedError = class extends Error {
|
|
10
|
+
};
|
|
8
11
|
function connect(opts = {}) {
|
|
9
12
|
const port = opts.port ?? PROTOCOL_PORT;
|
|
10
13
|
const host = opts.host ?? "127.0.0.1";
|
|
11
|
-
|
|
14
|
+
let token;
|
|
15
|
+
try {
|
|
16
|
+
token = opts.token === void 0 ? readDaemonToken() : opts.token;
|
|
17
|
+
} catch {
|
|
18
|
+
token = null;
|
|
19
|
+
}
|
|
20
|
+
const ws = new WebSocket(`ws://${host}:${port}`, token ? { headers: { authorization: `Bearer ${token}` } } : {});
|
|
12
21
|
const queue = [];
|
|
13
22
|
const MAX_QUEUED = 100;
|
|
14
23
|
const waiters = [];
|
|
@@ -66,12 +75,13 @@ function connect(opts = {}) {
|
|
|
66
75
|
if (!settled) {
|
|
67
76
|
settled = true;
|
|
68
77
|
clearTimeout(t);
|
|
69
|
-
reject(e);
|
|
78
|
+
reject(/Unexpected server response: 401/.test(e.message) ? new DaemonRefusedError("daemon refused the token") : e);
|
|
70
79
|
}
|
|
71
80
|
});
|
|
72
81
|
});
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
export {
|
|
85
|
+
DaemonRefusedError,
|
|
76
86
|
connect
|
|
77
87
|
};
|
|
@@ -1,5 +1,125 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
paths
|
|
3
|
+
} from "./chunk-FGOFRWIB.js";
|
|
4
|
+
|
|
5
|
+
// src/auth.ts
|
|
6
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
7
|
+
import { chmod, link, mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { dirname } from "path";
|
|
10
|
+
var TOKEN_RE = /^[0-9a-f]{64}$/;
|
|
11
|
+
var DAEMON_PROTOCOL_PREFIX = "ledge.";
|
|
12
|
+
function isDaemonToken(value) {
|
|
13
|
+
return TOKEN_RE.test(value);
|
|
14
|
+
}
|
|
15
|
+
function tokenEquals(presented, expected) {
|
|
16
|
+
const a = createHash("sha256").update(presented.slice(0, 256), "utf8").digest();
|
|
17
|
+
const b = createHash("sha256").update(expected, "utf8").digest();
|
|
18
|
+
return timingSafeEqual(a, b);
|
|
19
|
+
}
|
|
20
|
+
function hostAllowed(hostHeader) {
|
|
21
|
+
const raw = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
|
|
22
|
+
if (!raw || raw.length > 255) return false;
|
|
23
|
+
const host = raw.trim().toLowerCase();
|
|
24
|
+
if (!host || host.includes(" ") || host.endsWith(".")) return false;
|
|
25
|
+
let name;
|
|
26
|
+
if (host.startsWith("[")) {
|
|
27
|
+
const end = host.indexOf("]");
|
|
28
|
+
if (end < 0) return false;
|
|
29
|
+
name = host.slice(1, end);
|
|
30
|
+
const rest = host.slice(end + 1);
|
|
31
|
+
if (rest !== "" && !/^:\d{1,5}$/.test(rest)) return false;
|
|
32
|
+
} else {
|
|
33
|
+
const colon = host.lastIndexOf(":");
|
|
34
|
+
if (colon >= 0) {
|
|
35
|
+
if (!/^\d{1,5}$/.test(host.slice(colon + 1))) return false;
|
|
36
|
+
name = host.slice(0, colon);
|
|
37
|
+
} else name = host;
|
|
38
|
+
}
|
|
39
|
+
return name === "localhost" || name === "127.0.0.1" || name === "::1";
|
|
40
|
+
}
|
|
41
|
+
function headerValue(value, max) {
|
|
42
|
+
const raw = Array.isArray(value) ? value.join(",") : value;
|
|
43
|
+
return !raw || raw.length > max ? "" : raw;
|
|
44
|
+
}
|
|
45
|
+
function bearerToken(authorization) {
|
|
46
|
+
if (!authorization || authorization.length > 512) return "";
|
|
47
|
+
return /^Bearer[ \t]+(\S+)$/i.exec(authorization)?.[1] ?? "";
|
|
48
|
+
}
|
|
49
|
+
function protocolToken(header) {
|
|
50
|
+
const raw = headerValue(header, 512);
|
|
51
|
+
if (!raw) return "";
|
|
52
|
+
for (const part of raw.split(",").slice(0, 8)) {
|
|
53
|
+
const protocol = part.trim();
|
|
54
|
+
if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX)) return protocol.slice(DAEMON_PROTOCOL_PREFIX.length);
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
function requestAuthorized(headers, expected) {
|
|
59
|
+
const bearerOk = tokenEquals(bearerToken(typeof headers.authorization === "string" ? headers.authorization : void 0), expected);
|
|
60
|
+
const presented = protocolToken(headers["sec-websocket-protocol"]);
|
|
61
|
+
return bearerOk || presented !== "" && tokenEquals(presented, expected);
|
|
62
|
+
}
|
|
63
|
+
function selectDaemonProtocol(protocols, expected) {
|
|
64
|
+
for (const protocol of protocols) {
|
|
65
|
+
if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX) && tokenEquals(protocol.slice(DAEMON_PROTOCOL_PREFIX.length), expected)) return protocol;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
async function readTokenFile(file) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = (await readFile(file, "utf8")).trim();
|
|
72
|
+
return isDaemonToken(raw) ? raw : void 0;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (e.code === "ENOENT") return void 0;
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function lockTokenFile(file) {
|
|
79
|
+
await chmod(file, 384);
|
|
80
|
+
}
|
|
81
|
+
async function ensureDaemonToken() {
|
|
82
|
+
const file = paths().token;
|
|
83
|
+
await mkdir(dirname(file), { recursive: true });
|
|
84
|
+
const current = await readTokenFile(file);
|
|
85
|
+
if (current) {
|
|
86
|
+
await lockTokenFile(file);
|
|
87
|
+
return current;
|
|
88
|
+
}
|
|
89
|
+
const token = randomBytes(32).toString("hex");
|
|
90
|
+
const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
91
|
+
await writeFile(tmp, `${token}
|
|
92
|
+
`, { mode: 384, flag: "wx" });
|
|
93
|
+
await chmod(tmp, 384);
|
|
94
|
+
try {
|
|
95
|
+
await link(tmp, file);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
await unlink(tmp).catch(() => {
|
|
98
|
+
});
|
|
99
|
+
if (e.code !== "EEXIST") throw e;
|
|
100
|
+
const raced = await readTokenFile(file);
|
|
101
|
+
if (raced) {
|
|
102
|
+
await lockTokenFile(file);
|
|
103
|
+
return raced;
|
|
104
|
+
}
|
|
105
|
+
await writeFile(file, `${token}
|
|
106
|
+
`, { mode: 384 });
|
|
107
|
+
}
|
|
108
|
+
await unlink(tmp).catch(() => {
|
|
109
|
+
});
|
|
110
|
+
await lockTokenFile(file);
|
|
111
|
+
const stored = await readTokenFile(file);
|
|
112
|
+
if (!stored) throw new Error("daemon token was not stored");
|
|
113
|
+
return stored;
|
|
114
|
+
}
|
|
115
|
+
function readDaemonToken() {
|
|
116
|
+
try {
|
|
117
|
+
const raw = readFileSync(paths().token, "utf8").trim();
|
|
118
|
+
return isDaemonToken(raw) ? raw : void 0;
|
|
119
|
+
} catch {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
3
123
|
|
|
4
124
|
// ../protocol/src/session.ts
|
|
5
125
|
import { z } from "zod";
|
|
@@ -83,6 +203,9 @@ function isArchived(s) {
|
|
|
83
203
|
return s.archivedAt !== void 0;
|
|
84
204
|
}
|
|
85
205
|
|
|
206
|
+
// ../protocol/src/messages.ts
|
|
207
|
+
import { z as z13 } from "zod";
|
|
208
|
+
|
|
86
209
|
// ../protocol/src/chat.ts
|
|
87
210
|
import { z as z2 } from "zod";
|
|
88
211
|
var ChatStepSchema = z2.object({
|
|
@@ -99,6 +222,8 @@ var ChatProposalSchema = z2.object({
|
|
|
99
222
|
launchedId: z2.string().optional(),
|
|
100
223
|
error: z2.string().optional()
|
|
101
224
|
});
|
|
225
|
+
var MAX_PROPOSALS = 8;
|
|
226
|
+
var ChatProposalsSchema = z2.array(ChatProposalSchema).min(1).max(MAX_PROPOSALS);
|
|
102
227
|
var ChatRelaySchema = z2.object({
|
|
103
228
|
id: z2.string().min(1),
|
|
104
229
|
name: z2.string(),
|
|
@@ -126,9 +251,11 @@ var ChatTurnSchema = z2.object({
|
|
|
126
251
|
answeredAt: z2.string().datetime().optional(),
|
|
127
252
|
error: z2.string().optional(),
|
|
128
253
|
proposal: ChatProposalSchema.optional(),
|
|
254
|
+
proposals: ChatProposalsSchema.optional(),
|
|
129
255
|
relay: ChatRelaySchema.optional(),
|
|
130
256
|
projectProposal: ChatProjectProposalSchema.optional()
|
|
131
257
|
});
|
|
258
|
+
var proposalRows = (t) => t?.proposals ?? (t?.proposal ? [t.proposal] : []);
|
|
132
259
|
|
|
133
260
|
// ../protocol/src/repos.ts
|
|
134
261
|
import { z as z3 } from "zod";
|
|
@@ -144,7 +271,7 @@ var ReposInfoSchema = z3.object({ type: z3.literal("repos.info"), requestId: z3.
|
|
|
144
271
|
// ../protocol/src/scan.ts
|
|
145
272
|
import { z as z4 } from "zod";
|
|
146
273
|
var ReposScanSchema = z4.object({ type: z4.literal("repos.scan"), requestId: z4.string().min(1).max(100), machineId: z4.string().optional() });
|
|
147
|
-
var FoundRepoSchema = z4.object({ path: z4.string(), branch: z4.string().nullable(), remote: z4.string().optional() });
|
|
274
|
+
var FoundRepoSchema = z4.object({ path: z4.string(), branch: z4.string().nullable(), remote: z4.string().optional(), manifest: z4.string().max(200).optional() });
|
|
148
275
|
var ReposFoundSchema = z4.object({ type: z4.literal("repos.found"), requestId: z4.string(), repos: z4.array(FoundRepoSchema).max(500), truncated: z4.boolean(), error: z4.string().optional() });
|
|
149
276
|
|
|
150
277
|
// ../protocol/src/documents.ts
|
|
@@ -339,7 +466,24 @@ var ThreadItemSchema = z11.discriminatedUnion("kind", [
|
|
|
339
466
|
/** A message this session sent to another agent; `id` is the tool_use_id, `to` the address it used, `peer` the Ledge session id, `received` the paired item's id in the peer's thread. */
|
|
340
467
|
z11.object({ kind: z11.literal("sent"), ...base, to: z11.string(), peer: z11.string().optional(), summary: z11.string().optional(), text: z11.string(), status: z11.enum(["sending", "delivered", "read", "failed"]), received: z11.string().optional() }),
|
|
341
468
|
/** A message another agent sent to this session; `from` is its from-name (else its address), `sent` the paired item's id in the peer's thread. */
|
|
342
|
-
z11.object({ kind: z11.literal("received"), ...base, from: z11.string(), peer: z11.string().optional(), text: z11.string(), sent: z11.string().optional() })
|
|
469
|
+
z11.object({ kind: z11.literal("received"), ...base, from: z11.string(), peer: z11.string().optional(), text: z11.string(), sent: z11.string().optional() }),
|
|
470
|
+
/** An automated notice of the agent's harness about messages between sessions (Claude Code's `[Cross-session idle
|
|
471
|
+
* notice]` / `[Cross-session delivery notice]`): a system event, never the user's words. `name` is the session it is
|
|
472
|
+
* about as quoted, `peer` its Ledge session id; a delivery notice says the outcome of `count` messages to `recipients`. */
|
|
473
|
+
z11.object({
|
|
474
|
+
kind: z11.literal("notice"),
|
|
475
|
+
...base,
|
|
476
|
+
notice: z11.enum(["idle", "exited", "unavailable", "expired", "delivery", "other"]),
|
|
477
|
+
name: z11.string().optional(),
|
|
478
|
+
peer: z11.string().optional(),
|
|
479
|
+
time: z11.string().optional(),
|
|
480
|
+
summary: z11.string().optional(),
|
|
481
|
+
status: z11.enum(["held", "denied", "expired", "delivered", "refused", "dropped", "unknown"]).optional(),
|
|
482
|
+
count: z11.number().int().positive().optional(),
|
|
483
|
+
recipients: z11.array(z11.string()).optional(),
|
|
484
|
+
reason: z11.string().optional(),
|
|
485
|
+
text: z11.string()
|
|
486
|
+
})
|
|
343
487
|
]);
|
|
344
488
|
|
|
345
489
|
// ../protocol/src/decisions.ts
|
|
@@ -447,7 +591,7 @@ var ServerMessageSchema = z13.discriminatedUnion("type", [
|
|
|
447
591
|
/** The chat (spec 2026-10-11 §5): what Ledge reads, its words as they come, the final text, or why it stopped; the day's turns after each snapshot. */
|
|
448
592
|
z13.object({ type: z13.literal("chat.step"), qid: z13.string(), step: ChatStepSchema }),
|
|
449
593
|
z13.object({ type: z13.literal("chat.delta"), qid: z13.string(), text: z13.string() }),
|
|
450
|
-
z13.object({ type: z13.literal("chat.done"), qid: z13.string(), text: z13.string(), at: z13.string().datetime(), proposal: ChatProposalSchema.optional(), relay: ChatRelaySchema.optional(), projectProposal: ChatProjectProposalSchema.optional() }),
|
|
594
|
+
z13.object({ type: z13.literal("chat.done"), qid: z13.string(), text: z13.string(), at: z13.string().datetime(), proposal: ChatProposalSchema.optional(), proposals: ChatProposalsSchema.optional(), relay: ChatRelaySchema.optional(), projectProposal: ChatProjectProposalSchema.optional() }),
|
|
451
595
|
z13.object({ type: z13.literal("chat.error"), qid: z13.string(), message: z13.string() }),
|
|
452
596
|
z13.object({ type: z13.literal("chat.history"), turns: z13.array(ChatTurnSchema) }),
|
|
453
597
|
/** The daemon updating itself: npm is running, then the service restarts (the socket closes right after). */
|
|
@@ -486,6 +630,8 @@ var ClientMessageSchema = z13.discriminatedUnion("type", [
|
|
|
486
630
|
prompt: z13.string().optional(),
|
|
487
631
|
continue: z13.boolean().optional(),
|
|
488
632
|
qid: z13.string().min(1).optional(),
|
|
633
|
+
/** With `qid`: the card's rows to launch (#24), indexes into the turn's `proposals`, each once; the first row without it. */
|
|
634
|
+
rows: z13.array(z13.number().int().min(0).max(MAX_PROPOSALS - 1)).min(1).max(MAX_PROPOSALS).refine((r) => new Set(r).size === r.length, "a row twice").optional(),
|
|
489
635
|
/** Folders in addition to `cwd` (Claude: `--add-dir`); resolved like `cwd`. */
|
|
490
636
|
dirs: z13.array(z13.string().min(1)).max(20).optional(),
|
|
491
637
|
/** Routing by the fleet, removed before the message reaches the daemon. */
|
|
@@ -501,7 +647,14 @@ var ClientMessageSchema = z13.discriminatedUnion("type", [
|
|
|
501
647
|
/** Copied into the `folder.listing` that answers; the fleet relays only that listing, from the machine asked. */
|
|
502
648
|
requestId: z13.string().min(1).max(100).optional()
|
|
503
649
|
}),
|
|
504
|
-
z13.object({
|
|
650
|
+
z13.object({
|
|
651
|
+
type: z13.literal("folder.create"),
|
|
652
|
+
parent: z13.string().min(1),
|
|
653
|
+
name: z13.string().min(1),
|
|
654
|
+
/** As for `folder.list`: routing by the fleet, and the request its `folder.listing` answers. */
|
|
655
|
+
machineId: z13.string().optional(),
|
|
656
|
+
requestId: z13.string().min(1).max(100).optional()
|
|
657
|
+
}),
|
|
505
658
|
z13.object({ type: z13.literal("session.reply"), id: z13.string(), text: z13.string().min(1) }),
|
|
506
659
|
/** The chat card's instruction (spec 2026-10-12 relay §4): sent now if the session is free, else queued until it is. */
|
|
507
660
|
z13.object({ type: z13.literal("session.relay"), qid: z13.string().min(1) }),
|
|
@@ -658,7 +811,7 @@ function extractProposal(answer) {
|
|
|
658
811
|
return "";
|
|
659
812
|
});
|
|
660
813
|
const last = found.at(-1);
|
|
661
|
-
return last ? { text: text.replace(/\n{3,}/g, "\n\n").trim(), kind: last.kind, raw: last.raw } : { text: answer, kind: null, raw: null };
|
|
814
|
+
return last ? { text: text.replace(/\n{3,}/g, "\n\n").trim(), kind: last.kind, raw: last.raw, blocks: found } : { text: answer, kind: null, raw: null, blocks: [] };
|
|
662
815
|
}
|
|
663
816
|
function parseProposalBlock(raw) {
|
|
664
817
|
try {
|
|
@@ -697,8 +850,15 @@ function sessionTitle(s) {
|
|
|
697
850
|
}
|
|
698
851
|
|
|
699
852
|
export {
|
|
853
|
+
hostAllowed,
|
|
854
|
+
requestAuthorized,
|
|
855
|
+
selectDaemonProtocol,
|
|
856
|
+
ensureDaemonToken,
|
|
857
|
+
readDaemonToken,
|
|
700
858
|
SessionSchema,
|
|
701
859
|
isArchived,
|
|
860
|
+
MAX_PROPOSALS,
|
|
861
|
+
proposalRows,
|
|
702
862
|
NOTE_NOT_TYPED,
|
|
703
863
|
offers,
|
|
704
864
|
pendingKey,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
paths
|
|
3
|
-
} from "./chunk-L6IGH4LF.js";
|
|
4
1
|
import {
|
|
5
2
|
pickName
|
|
6
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KDYAHPYN.js";
|
|
4
|
+
import {
|
|
5
|
+
paths
|
|
6
|
+
} from "./chunk-FGOFRWIB.js";
|
|
7
7
|
|
|
8
8
|
// src/daemon/transcript.ts
|
|
9
9
|
import { closeSync, fstatSync, openSync, readSync } from "fs";
|
|
@@ -95,6 +95,33 @@ function claudePlan(path) {
|
|
|
95
95
|
}
|
|
96
96
|
return plan?.text;
|
|
97
97
|
}
|
|
98
|
+
function claudeCalls(path) {
|
|
99
|
+
const calls = [];
|
|
100
|
+
for (const line of tailLines(path)) {
|
|
101
|
+
let e;
|
|
102
|
+
try {
|
|
103
|
+
e = JSON.parse(line);
|
|
104
|
+
} catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (e.isSidechain === true || !Array.isArray(e.message?.content)) continue;
|
|
108
|
+
for (const b of e.message.content) {
|
|
109
|
+
if (e.type === "assistant" && b?.type === "tool_use" && typeof b.id === "string" && typeof b.name === "string") calls.push({ id: b.id, name: b.name, answered: false });
|
|
110
|
+
else if (e.type === "user" && b?.type === "tool_result") {
|
|
111
|
+
const c = calls.find((c2) => c2.id === b.tool_use_id);
|
|
112
|
+
if (c) c.answered = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return calls;
|
|
117
|
+
}
|
|
118
|
+
function claudeOpenCall(path, tool) {
|
|
119
|
+
const last = claudeCalls(path).filter((c) => c.name === tool).at(-1);
|
|
120
|
+
return last && !last.answered ? last.id : void 0;
|
|
121
|
+
}
|
|
122
|
+
function claudeToolAnswered(path, id) {
|
|
123
|
+
return claudeCalls(path).some((c) => c.id === id && c.answered);
|
|
124
|
+
}
|
|
98
125
|
|
|
99
126
|
// src/spawn.ts
|
|
100
127
|
import { basename, join as join2 } from "path";
|
|
@@ -105,6 +132,10 @@ var CODEX_HOOK_VAR = "LEDGE_CODEX_HOOK_SESSION";
|
|
|
105
132
|
function agentIdArgs(tool, id) {
|
|
106
133
|
return tool === "codex" ? ["-c", `shell_environment_policy.set.${CODEX_HOOK_VAR}="${id}"`] : [];
|
|
107
134
|
}
|
|
135
|
+
var CLAUDE_OWN_NAME = /^(-n|--name)(=|$)/;
|
|
136
|
+
function agentNameArgs(tool, name, args) {
|
|
137
|
+
return tool === "claude" && !args.some((a) => CLAUDE_OWN_NAME.test(a)) ? ["-n", name] : [];
|
|
138
|
+
}
|
|
108
139
|
var CLAUDE_OWN_SESSION = /^(--resume|-r|--continue|-c|--session-id|--fork-session)(=|$)/;
|
|
109
140
|
function claudeSessionId(tool, args) {
|
|
110
141
|
return tool === "claude" && !args.some((a) => CLAUDE_OWN_SESSION.test(a)) ? randomUUID() : void 0;
|
|
@@ -128,7 +159,7 @@ async function spawnSession(opts) {
|
|
|
128
159
|
const project = basename(opts.cwd) || opts.cwd;
|
|
129
160
|
const tmuxSession = `ledge-${id}`;
|
|
130
161
|
const agentSessionId = opts.command ? void 0 : claudeSessionId(opts.tool, opts.args);
|
|
131
|
-
const agent = opts.command ?? [AGENT_BIN[opts.tool], ...agentIdArgs(opts.tool, id), ...agentSessionId ? ["--session-id", agentSessionId] : [], ...opts.args];
|
|
162
|
+
const agent = opts.command ?? [AGENT_BIN[opts.tool], ...agentIdArgs(opts.tool, id), ...agentSessionId ? ["--session-id", agentSessionId] : [], ...agentNameArgs(opts.tool, name, opts.args), ...opts.args];
|
|
132
163
|
const env = opts.env ?? process.env;
|
|
133
164
|
const transcriptPath = agentSessionId ? claudeTranscriptPath(opts.cwd, agentSessionId, env) : void 0;
|
|
134
165
|
const command = opts.recordExit ? ["sh", "-c", '"$@"; echo $? > "$0"', exitFile(id), ...agent] : agent;
|
|
@@ -141,6 +172,8 @@ export {
|
|
|
141
172
|
agentMessages,
|
|
142
173
|
codexPlan,
|
|
143
174
|
claudePlan,
|
|
175
|
+
claudeOpenCall,
|
|
176
|
+
claudeToolAnswered,
|
|
144
177
|
CODEX_ENV_VAR,
|
|
145
178
|
CODEX_HOOK_VAR,
|
|
146
179
|
exitFile,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
run
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-TO4XO2ML.js";
|
|
4
4
|
import {
|
|
5
5
|
ensureLedgeDir,
|
|
6
6
|
paths
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-FGOFRWIB.js";
|
|
8
8
|
|
|
9
9
|
// src/cli/setup.ts
|
|
10
10
|
import { readFile as readFile2, writeFile, mkdir, rename, realpath, rm, stat, lstat, readlink, symlink, unlink } from "fs/promises";
|