@paradow/ledge 0.5.4 → 0.6.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.
@@ -0,0 +1,698 @@
1
+ // ../protocol/src/messages.ts
2
+ import { z as z13 } from "zod";
3
+
4
+ // ../protocol/src/session.ts
5
+ import { z } from "zod";
6
+ var ToolSchema = z.enum(["claude", "codex", "cursor"]);
7
+ var SessionStateSchema = z.enum(["starting", "working", "waiting_input", "waiting_permission", "idle", "exited", "error"]);
8
+ var PendingSchema = z.object({
9
+ kind: z.enum(["question", "permission", "plan", "diff"]),
10
+ text: z.string(),
11
+ /** What to review in full before deciding, when `text` is only its summary: a plan's markdown. */
12
+ body: z.string().optional(),
13
+ tool: z.string().optional(),
14
+ options: z.array(z.string()).optional(),
15
+ /** Each option's description, by index ('' when it has none); left out when no option has one. */
16
+ optionDetails: z.array(z.string()).optional(),
17
+ source: z.enum(["hook", "pane"]),
18
+ /** Set when the request comes from a helper (subagent): it is routed through its parent. */
19
+ helperId: z.string().optional(),
20
+ /** For a helper-routed request: the state the parent returns to once it is answered (main thread may stop meanwhile). */
21
+ parentState: SessionStateSchema.optional()
22
+ });
23
+ var ReplySchema = z.object({
24
+ text: z.string(),
25
+ at: z.string().datetime(),
26
+ /** The reply is a plan the agent proposes (Codex Plan mode): nothing in it is done yet. */
27
+ plan: z.boolean().optional()
28
+ });
29
+ var HelperSchema = z.object({
30
+ id: z.string(),
31
+ kind: z.string(),
32
+ task: z.string(),
33
+ state: SessionStateSchema
34
+ });
35
+ var TopicSourceSchema = z.enum(["user", "pane", "prompt"]);
36
+ var SessionSchema = z.object({
37
+ /** Origin is annotated by the overlay, never used as an unqualified session id on the wire. */
38
+ machineId: z.string().optional(),
39
+ machineName: z.string().optional(),
40
+ repoId: z.string().optional(),
41
+ /** Annotated by the fleet when its machine is unreachable and the last snapshot is kept (keepStale, spec 2026-10-14 §6). */
42
+ offline: z.boolean().optional(),
43
+ id: z.string().min(1),
44
+ name: z.string().min(1),
45
+ tool: ToolSchema,
46
+ cwd: z.string(),
47
+ project: z.string(),
48
+ title: z.string(),
49
+ /** One line saying what the session is about (spec 2026-10-11 §3). */
50
+ topic: z.string().optional(),
51
+ topicSource: TopicSourceSchema.optional(),
52
+ /** The repository (its main worktree's folder name and absolute path), the checked-out branch, and whether `cwd` is a linked worktree (§4). */
53
+ repo: z.string().optional(),
54
+ repoPath: z.string().optional(),
55
+ branch: z.string().optional(),
56
+ worktree: z.boolean().optional(),
57
+ tmuxSession: z.string().nullable(),
58
+ state: SessionStateSchema,
59
+ pending: PendingSchema.optional(),
60
+ helpers: z.array(HelperSchema).default([]),
61
+ lastReply: ReplySchema.optional(),
62
+ startedAt: z.string().datetime(),
63
+ lastActivityAt: z.string().datetime(),
64
+ lastHookAt: z.string().datetime().optional(),
65
+ /** The agent's own session id when Ledge chose it at launch (Claude Code `--session-id`). */
66
+ agentSessionId: z.string().optional(),
67
+ /** The agent's transcript: the latest one a hook named (it follows /clear and --resume), else the one the launch implies. */
68
+ transcriptPath: z.string().optional(),
69
+ exitCode: z.number().int().nullable().optional(),
70
+ exitedAt: z.string().datetime().optional(),
71
+ lastError: z.string().optional(),
72
+ source: z.enum(["launcher", "hook_only"])
73
+ });
74
+
75
+ // ../protocol/src/chat.ts
76
+ import { z as z2 } from "zod";
77
+ var ChatStepSchema = z2.object({
78
+ kind: z2.enum(["sessions", "pane", "reply", "git", "repo"]),
79
+ id: z2.string().optional(),
80
+ name: z2.string().optional(),
81
+ path: z2.string().optional()
82
+ });
83
+ var ChatProposalSchema = z2.object({
84
+ cwd: z2.string().min(1),
85
+ project: z2.string(),
86
+ prompt: z2.string().optional(),
87
+ tool: ToolSchema,
88
+ launchedId: z2.string().optional(),
89
+ error: z2.string().optional()
90
+ });
91
+ var ChatRelaySchema = z2.object({
92
+ id: z2.string().min(1),
93
+ name: z2.string(),
94
+ tool: ToolSchema,
95
+ text: z2.string().min(1),
96
+ status: z2.enum(["queued", "sent"]).optional(),
97
+ sentAt: z2.string().datetime().optional(),
98
+ error: z2.string().optional()
99
+ });
100
+ var ChatProjectProposalSchema = z2.object({
101
+ name: z2.string().min(1).max(80),
102
+ description: z2.string().max(500).optional(),
103
+ repos: z2.array(z2.object({ path: z2.string().min(1).max(4096), why: z2.string().min(1).max(200) })).min(1).max(50),
104
+ addTo: z2.string().min(1).max(80).optional()
105
+ });
106
+ var ChatTurnSchema = z2.object({
107
+ machineId: z2.string().optional(),
108
+ machineName: z2.string().optional(),
109
+ choices: z2.array(z2.object({ id: z2.string(), label: z2.string() })).optional(),
110
+ qid: z2.string().min(1),
111
+ question: z2.string(),
112
+ askedAt: z2.string().datetime(),
113
+ steps: z2.array(ChatStepSchema),
114
+ answer: z2.string().optional(),
115
+ answeredAt: z2.string().datetime().optional(),
116
+ error: z2.string().optional(),
117
+ proposal: ChatProposalSchema.optional(),
118
+ relay: ChatRelaySchema.optional(),
119
+ projectProposal: ChatProjectProposalSchema.optional()
120
+ });
121
+
122
+ // ../protocol/src/repos.ts
123
+ import { z as z3 } from "zod";
124
+ var ReposInspectSchema = z3.object({
125
+ type: z3.literal("repos.inspect"),
126
+ requestId: z3.string().min(1).max(100),
127
+ paths: z3.array(z3.string().min(1)).min(1).max(50),
128
+ machineId: z3.string().optional()
129
+ });
130
+ var RepoInfoSchema = z3.object({ path: z3.string(), exists: z3.boolean(), branch: z3.string().nullable() });
131
+ var ReposInfoSchema = z3.object({ type: z3.literal("repos.info"), requestId: z3.string(), repos: z3.array(RepoInfoSchema) });
132
+
133
+ // ../protocol/src/scan.ts
134
+ import { z as z4 } from "zod";
135
+ var ReposScanSchema = z4.object({ type: z4.literal("repos.scan"), requestId: z4.string().min(1).max(100), machineId: z4.string().optional() });
136
+ var FoundRepoSchema = z4.object({ path: z4.string(), branch: z4.string().nullable(), remote: z4.string().optional() });
137
+ 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() });
138
+
139
+ // ../protocol/src/documents.ts
140
+ import { z as z5 } from "zod";
141
+ var DOCUMENT_MAX_BYTES = 1048576;
142
+ var path = z5.string().min(1).max(4096);
143
+ var requestId = z5.string().min(1);
144
+ var DocumentReadSchema = z5.object({
145
+ type: z5.literal("document.read"),
146
+ requestId,
147
+ path,
148
+ machineId: z5.string().optional(),
149
+ sessionId: z5.string().min(1).optional(),
150
+ basePath: path.optional()
151
+ });
152
+ var DocumentContentSchema = z5.object({
153
+ type: z5.literal("document.content"),
154
+ requestId,
155
+ path,
156
+ text: z5.string(),
157
+ bytes: z5.number().int().nonnegative(),
158
+ modifiedAt: z5.string().datetime()
159
+ });
160
+ var DocumentErrorCodeSchema = z5.enum([
161
+ "not_found",
162
+ "not_file",
163
+ "unsupported_path",
164
+ "unsupported_type",
165
+ "permission_denied",
166
+ "too_large",
167
+ "invalid_utf8",
168
+ "ambiguous_path",
169
+ "session_missing",
170
+ "read_failed"
171
+ ]);
172
+ var DocumentErrorSchema = z5.object({
173
+ type: z5.literal("document.error"),
174
+ requestId,
175
+ path,
176
+ code: DocumentErrorCodeSchema
177
+ });
178
+
179
+ // ../protocol/src/routing.ts
180
+ import { z as z6 } from "zod";
181
+ var MachineInventorySchema = z6.object({
182
+ id: z6.string().min(1).max(80),
183
+ label: z6.string().min(1).max(120),
184
+ online: z6.boolean(),
185
+ local: z6.boolean(),
186
+ 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),
187
+ folders: z6.array(z6.object({ project: z6.string(), cwd: z6.string() })).max(100)
188
+ });
189
+ var RoutingContextSchema = z6.object({
190
+ machines: z6.array(MachineInventorySchema).min(1).max(20),
191
+ history: z6.array(z6.object({ question: z6.string().max(2e4), answer: z6.string().max(2e4).optional(), machineId: z6.string().optional() })).max(50)
192
+ });
193
+ var RouteResultSchema = z6.object({
194
+ /** Null means an answer or a clarification, never an implicit local destination. */
195
+ machineId: z6.string().nullable(),
196
+ text: z6.string().min(1).max(2e4),
197
+ choices: z6.array(z6.string()).max(20).optional()
198
+ });
199
+
200
+ // ../protocol/src/ssh.ts
201
+ import { z as z7 } from "zod";
202
+ var SshAliasSchema = z7.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/);
203
+
204
+ // ../protocol/src/machines.ts
205
+ import { z as z8 } from "zod";
206
+ var MachineIdSchema = z8.string().regex(/^[a-zA-Z0-9_-]{1,64}$/);
207
+ var MachineSchema = z8.object({
208
+ id: MachineIdSchema,
209
+ label: z8.string().trim().min(1).max(80),
210
+ ssh: SshAliasSchema.optional()
211
+ });
212
+ var MachinesSchema = z8.array(MachineSchema).min(1).max(20).superRefine((list, ctx) => {
213
+ if (list[0]?.id !== "local" || list[0].ssh !== void 0) ctx.addIssue({ code: "custom", message: "the first machine is local, without ssh", path: [0] });
214
+ const ids = /* @__PURE__ */ new Set(), aliases = /* @__PURE__ */ new Set();
215
+ list.forEach((m, i) => {
216
+ if (i > 0 && !m.ssh) ctx.addIssue({ code: "custom", message: "a remote machine needs an ssh alias", path: [i, "ssh"] });
217
+ if (ids.has(m.id)) ctx.addIssue({ code: "custom", message: `duplicate id ${m.id}`, path: [i, "id"] });
218
+ ids.add(m.id);
219
+ if (m.ssh) {
220
+ if (aliases.has(m.ssh)) ctx.addIssue({ code: "custom", message: `duplicate ssh alias ${m.ssh}`, path: [i, "ssh"] });
221
+ aliases.add(m.ssh);
222
+ }
223
+ });
224
+ });
225
+
226
+ // ../protocol/src/island.ts
227
+ import { z as z9 } from "zod";
228
+ var IslandScaleSchema = z9.union([z9.literal("auto"), z9.literal(1), z9.literal(1.25), z9.literal(1.5), z9.literal(2)]);
229
+ var IslandPrefsSchema = z9.object({
230
+ /** Shown at the top of the screen; hidden, Desktop still notifies. */
231
+ visible: z9.boolean(),
232
+ /** The primary screen, or the first other one. */
233
+ screen: z9.enum(["primary", "second"]),
234
+ position: z9.enum(["center", "left", "right"]),
235
+ /** A strip under a maximised window. */
236
+ discreet: z9.boolean(),
237
+ islandScale: IslandScaleSchema,
238
+ panelScale: IslandScaleSchema,
239
+ /** What the island shows besides asks and questions, which it always shows. */
240
+ shows: z9.object({
241
+ /** Allow, deny or answer in a line from the island; off, its cards only say « Open in Ledge ». */
242
+ quick: z9.boolean(),
243
+ /** The 6 s bubble when an agent finishes. */
244
+ finished: z9.boolean(),
245
+ /** An agent's sentence in a bubble. */
246
+ say: z9.boolean(),
247
+ /** The sub-agents next to their main agent. */
248
+ helpers: z9.boolean()
249
+ }),
250
+ /** The global shortcut that opens the island, as shown (`Ctrl Alt L`). */
251
+ shortcut: z9.string().trim().min(1).max(40)
252
+ });
253
+
254
+ // ../protocol/src/projects.ts
255
+ import { z as z10 } from "zod";
256
+ var ProjectColorSchema = z10.enum(["blue", "green", "amber", "violet", "teal", "pink", "red", "slate"]);
257
+ var ProjectIdSchema = z10.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/);
258
+ function normalizePath(raw) {
259
+ const p = raw.trim().replace(/\/+/g, "/");
260
+ return p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
261
+ }
262
+ var PathSchema = z10.string().max(4096).refine((p) => p.startsWith("/") && p === normalizePath(p), "an absolute, normalized path");
263
+ var RepoSchema = z10.object({ machine: MachineIdSchema, path: PathSchema });
264
+ var ProjectSchema = z10.object({
265
+ id: ProjectIdSchema,
266
+ name: z10.string().trim().min(1).max(80),
267
+ color: ProjectColorSchema,
268
+ description: z10.string().max(500).default(""),
269
+ repos: z10.array(RepoSchema).max(50)
270
+ });
271
+ var RuleSchema = z10.object({ machine: MachineIdSchema, path: PathSchema, project: ProjectIdSchema });
272
+ function projectsProblems(p) {
273
+ const out = [];
274
+ const ids = /* @__PURE__ */ new Set(), names = /* @__PURE__ */ new Set(), repos = /* @__PURE__ */ new Set(), rules = /* @__PURE__ */ new Set();
275
+ p.projects.forEach((project, i) => {
276
+ if (ids.has(project.id)) out.push({ code: "duplicate_id", path: ["projects", i, "id"] });
277
+ ids.add(project.id);
278
+ const name = project.name.trim().toLocaleLowerCase();
279
+ if (names.has(name)) out.push({ code: "duplicate_name", path: ["projects", i, "name"] });
280
+ names.add(name);
281
+ project.repos.forEach((repo, j) => {
282
+ const key = `${repo.machine}
283
+ ${repo.path}`;
284
+ if (repos.has(key)) out.push({ code: "duplicate_repo", path: ["projects", i, "repos", j] });
285
+ repos.add(key);
286
+ });
287
+ });
288
+ p.rules.forEach((rule, i) => {
289
+ const key = `${rule.machine}
290
+ ${rule.path}`;
291
+ if (rules.has(key)) out.push({ code: "duplicate_rule", path: ["rules", i] });
292
+ rules.add(key);
293
+ if (!ids.has(rule.project)) out.push({ code: "unknown_project", path: ["rules", i, "project"] });
294
+ });
295
+ return out;
296
+ }
297
+ var ProjectsSchema = z10.object({ projects: z10.array(ProjectSchema).max(100), rules: z10.array(RuleSchema).max(1e3) }).superRefine((p, ctx) => {
298
+ for (const problem of projectsProblems(p)) ctx.addIssue({ code: "custom", message: problem.code, path: problem.path });
299
+ });
300
+ var DEFAULT_PROJECTS = { projects: [], rules: [] };
301
+ var under = (cwd, path2) => path2 === "/" || cwd === path2 || cwd.startsWith(`${path2}/`);
302
+ function projectOf(session, state) {
303
+ const machine = session.machineId ?? "local";
304
+ const cwd = normalizePath(session.cwd);
305
+ const known = new Set(state.projects.map((p) => p.id));
306
+ const candidates = [
307
+ ...state.projects.flatMap((p) => p.repos.filter((r) => r.machine === machine).map((r) => ({ project: p.id, path: r.path, rule: false }))),
308
+ ...state.rules.filter((r) => r.machine === machine && known.has(r.project)).map((r) => ({ project: r.project, path: r.path, rule: true }))
309
+ ].filter((c) => under(cwd, c.path));
310
+ candidates.sort((x, y) => y.path.length - x.path.length || Number(y.rule) - Number(x.rule));
311
+ return candidates[0]?.project ?? null;
312
+ }
313
+
314
+ // ../protocol/src/thread.ts
315
+ import { z as z11 } from "zod";
316
+ var ToolStatusSchema = z11.enum(["running", "success", "failed", "unknown"]);
317
+ 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() });
318
+ var base = { id: z11.string().min(1), at: z11.string().datetime(), turn: z11.number().int().nonnegative(), helperId: z11.string().optional() };
319
+ var ThreadItemSchema = z11.discriminatedUnion("kind", [
320
+ z11.object({ kind: z11.literal("prompt"), ...base, text: z11.string() }),
321
+ z11.object({ kind: z11.literal("message"), ...base, text: z11.string() }),
322
+ /** `id` is the harness's tool_use_id when it gives one, so the result updates the same row. */
323
+ 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([]) }),
324
+ /** `unknown`: answered in the terminal, the daemon only saw the prompt go away; `always`: allowed and not asked again. */
325
+ z11.object({ kind: z11.literal("permission"), ...base, tool: z11.string(), text: z11.string(), decision: z11.enum(["accept", "deny", "always", "unknown"]).optional() }),
326
+ z11.object({ kind: z11.literal("reply"), ...base, text: z11.string() }),
327
+ z11.object({ kind: z11.literal("helper"), ...base, task: z11.string(), agentType: z11.string().optional(), state: z11.enum(["working", "exited"]) })
328
+ ]);
329
+
330
+ // ../protocol/src/decisions.ts
331
+ import { z as z12 } from "zod";
332
+ var DecisionSchema = z12.enum(["accept", "deny", "always"]);
333
+ var NOTE_MAX = 4e3;
334
+ var NOTE_NOT_TYPED = "note_not_typed";
335
+ var DECISION_OFFERS = {
336
+ claude: { always: ["permission", "diff", "plan"], note: ["permission", "diff", "plan"] },
337
+ codex: { always: ["permission", "diff"], note: ["permission", "diff", "plan"] },
338
+ cursor: { always: [], note: ["permission", "plan"] }
339
+ };
340
+ function offers(tool, kind, what) {
341
+ return DECISION_OFFERS[tool][what].includes(kind);
342
+ }
343
+ function cyrb53(str, seed) {
344
+ let h1 = 3735928559 ^ seed, h2 = 1103547991 ^ seed;
345
+ for (let i = 0; i < str.length; i++) {
346
+ const ch = str.charCodeAt(i);
347
+ h1 = Math.imul(h1 ^ ch, 2654435761);
348
+ h2 = Math.imul(h2 ^ ch, 1597334677);
349
+ }
350
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
351
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
352
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16).padStart(14, "0");
353
+ }
354
+ function pendingKey(p) {
355
+ const s = JSON.stringify([p.kind, p.text, p.body ?? null, p.tool ?? null, p.options ?? null, p.optionDetails ?? null, p.helperId ?? null]);
356
+ return cyrb53(s, 0) + cyrb53(s, 1);
357
+ }
358
+
359
+ // ../protocol/src/messages.ts
360
+ var PROTOCOL_PORT = 47831;
361
+ var SessionEventSchema = z13.object({
362
+ name: z13.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd", "PaneChanged", "PaneGone", "PanePrompt", "PaneIdle", "InjectFailed", "Decided", "PostToolUse", "PostToolUseFailure", "StopFailure", "SubagentStart", "SubagentStop", "PaneTitle", "Renamed"]),
363
+ at: z13.string().datetime(),
364
+ source: z13.enum(["hook", "pane", "daemon"]),
365
+ tool: z13.string().optional(),
366
+ text: z13.string().optional(),
367
+ options: z13.array(z13.string()).optional(),
368
+ /** Each option's description, by index ('' when it has none); left out when no option has one. */
369
+ optionDetails: z13.array(z13.string()).optional(),
370
+ kind: z13.enum(["question", "permission", "plan", "diff"]).optional(),
371
+ /** PanePrompt: what to review in full before deciding (a plan read in the terminal), when `text` is its summary. */
372
+ body: z13.string().optional(),
373
+ exitCode: z13.number().int().nullable().optional(),
374
+ helperId: z13.string().optional(),
375
+ /** The helper's description, once known (Claude Code: its `.meta.json`, written after SubagentStart). */
376
+ task: z13.string().optional(),
377
+ /** PostToolUseFailure: the user interrupted the turn (Esc); Claude then waits for input with no Stop. */
378
+ interrupt: z13.boolean().optional(),
379
+ /** Native tracking (spec 2026-10-13 §2): the harness's id of the tool call, so its result lands on the same row. */
380
+ toolUseId: z13.string().optional(),
381
+ /** The tool's input as the harness gave it, every string capped by the hook. */
382
+ toolInput: z13.record(z13.string(), z13.unknown()).optional(),
383
+ /** PostToolUse: the result as text; SubagentStop: nothing (the report travels in `text`). */
384
+ output: z13.string().optional(),
385
+ durationMs: z13.number().nonnegative().optional(),
386
+ /** PostToolUseFailure: the harness's error message. */
387
+ error: z13.string().optional(),
388
+ /** The session's transcript, or the helper's on SubagentStop; the daemon reads its tail for the agent's messages. */
389
+ transcriptPath: z13.string().optional(),
390
+ /** Decided: what was chosen when the daemon knows it. */
391
+ decision: DecisionSchema.optional()
392
+ });
393
+ var RecentSchema = z13.object({ cwd: z13.string().min(1), project: z13.string().min(1), lastAt: z13.string().datetime() });
394
+ var SEMVER = /^\d+\.\d+\.\d+$/;
395
+ var DaemonInfoSchema = z13.object({ version: z13.string(), install: z13.enum(["npm", "git", "unknown"]), systemd: z13.boolean(), manual: z13.string().optional(), routing: z13.boolean().optional() });
396
+ var UpdateFailureSchema = z13.enum(["git-install", "no-systemd", "sessions-at-risk", "npm-failed", "busy"]);
397
+ var ServerMessageSchema = z13.discriminatedUnion("type", [
398
+ z13.object({ type: z13.literal("chat.routed"), qid: z13.string(), result: RouteResultSchema }),
399
+ DocumentContentSchema,
400
+ DocumentErrorSchema,
401
+ ReposInfoSchema,
402
+ ReposFoundSchema,
403
+ /** `recents`: the folders of past sessions, most recent first. `launcher`: the command that runs `ledge` inside WSL
404
+ * (`[node, cli]`), so the overlay can attach a terminal to a session with `<launcher> attach <id>`. `daemon`: absent
405
+ * from daemons older than the auto-update. */
406
+ 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() }),
407
+ z13.object({ type: z13.literal("session.updated"), session: SessionSchema }),
408
+ z13.object({ type: z13.literal("session.removed"), id: z13.string() }),
409
+ z13.object({ type: z13.literal("session.preview"), id: z13.string(), lines: z13.array(z13.string()) }),
410
+ /** A session's thread, on request (spec 2026-10-13 §5); then each created or updated item, broadcast. */
411
+ z13.object({ type: z13.literal("session.thread"), id: z13.string(), items: z13.array(ThreadItemSchema) }),
412
+ z13.object({ type: z13.literal("session.thread.item"), id: z13.string(), item: ThreadItemSchema }),
413
+ /** Answer to the requester of a `session.launch`: the session exists (its `session.updated` is broadcast as usual). */
414
+ z13.object({ type: z13.literal("session.launched"), id: z13.string() }),
415
+ z13.object({ type: z13.literal("recents"), recents: z13.array(RecentSchema) }),
416
+ /** Answer to the requester of a `folder.list` or `folder.create`: `path` resolved (`~` expanded), `parent` null at the
417
+ * root, `entries` its subfolders by name (hidden ones left out). A failure keeps the asked path and says why in `error`,
418
+ * never in an `error` message, which the launch form reads as a failed launch. */
419
+ z13.object({
420
+ type: z13.literal("folder.listing"),
421
+ path: z13.string(),
422
+ parent: z13.string().nullable(),
423
+ entries: z13.array(z13.string()),
424
+ error: z13.string().optional(),
425
+ /** The request this answers, when it carried one. */
426
+ requestId: z13.string().optional(),
427
+ /** Those `entries` that are Git repositories, with their branch (the short commit when HEAD is detached). */
428
+ repos: z13.array(z13.object({ name: z13.string(), branch: z13.string() })).optional()
429
+ }),
430
+ /** 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. */
431
+ z13.object({ type: z13.literal("chat.step"), qid: z13.string(), step: ChatStepSchema }),
432
+ z13.object({ type: z13.literal("chat.delta"), qid: z13.string(), text: z13.string() }),
433
+ z13.object({ type: z13.literal("chat.done"), qid: z13.string(), text: z13.string(), at: z13.string().datetime(), proposal: ChatProposalSchema.optional(), relay: ChatRelaySchema.optional(), projectProposal: ChatProjectProposalSchema.optional() }),
434
+ z13.object({ type: z13.literal("chat.error"), qid: z13.string(), message: z13.string() }),
435
+ z13.object({ type: z13.literal("chat.history"), turns: z13.array(ChatTurnSchema) }),
436
+ /** The daemon updating itself: npm is running, then the service restarts (the socket closes right after). */
437
+ z13.object({ type: z13.literal("update.progress"), version: z13.string(), step: z13.enum(["installing", "restarting"]) }),
438
+ /** Nothing was changed. `detail`: the useful end of npm's output; `command`: what the person can run instead. */
439
+ z13.object({ type: z13.literal("update.failed"), version: z13.string(), reason: UpdateFailureSchema, detail: z13.string().optional(), command: z13.string().optional() }),
440
+ /** The shared machines list (spec 2026-10-14 §5), sent to local clients only. */
441
+ z13.object({ type: z13.literal("machines"), machines: MachinesSchema }),
442
+ /** The local daemon's projects and folder rules (spec 2026-10-16 build §5), sent to local clients only. */
443
+ z13.object({ type: z13.literal("projects"), projects: ProjectsSchema }),
444
+ /** The island's settings shared by the local fronts; null until a front sets them (the island then sends its own). Local clients only. */
445
+ z13.object({ type: z13.literal("island"), prefs: IslandPrefsSchema.nullable() }),
446
+ z13.object({
447
+ type: z13.literal("error"),
448
+ message: z13.string(),
449
+ id: z13.string().optional(),
450
+ /** Le type du message client refusé. */
451
+ request: z13.string().optional(),
452
+ /** Ce que le front doit reconnaître au-delà du texte (NOTE_NOT_TYPED) ; une chaîne libre, qu'un front plus ancien ignore. */
453
+ code: z13.string().optional()
454
+ })
455
+ ]);
456
+ var ClientMessageSchema = z13.discriminatedUnion("type", [
457
+ 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 }),
458
+ DocumentReadSchema,
459
+ ReposInspectSchema,
460
+ ReposScanSchema,
461
+ /** The overlay starts a session: `cwd` is a WSL path (`~` allowed), `name` a preferred bot name (the daemon picks a
462
+ * free one otherwise), `prompt` the agent's first instruction, `continue` resumes the folder's last conversation.
463
+ * With `qid` it is the chat card of that turn: the daemon launches the turn's proposal and ignores the other fields. */
464
+ z13.object({
465
+ type: z13.literal("session.launch"),
466
+ tool: ToolSchema,
467
+ cwd: z13.string().min(1),
468
+ name: z13.string().optional(),
469
+ prompt: z13.string().optional(),
470
+ continue: z13.boolean().optional(),
471
+ qid: z13.string().min(1).optional(),
472
+ /** Folders in addition to `cwd` (Claude: `--add-dir`); resolved like `cwd`. */
473
+ dirs: z13.array(z13.string().min(1)).max(20).optional(),
474
+ /** Routing by the fleet, removed before the message reaches the daemon. */
475
+ machineId: z13.string().optional()
476
+ }),
477
+ /** The launch form's folder browser: list a folder's subfolders (home when no `path`), or make `name` inside `parent`
478
+ * and list it. Both are answered with a `folder.listing`. */
479
+ z13.object({
480
+ type: z13.literal("folder.list"),
481
+ path: z13.string().optional(),
482
+ /** Routing by the fleet, removed before the message reaches the daemon. */
483
+ machineId: z13.string().optional(),
484
+ /** Copied into the `folder.listing` that answers; the fleet relays only that listing, from the machine asked. */
485
+ requestId: z13.string().min(1).max(100).optional()
486
+ }),
487
+ z13.object({ type: z13.literal("folder.create"), parent: z13.string().min(1), name: z13.string().min(1) }),
488
+ z13.object({ type: z13.literal("session.reply"), id: z13.string(), text: z13.string().min(1) }),
489
+ /** The chat card's instruction (spec 2026-10-12 relay §4): sent now if the session is free, else queued until it is. */
490
+ z13.object({ type: z13.literal("session.relay"), qid: z13.string().min(1) }),
491
+ /** Takes the card's queued instruction back out of the queue. */
492
+ z13.object({ type: z13.literal("session.relay.cancel"), qid: z13.string().min(1) }),
493
+ /** `always` and `note` only where the harness offers them (`offers`); a note goes with `deny`, for the agent to read. */
494
+ z13.object({
495
+ type: z13.literal("session.decide"),
496
+ id: z13.string(),
497
+ decision: DecisionSchema,
498
+ note: z13.string().trim().min(1).max(NOTE_MAX).optional(),
499
+ /** 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.
500
+ * `key` (pendingKey) covers its whole content; older fronts send kind and text only. */
501
+ expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z13.string().max(64).optional() }).optional()
502
+ }),
503
+ z13.object({ type: z13.literal("session.focus"), id: z13.string() }),
504
+ z13.object({ type: z13.literal("session.kill"), id: z13.string() }),
505
+ z13.object({ type: z13.literal("session.interrupt"), id: z13.string() }),
506
+ /** Removes a finished (exited) or read-only (hook_only) session from the list; refused for a live launcher session. */
507
+ z13.object({ type: z13.literal("session.dismiss"), id: z13.string() }),
508
+ /** 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. */
509
+ z13.object({ type: z13.literal("session.rename"), id: z13.string(), topic: z13.string().max(500) }),
510
+ z13.object({ type: z13.literal("session.thread"), id: z13.string() }),
511
+ z13.object({ type: z13.literal("session.register"), session: SessionSchema }),
512
+ /** `agent` and `cwd` describe the session when the daemon does not know it yet (a hook from an agent started without ledge). */
513
+ z13.object({ type: z13.literal("session.event"), id: z13.string(), event: SessionEventSchema, agent: ToolSchema.optional(), cwd: z13.string().optional() }),
514
+ z13.object({ type: z13.literal("session.unregister"), id: z13.string(), exitCode: z13.number().int().nullable() }),
515
+ z13.object({ type: z13.literal("snapshot.request") }),
516
+ z13.object({ type: z13.literal("chat.ask"), qid: z13.string().min(1), text: z13.string().min(1), tool: ToolSchema.optional() }),
517
+ z13.object({ type: z13.literal("chat.cancel"), qid: z13.string().min(1) }),
518
+ /** Starts a new conversation with Ledge: the day's turns are cleared and the next question opens a new harness session. */
519
+ z13.object({ type: z13.literal("chat.reset") }),
520
+ /** Installs `@paradow/ledge@version` and restarts the service (spec 2026-10-12 auto-update §6). */
521
+ z13.object({ type: z13.literal("update.daemon"), version: z13.string().regex(SEMVER) }),
522
+ /** Replaces the daemon's machines list (first connected front wins); refused unless valid. */
523
+ z13.object({ type: z13.literal("machines.set"), machines: MachinesSchema }),
524
+ /** Replaces the local daemon's projects and rules as a whole; refused unless valid. */
525
+ z13.object({ type: z13.literal("projects.set"), projects: ProjectsSchema }),
526
+ /** Replaces the island's settings as a whole; refused unless valid. */
527
+ z13.object({ type: z13.literal("island.set"), prefs: IslandPrefsSchema })
528
+ ]);
529
+ function parseClientMessage(raw) {
530
+ return ClientMessageSchema.parse(raw);
531
+ }
532
+ function parseServerMessage(raw) {
533
+ return ServerMessageSchema.parse(raw);
534
+ }
535
+
536
+ // ../protocol/src/names.ts
537
+ var NAMES = [
538
+ "Otto",
539
+ "Pim",
540
+ "Nova",
541
+ "Bix",
542
+ "Lu",
543
+ "Max",
544
+ "Ida",
545
+ "Rex",
546
+ "Zoe",
547
+ "Kit",
548
+ "Ari",
549
+ "Bo",
550
+ "Cal",
551
+ "Dee",
552
+ "Eli",
553
+ "Fay",
554
+ "Gus",
555
+ "Hal",
556
+ "Ivy",
557
+ "Jax",
558
+ "Kai",
559
+ "Lio",
560
+ "Mo",
561
+ "Nia",
562
+ "Oz",
563
+ "Pax",
564
+ "Quin",
565
+ "Rae",
566
+ "Sol",
567
+ "Tad",
568
+ "Uma",
569
+ "Vic",
570
+ "Wyn",
571
+ "Xen",
572
+ "Yan",
573
+ "Zia",
574
+ "Abe",
575
+ "Bea",
576
+ "Cy",
577
+ "Dot",
578
+ "Eva",
579
+ "Fin",
580
+ "Gil",
581
+ "Hen",
582
+ "Iro",
583
+ "Jo",
584
+ "Kip",
585
+ "Lex",
586
+ "Mia",
587
+ "Ned",
588
+ "Ola",
589
+ "Pat",
590
+ "Rio",
591
+ "Sam",
592
+ "Tia",
593
+ "Ulf",
594
+ "Val",
595
+ "Wes",
596
+ "Yui",
597
+ "Zed",
598
+ "Ash",
599
+ "Bry",
600
+ "Cleo",
601
+ "Dax"
602
+ ];
603
+ var cap = (s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
604
+ function pickName(taken, preferred) {
605
+ const used = new Set(taken);
606
+ const free = (n) => !used.has(n);
607
+ const wanted = preferred?.trim();
608
+ if (wanted) {
609
+ const base2 = cap(wanted);
610
+ if (free(base2)) return base2;
611
+ for (let i = 2; ; i++) if (free(`${base2}-${i}`)) return `${base2}-${i}`;
612
+ }
613
+ const first = NAMES.find(free);
614
+ if (first) return first;
615
+ for (let i = 2; ; i++) if (free(`${NAMES[0]}-${i}`)) return `${NAMES[0]}-${i}`;
616
+ }
617
+
618
+ // ../protocol/src/proposal.ts
619
+ import { z as z14 } from "zod";
620
+ var ProposalBlockSchema = z14.object({ cwd: z14.string().trim().min(1), prompt: z14.string().optional() });
621
+ var RelayBlockSchema = z14.object({ session: z14.string().trim().min(1), text: z14.string().trim().min(1) });
622
+ var ProjectBlockSchema = z14.object({
623
+ name: z14.string().trim().min(1).max(80),
624
+ description: z14.string().max(500).optional(),
625
+ repos: z14.array(z14.object({ path: z14.string().min(1).max(4096), why: z14.string().trim().min(1).max(200) })).min(1).max(50),
626
+ addTo: z14.string().trim().max(80).optional()
627
+ });
628
+ var BLOCK = /```ledge-(session|relay|project)[ \t]*\n?([\s\S]*?)(?:```|$)/g;
629
+ function extractProposal(answer) {
630
+ const found = [];
631
+ const text = answer.replace(BLOCK, (_m, kind, body) => {
632
+ found.push({ kind, raw: body.trim() });
633
+ return "";
634
+ });
635
+ const last = found.at(-1);
636
+ return last ? { text: text.replace(/\n{3,}/g, "\n\n").trim(), kind: last.kind, raw: last.raw } : { text: answer, kind: null, raw: null };
637
+ }
638
+ function parseProposalBlock(raw) {
639
+ try {
640
+ const r = ProposalBlockSchema.safeParse(JSON.parse(raw));
641
+ return r.success ? r.data : null;
642
+ } catch {
643
+ return null;
644
+ }
645
+ }
646
+ function parseRelayBlock(raw) {
647
+ try {
648
+ const r = RelayBlockSchema.safeParse(JSON.parse(raw));
649
+ return r.success ? r.data : null;
650
+ } catch {
651
+ return null;
652
+ }
653
+ }
654
+ function parseProjectBlock(raw) {
655
+ try {
656
+ const r = ProjectBlockSchema.safeParse(JSON.parse(raw));
657
+ return r.success ? r.data : null;
658
+ } catch {
659
+ return null;
660
+ }
661
+ }
662
+
663
+ // ../protocol/src/topic.ts
664
+ var TOPIC_MAX = 80;
665
+ function cleanTopic(text) {
666
+ const line = text.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0) ?? "";
667
+ const flat = line.replace(/\s+/g, " ");
668
+ return flat.length > TOPIC_MAX ? `${flat.slice(0, TOPIC_MAX - 1).trimEnd()}\u2026` : flat;
669
+ }
670
+ function sessionTitle(s) {
671
+ return `${s.name} \xB7 ${s.topic || s.project}`;
672
+ }
673
+
674
+ export {
675
+ SessionSchema,
676
+ NOTE_NOT_TYPED,
677
+ offers,
678
+ pendingKey,
679
+ DOCUMENT_MAX_BYTES,
680
+ SshAliasSchema,
681
+ MachinesSchema,
682
+ IslandPrefsSchema,
683
+ ProjectsSchema,
684
+ DEFAULT_PROJECTS,
685
+ projectOf,
686
+ RouteResultSchema,
687
+ PROTOCOL_PORT,
688
+ RecentSchema,
689
+ parseClientMessage,
690
+ parseServerMessage,
691
+ pickName,
692
+ extractProposal,
693
+ parseProposalBlock,
694
+ parseRelayBlock,
695
+ parseProjectBlock,
696
+ cleanTopic,
697
+ sessionTitle
698
+ };