@paradow/ledge 0.11.0 → 0.12.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-6JONTTMA.js → attach-UWADG6NX.js} +9 -9
- package/dist/cli/{bridge-ZOHP53DV.js → bridge-53ZTDSST.js} +1 -1
- package/dist/cli/chunk-A7YEHYLY.js +105 -0
- package/dist/cli/{who-WVYFPCII.js → chunk-FRGNPUDO.js} +3 -3
- package/dist/cli/chunk-HJTBQQIQ.js +6 -0
- package/dist/cli/{chunk-OHKQKXYM.js → chunk-QAEH5TJB.js} +73 -2
- package/dist/cli/{chunk-JOCNCNRP.js → chunk-VY45MACP.js} +394 -338
- package/dist/cli/{chunk-TSA5LD4D.js → chunk-X34LPKG7.js} +1 -1
- package/dist/cli/{chunk-USGAFLXF.js → chunk-XIO4OIHR.js} +1 -1
- package/dist/cli/{chunk-TC3XJUJA.js → chunk-XXFUYME7.js} +9 -1
- package/dist/cli/{chunk-YV7BMEEB.js → chunk-ZEHTNFPV.js} +2 -2
- package/dist/cli/{chunk-47G477C7.js → chunk-ZNRXLDSH.js} +9 -9
- package/dist/cli/{daemon-YY66JWIB.js → daemon-Z7Z2R35O.js} +326 -91
- package/dist/cli/{hook-7V2GJUNO.js → hook-OLZ2I4GM.js} +49 -8
- package/dist/cli/index.js +19 -12
- package/dist/cli/{launch-2IQZPPTY.js → launch-ZGICGFVU.js} +5 -5
- package/dist/cli/send-VV25P46X.js +14 -0
- package/dist/cli/{setup-SG43PPOZ.js → setup-TULNBDFB.js} +2 -2
- package/dist/cli/{status-QOXWNLLF.js → status-LLITXNOW.js} +2 -2
- package/dist/cli/{uninstall-SD736TBP.js → uninstall-OFSLCP4T.js} +2 -2
- package/dist/cli/{version-QSIZ47AV.js → version-XX2MWLAL.js} +1 -1
- package/dist/cli/who-K6VK7EXV.js +11 -0
- package/package.json +1 -1
- package/dist/cli/chunk-RVEGELWF.js +0 -6
|
@@ -2,125 +2,6 @@ import {
|
|
|
2
2
|
paths
|
|
3
3
|
} from "./chunk-FGOFRWIB.js";
|
|
4
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
|
-
|
|
124
5
|
// ../protocol/src/session.ts
|
|
125
6
|
import { z } from "zod";
|
|
126
7
|
var ToolSchema = z.enum(["claude", "codex", "cursor"]);
|
|
@@ -188,6 +69,9 @@ var SessionSchema = z.object({
|
|
|
188
69
|
startedAt: z.string().datetime(),
|
|
189
70
|
lastActivityAt: z.string().datetime(),
|
|
190
71
|
lastHookAt: z.string().datetime().optional(),
|
|
72
|
+
/** The model and effort Ledge launched the agent with (#58); left out, the agent's own default. */
|
|
73
|
+
model: z.string().optional(),
|
|
74
|
+
effort: z.string().optional(),
|
|
191
75
|
/** The agent's own session id when Ledge chose it at launch (Claude Code `--session-id`). */
|
|
192
76
|
agentSessionId: z.string().optional(),
|
|
193
77
|
/** The agent's transcript: the latest one a hook named (it follows /clear and --resume), else the one the launch implies. */
|
|
@@ -206,7 +90,7 @@ function isArchived(s) {
|
|
|
206
90
|
}
|
|
207
91
|
|
|
208
92
|
// ../protocol/src/messages.ts
|
|
209
|
-
import { z as
|
|
93
|
+
import { z as z14 } from "zod";
|
|
210
94
|
|
|
211
95
|
// ../protocol/src/chat.ts
|
|
212
96
|
import { z as z2 } from "zod";
|
|
@@ -270,34 +154,60 @@ var ReposInspectSchema = z3.object({
|
|
|
270
154
|
var RepoInfoSchema = z3.object({ path: z3.string(), exists: z3.boolean(), branch: z3.string().nullable() });
|
|
271
155
|
var ReposInfoSchema = z3.object({ type: z3.literal("repos.info"), requestId: z3.string(), repos: z3.array(RepoInfoSchema) });
|
|
272
156
|
|
|
273
|
-
// ../protocol/src/
|
|
157
|
+
// ../protocol/src/models.ts
|
|
274
158
|
import { z as z4 } from "zod";
|
|
275
|
-
var
|
|
276
|
-
var
|
|
277
|
-
var
|
|
159
|
+
var ModelIdSchema = z4.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:/@[\]=,+-]*$/, "not a model id");
|
|
160
|
+
var EffortSchema = z4.string().min(1).max(20).regex(/^[a-z]+$/, "not an effort");
|
|
161
|
+
var ModelChoiceSchema = z4.object({
|
|
162
|
+
id: ModelIdSchema,
|
|
163
|
+
label: z4.string(),
|
|
164
|
+
description: z4.string().optional(),
|
|
165
|
+
efforts: z4.array(EffortSchema).optional(),
|
|
166
|
+
defaultEffort: EffortSchema.optional(),
|
|
167
|
+
isDefault: z4.boolean().optional()
|
|
168
|
+
});
|
|
169
|
+
var ModelsListSchema = z4.object({
|
|
170
|
+
type: z4.literal("models.list"),
|
|
171
|
+
requestId: z4.string().min(1).max(100),
|
|
172
|
+
tool: ToolSchema,
|
|
173
|
+
machineId: z4.string().optional()
|
|
174
|
+
});
|
|
175
|
+
var ModelsListingSchema = z4.object({
|
|
176
|
+
type: z4.literal("models.listing"),
|
|
177
|
+
requestId: z4.string(),
|
|
178
|
+
tool: ToolSchema,
|
|
179
|
+
models: z4.array(ModelChoiceSchema),
|
|
180
|
+
error: z4.string().optional()
|
|
181
|
+
});
|
|
278
182
|
|
|
279
|
-
// ../protocol/src/
|
|
183
|
+
// ../protocol/src/scan.ts
|
|
280
184
|
import { z as z5 } from "zod";
|
|
185
|
+
var ReposScanSchema = z5.object({ type: z5.literal("repos.scan"), requestId: z5.string().min(1).max(100), machineId: z5.string().optional() });
|
|
186
|
+
var FoundRepoSchema = z5.object({ path: z5.string(), branch: z5.string().nullable(), remote: z5.string().optional(), manifest: z5.string().max(200).optional() });
|
|
187
|
+
var ReposFoundSchema = z5.object({ type: z5.literal("repos.found"), requestId: z5.string(), repos: z5.array(FoundRepoSchema).max(500), truncated: z5.boolean(), error: z5.string().optional() });
|
|
188
|
+
|
|
189
|
+
// ../protocol/src/documents.ts
|
|
190
|
+
import { z as z6 } from "zod";
|
|
281
191
|
var DOCUMENT_MAX_BYTES = 1048576;
|
|
282
|
-
var path =
|
|
283
|
-
var requestId =
|
|
284
|
-
var DocumentReadSchema =
|
|
285
|
-
type:
|
|
192
|
+
var path = z6.string().min(1).max(4096);
|
|
193
|
+
var requestId = z6.string().min(1);
|
|
194
|
+
var DocumentReadSchema = z6.object({
|
|
195
|
+
type: z6.literal("document.read"),
|
|
286
196
|
requestId,
|
|
287
197
|
path,
|
|
288
|
-
machineId:
|
|
289
|
-
sessionId:
|
|
198
|
+
machineId: z6.string().optional(),
|
|
199
|
+
sessionId: z6.string().min(1).optional(),
|
|
290
200
|
basePath: path.optional()
|
|
291
201
|
});
|
|
292
|
-
var DocumentContentSchema =
|
|
293
|
-
type:
|
|
202
|
+
var DocumentContentSchema = z6.object({
|
|
203
|
+
type: z6.literal("document.content"),
|
|
294
204
|
requestId,
|
|
295
205
|
path,
|
|
296
|
-
text:
|
|
297
|
-
bytes:
|
|
298
|
-
modifiedAt:
|
|
206
|
+
text: z6.string(),
|
|
207
|
+
bytes: z6.number().int().nonnegative(),
|
|
208
|
+
modifiedAt: z6.string().datetime()
|
|
299
209
|
});
|
|
300
|
-
var DocumentErrorCodeSchema =
|
|
210
|
+
var DocumentErrorCodeSchema = z6.enum([
|
|
301
211
|
"not_found",
|
|
302
212
|
"not_file",
|
|
303
213
|
"unsupported_path",
|
|
@@ -309,47 +219,47 @@ var DocumentErrorCodeSchema = z5.enum([
|
|
|
309
219
|
"session_missing",
|
|
310
220
|
"read_failed"
|
|
311
221
|
]);
|
|
312
|
-
var DocumentErrorSchema =
|
|
313
|
-
type:
|
|
222
|
+
var DocumentErrorSchema = z6.object({
|
|
223
|
+
type: z6.literal("document.error"),
|
|
314
224
|
requestId,
|
|
315
225
|
path,
|
|
316
226
|
code: DocumentErrorCodeSchema
|
|
317
227
|
});
|
|
318
228
|
|
|
319
229
|
// ../protocol/src/routing.ts
|
|
320
|
-
import { z as
|
|
321
|
-
var MachineInventorySchema =
|
|
322
|
-
id:
|
|
323
|
-
label:
|
|
324
|
-
online:
|
|
325
|
-
local:
|
|
326
|
-
sessions:
|
|
327
|
-
folders:
|
|
230
|
+
import { z as z7 } from "zod";
|
|
231
|
+
var MachineInventorySchema = z7.object({
|
|
232
|
+
id: z7.string().min(1).max(80),
|
|
233
|
+
label: z7.string().min(1).max(120),
|
|
234
|
+
online: z7.boolean(),
|
|
235
|
+
local: z7.boolean(),
|
|
236
|
+
sessions: z7.array(z7.object({ id: z7.string(), name: z7.string(), project: z7.string(), cwd: z7.string(), topic: z7.string().optional(), state: z7.string(), readOnly: z7.boolean() })).max(500),
|
|
237
|
+
folders: z7.array(z7.object({ project: z7.string(), cwd: z7.string() })).max(100)
|
|
328
238
|
});
|
|
329
|
-
var RoutingContextSchema =
|
|
330
|
-
machines:
|
|
331
|
-
history:
|
|
239
|
+
var RoutingContextSchema = z7.object({
|
|
240
|
+
machines: z7.array(MachineInventorySchema).min(1).max(20),
|
|
241
|
+
history: z7.array(z7.object({ question: z7.string().max(2e4), answer: z7.string().max(2e4).optional(), machineId: z7.string().optional() })).max(50)
|
|
332
242
|
});
|
|
333
|
-
var RouteResultSchema =
|
|
243
|
+
var RouteResultSchema = z7.object({
|
|
334
244
|
/** Null means an answer or a clarification, never an implicit local destination. */
|
|
335
|
-
machineId:
|
|
336
|
-
text:
|
|
337
|
-
choices:
|
|
245
|
+
machineId: z7.string().nullable(),
|
|
246
|
+
text: z7.string().min(1).max(2e4),
|
|
247
|
+
choices: z7.array(z7.string()).max(20).optional()
|
|
338
248
|
});
|
|
339
249
|
|
|
340
250
|
// ../protocol/src/ssh.ts
|
|
341
|
-
import { z as
|
|
342
|
-
var SshAliasSchema =
|
|
251
|
+
import { z as z8 } from "zod";
|
|
252
|
+
var SshAliasSchema = z8.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/);
|
|
343
253
|
|
|
344
254
|
// ../protocol/src/machines.ts
|
|
345
|
-
import { z as
|
|
346
|
-
var MachineIdSchema =
|
|
347
|
-
var MachineSchema =
|
|
255
|
+
import { z as z9 } from "zod";
|
|
256
|
+
var MachineIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]{1,64}$/);
|
|
257
|
+
var MachineSchema = z9.object({
|
|
348
258
|
id: MachineIdSchema,
|
|
349
|
-
label:
|
|
259
|
+
label: z9.string().trim().min(1).max(80),
|
|
350
260
|
ssh: SshAliasSchema.optional()
|
|
351
261
|
});
|
|
352
|
-
var MachinesSchema =
|
|
262
|
+
var MachinesSchema = z9.array(MachineSchema).min(1).max(20).superRefine((list, ctx) => {
|
|
353
263
|
if (list[0]?.id !== "local" || list[0].ssh !== void 0) ctx.addIssue({ code: "custom", message: "the first machine is local, without ssh", path: [0] });
|
|
354
264
|
const ids = /* @__PURE__ */ new Set(), aliases = /* @__PURE__ */ new Set();
|
|
355
265
|
list.forEach((m, i) => {
|
|
@@ -364,51 +274,51 @@ var MachinesSchema = z8.array(MachineSchema).min(1).max(20).superRefine((list, c
|
|
|
364
274
|
});
|
|
365
275
|
|
|
366
276
|
// ../protocol/src/island.ts
|
|
367
|
-
import { z as
|
|
368
|
-
var IslandScaleSchema =
|
|
369
|
-
var IslandPrefsSchema =
|
|
277
|
+
import { z as z10 } from "zod";
|
|
278
|
+
var IslandScaleSchema = z10.union([z10.literal("auto"), z10.literal(1), z10.literal(1.25), z10.literal(1.5), z10.literal(2)]);
|
|
279
|
+
var IslandPrefsSchema = z10.object({
|
|
370
280
|
/** Shown at the top of the screen; hidden, Desktop still notifies. */
|
|
371
|
-
visible:
|
|
281
|
+
visible: z10.boolean(),
|
|
372
282
|
/** The primary screen, or the first other one. */
|
|
373
|
-
screen:
|
|
374
|
-
position:
|
|
283
|
+
screen: z10.enum(["primary", "second"]),
|
|
284
|
+
position: z10.enum(["center", "left", "right"]),
|
|
375
285
|
/** A strip under a maximised window. */
|
|
376
|
-
discreet:
|
|
286
|
+
discreet: z10.boolean(),
|
|
377
287
|
islandScale: IslandScaleSchema,
|
|
378
288
|
panelScale: IslandScaleSchema,
|
|
379
289
|
/** What the island shows besides asks and questions, which it always shows. */
|
|
380
|
-
shows:
|
|
290
|
+
shows: z10.object({
|
|
381
291
|
/** Allow, deny or answer in a line from the island; off, its cards only say « Open in Ledge ». */
|
|
382
|
-
quick:
|
|
292
|
+
quick: z10.boolean(),
|
|
383
293
|
/** The 6 s bubble when an agent finishes. */
|
|
384
|
-
finished:
|
|
294
|
+
finished: z10.boolean(),
|
|
385
295
|
/** An agent's sentence in a bubble. */
|
|
386
|
-
say:
|
|
296
|
+
say: z10.boolean(),
|
|
387
297
|
/** The sub-agents next to their main agent. */
|
|
388
|
-
helpers:
|
|
298
|
+
helpers: z10.boolean()
|
|
389
299
|
}),
|
|
390
300
|
/** The global shortcut that opens the island, as shown (`Ctrl Alt L`). */
|
|
391
|
-
shortcut:
|
|
301
|
+
shortcut: z10.string().trim().min(1).max(40)
|
|
392
302
|
});
|
|
393
303
|
|
|
394
304
|
// ../protocol/src/projects.ts
|
|
395
|
-
import { z as
|
|
396
|
-
var ProjectColorSchema =
|
|
397
|
-
var ProjectIdSchema =
|
|
305
|
+
import { z as z11 } from "zod";
|
|
306
|
+
var ProjectColorSchema = z11.enum(["blue", "green", "amber", "violet", "teal", "pink", "red", "slate"]);
|
|
307
|
+
var ProjectIdSchema = z11.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/);
|
|
398
308
|
function normalizePath(raw) {
|
|
399
309
|
const p = raw.trim().replace(/\/+/g, "/");
|
|
400
310
|
return p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
|
|
401
311
|
}
|
|
402
|
-
var PathSchema =
|
|
403
|
-
var RepoSchema =
|
|
404
|
-
var ProjectSchema =
|
|
312
|
+
var PathSchema = z11.string().max(4096).refine((p) => p.startsWith("/") && p === normalizePath(p), "an absolute, normalized path");
|
|
313
|
+
var RepoSchema = z11.object({ machine: MachineIdSchema, path: PathSchema });
|
|
314
|
+
var ProjectSchema = z11.object({
|
|
405
315
|
id: ProjectIdSchema,
|
|
406
|
-
name:
|
|
316
|
+
name: z11.string().trim().min(1).max(80),
|
|
407
317
|
color: ProjectColorSchema,
|
|
408
|
-
description:
|
|
409
|
-
repos:
|
|
318
|
+
description: z11.string().max(500).default(""),
|
|
319
|
+
repos: z11.array(RepoSchema).max(50)
|
|
410
320
|
});
|
|
411
|
-
var RuleSchema =
|
|
321
|
+
var RuleSchema = z11.object({ machine: MachineIdSchema, path: PathSchema, project: ProjectIdSchema });
|
|
412
322
|
function projectsProblems(p) {
|
|
413
323
|
const out = [];
|
|
414
324
|
const ids = /* @__PURE__ */ new Set(), names = /* @__PURE__ */ new Set(), repos = /* @__PURE__ */ new Set(), rules = /* @__PURE__ */ new Set();
|
|
@@ -434,7 +344,7 @@ ${rule.path}`;
|
|
|
434
344
|
});
|
|
435
345
|
return out;
|
|
436
346
|
}
|
|
437
|
-
var ProjectsSchema =
|
|
347
|
+
var ProjectsSchema = z11.object({ projects: z11.array(ProjectSchema).max(100), rules: z11.array(RuleSchema).max(1e3) }).superRefine((p, ctx) => {
|
|
438
348
|
for (const problem of projectsProblems(p)) ctx.addIssue({ code: "custom", message: problem.code, path: problem.path });
|
|
439
349
|
});
|
|
440
350
|
var DEFAULT_PROJECTS = { projects: [], rules: [] };
|
|
@@ -452,45 +362,45 @@ function projectOf(session, state) {
|
|
|
452
362
|
}
|
|
453
363
|
|
|
454
364
|
// ../protocol/src/thread.ts
|
|
455
|
-
import { z as
|
|
456
|
-
var ToolStatusSchema =
|
|
457
|
-
var FileChangeSchema =
|
|
458
|
-
var base = { id:
|
|
459
|
-
var ThreadItemSchema =
|
|
460
|
-
|
|
461
|
-
|
|
365
|
+
import { z as z12 } from "zod";
|
|
366
|
+
var ToolStatusSchema = z12.enum(["running", "success", "failed", "unknown"]);
|
|
367
|
+
var FileChangeSchema = z12.object({ path: z12.string().min(1), from: z12.string().min(1).optional(), change: z12.enum(["add", "edit", "delete", "write"]), diff: z12.string().optional() });
|
|
368
|
+
var base = { id: z12.string().min(1), at: z12.string().datetime(), turn: z12.number().int().nonnegative(), helperId: z12.string().optional() };
|
|
369
|
+
var ThreadItemSchema = z12.discriminatedUnion("kind", [
|
|
370
|
+
z12.object({ kind: z12.literal("prompt"), ...base, text: z12.string() }),
|
|
371
|
+
z12.object({ kind: z12.literal("message"), ...base, text: z12.string() }),
|
|
462
372
|
/** `id` is the harness's tool_use_id when it gives one, so the result updates the same row. */
|
|
463
|
-
|
|
373
|
+
z12.object({ kind: z12.literal("tool"), ...base, tool: z12.string(), label: z12.string(), input: z12.string().optional(), output: z12.string().optional(), error: z12.string().optional(), status: ToolStatusSchema, durationMs: z12.number().nonnegative().optional(), files: z12.array(FileChangeSchema).default([]) }),
|
|
464
374
|
/** `unknown`: answered in the terminal, the daemon only saw the prompt go away; `always`: allowed and not asked again. */
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
/** 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. */
|
|
469
|
-
|
|
375
|
+
z12.object({ kind: z12.literal("permission"), ...base, tool: z12.string(), text: z12.string(), decision: z12.enum(["accept", "deny", "always", "unknown"]).optional() }),
|
|
376
|
+
z12.object({ kind: z12.literal("reply"), ...base, text: z12.string() }),
|
|
377
|
+
z12.object({ kind: z12.literal("helper"), ...base, task: z12.string(), agentType: z12.string().optional(), state: z12.enum(["working", "exited"]) }),
|
|
378
|
+
/** 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, `via` set when Ledge's mailbox carries it. */
|
|
379
|
+
z12.object({ kind: z12.literal("sent"), ...base, to: z12.string(), peer: z12.string().optional(), summary: z12.string().optional(), text: z12.string(), status: z12.enum(["sending", "delivered", "read", "failed"]), received: z12.string().optional(), via: z12.literal("ledge").optional() }),
|
|
470
380
|
/** 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. */
|
|
471
|
-
|
|
381
|
+
z12.object({ kind: z12.literal("received"), ...base, from: z12.string(), peer: z12.string().optional(), text: z12.string(), sent: z12.string().optional() }),
|
|
472
382
|
/** An automated notice of the agent's harness about messages between sessions (Claude Code's `[Cross-session idle
|
|
473
383
|
* notice]` / `[Cross-session delivery notice]`): a system event, never the user's words. `name` is the session it is
|
|
474
384
|
* about as quoted, `peer` its Ledge session id; a delivery notice says the outcome of `count` messages to `recipients`. */
|
|
475
|
-
|
|
476
|
-
kind:
|
|
385
|
+
z12.object({
|
|
386
|
+
kind: z12.literal("notice"),
|
|
477
387
|
...base,
|
|
478
|
-
notice:
|
|
479
|
-
name:
|
|
480
|
-
peer:
|
|
481
|
-
time:
|
|
482
|
-
summary:
|
|
483
|
-
status:
|
|
484
|
-
count:
|
|
485
|
-
recipients:
|
|
486
|
-
reason:
|
|
487
|
-
text:
|
|
388
|
+
notice: z12.enum(["idle", "exited", "unavailable", "expired", "delivery", "other"]),
|
|
389
|
+
name: z12.string().optional(),
|
|
390
|
+
peer: z12.string().optional(),
|
|
391
|
+
time: z12.string().optional(),
|
|
392
|
+
summary: z12.string().optional(),
|
|
393
|
+
status: z12.enum(["held", "denied", "expired", "delivered", "refused", "dropped", "unknown"]).optional(),
|
|
394
|
+
count: z12.number().int().positive().optional(),
|
|
395
|
+
recipients: z12.array(z12.string()).optional(),
|
|
396
|
+
reason: z12.string().optional(),
|
|
397
|
+
text: z12.string()
|
|
488
398
|
})
|
|
489
399
|
]);
|
|
490
400
|
|
|
491
401
|
// ../protocol/src/decisions.ts
|
|
492
|
-
import { z as
|
|
493
|
-
var DecisionSchema =
|
|
402
|
+
import { z as z13 } from "zod";
|
|
403
|
+
var DecisionSchema = z13.enum(["accept", "deny", "always"]);
|
|
494
404
|
var NOTE_MAX = 4e3;
|
|
495
405
|
var NOTE_NOT_TYPED = "note_not_typed";
|
|
496
406
|
var DECISION_OFFERS = {
|
|
@@ -519,192 +429,215 @@ function pendingKey(p) {
|
|
|
519
429
|
|
|
520
430
|
// ../protocol/src/messages.ts
|
|
521
431
|
var PROTOCOL_PORT = 47831;
|
|
522
|
-
var SessionEventSchema =
|
|
523
|
-
name:
|
|
524
|
-
at:
|
|
525
|
-
source:
|
|
526
|
-
tool:
|
|
527
|
-
text:
|
|
528
|
-
options:
|
|
432
|
+
var SessionEventSchema = z14.object({
|
|
433
|
+
name: z14.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd", "PaneChanged", "PaneGone", "PanePrompt", "PaneIdle", "InjectFailed", "Decided", "PostToolUse", "PostToolUseFailure", "StopFailure", "SubagentStart", "SubagentStop", "PaneTitle", "Renamed"]),
|
|
434
|
+
at: z14.string().datetime(),
|
|
435
|
+
source: z14.enum(["hook", "pane", "daemon"]),
|
|
436
|
+
tool: z14.string().optional(),
|
|
437
|
+
text: z14.string().optional(),
|
|
438
|
+
options: z14.array(z14.string()).optional(),
|
|
529
439
|
/** Each option's description, by index ('' when it has none); left out when no option has one. */
|
|
530
|
-
optionDetails:
|
|
531
|
-
kind:
|
|
440
|
+
optionDetails: z14.array(z14.string()).optional(),
|
|
441
|
+
kind: z14.enum(["question", "permission", "plan", "diff"]).optional(),
|
|
532
442
|
/** PanePrompt: what to review in full before deciding (a plan read in the terminal), when `text` is its summary. */
|
|
533
|
-
body:
|
|
534
|
-
exitCode:
|
|
535
|
-
helperId:
|
|
443
|
+
body: z14.string().optional(),
|
|
444
|
+
exitCode: z14.number().int().nullable().optional(),
|
|
445
|
+
helperId: z14.string().optional(),
|
|
536
446
|
/** The helper's description, once known (Claude Code: its `.meta.json`, written after SubagentStart). */
|
|
537
|
-
task:
|
|
447
|
+
task: z14.string().optional(),
|
|
538
448
|
/** PostToolUseFailure: the user interrupted the turn (Esc); Claude then waits for input with no Stop. */
|
|
539
|
-
interrupt:
|
|
449
|
+
interrupt: z14.boolean().optional(),
|
|
540
450
|
/** Native tracking (spec 2026-10-13 §2): the harness's id of the tool call, so its result lands on the same row. */
|
|
541
|
-
toolUseId:
|
|
451
|
+
toolUseId: z14.string().optional(),
|
|
542
452
|
/** The tool's input as the harness gave it, every string capped by the hook. */
|
|
543
|
-
toolInput:
|
|
453
|
+
toolInput: z14.record(z14.string(), z14.unknown()).optional(),
|
|
544
454
|
/** PostToolUse: the result as text; SubagentStop: nothing (the report travels in `text`). */
|
|
545
|
-
output:
|
|
546
|
-
durationMs:
|
|
455
|
+
output: z14.string().optional(),
|
|
456
|
+
durationMs: z14.number().nonnegative().optional(),
|
|
547
457
|
/** PostToolUseFailure: the harness's error message. */
|
|
548
|
-
error:
|
|
458
|
+
error: z14.string().optional(),
|
|
549
459
|
/** The session's transcript, or the helper's on SubagentStop; the daemon reads its tail for the agent's messages. */
|
|
550
|
-
transcriptPath:
|
|
460
|
+
transcriptPath: z14.string().optional(),
|
|
551
461
|
/** Decided: what was chosen when the daemon knows it. */
|
|
552
462
|
decision: DecisionSchema.optional(),
|
|
553
463
|
/** The agent's own session id when the hook names it, so the daemon can resume that conversation. */
|
|
554
|
-
agentSessionId:
|
|
464
|
+
agentSessionId: z14.string().optional()
|
|
555
465
|
});
|
|
556
|
-
var RecentSchema =
|
|
466
|
+
var RecentSchema = z14.object({ cwd: z14.string().min(1), project: z14.string().min(1), lastAt: z14.string().datetime() });
|
|
557
467
|
var SEMVER = /^\d+\.\d+\.\d+$/;
|
|
558
|
-
var DaemonInfoSchema =
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
468
|
+
var DaemonInfoSchema = z14.object({
|
|
469
|
+
version: z14.string(),
|
|
470
|
+
install: z14.enum(["npm", "git", "unknown"]),
|
|
471
|
+
systemd: z14.boolean(),
|
|
472
|
+
manual: z14.string().optional(),
|
|
473
|
+
routing: z14.boolean().optional(),
|
|
474
|
+
archive: z14.boolean().optional(),
|
|
475
|
+
/** Takes `model` and `effort` on `session.launch` and answers `models.list` (#58). */
|
|
476
|
+
models: z14.boolean().optional(),
|
|
477
|
+
/** Its home folder, so a client shows the folders under it as `~` (absent from older daemons). */
|
|
478
|
+
home: z14.string().optional()
|
|
479
|
+
});
|
|
480
|
+
var UpdateFailureSchema = z14.enum(["git-install", "no-systemd", "sessions-at-risk", "npm-failed", "busy"]);
|
|
481
|
+
var ServerMessageSchema = z14.discriminatedUnion("type", [
|
|
482
|
+
z14.object({ type: z14.literal("chat.routed"), qid: z14.string(), result: RouteResultSchema }),
|
|
562
483
|
DocumentContentSchema,
|
|
563
484
|
DocumentErrorSchema,
|
|
564
485
|
ReposInfoSchema,
|
|
565
486
|
ReposFoundSchema,
|
|
487
|
+
ModelsListingSchema,
|
|
566
488
|
/** `recents`: the folders of past sessions, most recent first. `launcher`: the command that runs `ledge` inside WSL
|
|
567
489
|
* (`[node, cli]`), so the overlay can attach a terminal to a session with `<launcher> attach <id>`. `daemon`: absent
|
|
568
490
|
* from daemons older than the auto-update. */
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
491
|
+
z14.object({ type: z14.literal("snapshot"), sessions: z14.array(SessionSchema), recents: z14.array(RecentSchema).optional(), launcher: z14.array(z14.string()).optional(), daemon: DaemonInfoSchema.optional(), sshAlias: SshAliasSchema.optional() }),
|
|
492
|
+
z14.object({ type: z14.literal("session.updated"), session: SessionSchema }),
|
|
493
|
+
z14.object({ type: z14.literal("session.removed"), id: z14.string() }),
|
|
494
|
+
z14.object({ type: z14.literal("session.preview"), id: z14.string(), lines: z14.array(z14.string()) }),
|
|
573
495
|
/** A session's thread, on request (spec 2026-10-13 §5); then each created or updated item, broadcast. */
|
|
574
|
-
|
|
575
|
-
|
|
496
|
+
z14.object({ type: z14.literal("session.thread"), id: z14.string(), items: z14.array(ThreadItemSchema) }),
|
|
497
|
+
z14.object({ type: z14.literal("session.thread.item"), id: z14.string(), item: ThreadItemSchema }),
|
|
576
498
|
/** Answer to the requester of a `session.launch`: the session exists (its `session.updated` is broadcast as usual). */
|
|
577
|
-
|
|
578
|
-
|
|
499
|
+
z14.object({ type: z14.literal("session.launched"), id: z14.string() }),
|
|
500
|
+
z14.object({ type: z14.literal("recents"), recents: z14.array(RecentSchema) }),
|
|
579
501
|
/** Answer to the requester of a `folder.list` or `folder.create`: `path` resolved (`~` expanded), `parent` null at the
|
|
580
502
|
* root, `entries` its subfolders by name (hidden ones left out). A failure keeps the asked path and says why in `error`,
|
|
581
503
|
* never in an `error` message, which the launch form reads as a failed launch. */
|
|
582
|
-
|
|
583
|
-
type:
|
|
584
|
-
path:
|
|
585
|
-
parent:
|
|
586
|
-
entries:
|
|
587
|
-
error:
|
|
504
|
+
z14.object({
|
|
505
|
+
type: z14.literal("folder.listing"),
|
|
506
|
+
path: z14.string(),
|
|
507
|
+
parent: z14.string().nullable(),
|
|
508
|
+
entries: z14.array(z14.string()),
|
|
509
|
+
error: z14.string().optional(),
|
|
588
510
|
/** The request this answers, when it carried one. */
|
|
589
|
-
requestId:
|
|
511
|
+
requestId: z14.string().optional(),
|
|
590
512
|
/** Those `entries` that are Git repositories, with their branch (the short commit when HEAD is detached). */
|
|
591
|
-
repos:
|
|
513
|
+
repos: z14.array(z14.object({ name: z14.string(), branch: z14.string() })).optional()
|
|
592
514
|
}),
|
|
593
515
|
/** 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. */
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
516
|
+
z14.object({ type: z14.literal("chat.step"), qid: z14.string(), step: ChatStepSchema }),
|
|
517
|
+
z14.object({ type: z14.literal("chat.delta"), qid: z14.string(), text: z14.string() }),
|
|
518
|
+
z14.object({ type: z14.literal("chat.done"), qid: z14.string(), text: z14.string(), at: z14.string().datetime(), proposal: ChatProposalSchema.optional(), proposals: ChatProposalsSchema.optional(), relay: ChatRelaySchema.optional(), projectProposal: ChatProjectProposalSchema.optional() }),
|
|
519
|
+
z14.object({ type: z14.literal("chat.error"), qid: z14.string(), message: z14.string() }),
|
|
520
|
+
z14.object({ type: z14.literal("chat.history"), turns: z14.array(ChatTurnSchema) }),
|
|
599
521
|
/** The daemon updating itself: npm is running, then the service restarts (the socket closes right after). */
|
|
600
|
-
|
|
522
|
+
z14.object({ type: z14.literal("update.progress"), version: z14.string(), step: z14.enum(["installing", "restarting"]) }),
|
|
601
523
|
/** Nothing was changed. `detail`: the useful end of npm's output; `command`: what the person can run instead. */
|
|
602
|
-
|
|
524
|
+
z14.object({ type: z14.literal("update.failed"), version: z14.string(), reason: UpdateFailureSchema, detail: z14.string().optional(), command: z14.string().optional() }),
|
|
603
525
|
/** The shared machines list (spec 2026-10-14 §5), sent to local clients only. */
|
|
604
|
-
|
|
526
|
+
z14.object({ type: z14.literal("machines"), machines: MachinesSchema }),
|
|
605
527
|
/** The local daemon's projects and folder rules (spec 2026-10-16 build §5), sent to local clients only. */
|
|
606
|
-
|
|
528
|
+
z14.object({ type: z14.literal("projects"), projects: ProjectsSchema }),
|
|
607
529
|
/** The island's settings shared by the local fronts; null until a front sets them (the island then sends its own). Local clients only. */
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
530
|
+
z14.object({ type: z14.literal("island"), prefs: IslandPrefsSchema.nullable() }),
|
|
531
|
+
/** The daemon's immediate answer to a `session.message`: delivered (`accepted`), held for later (`queued`), left to Claude's own SendMessage (`native`), or refused with a `reason`. */
|
|
532
|
+
z14.object({ type: z14.literal("message.ack"), rid: z14.string(), outcome: z14.enum(["accepted", "queued", "native", "refused"]), reason: z14.string().optional() }),
|
|
533
|
+
z14.object({
|
|
534
|
+
type: z14.literal("error"),
|
|
535
|
+
message: z14.string(),
|
|
536
|
+
id: z14.string().optional(),
|
|
613
537
|
/** Le type du message client refusé. */
|
|
614
|
-
request:
|
|
538
|
+
request: z14.string().optional(),
|
|
615
539
|
/** Ce que le front doit reconnaître au-delà du texte (NOTE_NOT_TYPED) ; une chaîne libre, qu'un front plus ancien ignore. */
|
|
616
|
-
code:
|
|
540
|
+
code: z14.string().optional()
|
|
617
541
|
})
|
|
618
542
|
]);
|
|
619
|
-
var
|
|
620
|
-
|
|
543
|
+
var MESSAGE_MAX = 2e4;
|
|
544
|
+
var SUMMARY_MAX = 500;
|
|
545
|
+
var ClientMessageSchema = z14.discriminatedUnion("type", [
|
|
546
|
+
z14.object({ type: z14.literal("chat.route"), qid: z14.string().min(1).max(100), text: z14.string().min(1).max(2e4), tool: ToolSchema.optional(), context: RoutingContextSchema }),
|
|
621
547
|
DocumentReadSchema,
|
|
622
548
|
ReposInspectSchema,
|
|
623
549
|
ReposScanSchema,
|
|
550
|
+
ModelsListSchema,
|
|
624
551
|
/** The overlay starts a session: `cwd` is a WSL path (`~` allowed), `name` a preferred bot name (the daemon picks a
|
|
625
552
|
* free one otherwise), `prompt` the agent's first instruction, `continue` resumes the folder's last conversation.
|
|
626
|
-
* With `qid` it is the chat card of that turn: the daemon launches the turn's proposal and ignores the other fields
|
|
627
|
-
|
|
628
|
-
|
|
553
|
+
* With `qid` it is the chat card of that turn: the daemon launches the turn's proposal and ignores the other fields
|
|
554
|
+
* but `rows`, `model` and `effort`. */
|
|
555
|
+
z14.object({
|
|
556
|
+
type: z14.literal("session.launch"),
|
|
629
557
|
tool: ToolSchema,
|
|
630
|
-
cwd:
|
|
631
|
-
name:
|
|
632
|
-
prompt:
|
|
633
|
-
continue:
|
|
634
|
-
qid:
|
|
558
|
+
cwd: z14.string().min(1),
|
|
559
|
+
name: z14.string().optional(),
|
|
560
|
+
prompt: z14.string().optional(),
|
|
561
|
+
continue: z14.boolean().optional(),
|
|
562
|
+
qid: z14.string().min(1).optional(),
|
|
635
563
|
/** With `qid`: the card's rows to launch (#24), indexes into the turn's `proposals`, each once; the first row without it. */
|
|
636
|
-
rows:
|
|
564
|
+
rows: z14.array(z14.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(),
|
|
637
565
|
/** Folders in addition to `cwd` (Claude: `--add-dir`); resolved like `cwd`. */
|
|
638
|
-
dirs:
|
|
566
|
+
dirs: z14.array(z14.string().min(1)).max(20).optional(),
|
|
567
|
+
/** The agent's model and effort (#58), the agent's own default when left out; with `qid`, for every row launched. */
|
|
568
|
+
model: ModelIdSchema.optional(),
|
|
569
|
+
effort: EffortSchema.optional(),
|
|
639
570
|
/** Routing by the fleet, removed before the message reaches the daemon. */
|
|
640
|
-
machineId:
|
|
571
|
+
machineId: z14.string().optional()
|
|
641
572
|
}),
|
|
642
573
|
/** The launch form's folder browser: list a folder's subfolders (home when no `path`), or make `name` inside `parent`
|
|
643
574
|
* and list it. Both are answered with a `folder.listing`. */
|
|
644
|
-
|
|
645
|
-
type:
|
|
646
|
-
path:
|
|
575
|
+
z14.object({
|
|
576
|
+
type: z14.literal("folder.list"),
|
|
577
|
+
path: z14.string().optional(),
|
|
647
578
|
/** Routing by the fleet, removed before the message reaches the daemon. */
|
|
648
|
-
machineId:
|
|
579
|
+
machineId: z14.string().optional(),
|
|
649
580
|
/** Copied into the `folder.listing` that answers; the fleet relays only that listing, from the machine asked. */
|
|
650
|
-
requestId:
|
|
581
|
+
requestId: z14.string().min(1).max(100).optional()
|
|
651
582
|
}),
|
|
652
|
-
|
|
653
|
-
type:
|
|
654
|
-
parent:
|
|
655
|
-
name:
|
|
583
|
+
z14.object({
|
|
584
|
+
type: z14.literal("folder.create"),
|
|
585
|
+
parent: z14.string().min(1),
|
|
586
|
+
name: z14.string().min(1),
|
|
656
587
|
/** As for `folder.list`: routing by the fleet, and the request its `folder.listing` answers. */
|
|
657
|
-
machineId:
|
|
658
|
-
requestId:
|
|
588
|
+
machineId: z14.string().optional(),
|
|
589
|
+
requestId: z14.string().min(1).max(100).optional()
|
|
659
590
|
}),
|
|
660
|
-
|
|
591
|
+
z14.object({ type: z14.literal("session.reply"), id: z14.string(), text: z14.string().min(1) }),
|
|
661
592
|
/** The chat card's instruction (spec 2026-10-12 relay §4): sent now if the session is free, else queued until it is. */
|
|
662
|
-
|
|
593
|
+
z14.object({ type: z14.literal("session.relay"), qid: z14.string().min(1) }),
|
|
663
594
|
/** Takes the card's queued instruction back out of the queue. */
|
|
664
|
-
|
|
595
|
+
z14.object({ type: z14.literal("session.relay.cancel"), qid: z14.string().min(1) }),
|
|
665
596
|
/** `always` and `note` only where the harness offers them (`offers`); a note goes with `deny`, for the agent to read. */
|
|
666
|
-
|
|
667
|
-
type:
|
|
668
|
-
id:
|
|
597
|
+
z14.object({
|
|
598
|
+
type: z14.literal("session.decide"),
|
|
599
|
+
id: z14.string(),
|
|
669
600
|
decision: DecisionSchema,
|
|
670
|
-
note:
|
|
601
|
+
note: z14.string().trim().min(1).max(NOTE_MAX).optional(),
|
|
671
602
|
/** The request the user saw when deciding: refused (the request changed) unless it is still the pending one. Left out by older fronts and the island.
|
|
672
603
|
* `key` (pendingKey) covers its whole content; older fronts send kind and text only. */
|
|
673
|
-
expect: PendingSchema.pick({ kind: true, text: true }).extend({ key:
|
|
604
|
+
expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z14.string().max(64).optional() }).optional()
|
|
674
605
|
}),
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
606
|
+
z14.object({ type: z14.literal("session.focus"), id: z14.string() }),
|
|
607
|
+
z14.object({ type: z14.literal("session.kill"), id: z14.string() }),
|
|
608
|
+
z14.object({ type: z14.literal("session.interrupt"), id: z14.string() }),
|
|
678
609
|
/** Removes a finished (exited) or read-only (hook_only) session from the list; refused for a live launcher session. */
|
|
679
|
-
|
|
610
|
+
z14.object({ type: z14.literal("session.dismiss"), id: z14.string() }),
|
|
680
611
|
/** Puts a session in the archive: it leaves the live list but is kept. */
|
|
681
|
-
|
|
612
|
+
z14.object({ type: z14.literal("session.archive"), id: z14.string() }),
|
|
682
613
|
/** Takes an archived session back out of the archive, as it was. */
|
|
683
|
-
|
|
614
|
+
z14.object({ type: z14.literal("session.restore"), id: z14.string() }),
|
|
684
615
|
/** Starts the agent again on an archived session's conversation. */
|
|
685
|
-
|
|
616
|
+
z14.object({ type: z14.literal("session.resume"), id: z14.string() }),
|
|
686
617
|
/** Deletes a session for good, archived or not. */
|
|
687
|
-
|
|
618
|
+
z14.object({ type: z14.literal("session.delete"), id: z14.string() }),
|
|
688
619
|
/** The user names the session's topic from the overlay (500 characters at most, the hook's prompt cap); an empty topic hands back to the automatic sources. */
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
620
|
+
z14.object({ type: z14.literal("session.rename"), id: z14.string(), topic: z14.string().max(500) }),
|
|
621
|
+
z14.object({ type: z14.literal("session.thread"), id: z14.string() }),
|
|
622
|
+
z14.object({ type: z14.literal("session.register"), session: SessionSchema }),
|
|
692
623
|
/** `agent` and `cwd` describe the session when the daemon does not know it yet (a hook from an agent started without ledge). */
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
624
|
+
z14.object({ type: z14.literal("session.event"), id: z14.string(), event: SessionEventSchema, agent: ToolSchema.optional(), cwd: z14.string().optional() }),
|
|
625
|
+
z14.object({ type: z14.literal("session.unregister"), id: z14.string(), exitCode: z14.number().int().nullable() }),
|
|
626
|
+
z14.object({ type: z14.literal("snapshot.request") }),
|
|
627
|
+
z14.object({ type: z14.literal("chat.ask"), qid: z14.string().min(1), text: z14.string().min(1), tool: ToolSchema.optional() }),
|
|
628
|
+
z14.object({ type: z14.literal("chat.cancel"), qid: z14.string().min(1) }),
|
|
698
629
|
/** Starts a new conversation with Ledge: the day's turns are cleared and the next question opens a new harness session. */
|
|
699
|
-
|
|
630
|
+
z14.object({ type: z14.literal("chat.reset") }),
|
|
700
631
|
/** Installs `@paradow/ledge@version` and restarts the service (spec 2026-10-12 auto-update §6). */
|
|
701
|
-
|
|
632
|
+
z14.object({ type: z14.literal("update.daemon"), version: z14.string().regex(SEMVER) }),
|
|
702
633
|
/** Replaces the daemon's machines list (first connected front wins); refused unless valid. */
|
|
703
|
-
|
|
634
|
+
z14.object({ type: z14.literal("machines.set"), machines: MachinesSchema }),
|
|
704
635
|
/** Replaces the local daemon's projects and rules as a whole; refused unless valid. */
|
|
705
|
-
|
|
636
|
+
z14.object({ type: z14.literal("projects.set"), projects: ProjectsSchema }),
|
|
706
637
|
/** Replaces the island's settings as a whole; refused unless valid. */
|
|
707
|
-
|
|
638
|
+
z14.object({ type: z14.literal("island.set"), prefs: IslandPrefsSchema }),
|
|
639
|
+
/** One session writing to another by Ledge name (`to` may end in ` [ref]`); `intercept` (the Claude hook): answer `native` unless `to` is a live non-Claude session. */
|
|
640
|
+
z14.object({ type: z14.literal("session.message"), rid: z14.string().min(1), from: z14.string().min(1), to: z14.string().min(1), text: z14.string().min(1).max(MESSAGE_MAX), summary: z14.string().max(SUMMARY_MAX).optional(), intercept: z14.boolean().optional() })
|
|
708
641
|
]);
|
|
709
642
|
function parseClientMessage(raw) {
|
|
710
643
|
return ClientMessageSchema.parse(raw);
|
|
@@ -796,14 +729,14 @@ function pickName(taken, preferred) {
|
|
|
796
729
|
}
|
|
797
730
|
|
|
798
731
|
// ../protocol/src/proposal.ts
|
|
799
|
-
import { z as
|
|
800
|
-
var ProposalBlockSchema =
|
|
801
|
-
var RelayBlockSchema =
|
|
802
|
-
var ProjectBlockSchema =
|
|
803
|
-
name:
|
|
804
|
-
description:
|
|
805
|
-
repos:
|
|
806
|
-
addTo:
|
|
732
|
+
import { z as z15 } from "zod";
|
|
733
|
+
var ProposalBlockSchema = z15.object({ cwd: z15.string().trim().min(1), prompt: z15.string().optional() });
|
|
734
|
+
var RelayBlockSchema = z15.object({ session: z15.string().trim().min(1), text: z15.string().trim().min(1) });
|
|
735
|
+
var ProjectBlockSchema = z15.object({
|
|
736
|
+
name: z15.string().trim().min(1).max(80),
|
|
737
|
+
description: z15.string().max(500).optional(),
|
|
738
|
+
repos: z15.array(z15.object({ path: z15.string().min(1).max(4096), why: z15.string().trim().min(1).max(200) })).min(1).max(50),
|
|
739
|
+
addTo: z15.string().trim().max(80).optional()
|
|
807
740
|
});
|
|
808
741
|
var BLOCK = /```ledge-(session|relay|project)[ \t]*\n?([\s\S]*?)(?:```|$)/g;
|
|
809
742
|
function extractProposal(answer) {
|
|
@@ -851,12 +784,126 @@ function sessionTitle(s) {
|
|
|
851
784
|
return `${s.name} \xB7 ${s.topic || s.project}`;
|
|
852
785
|
}
|
|
853
786
|
|
|
787
|
+
// src/auth.ts
|
|
788
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
789
|
+
import { chmod, link, mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
790
|
+
import { readFileSync } from "fs";
|
|
791
|
+
import { dirname } from "path";
|
|
792
|
+
var TOKEN_RE = /^[0-9a-f]{64}$/;
|
|
793
|
+
var DAEMON_PROTOCOL_PREFIX = "ledge.";
|
|
794
|
+
function isDaemonToken(value) {
|
|
795
|
+
return TOKEN_RE.test(value);
|
|
796
|
+
}
|
|
797
|
+
function tokenEquals(presented, expected) {
|
|
798
|
+
const a = createHash("sha256").update(presented.slice(0, 256), "utf8").digest();
|
|
799
|
+
const b = createHash("sha256").update(expected, "utf8").digest();
|
|
800
|
+
return timingSafeEqual(a, b);
|
|
801
|
+
}
|
|
802
|
+
function hostAllowed(hostHeader) {
|
|
803
|
+
const raw = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
|
|
804
|
+
if (!raw || raw.length > 255) return false;
|
|
805
|
+
const host = raw.trim().toLowerCase();
|
|
806
|
+
if (!host || host.includes(" ") || host.endsWith(".")) return false;
|
|
807
|
+
let name;
|
|
808
|
+
if (host.startsWith("[")) {
|
|
809
|
+
const end = host.indexOf("]");
|
|
810
|
+
if (end < 0) return false;
|
|
811
|
+
name = host.slice(1, end);
|
|
812
|
+
const rest = host.slice(end + 1);
|
|
813
|
+
if (rest !== "" && !/^:\d{1,5}$/.test(rest)) return false;
|
|
814
|
+
} else {
|
|
815
|
+
const colon = host.lastIndexOf(":");
|
|
816
|
+
if (colon >= 0) {
|
|
817
|
+
if (!/^\d{1,5}$/.test(host.slice(colon + 1))) return false;
|
|
818
|
+
name = host.slice(0, colon);
|
|
819
|
+
} else name = host;
|
|
820
|
+
}
|
|
821
|
+
return name === "localhost" || name === "127.0.0.1" || name === "::1";
|
|
822
|
+
}
|
|
823
|
+
function headerValue(value, max) {
|
|
824
|
+
const raw = Array.isArray(value) ? value.join(",") : value;
|
|
825
|
+
return !raw || raw.length > max ? "" : raw;
|
|
826
|
+
}
|
|
827
|
+
function bearerToken(authorization) {
|
|
828
|
+
if (!authorization || authorization.length > 512) return "";
|
|
829
|
+
return /^Bearer[ \t]+(\S+)$/i.exec(authorization)?.[1] ?? "";
|
|
830
|
+
}
|
|
831
|
+
function protocolToken(header) {
|
|
832
|
+
const raw = headerValue(header, 512);
|
|
833
|
+
if (!raw) return "";
|
|
834
|
+
for (const part of raw.split(",").slice(0, 8)) {
|
|
835
|
+
const protocol = part.trim();
|
|
836
|
+
if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX)) return protocol.slice(DAEMON_PROTOCOL_PREFIX.length);
|
|
837
|
+
}
|
|
838
|
+
return "";
|
|
839
|
+
}
|
|
840
|
+
function requestAuthorized(headers, expected) {
|
|
841
|
+
const bearerOk = tokenEquals(bearerToken(typeof headers.authorization === "string" ? headers.authorization : void 0), expected);
|
|
842
|
+
const presented = protocolToken(headers["sec-websocket-protocol"]);
|
|
843
|
+
return bearerOk || presented !== "" && tokenEquals(presented, expected);
|
|
844
|
+
}
|
|
845
|
+
function selectDaemonProtocol(protocols, expected) {
|
|
846
|
+
for (const protocol of protocols) {
|
|
847
|
+
if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX) && tokenEquals(protocol.slice(DAEMON_PROTOCOL_PREFIX.length), expected)) return protocol;
|
|
848
|
+
}
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
async function readTokenFile(file) {
|
|
852
|
+
try {
|
|
853
|
+
const raw = (await readFile(file, "utf8")).trim();
|
|
854
|
+
return isDaemonToken(raw) ? raw : void 0;
|
|
855
|
+
} catch (e) {
|
|
856
|
+
if (e.code === "ENOENT") return void 0;
|
|
857
|
+
throw e;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
async function lockTokenFile(file) {
|
|
861
|
+
await chmod(file, 384);
|
|
862
|
+
}
|
|
863
|
+
async function ensureDaemonToken() {
|
|
864
|
+
const file = paths().token;
|
|
865
|
+
await mkdir(dirname(file), { recursive: true });
|
|
866
|
+
const current = await readTokenFile(file);
|
|
867
|
+
if (current) {
|
|
868
|
+
await lockTokenFile(file);
|
|
869
|
+
return current;
|
|
870
|
+
}
|
|
871
|
+
const token = randomBytes(32).toString("hex");
|
|
872
|
+
const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
873
|
+
await writeFile(tmp, `${token}
|
|
874
|
+
`, { mode: 384, flag: "wx" });
|
|
875
|
+
await chmod(tmp, 384);
|
|
876
|
+
try {
|
|
877
|
+
await link(tmp, file);
|
|
878
|
+
} catch (e) {
|
|
879
|
+
await unlink(tmp).catch(() => {
|
|
880
|
+
});
|
|
881
|
+
if (e.code !== "EEXIST") throw e;
|
|
882
|
+
const raced = await readTokenFile(file);
|
|
883
|
+
if (raced) {
|
|
884
|
+
await lockTokenFile(file);
|
|
885
|
+
return raced;
|
|
886
|
+
}
|
|
887
|
+
await writeFile(file, `${token}
|
|
888
|
+
`, { mode: 384 });
|
|
889
|
+
}
|
|
890
|
+
await unlink(tmp).catch(() => {
|
|
891
|
+
});
|
|
892
|
+
await lockTokenFile(file);
|
|
893
|
+
const stored = await readTokenFile(file);
|
|
894
|
+
if (!stored) throw new Error("daemon token was not stored");
|
|
895
|
+
return stored;
|
|
896
|
+
}
|
|
897
|
+
function readDaemonToken() {
|
|
898
|
+
try {
|
|
899
|
+
const raw = readFileSync(paths().token, "utf8").trim();
|
|
900
|
+
return isDaemonToken(raw) ? raw : void 0;
|
|
901
|
+
} catch {
|
|
902
|
+
return void 0;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
854
906
|
export {
|
|
855
|
-
hostAllowed,
|
|
856
|
-
requestAuthorized,
|
|
857
|
-
selectDaemonProtocol,
|
|
858
|
-
ensureDaemonToken,
|
|
859
|
-
readDaemonToken,
|
|
860
907
|
SessionSchema,
|
|
861
908
|
isArchived,
|
|
862
909
|
MAX_PROPOSALS,
|
|
@@ -871,9 +918,13 @@ export {
|
|
|
871
918
|
ProjectsSchema,
|
|
872
919
|
DEFAULT_PROJECTS,
|
|
873
920
|
projectOf,
|
|
921
|
+
ModelIdSchema,
|
|
922
|
+
EffortSchema,
|
|
874
923
|
RouteResultSchema,
|
|
875
924
|
PROTOCOL_PORT,
|
|
876
925
|
RecentSchema,
|
|
926
|
+
MESSAGE_MAX,
|
|
927
|
+
SUMMARY_MAX,
|
|
877
928
|
parseClientMessage,
|
|
878
929
|
parseServerMessage,
|
|
879
930
|
pickName,
|
|
@@ -882,5 +933,10 @@ export {
|
|
|
882
933
|
parseRelayBlock,
|
|
883
934
|
parseProjectBlock,
|
|
884
935
|
cleanTopic,
|
|
885
|
-
sessionTitle
|
|
936
|
+
sessionTitle,
|
|
937
|
+
hostAllowed,
|
|
938
|
+
requestAuthorized,
|
|
939
|
+
selectDaemonProtocol,
|
|
940
|
+
ensureDaemonToken,
|
|
941
|
+
readDaemonToken
|
|
886
942
|
};
|