@paradow/ledge 0.9.0 → 0.10.0
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-HPGWEUKX.js → attach-2DEXQEC2.js} +7 -6
- package/dist/cli/{bridge-2CVD7BMI.js → bridge-ZOHP53DV.js} +9 -3
- package/dist/cli/{chunk-L6IGH4LF.js → chunk-FGOFRWIB.js} +2 -13
- package/dist/cli/{chunk-564TJJZ6.js → chunk-GMKS55KF.js} +33 -4
- package/dist/cli/{chunk-N3MPPCON.js → chunk-IC5RHOX6.js} +15 -6
- package/dist/cli/{chunk-WM4MAV46.js → chunk-JOCNCNRP.js} +138 -1
- package/dist/cli/{chunk-JVELEEIO.js → chunk-OHKQKXYM.js} +9 -2
- package/dist/cli/{chunk-Q5CLZPYJ.js → chunk-Q7RCYJ4X.js} +2 -2
- package/dist/cli/{chunk-XPWGZIYL.js → chunk-TC3XJUJA.js} +14 -4
- package/dist/cli/{chunk-3TNPEAMR.js → chunk-TO4XO2ML.js} +4 -2
- package/dist/cli/chunk-XOPH7YDV.js +6 -0
- package/dist/cli/chunk-YUSY3E47.js +17 -0
- package/dist/cli/{daemon-SGTQRTHV.js → daemon-RI47DJDX.js} +127 -35
- package/dist/cli/{hook-WQYL4YFH.js → hook-W46IWB7I.js} +6 -5
- package/dist/cli/index.js +10 -10
- package/dist/cli/{launch-I4T5MFWF.js → launch-S6XFRSZA.js} +7 -6
- package/dist/cli/{setup-RMPIUCPH.js → setup-7AEH5DC3.js} +4 -3
- package/dist/cli/{status-GGXF4DSQ.js → status-QOXWNLLF.js} +3 -2
- package/dist/cli/{uninstall-GI4WT3UQ.js → uninstall-7U7ENKOS.js} +4 -3
- package/dist/cli/{version-XFBINYKQ.js → version-SO5OMPGD.js} +1 -1
- package/dist/cli/{who-EJWN7HM6.js → who-WVYFPCII.js} +3 -2
- package/package.json +1 -1
- package/dist/cli/chunk-LWPKX7SB.js +0 -6
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
attachEnv
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-IC5RHOX6.js";
|
|
4
|
+
import "./chunk-GMKS55KF.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-TC3XJUJA.js";
|
|
12
12
|
import {
|
|
13
13
|
PROTOCOL_PORT
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-JOCNCNRP.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-JOCNCNRP.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
|
};
|
|
@@ -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,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
paths
|
|
3
|
-
} from "./chunk-L6IGH4LF.js";
|
|
4
1
|
import {
|
|
5
2
|
pickName
|
|
6
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JOCNCNRP.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";
|
|
@@ -145,6 +172,8 @@ export {
|
|
|
145
172
|
agentMessages,
|
|
146
173
|
codexPlan,
|
|
147
174
|
claudePlan,
|
|
175
|
+
claudeOpenCall,
|
|
176
|
+
claudeToolAnswered,
|
|
148
177
|
CODEX_ENV_VAR,
|
|
149
178
|
CODEX_HOOK_VAR,
|
|
150
179
|
exitFile,
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
exitFile,
|
|
3
3
|
spawnSession
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-GMKS55KF.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-TC3XJUJA.js";
|
|
15
15
|
import {
|
|
16
16
|
PROTOCOL_PORT
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-JOCNCNRP.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
|
};
|
|
@@ -1,3 +1,126 @@
|
|
|
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
|
+
}
|
|
123
|
+
|
|
1
124
|
// ../protocol/src/session.ts
|
|
2
125
|
import { z } from "zod";
|
|
3
126
|
var ToolSchema = z.enum(["claude", "codex", "cursor"]);
|
|
@@ -53,6 +176,8 @@ var SessionSchema = z.object({
|
|
|
53
176
|
repoPath: z.string().optional(),
|
|
54
177
|
branch: z.string().optional(),
|
|
55
178
|
worktree: z.boolean().optional(),
|
|
179
|
+
/** The folder the agent works in now, as its last hook reported it: `cwd` stays where it started, this follows a worktree it entered. */
|
|
180
|
+
workdir: z.string().optional(),
|
|
56
181
|
tmuxSession: z.string().nullable(),
|
|
57
182
|
state: SessionStateSchema,
|
|
58
183
|
pending: PendingSchema.optional(),
|
|
@@ -524,7 +649,14 @@ var ClientMessageSchema = z13.discriminatedUnion("type", [
|
|
|
524
649
|
/** Copied into the `folder.listing` that answers; the fleet relays only that listing, from the machine asked. */
|
|
525
650
|
requestId: z13.string().min(1).max(100).optional()
|
|
526
651
|
}),
|
|
527
|
-
z13.object({
|
|
652
|
+
z13.object({
|
|
653
|
+
type: z13.literal("folder.create"),
|
|
654
|
+
parent: z13.string().min(1),
|
|
655
|
+
name: z13.string().min(1),
|
|
656
|
+
/** As for `folder.list`: routing by the fleet, and the request its `folder.listing` answers. */
|
|
657
|
+
machineId: z13.string().optional(),
|
|
658
|
+
requestId: z13.string().min(1).max(100).optional()
|
|
659
|
+
}),
|
|
528
660
|
z13.object({ type: z13.literal("session.reply"), id: z13.string(), text: z13.string().min(1) }),
|
|
529
661
|
/** The chat card's instruction (spec 2026-10-12 relay §4): sent now if the session is free, else queued until it is. */
|
|
530
662
|
z13.object({ type: z13.literal("session.relay"), qid: z13.string().min(1) }),
|
|
@@ -720,6 +852,11 @@ function sessionTitle(s) {
|
|
|
720
852
|
}
|
|
721
853
|
|
|
722
854
|
export {
|
|
855
|
+
hostAllowed,
|
|
856
|
+
requestAuthorized,
|
|
857
|
+
selectDaemonProtocol,
|
|
858
|
+
ensureDaemonToken,
|
|
859
|
+
readDaemonToken,
|
|
723
860
|
SessionSchema,
|
|
724
861
|
isArchived,
|
|
725
862
|
MAX_PROPOSALS,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cleanTopic,
|
|
3
3
|
sessionTitle
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JOCNCNRP.js";
|
|
5
5
|
|
|
6
6
|
// src/daemon/crossSession.ts
|
|
7
7
|
var FIRST_LINE = /^Another Claude session sent a message:\s*\n/;
|
|
@@ -101,6 +101,11 @@ function pendingTool(p) {
|
|
|
101
101
|
if (p.kind === "plan") return "ExitPlanMode";
|
|
102
102
|
return p.tool;
|
|
103
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
|
+
}
|
|
104
109
|
function clearedByToolRun(p, tool) {
|
|
105
110
|
const t = pendingTool(p);
|
|
106
111
|
return p.source === "pane" || t === void 0 || t === tool;
|
|
@@ -277,7 +282,7 @@ function reduceEvent(s, ev) {
|
|
|
277
282
|
if (ev.name === "UserPromptSubmit" && ev.text && !s.topic && !notice2 && !crossSession(ev.text)) s = withTopic(s, ev.text, "prompt");
|
|
278
283
|
const incoming = pendingFor(ev);
|
|
279
284
|
if (incoming) {
|
|
280
|
-
if (ev.name === "Notification" && s.pending
|
|
285
|
+
if (ev.name === "Notification" && s.pending) return { ...s, lastHookAt: at, lastActivityAt: at };
|
|
281
286
|
const pending = mergePending(s.pending, incoming);
|
|
282
287
|
const state = pending.kind === "question" ? "waiting_input" : "waiting_permission";
|
|
283
288
|
return { ...s, state, pending, lastHookAt: at, lastActivityAt: at };
|
|
@@ -337,6 +342,8 @@ export {
|
|
|
337
342
|
crossSession,
|
|
338
343
|
crossSessionNotices,
|
|
339
344
|
QUESTION_TOOLS,
|
|
345
|
+
pendingTool,
|
|
346
|
+
answeredBy,
|
|
340
347
|
reduce,
|
|
341
348
|
repositoryId,
|
|
342
349
|
gitInfo,
|
|
@@ -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";
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROTOCOL_PORT,
|
|
3
|
-
parseServerMessage
|
|
4
|
-
|
|
3
|
+
parseServerMessage,
|
|
4
|
+
readDaemonToken
|
|
5
|
+
} from "./chunk-JOCNCNRP.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
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
paths
|
|
3
|
+
} from "./chunk-FGOFRWIB.js";
|
|
4
|
+
|
|
5
|
+
// src/log.ts
|
|
6
|
+
import { appendFileSync } from "fs";
|
|
7
|
+
function log(line) {
|
|
8
|
+
try {
|
|
9
|
+
appendFileSync(paths().log, `${(/* @__PURE__ */ new Date()).toISOString()} | ${line}
|
|
10
|
+
`);
|
|
11
|
+
} catch {
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
log
|
|
17
|
+
};
|
|
@@ -4,35 +4,37 @@ import {
|
|
|
4
4
|
} from "./chunk-OLLQBK6N.js";
|
|
5
5
|
import {
|
|
6
6
|
QUESTION_TOOLS,
|
|
7
|
+
answeredBy,
|
|
7
8
|
crossSession,
|
|
8
9
|
crossSessionNotices,
|
|
9
10
|
gitInfo,
|
|
10
11
|
gitSummary,
|
|
12
|
+
pendingTool,
|
|
11
13
|
reduce,
|
|
12
14
|
repositoryId
|
|
13
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-OHKQKXYM.js";
|
|
14
16
|
import {
|
|
15
17
|
agentMessages,
|
|
18
|
+
claudeOpenCall,
|
|
16
19
|
claudePlan,
|
|
20
|
+
claudeToolAnswered,
|
|
17
21
|
spawnSession
|
|
18
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-GMKS55KF.js";
|
|
19
23
|
import {
|
|
20
24
|
LEDGE_VERSION
|
|
21
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-XOPH7YDV.js";
|
|
22
26
|
import {
|
|
23
27
|
SERVICE,
|
|
24
28
|
inDaemonService,
|
|
25
29
|
tmuxServerAtRisk
|
|
26
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-Q7RCYJ4X.js";
|
|
27
31
|
import {
|
|
28
32
|
realTmux,
|
|
29
33
|
runFile
|
|
30
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-TO4XO2ML.js";
|
|
31
35
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
paths
|
|
35
|
-
} from "./chunk-L6IGH4LF.js";
|
|
36
|
+
log
|
|
37
|
+
} from "./chunk-YUSY3E47.js";
|
|
36
38
|
import {
|
|
37
39
|
DEFAULT_PROJECTS,
|
|
38
40
|
DOCUMENT_MAX_BYTES,
|
|
@@ -46,7 +48,9 @@ import {
|
|
|
46
48
|
RouteResultSchema,
|
|
47
49
|
SessionSchema,
|
|
48
50
|
SshAliasSchema,
|
|
51
|
+
ensureDaemonToken,
|
|
49
52
|
extractProposal,
|
|
53
|
+
hostAllowed,
|
|
50
54
|
isArchived,
|
|
51
55
|
offers,
|
|
52
56
|
parseClientMessage,
|
|
@@ -57,8 +61,14 @@ import {
|
|
|
57
61
|
pendingKey,
|
|
58
62
|
pickName,
|
|
59
63
|
projectOf,
|
|
60
|
-
proposalRows
|
|
61
|
-
|
|
64
|
+
proposalRows,
|
|
65
|
+
requestAuthorized,
|
|
66
|
+
selectDaemonProtocol
|
|
67
|
+
} from "./chunk-JOCNCNRP.js";
|
|
68
|
+
import {
|
|
69
|
+
ensureLedgeDir,
|
|
70
|
+
paths
|
|
71
|
+
} from "./chunk-FGOFRWIB.js";
|
|
62
72
|
|
|
63
73
|
// src/daemon/index.ts
|
|
64
74
|
import { writeFile as writeFile6, rm as rm5 } from "fs/promises";
|
|
@@ -583,6 +593,7 @@ var History = class {
|
|
|
583
593
|
};
|
|
584
594
|
|
|
585
595
|
// src/daemon/server.ts
|
|
596
|
+
import { createServer } from "http";
|
|
586
597
|
import { homedir as homedir5 } from "os";
|
|
587
598
|
import { basename as basename3 } from "path";
|
|
588
599
|
import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
|
|
@@ -637,6 +648,9 @@ function dialogMismatch(shown, pending, width) {
|
|
|
637
648
|
if (pending.tool !== void 0 && COMMAND_TOOLS.has(pending.tool)) return shown.command ? commandMismatch(shown.command, pending.text, width) : UNCHECKED;
|
|
638
649
|
return null;
|
|
639
650
|
}
|
|
651
|
+
function bottomLines(pane, n = 5) {
|
|
652
|
+
return pane.split("\n").filter((l) => l.trim() !== "").slice(-n).join("\n");
|
|
653
|
+
}
|
|
640
654
|
var flatten = (text) => text.replace(/\s*[\r\n]+\s*/g, " ").trim();
|
|
641
655
|
|
|
642
656
|
// src/adapters/claude.ts
|
|
@@ -766,7 +780,7 @@ function promptStillVisible(lastLines2) {
|
|
|
766
780
|
}
|
|
767
781
|
var squash = (t) => t.replace(/\s+/g, " ").trim();
|
|
768
782
|
function promptGone(pane, pending) {
|
|
769
|
-
if (pending.kind !== "question" || !squash(pending.text)) return !promptStillVisible(pane
|
|
783
|
+
if (pending.kind !== "question" || !squash(pending.text)) return !promptStillVisible(bottomLines(pane));
|
|
770
784
|
const m = detectPrompt(pane);
|
|
771
785
|
if (!m || m.kind !== "question") return true;
|
|
772
786
|
const shown = squash(m.text);
|
|
@@ -2569,10 +2583,18 @@ async function readDocument(request, context) {
|
|
|
2569
2583
|
if (/^(?:file:|[a-zA-Z]:[\\/]|\\\\)/.test(source) || source.includes("\0")) return failure(request, "unsupported_path");
|
|
2570
2584
|
if (request.basePath && (!isAbsolute3(request.basePath) || !markdown(request.basePath))) return failure(request, "unsupported_path");
|
|
2571
2585
|
const expanded = source === "~" ? context.home : source.startsWith("~/") ? resolve6(context.home, source.slice(2)) : source;
|
|
2572
|
-
const
|
|
2573
|
-
if (!isAbsolute3(expanded) && !
|
|
2574
|
-
const
|
|
2575
|
-
if (!markdown(
|
|
2586
|
+
const bases = request.basePath ? [dirname5(request.basePath)] : [...new Set([context.workdir, context.cwd].filter((d) => !!d))];
|
|
2587
|
+
if (!isAbsolute3(expanded) && !bases.length) return failure(request, "ambiguous_path");
|
|
2588
|
+
const paths2 = isAbsolute3(expanded) ? [resolve6(expanded)] : bases.map((base) => resolve6(base, expanded));
|
|
2589
|
+
if (!markdown(paths2[0])) return failure(request, "unsupported_type");
|
|
2590
|
+
let read = failure(request, "not_found");
|
|
2591
|
+
for (const path of paths2) {
|
|
2592
|
+
read = await readAt(request, path);
|
|
2593
|
+
if (read.type === "document.content" || read.code !== "not_found") return read;
|
|
2594
|
+
}
|
|
2595
|
+
return read;
|
|
2596
|
+
}
|
|
2597
|
+
async function readAt(request, path) {
|
|
2576
2598
|
let file;
|
|
2577
2599
|
try {
|
|
2578
2600
|
file = await open2(path, constants2.O_RDONLY | constants2.O_NONBLOCK);
|
|
@@ -2859,6 +2881,25 @@ var ALLOWED_ORIGINS = [
|
|
|
2859
2881
|
function originAllowed(origin) {
|
|
2860
2882
|
return origin === void 0 || ALLOWED_ORIGINS.includes(origin);
|
|
2861
2883
|
}
|
|
2884
|
+
function requestPath(url) {
|
|
2885
|
+
const raw = url ?? "/";
|
|
2886
|
+
const q = raw.indexOf("?");
|
|
2887
|
+
return q === -1 ? raw : raw.slice(0, q);
|
|
2888
|
+
}
|
|
2889
|
+
function refusal(req, token) {
|
|
2890
|
+
const path = requestPath(req.url);
|
|
2891
|
+
if (!(path === "/" || path.startsWith("/ssh/") && SshAliasSchema.safeParse(path.slice(5)).success)) return "path";
|
|
2892
|
+
if (!originAllowed(req.headers.origin)) return "origin";
|
|
2893
|
+
if (!hostAllowed(req.headers.host)) return "host";
|
|
2894
|
+
if (!requestAuthorized(req.headers, token)) return "auth";
|
|
2895
|
+
return null;
|
|
2896
|
+
}
|
|
2897
|
+
function unauthorized(req, res) {
|
|
2898
|
+
req.resume();
|
|
2899
|
+
const body = "Unauthorized\n";
|
|
2900
|
+
res.writeHead(401, { "Content-Type": "text/plain; charset=utf-8", "Content-Length": Buffer.byteLength(body), "Cache-Control": "no-store", Connection: "close" });
|
|
2901
|
+
res.end(body);
|
|
2902
|
+
}
|
|
2862
2903
|
function logDroppedRequest(id, ev, before, after) {
|
|
2863
2904
|
if (ev.name !== "PermissionRequest" || !after || after.pending !== before?.pending) return;
|
|
2864
2905
|
const held = before?.pending;
|
|
@@ -2874,22 +2915,36 @@ async function startServer(opts) {
|
|
|
2874
2915
|
const agentEnv = opts.agentEnv ?? (async () => ({ ...process.env, ...await loginShellEnv() }));
|
|
2875
2916
|
const chat = opts.chat ?? createChat({ store, tmux, home: paths().dir, userHome: opts.home, env: agentEnv, bin: opts.claudeBin, now, projects: () => projects, forge: createForge({ env: agentEnv }) });
|
|
2876
2917
|
const router = createRouter({ env: agentEnv });
|
|
2918
|
+
const token = await ensureDaemonToken();
|
|
2919
|
+
const httpServer = createServer(unauthorized);
|
|
2920
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
2921
|
+
httpServer.on("connection", (socket) => {
|
|
2922
|
+
sockets.add(socket);
|
|
2923
|
+
socket.on("close", () => sockets.delete(socket));
|
|
2924
|
+
});
|
|
2877
2925
|
const wss = new WebSocketServer({
|
|
2878
|
-
|
|
2879
|
-
port: opts.port ?? PROTOCOL_PORT,
|
|
2926
|
+
server: httpServer,
|
|
2880
2927
|
maxPayload: MAX_FRAME,
|
|
2881
2928
|
verifyClient: ({ req }) => {
|
|
2882
|
-
const
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2929
|
+
const why = refusal(req, token);
|
|
2930
|
+
if (why) {
|
|
2931
|
+
const origin = String(req.headers.origin ?? "none").replace(/[\r\n]/g, "").slice(0, 120);
|
|
2932
|
+
log(`refused a connection (${why}) from origin ${origin}`);
|
|
2933
|
+
}
|
|
2934
|
+
return why === null;
|
|
2935
|
+
},
|
|
2936
|
+
handleProtocols: (protocols) => selectDaemonProtocol(protocols, token)
|
|
2887
2937
|
});
|
|
2888
2938
|
await new Promise((res, rej) => {
|
|
2889
|
-
|
|
2890
|
-
wss.once("error",
|
|
2939
|
+
const onError = (e) => rej(e);
|
|
2940
|
+
wss.once("error", onError);
|
|
2941
|
+
httpServer.listen(opts.port ?? PROTOCOL_PORT, opts.host ?? "127.0.0.1", () => {
|
|
2942
|
+
wss.off("error", onError);
|
|
2943
|
+
res();
|
|
2944
|
+
});
|
|
2891
2945
|
});
|
|
2892
|
-
|
|
2946
|
+
wss.on("error", (e) => log(`daemon server error: ${e.message}`));
|
|
2947
|
+
const port = httpServer.address().port;
|
|
2893
2948
|
const machinesFile = opts.machinesFile ?? paths().machines;
|
|
2894
2949
|
let machines = readMachines(machinesFile).map((m) => ({ ...m }));
|
|
2895
2950
|
const machinesMsg = () => ({ type: "machines", machines });
|
|
@@ -2971,6 +3026,16 @@ async function startServer(opts) {
|
|
|
2971
3026
|
}
|
|
2972
3027
|
}
|
|
2973
3028
|
const inFlight = /* @__PURE__ */ new Set();
|
|
3029
|
+
const CONFIRM_MS = opts.confirmMs ?? 3e3;
|
|
3030
|
+
const awaited = /* @__PURE__ */ new Map();
|
|
3031
|
+
function settleByHook(id, ev) {
|
|
3032
|
+
const a = awaited.get(id);
|
|
3033
|
+
const cur = store.get(id)?.pending;
|
|
3034
|
+
if (!a || a.settled || !answeredBy(a.pending, ev) || !cur || pendingKey(cur) !== pendingKey(a.pending)) return;
|
|
3035
|
+
a.settled = true;
|
|
3036
|
+
a.byHook = true;
|
|
3037
|
+
store.apply(id, { name: "Decided", at: now(), source: "daemon", ...a.decision ? { decision: a.decision } : {} });
|
|
3038
|
+
}
|
|
2974
3039
|
async function injectAndVerify(id, planFor, requirePrompt, decision) {
|
|
2975
3040
|
const s = store.get(id);
|
|
2976
3041
|
if (!s) throw new Error(`unknown session ${id}`);
|
|
@@ -2986,7 +3051,7 @@ async function startServer(opts) {
|
|
|
2986
3051
|
let before = null;
|
|
2987
3052
|
if (requirePrompt) {
|
|
2988
3053
|
before = s.tmuxSession ? await tmux.capture(s.tmuxSession, DETECT_LINES) : null;
|
|
2989
|
-
if (before === null || !adapter.promptStillVisible(before
|
|
3054
|
+
if (before === null || !adapter.promptStillVisible(bottomLines(before))) throw new Error(NO_PROMPT);
|
|
2990
3055
|
if (!stillTheSame()) throw new Error(STALE);
|
|
2991
3056
|
if (decided) {
|
|
2992
3057
|
const shown = adapter.detectPrompt(before) ?? adapter.detectPrompt(await tmux.capture(s.tmuxSession, CHECK_LINES) ?? "");
|
|
@@ -2995,21 +3060,41 @@ async function startServer(opts) {
|
|
|
2995
3060
|
}
|
|
2996
3061
|
}
|
|
2997
3062
|
const plan = typeof planFor === "function" ? planFor(before ?? "") : planFor;
|
|
2998
|
-
const
|
|
3063
|
+
const stillShown = (p) => decided && adapter.promptGone ? !adapter.promptGone(p, decided) : adapter.promptStillVisible(bottomLines(p));
|
|
3064
|
+
const tool = s.tool === "claude" && decided?.source === "hook" ? pendingTool(decided) : void 0;
|
|
3065
|
+
const answer = tool ? { pending: decided, ...decision ? { decision } : {}, settled: false, byHook: false } : void 0;
|
|
3066
|
+
const transcript = tool && !decided.helperId && s.transcriptPath ? s.transcriptPath : void 0;
|
|
3067
|
+
const call = transcript ? claudeOpenCall(transcript, tool) : void 0;
|
|
3068
|
+
const promptGone3 = async () => {
|
|
3069
|
+
if (!answer) return verifyGone(() => tmux.capture(s.tmuxSession, DETECT_LINES), stillShown, opts.verify);
|
|
3070
|
+
const interval = opts.verify?.intervalMs ?? 200;
|
|
3071
|
+
for (const end = Date.now() + CONFIRM_MS; ; ) {
|
|
3072
|
+
if (answer.byHook || call && claudeToolAnswered(transcript, call)) return true;
|
|
3073
|
+
const pane = await tmux.capture(s.tmuxSession, DETECT_LINES);
|
|
3074
|
+
if (answer.byHook || pane !== null && !stillShown(pane)) return true;
|
|
3075
|
+
if (Date.now() >= end) return false;
|
|
3076
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
3077
|
+
}
|
|
3078
|
+
};
|
|
3079
|
+
if (answer) awaited.set(id, answer);
|
|
2999
3080
|
try {
|
|
3000
3081
|
await runPlan(s, plan);
|
|
3001
3082
|
} catch (e) {
|
|
3002
3083
|
if (e instanceof NoteNotTyped) {
|
|
3003
3084
|
if (await promptGone3()) {
|
|
3004
|
-
if (
|
|
3085
|
+
if (answer) answer.settled = true;
|
|
3086
|
+
if (!answer?.byHook && stillTheSame()) store.apply(id, { name: "Decided", at: now(), source: "daemon", ...decision ? { decision } : {} });
|
|
3005
3087
|
throw e;
|
|
3006
3088
|
}
|
|
3007
3089
|
e = new Error(NOT_TAKEN);
|
|
3008
3090
|
}
|
|
3091
|
+
if (answer?.byHook) return true;
|
|
3009
3092
|
store.apply(id, { name: "InjectFailed", at: now(), source: "daemon", text: e.message });
|
|
3010
3093
|
throw e;
|
|
3011
3094
|
}
|
|
3012
3095
|
const gone = await promptGone3();
|
|
3096
|
+
if (answer) answer.settled = true;
|
|
3097
|
+
if (answer?.byHook) return true;
|
|
3013
3098
|
if (!stillTheSame()) return false;
|
|
3014
3099
|
if (gone) {
|
|
3015
3100
|
store.apply(id, { name: "Decided", at: now(), source: "daemon", ...decision ? { decision } : {} });
|
|
@@ -3019,6 +3104,7 @@ async function startServer(opts) {
|
|
|
3019
3104
|
throw new Error(NOT_TAKEN);
|
|
3020
3105
|
} finally {
|
|
3021
3106
|
inFlight.delete(id);
|
|
3107
|
+
awaited.delete(id);
|
|
3022
3108
|
}
|
|
3023
3109
|
}
|
|
3024
3110
|
const READ_ONLY = "session started without ledge, read only";
|
|
@@ -3085,7 +3171,7 @@ async function startServer(opts) {
|
|
|
3085
3171
|
case "document.read": {
|
|
3086
3172
|
const session = msg.sessionId ? store.get(msg.sessionId) : void 0;
|
|
3087
3173
|
if (msg.sessionId && !session) return send(ws, { type: "document.error", requestId: msg.requestId, path: msg.path, code: "session_missing" });
|
|
3088
|
-
return send(ws, await readDocument(msg, { cwd: session?.cwd, home: opts.home ?? process.env.HOME ?? "" }));
|
|
3174
|
+
return send(ws, await readDocument(msg, { cwd: session?.cwd, workdir: session?.workdir, home: opts.home ?? process.env.HOME ?? "" }));
|
|
3089
3175
|
}
|
|
3090
3176
|
case "repos.inspect":
|
|
3091
3177
|
return send(ws, { type: "repos.info", requestId: msg.requestId, repos: await inspectRepos(msg.paths, { home: opts.home }) });
|
|
@@ -3176,7 +3262,7 @@ async function startServer(opts) {
|
|
|
3176
3262
|
case "folder.list":
|
|
3177
3263
|
return send(ws, await listFolder(msg.path, opts.home, msg.requestId));
|
|
3178
3264
|
case "folder.create": {
|
|
3179
|
-
const listing = await createFolder(msg.parent, msg.name, opts.home);
|
|
3265
|
+
const listing = await createFolder(msg.parent, msg.name, opts.home, msg.requestId);
|
|
3180
3266
|
if (!listing.error) log(`created folder ${listing.path}`);
|
|
3181
3267
|
return send(ws, listing);
|
|
3182
3268
|
}
|
|
@@ -3203,7 +3289,11 @@ async function startServer(opts) {
|
|
|
3203
3289
|
const base = { id: msg.id, name: pickName(taken), tool: msg.agent ?? "claude", cwd, project: basename3(cwd) || "unknown", title: "untracked session", tmuxSession: null, state: "working", helpers: [], startedAt: msg.event.at, lastActivityAt: msg.event.at, source: "hook_only" };
|
|
3204
3290
|
store.upsert(base);
|
|
3205
3291
|
store.apply(msg.id, msg.event);
|
|
3206
|
-
} else
|
|
3292
|
+
} else {
|
|
3293
|
+
if (msg.cwd) store.patch(msg.id, { workdir: msg.cwd });
|
|
3294
|
+
settleByHook(msg.id, ev);
|
|
3295
|
+
store.apply(msg.id, msg.event);
|
|
3296
|
+
}
|
|
3207
3297
|
logDroppedRequest(msg.id, msg.event, before, store.get(msg.id));
|
|
3208
3298
|
return;
|
|
3209
3299
|
}
|
|
@@ -3336,14 +3426,15 @@ async function startServer(opts) {
|
|
|
3336
3426
|
}
|
|
3337
3427
|
wss.on("connection", (ws, req) => {
|
|
3338
3428
|
ws.on("error", (e) => log(`socket error: ${e.message}`));
|
|
3339
|
-
|
|
3429
|
+
const path = requestPath(req.url);
|
|
3430
|
+
if (path.startsWith("/ssh/")) {
|
|
3340
3431
|
if (remoteClients.size >= 20) {
|
|
3341
3432
|
ws.close(1013, "Too many remote connections");
|
|
3342
3433
|
return;
|
|
3343
3434
|
}
|
|
3344
3435
|
remoteClients.add(ws);
|
|
3345
3436
|
ws.once("close", () => remoteClients.delete(ws));
|
|
3346
|
-
const parsed = SshAliasSchema.safeParse(
|
|
3437
|
+
const parsed = SshAliasSchema.safeParse(path.slice(5));
|
|
3347
3438
|
if (!parsed.success) {
|
|
3348
3439
|
ws.close(1008, "Invalid SSH alias");
|
|
3349
3440
|
remoteClients.delete(ws);
|
|
@@ -3388,7 +3479,8 @@ async function startServer(opts) {
|
|
|
3388
3479
|
for (const u of unsubscribeHistory) u();
|
|
3389
3480
|
relay.close();
|
|
3390
3481
|
for (const c of wss.clients) c.terminate();
|
|
3391
|
-
|
|
3482
|
+
for (const socket of sockets) socket.destroy();
|
|
3483
|
+
wss.close(() => httpServer.close(() => res()));
|
|
3392
3484
|
})
|
|
3393
3485
|
};
|
|
3394
3486
|
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
QUESTION_TOOLS,
|
|
3
3
|
gitInfo
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-OHKQKXYM.js";
|
|
5
5
|
import {
|
|
6
6
|
CODEX_ENV_VAR,
|
|
7
7
|
CODEX_HOOK_VAR,
|
|
8
8
|
codexPlan
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-GMKS55KF.js";
|
|
10
10
|
import {
|
|
11
11
|
log
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-YUSY3E47.js";
|
|
13
13
|
import {
|
|
14
14
|
connect
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
} from "./chunk-TC3XJUJA.js";
|
|
16
|
+
import "./chunk-JOCNCNRP.js";
|
|
17
|
+
import "./chunk-FGOFRWIB.js";
|
|
17
18
|
|
|
18
19
|
// src/cli/hook.ts
|
|
19
20
|
import { readFileSync, openSync, fstatSync, readSync, closeSync } from "fs";
|
package/dist/cli/index.js
CHANGED
|
@@ -17,7 +17,7 @@ async function hookMain(args) {
|
|
|
17
17
|
try {
|
|
18
18
|
const chunks = [];
|
|
19
19
|
for await (const c of process.stdin) chunks.push(c);
|
|
20
|
-
const { runHook } = await import("./hook-
|
|
20
|
+
const { runHook } = await import("./hook-W46IWB7I.js");
|
|
21
21
|
const out = await runHook(event, Buffer.concat(chunks).toString("utf8"), { tool });
|
|
22
22
|
if (out) process.stdout.write(`${out}
|
|
23
23
|
`);
|
|
@@ -29,7 +29,7 @@ async function main(argv) {
|
|
|
29
29
|
const [cmd, ...rest] = argv;
|
|
30
30
|
switch (cmd) {
|
|
31
31
|
case "daemon": {
|
|
32
|
-
const { startDaemon } = await import("./daemon-
|
|
32
|
+
const { startDaemon } = await import("./daemon-RI47DJDX.js");
|
|
33
33
|
await startDaemon();
|
|
34
34
|
await new Promise(() => {
|
|
35
35
|
});
|
|
@@ -37,44 +37,44 @@ async function main(argv) {
|
|
|
37
37
|
}
|
|
38
38
|
case "--version":
|
|
39
39
|
case "version": {
|
|
40
|
-
const { LEDGE_VERSION } = await import("./version-
|
|
40
|
+
const { LEDGE_VERSION } = await import("./version-SO5OMPGD.js");
|
|
41
41
|
console.log(LEDGE_VERSION);
|
|
42
42
|
return 0;
|
|
43
43
|
}
|
|
44
44
|
case "setup": {
|
|
45
|
-
const { runSetup } = await import("./setup-
|
|
45
|
+
const { runSetup } = await import("./setup-7AEH5DC3.js");
|
|
46
46
|
for (const l of await runSetup()) console.log(l);
|
|
47
47
|
return 0;
|
|
48
48
|
}
|
|
49
49
|
case "uninstall": {
|
|
50
|
-
const { runUninstall } = await import("./uninstall-
|
|
50
|
+
const { runUninstall } = await import("./uninstall-7U7ENKOS.js");
|
|
51
51
|
const r = await runUninstall({ purge: rest.includes("--purge") });
|
|
52
52
|
for (const l of r.lines) console.log(l);
|
|
53
53
|
return r.ok ? 0 : 1;
|
|
54
54
|
}
|
|
55
55
|
case "status": {
|
|
56
|
-
const { printStatus } = await import("./status-
|
|
56
|
+
const { printStatus } = await import("./status-QOXWNLLF.js");
|
|
57
57
|
console.log(await printStatus());
|
|
58
58
|
return 0;
|
|
59
59
|
}
|
|
60
60
|
case "who": {
|
|
61
|
-
const { printWho } = await import("./who-
|
|
61
|
+
const { printWho } = await import("./who-WVYFPCII.js");
|
|
62
62
|
console.log(await printWho());
|
|
63
63
|
return 0;
|
|
64
64
|
}
|
|
65
65
|
case "bridge": {
|
|
66
|
-
const { bridge } = await import("./bridge-
|
|
66
|
+
const { bridge } = await import("./bridge-ZOHP53DV.js");
|
|
67
67
|
await bridge();
|
|
68
68
|
return 0;
|
|
69
69
|
}
|
|
70
70
|
case "claude":
|
|
71
71
|
case "codex":
|
|
72
72
|
case "cursor": {
|
|
73
|
-
const { launch } = await import("./launch-
|
|
73
|
+
const { launch } = await import("./launch-S6XFRSZA.js");
|
|
74
74
|
return launch(cmd, rest);
|
|
75
75
|
}
|
|
76
76
|
case "attach": {
|
|
77
|
-
const { attach } = await import("./attach-
|
|
77
|
+
const { attach } = await import("./attach-2DEXQEC2.js");
|
|
78
78
|
return attach(rest[0] ?? "");
|
|
79
79
|
}
|
|
80
80
|
default:
|
|
@@ -5,15 +5,16 @@ import {
|
|
|
5
5
|
ensureDaemon,
|
|
6
6
|
launch,
|
|
7
7
|
parseLaunchArgs
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-IC5RHOX6.js";
|
|
9
9
|
import {
|
|
10
10
|
agentEnv,
|
|
11
11
|
exitFile
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-GMKS55KF.js";
|
|
13
|
+
import "./chunk-TO4XO2ML.js";
|
|
14
|
+
import "./chunk-YUSY3E47.js";
|
|
15
|
+
import "./chunk-TC3XJUJA.js";
|
|
16
|
+
import "./chunk-JOCNCNRP.js";
|
|
17
|
+
import "./chunk-FGOFRWIB.js";
|
|
17
18
|
export {
|
|
18
19
|
DaemonError,
|
|
19
20
|
agentEnv,
|
|
@@ -13,9 +13,10 @@ import {
|
|
|
13
13
|
runSetup,
|
|
14
14
|
stopDetachedDaemon,
|
|
15
15
|
systemdUnit
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-
|
|
16
|
+
} from "./chunk-Q7RCYJ4X.js";
|
|
17
|
+
import "./chunk-TO4XO2ML.js";
|
|
18
|
+
import "./chunk-YUSY3E47.js";
|
|
19
|
+
import "./chunk-FGOFRWIB.js";
|
|
19
20
|
export {
|
|
20
21
|
CLAUDE_EVENTS,
|
|
21
22
|
CODEX_EVENTS,
|
|
@@ -2,11 +2,12 @@ import {
|
|
|
2
2
|
SERVICE,
|
|
3
3
|
execRunner,
|
|
4
4
|
stopDetachedDaemon
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-Q7RCYJ4X.js";
|
|
6
|
+
import "./chunk-TO4XO2ML.js";
|
|
7
|
+
import "./chunk-YUSY3E47.js";
|
|
7
8
|
import {
|
|
8
9
|
paths
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-FGOFRWIB.js";
|
|
10
11
|
|
|
11
12
|
// src/cli/uninstall.ts
|
|
12
13
|
import { readFile, writeFile, rename, rm, lstat, realpath } from "fs/promises";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
connect
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-TC3XJUJA.js";
|
|
4
4
|
import {
|
|
5
5
|
isArchived
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-JOCNCNRP.js";
|
|
7
|
+
import "./chunk-FGOFRWIB.js";
|
|
7
8
|
|
|
8
9
|
// src/cli/who.ts
|
|
9
10
|
import { basename } from "path";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paradow/ledge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Follow and answer your WSL agent sessions (Claude Code, Codex, Cursor Agent) from the Ledge island on Windows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "ledge": "dist/cli/index.js" },
|