@paradow/ledge 0.10.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.
@@ -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 z13 } from "zod";
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/scan.ts
157
+ // ../protocol/src/models.ts
274
158
  import { z as z4 } from "zod";
275
- var ReposScanSchema = z4.object({ type: z4.literal("repos.scan"), requestId: z4.string().min(1).max(100), machineId: z4.string().optional() });
276
- var FoundRepoSchema = z4.object({ path: z4.string(), branch: z4.string().nullable(), remote: z4.string().optional(), manifest: z4.string().max(200).optional() });
277
- var ReposFoundSchema = z4.object({ type: z4.literal("repos.found"), requestId: z4.string(), repos: z4.array(FoundRepoSchema).max(500), truncated: z4.boolean(), error: z4.string().optional() });
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/documents.ts
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 = z5.string().min(1).max(4096);
283
- var requestId = z5.string().min(1);
284
- var DocumentReadSchema = z5.object({
285
- type: z5.literal("document.read"),
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: z5.string().optional(),
289
- sessionId: z5.string().min(1).optional(),
198
+ machineId: z6.string().optional(),
199
+ sessionId: z6.string().min(1).optional(),
290
200
  basePath: path.optional()
291
201
  });
292
- var DocumentContentSchema = z5.object({
293
- type: z5.literal("document.content"),
202
+ var DocumentContentSchema = z6.object({
203
+ type: z6.literal("document.content"),
294
204
  requestId,
295
205
  path,
296
- text: z5.string(),
297
- bytes: z5.number().int().nonnegative(),
298
- modifiedAt: z5.string().datetime()
206
+ text: z6.string(),
207
+ bytes: z6.number().int().nonnegative(),
208
+ modifiedAt: z6.string().datetime()
299
209
  });
300
- var DocumentErrorCodeSchema = z5.enum([
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 = z5.object({
313
- type: z5.literal("document.error"),
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 z6 } from "zod";
321
- var MachineInventorySchema = z6.object({
322
- id: z6.string().min(1).max(80),
323
- label: z6.string().min(1).max(120),
324
- online: z6.boolean(),
325
- local: z6.boolean(),
326
- sessions: z6.array(z6.object({ id: z6.string(), name: z6.string(), project: z6.string(), cwd: z6.string(), topic: z6.string().optional(), state: z6.string(), readOnly: z6.boolean() })).max(500),
327
- folders: z6.array(z6.object({ project: z6.string(), cwd: z6.string() })).max(100)
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 = z6.object({
330
- machines: z6.array(MachineInventorySchema).min(1).max(20),
331
- history: z6.array(z6.object({ question: z6.string().max(2e4), answer: z6.string().max(2e4).optional(), machineId: z6.string().optional() })).max(50)
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 = z6.object({
243
+ var RouteResultSchema = z7.object({
334
244
  /** Null means an answer or a clarification, never an implicit local destination. */
335
- machineId: z6.string().nullable(),
336
- text: z6.string().min(1).max(2e4),
337
- choices: z6.array(z6.string()).max(20).optional()
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 z7 } from "zod";
342
- var SshAliasSchema = z7.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/);
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 z8 } from "zod";
346
- var MachineIdSchema = z8.string().regex(/^[a-zA-Z0-9_-]{1,64}$/);
347
- var MachineSchema = z8.object({
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: z8.string().trim().min(1).max(80),
259
+ label: z9.string().trim().min(1).max(80),
350
260
  ssh: SshAliasSchema.optional()
351
261
  });
352
- var MachinesSchema = z8.array(MachineSchema).min(1).max(20).superRefine((list, ctx) => {
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 z9 } from "zod";
368
- var IslandScaleSchema = z9.union([z9.literal("auto"), z9.literal(1), z9.literal(1.25), z9.literal(1.5), z9.literal(2)]);
369
- var IslandPrefsSchema = z9.object({
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: z9.boolean(),
281
+ visible: z10.boolean(),
372
282
  /** The primary screen, or the first other one. */
373
- screen: z9.enum(["primary", "second"]),
374
- position: z9.enum(["center", "left", "right"]),
283
+ screen: z10.enum(["primary", "second"]),
284
+ position: z10.enum(["center", "left", "right"]),
375
285
  /** A strip under a maximised window. */
376
- discreet: z9.boolean(),
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: z9.object({
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: z9.boolean(),
292
+ quick: z10.boolean(),
383
293
  /** The 6 s bubble when an agent finishes. */
384
- finished: z9.boolean(),
294
+ finished: z10.boolean(),
385
295
  /** An agent's sentence in a bubble. */
386
- say: z9.boolean(),
296
+ say: z10.boolean(),
387
297
  /** The sub-agents next to their main agent. */
388
- helpers: z9.boolean()
298
+ helpers: z10.boolean()
389
299
  }),
390
300
  /** The global shortcut that opens the island, as shown (`Ctrl Alt L`). */
391
- shortcut: z9.string().trim().min(1).max(40)
301
+ shortcut: z10.string().trim().min(1).max(40)
392
302
  });
393
303
 
394
304
  // ../protocol/src/projects.ts
395
- import { z as z10 } from "zod";
396
- var ProjectColorSchema = z10.enum(["blue", "green", "amber", "violet", "teal", "pink", "red", "slate"]);
397
- var ProjectIdSchema = z10.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/);
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 = z10.string().max(4096).refine((p) => p.startsWith("/") && p === normalizePath(p), "an absolute, normalized path");
403
- var RepoSchema = z10.object({ machine: MachineIdSchema, path: PathSchema });
404
- var ProjectSchema = z10.object({
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: z10.string().trim().min(1).max(80),
316
+ name: z11.string().trim().min(1).max(80),
407
317
  color: ProjectColorSchema,
408
- description: z10.string().max(500).default(""),
409
- repos: z10.array(RepoSchema).max(50)
318
+ description: z11.string().max(500).default(""),
319
+ repos: z11.array(RepoSchema).max(50)
410
320
  });
411
- var RuleSchema = z10.object({ machine: MachineIdSchema, path: PathSchema, project: ProjectIdSchema });
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 = z10.object({ projects: z10.array(ProjectSchema).max(100), rules: z10.array(RuleSchema).max(1e3) }).superRefine((p, ctx) => {
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 z11 } from "zod";
456
- var ToolStatusSchema = z11.enum(["running", "success", "failed", "unknown"]);
457
- var FileChangeSchema = z11.object({ path: z11.string().min(1), from: z11.string().min(1).optional(), change: z11.enum(["add", "edit", "delete", "write"]), diff: z11.string().optional() });
458
- var base = { id: z11.string().min(1), at: z11.string().datetime(), turn: z11.number().int().nonnegative(), helperId: z11.string().optional() };
459
- var ThreadItemSchema = z11.discriminatedUnion("kind", [
460
- z11.object({ kind: z11.literal("prompt"), ...base, text: z11.string() }),
461
- z11.object({ kind: z11.literal("message"), ...base, text: z11.string() }),
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
- z11.object({ kind: z11.literal("tool"), ...base, tool: z11.string(), label: z11.string(), input: z11.string().optional(), output: z11.string().optional(), error: z11.string().optional(), status: ToolStatusSchema, durationMs: z11.number().nonnegative().optional(), files: z11.array(FileChangeSchema).default([]) }),
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
- z11.object({ kind: z11.literal("permission"), ...base, tool: z11.string(), text: z11.string(), decision: z11.enum(["accept", "deny", "always", "unknown"]).optional() }),
466
- z11.object({ kind: z11.literal("reply"), ...base, text: z11.string() }),
467
- z11.object({ kind: z11.literal("helper"), ...base, task: z11.string(), agentType: z11.string().optional(), state: z11.enum(["working", "exited"]) }),
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
- z11.object({ kind: z11.literal("sent"), ...base, to: z11.string(), peer: z11.string().optional(), summary: z11.string().optional(), text: z11.string(), status: z11.enum(["sending", "delivered", "read", "failed"]), received: z11.string().optional() }),
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
- z11.object({ kind: z11.literal("received"), ...base, from: z11.string(), peer: z11.string().optional(), text: z11.string(), sent: z11.string().optional() }),
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
- z11.object({
476
- kind: z11.literal("notice"),
385
+ z12.object({
386
+ kind: z12.literal("notice"),
477
387
  ...base,
478
- notice: z11.enum(["idle", "exited", "unavailable", "expired", "delivery", "other"]),
479
- name: z11.string().optional(),
480
- peer: z11.string().optional(),
481
- time: z11.string().optional(),
482
- summary: z11.string().optional(),
483
- status: z11.enum(["held", "denied", "expired", "delivered", "refused", "dropped", "unknown"]).optional(),
484
- count: z11.number().int().positive().optional(),
485
- recipients: z11.array(z11.string()).optional(),
486
- reason: z11.string().optional(),
487
- text: z11.string()
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 z12 } from "zod";
493
- var DecisionSchema = z12.enum(["accept", "deny", "always"]);
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 = z13.object({
523
- name: z13.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd", "PaneChanged", "PaneGone", "PanePrompt", "PaneIdle", "InjectFailed", "Decided", "PostToolUse", "PostToolUseFailure", "StopFailure", "SubagentStart", "SubagentStop", "PaneTitle", "Renamed"]),
524
- at: z13.string().datetime(),
525
- source: z13.enum(["hook", "pane", "daemon"]),
526
- tool: z13.string().optional(),
527
- text: z13.string().optional(),
528
- options: z13.array(z13.string()).optional(),
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: z13.array(z13.string()).optional(),
531
- kind: z13.enum(["question", "permission", "plan", "diff"]).optional(),
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: z13.string().optional(),
534
- exitCode: z13.number().int().nullable().optional(),
535
- helperId: z13.string().optional(),
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: z13.string().optional(),
447
+ task: z14.string().optional(),
538
448
  /** PostToolUseFailure: the user interrupted the turn (Esc); Claude then waits for input with no Stop. */
539
- interrupt: z13.boolean().optional(),
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: z13.string().optional(),
451
+ toolUseId: z14.string().optional(),
542
452
  /** The tool's input as the harness gave it, every string capped by the hook. */
543
- toolInput: z13.record(z13.string(), z13.unknown()).optional(),
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: z13.string().optional(),
546
- durationMs: z13.number().nonnegative().optional(),
455
+ output: z14.string().optional(),
456
+ durationMs: z14.number().nonnegative().optional(),
547
457
  /** PostToolUseFailure: the harness's error message. */
548
- error: z13.string().optional(),
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: z13.string().optional(),
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: z13.string().optional()
464
+ agentSessionId: z14.string().optional()
555
465
  });
556
- var RecentSchema = z13.object({ cwd: z13.string().min(1), project: z13.string().min(1), lastAt: z13.string().datetime() });
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 = z13.object({ version: z13.string(), install: z13.enum(["npm", "git", "unknown"]), systemd: z13.boolean(), manual: z13.string().optional(), routing: z13.boolean().optional(), archive: z13.boolean().optional() });
559
- var UpdateFailureSchema = z13.enum(["git-install", "no-systemd", "sessions-at-risk", "npm-failed", "busy"]);
560
- var ServerMessageSchema = z13.discriminatedUnion("type", [
561
- z13.object({ type: z13.literal("chat.routed"), qid: z13.string(), result: RouteResultSchema }),
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
- z13.object({ type: z13.literal("snapshot"), sessions: z13.array(SessionSchema), recents: z13.array(RecentSchema).optional(), launcher: z13.array(z13.string()).optional(), daemon: DaemonInfoSchema.optional(), sshAlias: SshAliasSchema.optional() }),
570
- z13.object({ type: z13.literal("session.updated"), session: SessionSchema }),
571
- z13.object({ type: z13.literal("session.removed"), id: z13.string() }),
572
- z13.object({ type: z13.literal("session.preview"), id: z13.string(), lines: z13.array(z13.string()) }),
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
- z13.object({ type: z13.literal("session.thread"), id: z13.string(), items: z13.array(ThreadItemSchema) }),
575
- z13.object({ type: z13.literal("session.thread.item"), id: z13.string(), item: ThreadItemSchema }),
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
- z13.object({ type: z13.literal("session.launched"), id: z13.string() }),
578
- z13.object({ type: z13.literal("recents"), recents: z13.array(RecentSchema) }),
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
- z13.object({
583
- type: z13.literal("folder.listing"),
584
- path: z13.string(),
585
- parent: z13.string().nullable(),
586
- entries: z13.array(z13.string()),
587
- error: z13.string().optional(),
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: z13.string().optional(),
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: z13.array(z13.object({ name: z13.string(), branch: z13.string() })).optional()
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
- z13.object({ type: z13.literal("chat.step"), qid: z13.string(), step: ChatStepSchema }),
595
- z13.object({ type: z13.literal("chat.delta"), qid: z13.string(), text: z13.string() }),
596
- z13.object({ type: z13.literal("chat.done"), qid: z13.string(), text: z13.string(), at: z13.string().datetime(), proposal: ChatProposalSchema.optional(), proposals: ChatProposalsSchema.optional(), relay: ChatRelaySchema.optional(), projectProposal: ChatProjectProposalSchema.optional() }),
597
- z13.object({ type: z13.literal("chat.error"), qid: z13.string(), message: z13.string() }),
598
- z13.object({ type: z13.literal("chat.history"), turns: z13.array(ChatTurnSchema) }),
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
- z13.object({ type: z13.literal("update.progress"), version: z13.string(), step: z13.enum(["installing", "restarting"]) }),
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
- z13.object({ type: z13.literal("update.failed"), version: z13.string(), reason: UpdateFailureSchema, detail: z13.string().optional(), command: z13.string().optional() }),
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
- z13.object({ type: z13.literal("machines"), machines: MachinesSchema }),
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
- z13.object({ type: z13.literal("projects"), projects: ProjectsSchema }),
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
- z13.object({ type: z13.literal("island"), prefs: IslandPrefsSchema.nullable() }),
609
- z13.object({
610
- type: z13.literal("error"),
611
- message: z13.string(),
612
- id: z13.string().optional(),
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: z13.string().optional(),
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: z13.string().optional()
540
+ code: z14.string().optional()
617
541
  })
618
542
  ]);
619
- var ClientMessageSchema = z13.discriminatedUnion("type", [
620
- z13.object({ type: z13.literal("chat.route"), qid: z13.string().min(1).max(100), text: z13.string().min(1).max(2e4), tool: ToolSchema.optional(), context: RoutingContextSchema }),
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
- z13.object({
628
- type: z13.literal("session.launch"),
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: z13.string().min(1),
631
- name: z13.string().optional(),
632
- prompt: z13.string().optional(),
633
- continue: z13.boolean().optional(),
634
- qid: z13.string().min(1).optional(),
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: z13.array(z13.number().int().min(0).max(MAX_PROPOSALS - 1)).min(1).max(MAX_PROPOSALS).refine((r) => new Set(r).size === r.length, "a row twice").optional(),
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: z13.array(z13.string().min(1)).max(20).optional(),
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: z13.string().optional()
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
- z13.object({
645
- type: z13.literal("folder.list"),
646
- path: z13.string().optional(),
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: z13.string().optional(),
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: z13.string().min(1).max(100).optional()
581
+ requestId: z14.string().min(1).max(100).optional()
651
582
  }),
652
- z13.object({
653
- type: z13.literal("folder.create"),
654
- parent: z13.string().min(1),
655
- name: z13.string().min(1),
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: z13.string().optional(),
658
- requestId: z13.string().min(1).max(100).optional()
588
+ machineId: z14.string().optional(),
589
+ requestId: z14.string().min(1).max(100).optional()
659
590
  }),
660
- z13.object({ type: z13.literal("session.reply"), id: z13.string(), text: z13.string().min(1) }),
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
- z13.object({ type: z13.literal("session.relay"), qid: z13.string().min(1) }),
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
- z13.object({ type: z13.literal("session.relay.cancel"), qid: z13.string().min(1) }),
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
- z13.object({
667
- type: z13.literal("session.decide"),
668
- id: z13.string(),
597
+ z14.object({
598
+ type: z14.literal("session.decide"),
599
+ id: z14.string(),
669
600
  decision: DecisionSchema,
670
- note: z13.string().trim().min(1).max(NOTE_MAX).optional(),
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: z13.string().max(64).optional() }).optional()
604
+ expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z14.string().max(64).optional() }).optional()
674
605
  }),
675
- z13.object({ type: z13.literal("session.focus"), id: z13.string() }),
676
- z13.object({ type: z13.literal("session.kill"), id: z13.string() }),
677
- z13.object({ type: z13.literal("session.interrupt"), id: z13.string() }),
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
- z13.object({ type: z13.literal("session.dismiss"), id: z13.string() }),
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
- z13.object({ type: z13.literal("session.archive"), id: z13.string() }),
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
- z13.object({ type: z13.literal("session.restore"), id: z13.string() }),
614
+ z14.object({ type: z14.literal("session.restore"), id: z14.string() }),
684
615
  /** Starts the agent again on an archived session's conversation. */
685
- z13.object({ type: z13.literal("session.resume"), id: z13.string() }),
616
+ z14.object({ type: z14.literal("session.resume"), id: z14.string() }),
686
617
  /** Deletes a session for good, archived or not. */
687
- z13.object({ type: z13.literal("session.delete"), id: z13.string() }),
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
- z13.object({ type: z13.literal("session.rename"), id: z13.string(), topic: z13.string().max(500) }),
690
- z13.object({ type: z13.literal("session.thread"), id: z13.string() }),
691
- z13.object({ type: z13.literal("session.register"), session: SessionSchema }),
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
- z13.object({ type: z13.literal("session.event"), id: z13.string(), event: SessionEventSchema, agent: ToolSchema.optional(), cwd: z13.string().optional() }),
694
- z13.object({ type: z13.literal("session.unregister"), id: z13.string(), exitCode: z13.number().int().nullable() }),
695
- z13.object({ type: z13.literal("snapshot.request") }),
696
- z13.object({ type: z13.literal("chat.ask"), qid: z13.string().min(1), text: z13.string().min(1), tool: ToolSchema.optional() }),
697
- z13.object({ type: z13.literal("chat.cancel"), qid: z13.string().min(1) }),
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
- z13.object({ type: z13.literal("chat.reset") }),
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
- z13.object({ type: z13.literal("update.daemon"), version: z13.string().regex(SEMVER) }),
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
- z13.object({ type: z13.literal("machines.set"), machines: MachinesSchema }),
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
- z13.object({ type: z13.literal("projects.set"), projects: ProjectsSchema }),
636
+ z14.object({ type: z14.literal("projects.set"), projects: ProjectsSchema }),
706
637
  /** Replaces the island's settings as a whole; refused unless valid. */
707
- z13.object({ type: z13.literal("island.set"), prefs: IslandPrefsSchema })
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 z14 } from "zod";
800
- var ProposalBlockSchema = z14.object({ cwd: z14.string().trim().min(1), prompt: z14.string().optional() });
801
- var RelayBlockSchema = z14.object({ session: z14.string().trim().min(1), text: z14.string().trim().min(1) });
802
- var ProjectBlockSchema = z14.object({
803
- name: z14.string().trim().min(1).max(80),
804
- description: z14.string().max(500).optional(),
805
- repos: z14.array(z14.object({ path: z14.string().min(1).max(4096), why: z14.string().trim().min(1).max(200) })).min(1).max(50),
806
- addTo: z14.string().trim().max(80).optional()
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
  };