@paradow/ledge 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/cli/action-WNQ3NJGD.js +27 -0
  2. package/dist/cli/{attach-IVICSJYB.js → attach-UHM7QQ4W.js} +9 -8
  3. package/dist/cli/{bridge-H3ZPGIUY.js → bridge-R37AHVXS.js} +2 -1
  4. package/dist/cli/{chunk-ZQON4EV7.js → chunk-5VFFJ3ZC.js} +2 -2
  5. package/dist/cli/{chunk-ALEZJFJ3.js → chunk-7XMGYJTN.js} +7 -7
  6. package/dist/cli/chunk-ARB4BKZJ.js +2302 -0
  7. package/dist/cli/{chunk-24AHC55Y.js → chunk-AVKFQSRP.js} +7 -21
  8. package/dist/cli/{chunk-2VSEIOGA.js → chunk-JNLARCUC.js} +7 -4
  9. package/dist/cli/{chunk-EKXZ45MQ.js → chunk-KRFXWD5S.js} +821 -201
  10. package/dist/cli/chunk-O6FTIYJV.js +85 -0
  11. package/dist/cli/chunk-ONFWK2WW.js +6 -0
  12. package/dist/cli/{chunk-EBBAOQ2Z.js → chunk-PUJLMKZC.js} +3 -3
  13. package/dist/cli/{chunk-ZZMZDAF6.js → chunk-RKV5KYKL.js} +3 -3
  14. package/dist/cli/{chunk-WRE3PUTV.js → chunk-RYUDIFSI.js} +2 -2
  15. package/dist/cli/{chunk-AJFC33TH.js → chunk-TOFVC6IV.js} +2 -2
  16. package/dist/cli/{chunk-KACRWFN3.js → chunk-TZBI2RVU.js} +1 -1
  17. package/dist/cli/chunk-U67V476Y.js +35 -0
  18. package/dist/cli/{chunk-35BPMR6P.js → chunk-ZDEEHFU3.js} +9 -4
  19. package/dist/cli/{daemon-5FPZG5R3.js → daemon-DLJCIC7L.js} +8 -7
  20. package/dist/cli/{decide-2ZKSX7O5.js → decide-UWJMKDR4.js} +4 -3
  21. package/dist/cli/{handover-AZ35QZRK.js → handover-5JHFA5EF.js} +5 -4
  22. package/dist/cli/{hook-KANGO46Y.js → hook-XBSMUMZ7.js} +10 -9
  23. package/dist/cli/index.js +25 -18
  24. package/dist/cli/{launch-DEKJ5HI4.js → launch-OVB66BZJ.js} +9 -8
  25. package/dist/cli/{send-BFPJBXXH.js → send-Y2C3BIGK.js} +5 -4
  26. package/dist/cli/{setup-47ODKJBM.js → setup-K42IVMHS.js} +1 -0
  27. package/dist/cli/{status-JPF4NZNI.js → status-5YEORNTH.js} +3 -2
  28. package/dist/cli/{task-UTRG4KA5.js → task-R44VWZHR.js} +5 -4
  29. package/dist/cli/team-F7B3IG3I.js +205 -0
  30. package/dist/cli/tui-GL6Q3KOX.js +19115 -0
  31. package/dist/cli/{uninstall-QNWRD27D.js → uninstall-4IMMR55T.js} +1 -0
  32. package/dist/cli/version-ZPC6END5.js +7 -0
  33. package/dist/cli/who-YMSQI64I.js +12 -0
  34. package/package.json +2 -1
  35. package/dist/cli/chunk-EX66KH66.js +0 -1856
  36. package/dist/cli/chunk-NFXFY5CT.js +0 -6
  37. package/dist/cli/team-SZJHGA7D.js +0 -101
  38. package/dist/cli/tui-GEJHBWP5.js +0 -4873
  39. package/dist/cli/version-YUO6SLAF.js +0 -6
  40. package/dist/cli/who-NYPVBO64.js +0 -11
@@ -0,0 +1,2302 @@
1
+ import {
2
+ paths
3
+ } from "./chunk-HOOHS3MX.js";
4
+
5
+ // ../protocol/src/handover.ts
6
+ import { z } from "zod";
7
+ var ContextSchema = z.object({ used: z.number().int().min(0), window: z.number().int().positive(), at: z.string().datetime() });
8
+ var CONTEXT_NEAR = 0.8;
9
+ var CONTEXT_NEAR_MIN = 0.5;
10
+ var CONTEXT_NEAR_MAX = 0.95;
11
+ var ContextPrefsSchema = z.object({ near: z.number().min(CONTEXT_NEAR_MIN).max(CONTEXT_NEAR_MAX) });
12
+ var contextShare = (c) => c ? Math.min(1, c.used / c.window) : void 0;
13
+ var contextNear = (c, near = CONTEXT_NEAR) => (contextShare(c) ?? 0) >= near;
14
+ var HandoverStateSchema = z.enum(["briefing", "launching", "done", "failed"]);
15
+ var HandoverSchema = z.object({
16
+ state: HandoverStateSchema,
17
+ at: z.string().datetime(),
18
+ /** The relief, once launched. */
19
+ to: z.string().optional(),
20
+ toName: z.string().optional(),
21
+ reason: z.string().optional(),
22
+ /** Started by Ledge itself (a seat working without asking), not by the user. */
23
+ auto: z.literal(true).optional()
24
+ });
25
+ var RelievesSchema = z.object({ id: z.string(), name: z.string(), at: z.string().datetime() });
26
+ var BRIEF_MAX = 2e4;
27
+ var ContextSetSchema = z.object({ type: z.literal("context.set"), prefs: ContextPrefsSchema });
28
+ var ContextPrefsMessageSchema = z.object({ type: z.literal("context.prefs"), prefs: ContextPrefsSchema });
29
+ var SessionHandoverSchema = z.object({ type: z.literal("session.handover"), rid: z.string().min(1).max(100), id: z.string().min(1) });
30
+ var HandoverBriefSchema = z.object({ type: z.literal("handover.brief"), rid: z.string().min(1).max(100), by: z.string().min(1), text: z.string().min(1).max(BRIEF_MAX) });
31
+ var HandoverShowSchema = z.object({ type: z.literal("handover.show"), rid: z.string().min(1).max(100), by: z.string().min(1) });
32
+ var SessionHandoverResultSchema = z.object({ type: z.literal("session.handover.result"), rid: z.string(), ok: z.boolean(), reason: z.string().optional() });
33
+ var HandoverResultSchema = z.object({ type: z.literal("handover.result"), rid: z.string(), text: z.string().optional(), reason: z.string().optional() });
34
+
35
+ // ../protocol/src/session.ts
36
+ import { z as z2 } from "zod";
37
+ var ToolSchema = z2.enum(["claude", "codex", "cursor", "omp"]);
38
+ var OmpApprovalSchema = z2.enum(["always-ask", "write", "yolo"]);
39
+ var AutonomySchema = z2.enum(["ask", "edits", "auto"]);
40
+ function autonomyArgs(tool, autonomy) {
41
+ if (!autonomy || autonomy === "ask") return [];
42
+ switch (tool) {
43
+ case "claude":
44
+ return ["--permission-mode", autonomy === "edits" ? "acceptEdits" : "auto"];
45
+ case "codex":
46
+ return autonomy === "edits" ? ["--ask-for-approval", "on-request"] : ["--ask-for-approval", "never", "--sandbox", "workspace-write"];
47
+ case "cursor":
48
+ return autonomy === "auto" ? ["--force"] : [];
49
+ case "omp":
50
+ return [];
51
+ }
52
+ }
53
+ function ompApprovalFor(autonomy) {
54
+ return autonomy === "auto" ? "yolo" : autonomy ? "always-ask" : void 0;
55
+ }
56
+ var BackendSchema = z2.enum(["tmux", "omp-rpc"]);
57
+ var SessionStateSchema = z2.enum(["starting", "working", "waiting_input", "waiting_permission", "idle", "exited", "error"]);
58
+ var AskQuestionSchema = z2.object({
59
+ id: z2.string(),
60
+ question: z2.string(),
61
+ header: z2.string().optional(),
62
+ multi: z2.boolean().optional(),
63
+ options: z2.array(z2.object({ label: z2.string(), description: z2.string().optional() }))
64
+ });
65
+ var PendingSchema = z2.object({
66
+ kind: z2.enum(["question", "permission", "plan", "diff"]),
67
+ text: z2.string(),
68
+ /** What to review in full before deciding, when `text` is only its summary: a plan's markdown. */
69
+ body: z2.string().optional(),
70
+ tool: z2.string().optional(),
71
+ options: z2.array(z2.string()).optional(),
72
+ /** Each option's description, by index ('' when it has none); left out when no option has one. */
73
+ optionDetails: z2.array(z2.string()).optional(),
74
+ /** `rpc`: the harness's own protocol asked (OMP): answered on its request id, never by typing. */
75
+ source: z2.enum(["hook", "pane", "rpc"]),
76
+ /** OMP: every question of one `ask` call, answered together with `session.answer`; `requestId` is the dialog it answers. */
77
+ questions: z2.array(AskQuestionSchema).optional(),
78
+ requestId: z2.string().optional(),
79
+ /** Set when the request comes from a helper (subagent): it is routed through its parent. */
80
+ helperId: z2.string().optional(),
81
+ /** For a helper-routed request: the state the parent returns to once it is answered (main thread may stop meanwhile). */
82
+ parentState: SessionStateSchema.optional()
83
+ });
84
+ var ReplySchema = z2.object({
85
+ text: z2.string(),
86
+ at: z2.string().datetime(),
87
+ /** The reply is a plan the agent proposes (Codex Plan mode): nothing in it is done yet. */
88
+ plan: z2.boolean().optional()
89
+ });
90
+ var HelperSchema = z2.object({
91
+ id: z2.string(),
92
+ kind: z2.string(),
93
+ task: z2.string(),
94
+ state: SessionStateSchema,
95
+ /** When this helper finished; retained across daemon restarts until its five-minute expiry. */
96
+ finishedAt: z2.string().datetime().optional()
97
+ });
98
+ var TopicSourceSchema = z2.enum(["user", "pane", "prompt"]);
99
+ var SessionSchema = z2.object({
100
+ /** Origin is annotated by the overlay, never used as an unqualified session id on the wire. */
101
+ machineId: z2.string().optional(),
102
+ machineName: z2.string().optional(),
103
+ repoId: z2.string().optional(),
104
+ /** Annotated by the fleet when its machine is unreachable and the last snapshot is kept (keepStale, spec 2026-10-14 §6). */
105
+ offline: z2.boolean().optional(),
106
+ id: z2.string().min(1),
107
+ name: z2.string().min(1),
108
+ tool: ToolSchema,
109
+ cwd: z2.string(),
110
+ project: z2.string(),
111
+ title: z2.string(),
112
+ /** One line saying what the session is about (spec 2026-10-11 §3). */
113
+ topic: z2.string().optional(),
114
+ topicSource: TopicSourceSchema.optional(),
115
+ /** The repository (its main worktree's folder name and absolute path), the checked-out branch, and whether `cwd` is a linked worktree (§4). */
116
+ repo: z2.string().optional(),
117
+ repoPath: z2.string().optional(),
118
+ branch: z2.string().optional(),
119
+ worktree: z2.boolean().optional(),
120
+ /** The folder the agent works in now, as its last hook reported it: `cwd` stays where it started, this follows a worktree it entered. */
121
+ workdir: z2.string().optional(),
122
+ tmuxSession: z2.string().nullable(),
123
+ /** Left out for a tmux session. `omp-rpc`: no pane, nothing to attach to; the daemon holds the process (see `agentSessionFile`). */
124
+ backend: BackendSchema.optional(),
125
+ state: SessionStateSchema,
126
+ pending: PendingSchema.optional(),
127
+ helpers: z2.array(HelperSchema).default([]),
128
+ /** OMP: the instructions waiting in its own queue (typed while it works), as it reports them; left out when none. */
129
+ queue: z2.array(z2.object({ text: z2.string(), mode: z2.enum(["steer", "follow_up"]) })).optional(),
130
+ lastReply: ReplySchema.optional(),
131
+ /** The last message this session sent to another agent: its tool_use_id, the address, the peer's Ledge session id, a one-line summary. */
132
+ lastMessage: z2.object({ id: z2.string(), to: z2.string(), peer: z2.string().optional(), summary: z2.string(), at: z2.string().datetime() }).optional(),
133
+ startedAt: z2.string().datetime(),
134
+ lastActivityAt: z2.string().datetime(),
135
+ lastHookAt: z2.string().datetime().optional(),
136
+ /** The model and effort Ledge launched the agent with (#58); left out, the agent's own default. */
137
+ model: z2.string().optional(),
138
+ effort: z2.string().optional(),
139
+ /** OMP: the approval mode it was launched with, kept by a resume; left out, `always-ask`. */
140
+ approval: OmpApprovalSchema.optional(),
141
+ /** The role it was launched with or took in a team (spec 2026-10-25 roles §3.3): its id, its label as it was then. */
142
+ role: z2.string().optional(),
143
+ roleLabel: z2.string().optional(),
144
+ /** The live team it sits in (`team-…`); left out, the project's common table. */
145
+ team: z2.string().optional(),
146
+ /** Its role's autonomy, as launched (kept by a resume); left out, `ask`. */
147
+ autonomy: AutonomySchema.optional(),
148
+ /** Its context as its agent last reported it, after a turn (context handover spec §3.1); left out, not measured. */
149
+ context: ContextSchema.optional(),
150
+ /** Its relief by a fresh session (§3.3): asked, launched, done or failed. */
151
+ handover: HandoverSchema.optional(),
152
+ /** The session it relieves, when it is a relief. */
153
+ relieves: RelievesSchema.optional(),
154
+ /** A model change asked while the agent worked (`session.model`), applied at its next free moment; a field left out is the agent's default. */
155
+ modelNext: z2.object({ model: z2.string().optional(), effort: z2.string().optional() }).optional(),
156
+ /** The agent's own session id when Ledge chose it at launch (Claude Code `--session-id`). */
157
+ agentSessionId: z2.string().optional(),
158
+ /** OMP: the exact journal file of the conversation, what a resume names (never "the latest of the folder"). */
159
+ agentSessionFile: z2.string().optional(),
160
+ /** The agent's transcript: the latest one a hook named (it follows /clear and --resume), else the one the launch implies. */
161
+ transcriptPath: z2.string().optional(),
162
+ exitCode: z2.number().int().nullable().optional(),
163
+ exitedAt: z2.string().datetime().optional(),
164
+ lastError: z2.string().optional(),
165
+ source: z2.enum(["launcher", "hook_only"]),
166
+ /** When the session was put in the archive (session management): set, it leaves the live list and waits to be restored, resumed or deleted. */
167
+ archivedAt: z2.string().datetime().optional(),
168
+ /** When the session was last taken out of the archive: the poller's retirement clock restarts from it, not from `exitedAt`. */
169
+ restoredAt: z2.string().datetime().optional()
170
+ });
171
+ function backendOf(s) {
172
+ return s.backend ?? "tmux";
173
+ }
174
+ function isArchived(s) {
175
+ return s.archivedAt !== void 0;
176
+ }
177
+
178
+ // ../protocol/src/teams.ts
179
+ import { z as z4 } from "zod";
180
+
181
+ // ../protocol/src/models.ts
182
+ import { z as z3 } from "zod";
183
+ var ModelIdSchema = z3.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:/@[\]=,+-]*$/, "not a model id");
184
+ var EffortSchema = z3.string().min(1).max(20).regex(/^[a-z]+$/, "not an effort");
185
+ var ModelChoiceSchema = z3.object({
186
+ id: ModelIdSchema,
187
+ label: z3.string(),
188
+ description: z3.string().optional(),
189
+ efforts: z3.array(EffortSchema).optional(),
190
+ defaultEffort: EffortSchema.optional(),
191
+ isDefault: z3.boolean().optional()
192
+ });
193
+ var ModelsListSchema = z3.object({
194
+ type: z3.literal("models.list"),
195
+ requestId: z3.string().min(1).max(100),
196
+ tool: ToolSchema,
197
+ machineId: z3.string().optional()
198
+ });
199
+ var ModelsListingSchema = z3.object({
200
+ type: z3.literal("models.listing"),
201
+ requestId: z3.string(),
202
+ tool: ToolSchema,
203
+ models: z3.array(ModelChoiceSchema),
204
+ error: z3.string().optional()
205
+ });
206
+
207
+ // ../protocol/src/teams.ts
208
+ var RoleIdSchema = z4.string().regex(/^[a-z0-9][a-z0-9_-]{0,39}$/);
209
+ var ROLE_LABEL_MAX = 40;
210
+ var ROLE_PROMPT_MAX = 8e3;
211
+ var RoleSchema = z4.object({
212
+ id: RoleIdSchema,
213
+ label: z4.string().min(1).max(ROLE_LABEL_MAX),
214
+ prompt: z4.string().max(ROLE_PROMPT_MAX),
215
+ tool: ToolSchema.optional(),
216
+ model: ModelIdSchema.optional(),
217
+ effort: EffortSchema.optional(),
218
+ autonomy: AutonomySchema.optional(),
219
+ source: z4.enum(["repo", "home"]),
220
+ path: z4.string(),
221
+ shadows: z4.string().optional()
222
+ });
223
+ var DefinitionProblemSchema = z4.object({ path: z4.string(), reason: z4.string() });
224
+ var TEAM_SEATS_MAX = 8;
225
+ var SeatSchema = z4.object({
226
+ role: RoleIdSchema,
227
+ tool: ToolSchema.optional(),
228
+ model: ModelIdSchema.optional(),
229
+ effort: EffortSchema.optional(),
230
+ name: z4.string().min(1).max(40).optional()
231
+ });
232
+ var uniqueRoles = (seats) => new Set(seats.map((s) => s.role)).size === seats.length;
233
+ var SeatsSchema = z4.array(SeatSchema).min(1).max(TEAM_SEATS_MAX).refine(uniqueRoles, "a role twice in one team");
234
+ var TeamNameSchema = z4.string().trim().min(1).max(60);
235
+ var TeamTemplateSchema = z4.object({ id: z4.string().min(1).max(64), name: TeamNameSchema, seats: SeatsSchema, source: z4.enum(["repo", "home"]), path: z4.string() });
236
+ var TeamIdSchema = z4.string().regex(/^team-[0-9a-f]{4,8}$/);
237
+ var TeamStateSchema = z4.enum(["active", "resting", "dissolved"]);
238
+ var TeamSchema = z4.object({
239
+ id: TeamIdSchema,
240
+ name: TeamNameSchema,
241
+ template: z4.string().optional(),
242
+ project: z4.string().nullable(),
243
+ machine: z4.string(),
244
+ cwd: z4.string(),
245
+ state: TeamStateSchema,
246
+ seats: z4.array(z4.object({ role: RoleIdSchema, sessionId: z4.string().nullable() })).max(TEAM_SEATS_MAX * 2),
247
+ createdAt: z4.string(),
248
+ updatedAt: z4.string(),
249
+ restingAt: z4.string().optional(),
250
+ dissolvedAt: z4.string().optional()
251
+ });
252
+ var RolesListSchema = z4.object({ type: z4.literal("roles.list"), cwd: z4.string().min(1).optional(), requestId: z4.string().max(100).optional(), machineId: z4.string().optional() });
253
+ var RolesListingSchema = z4.object({
254
+ type: z4.literal("roles.listing"),
255
+ requestId: z4.string().optional(),
256
+ roles: z4.array(RoleSchema),
257
+ templates: z4.array(TeamTemplateSchema).default([]),
258
+ problems: z4.array(DefinitionProblemSchema),
259
+ /** Set by the fleet when the project's machine could not answer (#99): 'offline', or 'outdated' for a daemon without roles.list. */
260
+ error: z4.string().optional()
261
+ });
262
+ var TeamLaunchSchema = z4.object({
263
+ type: z4.literal("team.launch"),
264
+ rid: z4.string().min(1).max(100),
265
+ cwd: z4.string().min(1).optional(),
266
+ template: z4.string().min(1).max(64).optional(),
267
+ seats: SeatsSchema.optional(),
268
+ name: TeamNameSchema.optional(),
269
+ qid: z4.string().min(1).optional(),
270
+ machineId: z4.string().optional()
271
+ });
272
+ var TeamJoinSchema = z4.object({ type: z4.literal("team.join"), team: TeamIdSchema, session: z4.string().min(1), role: RoleIdSchema });
273
+ var TeamLeaveSchema = z4.object({ type: z4.literal("team.leave"), team: TeamIdSchema, session: z4.string().min(1) });
274
+ var TeamResumeSchema = z4.object({ type: z4.literal("team.resume"), team: TeamIdSchema });
275
+ var TeamDissolveSchema = z4.object({ type: z4.literal("team.dissolve"), team: TeamIdSchema });
276
+ var TeamSaveSchema = z4.object({ type: z4.literal("team.save"), rid: z4.string().min(1).max(100), name: TeamNameSchema, seats: SeatsSchema, qid: z4.string().min(1).optional() });
277
+ var TeamsMessageSchema = z4.object({ type: z4.literal("teams"), teams: z4.array(TeamSchema) });
278
+ var TeamUpdatedSchema = z4.object({ type: z4.literal("team.updated"), team: TeamSchema });
279
+ var TeamLaunchedSchema = z4.object({ type: z4.literal("team.launched"), rid: z4.string(), team: TeamSchema });
280
+ var TeamFailedSchema = z4.object({ type: z4.literal("team.failed"), rid: z4.string(), reason: z4.string(), team: TeamSchema.optional() });
281
+ var TeamSavedSchema = z4.object({ type: z4.literal("team.saved"), rid: z4.string(), path: z4.string().optional(), reason: z4.string().optional() });
282
+ var isTeamLive = (t) => t.state !== "dissolved";
283
+
284
+ // ../protocol/src/tasks.ts
285
+ import { z as z5 } from "zod";
286
+ var TASK_TITLE_MAX = 200;
287
+ var TASK_TEXT_MAX = 2e4;
288
+ var LEDGE_GIVER = "ledge";
289
+ var TASK_OPS = ["give", "take", "pass", "done", "refuse", "cancel", "drop"];
290
+ var TRANSMISSION_OPS = ["give", "pass", "done", "refuse", "cancel"];
291
+ var TaskLogEntrySchema = z5.object({
292
+ at: z5.string(),
293
+ by: z5.string(),
294
+ what: z5.string(),
295
+ op: z5.enum(TASK_OPS).optional(),
296
+ from: z5.string().optional(),
297
+ to: z5.string().optional(),
298
+ fromName: z5.string().optional(),
299
+ toName: z5.string().optional(),
300
+ note: z5.string().optional()
301
+ });
302
+ var TaskSchema = z5.object({
303
+ id: z5.string(),
304
+ title: z5.string(),
305
+ text: z5.string().optional(),
306
+ giver: z5.string(),
307
+ giverName: z5.string(),
308
+ owner: z5.string(),
309
+ ownerName: z5.string(),
310
+ state: z5.enum(["open", "taken", "done", "refused", "dropped"]),
311
+ result: z5.string().optional(),
312
+ /** The live team its owner sat in when it was given (or passed), so a team's tasks can be listed. */
313
+ team: z5.string().optional(),
314
+ /** A task Ledge gave and the user put away from « À toi » once closed. */
315
+ seen: z5.boolean().optional(),
316
+ at: z5.string(),
317
+ updatedAt: z5.string(),
318
+ log: z5.array(TaskLogEntrySchema)
319
+ });
320
+ var handOver = (op) => TRANSMISSION_OPS.includes(op ?? "");
321
+ function transmissions(t) {
322
+ return t.log.flatMap((e, index) => handOver(e.op) && e.from !== void 0 && e.to !== void 0 ? [{ index, at: e.at, op: e.op, from: e.from, to: e.to, fromName: e.fromName ?? "", toName: e.toName ?? "", ...e.note !== void 0 ? { note: e.note } : {} }] : []);
323
+ }
324
+ var givenByLedge = (t) => t.giver === LEDGE_GIVER;
325
+ var taskClosed = (t) => t.state === "done" || t.state === "refused" || t.state === "dropped";
326
+ var TaskCmdSchema = z5.object({
327
+ type: z5.literal("task.cmd"),
328
+ rid: z5.string().min(1),
329
+ by: z5.string().min(1),
330
+ op: z5.enum(["give", "take", "done", "refuse", "pass", "show", "list", "seen", "cancel"]),
331
+ task: z5.string().min(1).max(40).optional(),
332
+ to: z5.string().min(1).optional(),
333
+ title: z5.string().min(1).max(TASK_TITLE_MAX).optional(),
334
+ text: z5.string().max(TASK_TEXT_MAX).optional(),
335
+ all: z5.boolean().optional(),
336
+ team: z5.string().min(1).max(40).optional(),
337
+ qid: z5.string().min(1).optional(),
338
+ row: z5.number().int().min(0).max(7).optional()
339
+ });
340
+ var TaskUpdatedSchema = z5.object({ type: z5.literal("task.updated"), task: TaskSchema });
341
+ var TaskResultSchema = z5.object({
342
+ type: z5.literal("task.result"),
343
+ rid: z5.string(),
344
+ task: TaskSchema.optional(),
345
+ tasks: z5.array(TaskSchema).optional(),
346
+ reason: z5.string().optional()
347
+ });
348
+
349
+ // ../protocol/src/actions.ts
350
+ import { z as z8 } from "zod";
351
+
352
+ // ../protocol/src/proposal.ts
353
+ import { z as z6 } from "zod";
354
+ var ProposalBlockSchema = z6.object({ cwd: z6.string().trim().min(1), prompt: z6.string().optional() });
355
+ var RelayBlockSchema = z6.object({ session: z6.string().trim().min(1), text: z6.string().trim().min(1) });
356
+ var ProjectBlockSchema = z6.object({
357
+ name: z6.string().trim().min(1).max(80),
358
+ description: z6.string().max(500).optional(),
359
+ repos: z6.array(z6.object({ path: z6.string().min(1).max(4096), why: z6.string().trim().min(1).max(200) })).min(1).max(50),
360
+ addTo: z6.string().trim().max(80).optional()
361
+ });
362
+ var CloseBlockSchema = z6.object({ session: z6.string().trim().min(1) });
363
+ var TeamBlockSchema = z6.object({
364
+ cwd: z6.string().trim().min(1),
365
+ template: z6.string().trim().min(1).max(64).optional(),
366
+ name: z6.string().trim().min(1).max(60).optional(),
367
+ seats: z6.array(z6.object({ role: z6.string().trim().min(1).max(40), tool: z6.enum(["claude", "codex", "cursor", "omp"]).optional(), model: z6.string().max(200).optional(), effort: z6.string().max(20).optional(), name: z6.string().max(40).optional() })).min(1).max(8).optional()
368
+ }).refine((b) => !!b.template || !!b.seats?.length, "a template or seats");
369
+ var TaskBlockSchema = z6.object({
370
+ to: z6.string().trim().min(1).max(80),
371
+ team: z6.string().trim().max(40).optional(),
372
+ title: z6.string().trim().min(1).max(200),
373
+ text: z6.string().max(2e4).optional()
374
+ });
375
+ var BLOCK = /```ledge-(session|relay|project|close|team|task|action)[ \t]*\n?([\s\S]*?)(?:```|$)/g;
376
+ function extractProposal(answer) {
377
+ const found = [];
378
+ const text = answer.replace(BLOCK, (_m, kind, body) => {
379
+ found.push({ kind, raw: body.trim() });
380
+ return "";
381
+ });
382
+ const last = found.at(-1);
383
+ return last ? { text: text.replace(/\n{3,}/g, "\n\n").trim(), kind: last.kind, raw: last.raw, blocks: found } : { text: answer, kind: null, raw: null, blocks: [] };
384
+ }
385
+ function parseRelayBlock(raw) {
386
+ try {
387
+ const r = RelayBlockSchema.safeParse(JSON.parse(raw));
388
+ return r.success ? r.data : null;
389
+ } catch {
390
+ return null;
391
+ }
392
+ }
393
+ function parseCloseBlock(raw) {
394
+ try {
395
+ const r = CloseBlockSchema.safeParse(JSON.parse(raw));
396
+ return r.success ? r.data : null;
397
+ } catch {
398
+ return null;
399
+ }
400
+ }
401
+ function parseTeamBlock(raw) {
402
+ try {
403
+ const r = TeamBlockSchema.safeParse(JSON.parse(raw));
404
+ return r.success ? r.data : null;
405
+ } catch {
406
+ return null;
407
+ }
408
+ }
409
+ function parseTaskBlock(raw) {
410
+ try {
411
+ const r = TaskBlockSchema.safeParse(JSON.parse(raw));
412
+ return r.success ? r.data : null;
413
+ } catch {
414
+ return null;
415
+ }
416
+ }
417
+ function parseProjectBlock(raw) {
418
+ try {
419
+ const r = ProjectBlockSchema.safeParse(JSON.parse(raw));
420
+ return r.success ? r.data : null;
421
+ } catch {
422
+ return null;
423
+ }
424
+ }
425
+
426
+ // ../protocol/src/decisions.ts
427
+ import { z as z7 } from "zod";
428
+ var DecisionSchema = z7.enum(["accept", "deny", "always"]);
429
+ var NOTE_MAX = 4e3;
430
+ var NOTE_NOT_TYPED = "note_not_typed";
431
+ var DECISION_OFFERS = {
432
+ claude: { always: ["permission", "diff", "plan"], note: ["permission", "diff", "plan"] },
433
+ codex: { always: ["permission", "diff"], note: ["permission", "diff", "plan"] },
434
+ cursor: { always: [], note: ["permission", "plan"] },
435
+ /** OMP answers its own native requests (spec 2026-10-20 §3.3): no « always », no note until those are typed. */
436
+ omp: { always: [], note: [] }
437
+ };
438
+ function offers(tool, kind, what) {
439
+ return DECISION_OFFERS[tool][what].includes(kind);
440
+ }
441
+ function cyrb53(str, seed) {
442
+ let h1 = 3735928559 ^ seed, h2 = 1103547991 ^ seed;
443
+ for (let i = 0; i < str.length; i++) {
444
+ const ch = str.charCodeAt(i);
445
+ h1 = Math.imul(h1 ^ ch, 2654435761);
446
+ h2 = Math.imul(h2 ^ ch, 1597334677);
447
+ }
448
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
449
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
450
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16).padStart(14, "0");
451
+ }
452
+ function pendingKey(p) {
453
+ const s = JSON.stringify([p.kind, p.text, p.body ?? null, p.tool ?? null, p.options ?? null, p.optionDetails ?? null, p.helperId ?? null]);
454
+ return cyrb53(s, 0) + cyrb53(s, 1);
455
+ }
456
+
457
+ // ../protocol/src/actions.ts
458
+ var ActionRiskSchema = z8.enum(["read", "reversible", "destructive"]);
459
+ var ACTION_IDS = [
460
+ "session.launch",
461
+ "session.relay",
462
+ "session.close",
463
+ "project.propose",
464
+ "team.launch",
465
+ "task.give",
466
+ "session.model",
467
+ "session.interrupt",
468
+ "session.archive",
469
+ "session.restore",
470
+ "session.resume",
471
+ "session.decide",
472
+ "team.join",
473
+ "team.leave",
474
+ "team.resume",
475
+ "team.dissolve",
476
+ "team.save",
477
+ "task.cancel",
478
+ "task.pass"
479
+ ];
480
+ var ActionIdSchema = z8.enum(ACTION_IDS);
481
+ var def = (d) => d;
482
+ var SessionRef = z8.string().trim().min(1).max(80);
483
+ var TeamRef = z8.string().trim().min(1).max(60);
484
+ var TaskRef = z8.string().trim().min(1).max(40);
485
+ var LaunchArgs = ProposalBlockSchema.extend({ model: ModelIdSchema.optional(), effort: EffortSchema.optional() });
486
+ var ACTIONS = [
487
+ def({
488
+ id: "session.launch",
489
+ args: LaunchArgs,
490
+ card: "launch",
491
+ phone: "launch",
492
+ risk: "reversible",
493
+ max: 8,
494
+ summary: "ouvrir une session d'agent dans un dossier, avec sa premi\xE8re instruction",
495
+ when: "quand l'utilisateur demande explicitement d'ouvrir ou de lancer une session, et seulement si le dossier est s\xFBr : un chemin absolu lu dans sessions.md ou folders.md, ou donn\xE9 tel quel par l'utilisateur ; une action par session, et pour un dossier incertain cite les candidats et demande lequel. model et effort seulement si l'utilisateur les demande",
496
+ example: { cwd: "/home/alice/projets/api", prompt: "Lance les tests et corrige ceux qui \xE9chouent" }
497
+ }),
498
+ def({
499
+ id: "session.relay",
500
+ args: RelayBlockSchema,
501
+ card: "relay",
502
+ phone: null,
503
+ risk: "reversible",
504
+ max: 1,
505
+ summary: "envoyer une instruction \xE0 une session (tout de suite si elle est libre, sinon d\xE8s qu'elle le sera)",
506
+ when: "quand l'utilisateur demande explicitement de dire, demander ou envoyer quelque chose \xE0 une session, et seulement si une seule session correspond et qu'elle n'est pas en lecture seule ; le texte est formul\xE9 pour l'agent",
507
+ example: { session: "Nova", text: "Ajoute un test pour le cas o\xF9 la liste est vide" }
508
+ }),
509
+ def({
510
+ id: "session.close",
511
+ args: CloseBlockSchema,
512
+ card: "close",
513
+ phone: "close",
514
+ risk: "reversible",
515
+ max: 8,
516
+ cli: "ledge close <session>",
517
+ summary: "fermer proprement une session ; sa conversation reste reprenable",
518
+ when: "quand l'utilisateur demande explicitement de fermer, clore, arr\xEAter ou quitter des sessions qui correspondent sans ambigu\xEFt\xE9, pas en lecture seule ni d\xE9j\xE0 termin\xE9es. S'il veut d'abord un point de reprise, propose seulement le relais de cette demande",
519
+ example: { session: "Nova" }
520
+ }),
521
+ def({
522
+ id: "project.propose",
523
+ args: ProjectBlockSchema,
524
+ card: "project",
525
+ phone: null,
526
+ risk: "reversible",
527
+ max: 1,
528
+ summary: "cr\xE9er un projet, ou compl\xE9ter un projet existant (addTo), avec ses d\xE9p\xF4ts",
529
+ when: "seulement si l'utilisateur demande explicitement de cr\xE9er ou de compl\xE9ter un projet : lis projects.md, ne cite que des chemins absolus lus dans sessions.md, folders.md ou projects.md, jamais un d\xE9p\xF4t qui appartient d\xE9j\xE0 \xE0 un projet ; ta r\xE9ponse dit le nom, chaque d\xE9p\xF4t et pourquoi",
530
+ example: { name: "Facturation", description: "Le service et son front", repos: [{ path: "/home/alice/projets/billing-api", why: "le service" }] }
531
+ }),
532
+ def({
533
+ id: "team.launch",
534
+ args: TeamBlockSchema,
535
+ card: "team",
536
+ phone: "launch",
537
+ risk: "reversible",
538
+ max: 1,
539
+ cli: "ledge team up <mod\xE8le> --cwd <dossier>",
540
+ summary: "monter une \xE9quipe dans un dossier : un mod\xE8le de teams.md (template), ou des si\xE8ges (r\xF4le de roles.md, agent, et si demand\xE9 model, effort)",
541
+ when: "quand l'utilisateur demande explicitement de monter, lancer ou composer une \xE9quipe, avec la m\xEAme r\xE8gle de dossier s\xFBr qu'une session ; 8 si\xE8ges au plus, un r\xF4le une seule fois, seulement des r\xF4les de roles.md disponibles dans ce dossier (s'il en manque un, dis lequel et le fichier \xE0 \xE9crire, ~/.ledge/roles/<id>.md). Ne choisis jamais l'autonomie : c'est celle du r\xF4le",
542
+ example: { cwd: "/home/alice/projets/api", name: "api", seats: [{ role: "dev-back", tool: "claude" }, { role: "rev-back", tool: "codex" }] }
543
+ }),
544
+ def({
545
+ id: "task.give",
546
+ args: TaskBlockSchema,
547
+ card: "task",
548
+ phone: null,
549
+ risk: "reversible",
550
+ max: 8,
551
+ cli: 'ledge task give <session|@r\xF4le> "<titre>" "<d\xE9tail>"',
552
+ summary: "confier une t\xE2che \xE0 une session, ou \xE0 un @r\xF4le d'une \xE9quipe (team : son id dans teams.md ; sans team, un si\xE8ge de l'\xE9quipe propos\xE9e dans la m\xEAme r\xE9ponse)",
553
+ when: "quand l'utilisateur demande explicitement de confier une t\xE2che, ou juste apr\xE8s team.launch quand il a dit ce que l'\xE9quipe doit faire. Une fois l'\xE9quipe lanc\xE9e, ne propose rien pour ce que les si\xE8ges se confient entre eux",
554
+ example: { to: "@dev-back", title: "Pagination de /invoices", text: "Ajoute la pagination par curseur et ses tests" }
555
+ }),
556
+ def({
557
+ id: "session.model",
558
+ card: "generic",
559
+ phone: null,
560
+ risk: "reversible",
561
+ max: 8,
562
+ args: z8.object({ session: SessionRef, model: ModelIdSchema.nullable(), effort: EffortSchema.nullable() }),
563
+ summary: "changer le mod\xE8le et l'effort d'une session en cours (null : le d\xE9faut de l'agent) ; elle red\xE9marre sur la m\xEAme conversation, \xE0 son prochain moment libre",
564
+ when: "quand l'utilisateur demande de passer une session sur un autre mod\xE8le ou un autre effort",
565
+ example: { session: "Nova", model: "claude-opus-5-5", effort: "high" }
566
+ }),
567
+ def({
568
+ id: "session.interrupt",
569
+ args: z8.object({ session: SessionRef }),
570
+ card: "generic",
571
+ phone: null,
572
+ risk: "reversible",
573
+ max: 8,
574
+ summary: "interrompre le tour en cours d'une session (comme \xC9chap) ; la session reste ouverte",
575
+ when: "quand l'utilisateur demande d'interrompre, de stopper ou d'arr\xEAter ce que fait une session sans la fermer",
576
+ example: { session: "Nova" }
577
+ }),
578
+ def({
579
+ id: "session.archive",
580
+ args: z8.object({ session: SessionRef }),
581
+ card: "generic",
582
+ phone: null,
583
+ risk: "reversible",
584
+ max: 8,
585
+ summary: "archiver une session termin\xE9e : elle quitte la liste mais est gard\xE9e (restaurable)",
586
+ when: "quand l'utilisateur demande d'archiver ou de ranger des sessions ; une session encore en cours se ferme d'abord : propose session.close puis session.archive, dans cet ordre, dans la m\xEAme r\xE9ponse",
587
+ example: { session: "Nova" }
588
+ }),
589
+ def({
590
+ id: "session.restore",
591
+ args: z8.object({ session: SessionRef }),
592
+ card: "generic",
593
+ phone: null,
594
+ risk: "reversible",
595
+ max: 8,
596
+ summary: "sortir une session de l'archive, telle qu'elle \xE9tait",
597
+ when: "quand l'utilisateur demande de restaurer une session archiv\xE9e",
598
+ example: { session: "Nova" }
599
+ }),
600
+ def({
601
+ id: "session.resume",
602
+ args: z8.object({ session: SessionRef }),
603
+ card: "generic",
604
+ phone: "act",
605
+ risk: "reversible",
606
+ max: 8,
607
+ summary: "relancer l'agent d'une session termin\xE9e sur sa conversation",
608
+ when: "quand l'utilisateur demande de reprendre ou relancer une session termin\xE9e",
609
+ example: { session: "Nova" }
610
+ }),
611
+ def({
612
+ id: "session.decide",
613
+ card: "generic",
614
+ phone: "act",
615
+ max: 1,
616
+ args: z8.object({ session: SessionRef, decision: z8.enum(["accept", "deny"]), note: z8.string().trim().min(1).max(NOTE_MAX).optional() }),
617
+ risk: (a) => a.decision === "accept" ? "destructive" : "reversible",
618
+ summary: "accepter ou refuser la demande en attente d'une session (permission, diff ou plan), note possible avec un refus ; jamais \xAB toujours \xBB",
619
+ when: "seulement quand l'utilisateur demande explicitement d'accepter ou de refuser la demande d'une session pr\xE9cise, et que sessions.md montre une demande en attente pour elle ; cite la demande telle quelle dans ta r\xE9ponse",
620
+ example: { session: "Nova", decision: "deny", note: "Pas sur main : cr\xE9e une branche" }
621
+ }),
622
+ def({
623
+ id: "team.join",
624
+ args: z8.object({ team: TeamRef, session: SessionRef, role: RoleIdSchema }),
625
+ card: "generic",
626
+ phone: null,
627
+ risk: "reversible",
628
+ max: 8,
629
+ summary: "faire entrer une session dans une \xE9quipe en cours, \xE0 un r\xF4le de roles.md",
630
+ when: "quand l'utilisateur demande d'ajouter une session existante \xE0 une \xE9quipe",
631
+ example: { team: "team-3fa2", session: "Nova", role: "dev-back" }
632
+ }),
633
+ def({
634
+ id: "team.leave",
635
+ args: z8.object({ team: TeamRef, session: SessionRef }),
636
+ card: "generic",
637
+ phone: null,
638
+ risk: "reversible",
639
+ max: 8,
640
+ summary: "faire sortir une session d'une \xE9quipe (la session continue)",
641
+ when: "quand l'utilisateur demande de retirer une session d'une \xE9quipe",
642
+ example: { team: "team-3fa2", session: "Nova" }
643
+ }),
644
+ def({
645
+ id: "team.resume",
646
+ args: z8.object({ team: TeamRef }),
647
+ card: "generic",
648
+ phone: "act",
649
+ risk: "reversible",
650
+ max: 1,
651
+ summary: "reprendre une \xE9quipe au repos : chaque si\xE8ge reprend sa conversation",
652
+ when: "quand l'utilisateur demande de reprendre ou relancer une \xE9quipe au repos",
653
+ example: { team: "team-3fa2" }
654
+ }),
655
+ def({
656
+ id: "team.dissolve",
657
+ args: z8.object({ team: TeamRef }),
658
+ card: "generic",
659
+ phone: "act",
660
+ risk: "destructive",
661
+ max: 1,
662
+ cli: "ledge team dissolve <\xE9quipe>",
663
+ summary: "dissoudre une \xE9quipe : elle dispara\xEEt, ses sessions restent ; irr\xE9versible",
664
+ when: "seulement quand l'utilisateur demande explicitement de dissoudre une \xE9quipe pr\xE9cise",
665
+ example: { team: "team-3fa2" }
666
+ }),
667
+ def({
668
+ id: "team.save",
669
+ args: z8.object({ team: TeamRef, name: TeamNameSchema }),
670
+ card: "generic",
671
+ phone: null,
672
+ risk: "reversible",
673
+ max: 1,
674
+ summary: "enregistrer une \xE9quipe en cours comme mod\xE8le (~/.ledge/teams/<nom>.yaml)",
675
+ when: "quand l'utilisateur demande d'enregistrer ou de garder la composition d'une \xE9quipe",
676
+ example: { team: "team-3fa2", name: "api" }
677
+ }),
678
+ def({
679
+ id: "task.cancel",
680
+ args: z8.object({ task: TaskRef, reason: z8.string().trim().min(1).max(500).optional() }),
681
+ card: "generic",
682
+ phone: "act",
683
+ risk: "destructive",
684
+ max: 8,
685
+ summary: "annuler une t\xE2che ouverte ou prise ; la session qui la tient est pr\xE9venue ; irr\xE9versible",
686
+ when: "seulement quand l'utilisateur demande explicitement d'annuler une t\xE2che de tasks.md",
687
+ example: { task: "t91b0d4", reason: "Plus n\xE9cessaire" }
688
+ }),
689
+ def({
690
+ id: "task.pass",
691
+ args: z8.object({ task: TaskRef, to: SessionRef, note: z8.string().trim().min(1).max(TASK_TEXT_MAX).optional() }),
692
+ card: "generic",
693
+ phone: null,
694
+ risk: "reversible",
695
+ max: 8,
696
+ summary: "passer une t\xE2che de tasks.md \xE0 une autre session (elle redevient ouverte)",
697
+ when: "quand l'utilisateur demande de r\xE9assigner ou de passer une t\xE2che \xE0 une autre session",
698
+ example: { task: "t91b0d4", to: "Bix" }
699
+ })
700
+ ];
701
+ var byId = new Map(ACTIONS.map((a) => [a.id, a]));
702
+ var actionById = (id) => byId.get(id);
703
+ var riskOf = (a, args) => typeof a.risk === "function" ? a.risk(args) : a.risk;
704
+ var ActionBlockSchema = z8.object({ action: z8.string().trim().min(1).max(40), args: z8.record(z8.unknown()).default({}) });
705
+ var LEGACY_FENCE_ACTION = {
706
+ session: "session.launch",
707
+ relay: "session.relay",
708
+ close: "session.close",
709
+ project: "project.propose",
710
+ team: "team.launch",
711
+ task: "task.give"
712
+ };
713
+ function parseActionBlock(kind, raw) {
714
+ let json;
715
+ try {
716
+ json = JSON.parse(raw);
717
+ } catch {
718
+ return { error: "invalid JSON" };
719
+ }
720
+ let id;
721
+ let args;
722
+ if (kind === "action") {
723
+ const b = ActionBlockSchema.safeParse(json);
724
+ if (!b.success) return { error: "not an {action, args} object" };
725
+ id = b.data.action;
726
+ args = b.data.args;
727
+ } else {
728
+ id = LEGACY_FENCE_ACTION[kind];
729
+ args = json;
730
+ }
731
+ const action = actionById(id);
732
+ if (!action) return { error: `unknown action ${id}` };
733
+ const parsed = action.args.safeParse(args);
734
+ if (!parsed.success) return { error: `invalid args for ${id}: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"} ${i.message}`).join("; ")}` };
735
+ return { action, args: parsed.data };
736
+ }
737
+ var NEVER_PROPOSED = [
738
+ "tuer (kill) ou supprimer une session, la retirer de la liste",
739
+ "mettre \xE0 jour le daemon",
740
+ "changer les machines, les r\xE9glages, l'acc\xE8s mobile",
741
+ "modifier les projets autrement que par la carte de projet",
742
+ "cr\xE9er un dossier",
743
+ "r\xE9pondre \xAB toujours \xBB \xE0 une demande, ou r\xE9pondre aux questions \xE0 choix d'OMP"
744
+ ];
745
+
746
+ // ../protocol/src/messages.ts
747
+ import { z as z21 } from "zod";
748
+
749
+ // ../protocol/src/chat.ts
750
+ import { z as z10 } from "zod";
751
+
752
+ // ../protocol/src/attachments.ts
753
+ import { z as z9 } from "zod";
754
+ var ATTACHMENT_ID = /^[0-9a-f]{64}$/;
755
+ var AttachmentIdSchema = z9.string().regex(ATTACHMENT_ID);
756
+ var IMAGE_MIMES = ["image/webp", "image/jpeg", "image/png", "image/gif"];
757
+ var ImageMimeSchema = z9.enum(IMAGE_MIMES);
758
+ var ATTACHMENT_MAX = 4e5;
759
+ var ATTACHMENT_LOCAL_MAX = 2e6;
760
+ var PREVIEW_MAX = 4e4;
761
+ var ATTACHMENTS_MAX = 5;
762
+ var b64Max = (bytes) => Math.ceil(bytes / 3) * 4;
763
+ var AttachmentRefSchema = z9.object({
764
+ id: AttachmentIdSchema,
765
+ mime: ImageMimeSchema,
766
+ width: z9.number().int().positive(),
767
+ height: z9.number().int().positive(),
768
+ bytes: z9.number().int().positive(),
769
+ preview: z9.boolean()
770
+ });
771
+ var ImageUploadSchema = z9.object({
772
+ mime: ImageMimeSchema,
773
+ data: z9.string().min(1).max(b64Max(ATTACHMENT_LOCAL_MAX)),
774
+ preview: z9.object({ mime: ImageMimeSchema, data: z9.string().min(1).max(b64Max(PREVIEW_MAX)) }).optional()
775
+ });
776
+ var AttachmentErrorReasonSchema = z9.enum(["too_large", "not_image", "unreadable", "not_found", "offline"]);
777
+ var requestId = z9.string().min(1).max(100);
778
+ var AttachmentPutSchema = z9.object({ type: z9.literal("attachment.put"), requestId, id: z9.string().optional(), image: ImageUploadSchema });
779
+ var AttachmentGetSchema = z9.object({ type: z9.literal("attachment.get"), requestId, attachment: AttachmentIdSchema, preview: z9.boolean().optional(), id: z9.string().optional(), machine: z9.string().optional() });
780
+ var AttachmentStoredSchema = z9.object({ type: z9.literal("attachment.stored"), requestId, ref: AttachmentRefSchema });
781
+ var AttachmentDataSchema = z9.object({ type: z9.literal("attachment.data"), requestId, attachment: AttachmentIdSchema, mime: ImageMimeSchema, data: z9.string() });
782
+ var AttachmentErrorSchema = z9.object({ type: z9.literal("attachment.error"), requestId, reason: AttachmentErrorReasonSchema });
783
+
784
+ // ../protocol/src/chat.ts
785
+ var ChatStepSchema = z10.object({
786
+ kind: z10.enum(["sessions", "pane", "reply", "git", "repo"]),
787
+ id: z10.string().optional(),
788
+ name: z10.string().optional(),
789
+ path: z10.string().optional()
790
+ });
791
+ var ChatProposalSchema = z10.object({
792
+ cwd: z10.string().min(1),
793
+ project: z10.string(),
794
+ prompt: z10.string().optional(),
795
+ tool: ToolSchema,
796
+ /** What Ledge proposed on the card's picker (spec 2026-10-26 action registry §4), the agent's own default when left out. */
797
+ model: ModelIdSchema.optional(),
798
+ effort: EffortSchema.optional(),
799
+ launchedId: z10.string().optional(),
800
+ error: z10.string().optional()
801
+ });
802
+ var MAX_PROPOSALS = 8;
803
+ var ChatProposalsSchema = z10.array(ChatProposalSchema).min(1).max(MAX_PROPOSALS);
804
+ var ChatRelaySchema = z10.object({
805
+ id: z10.string().min(1),
806
+ name: z10.string(),
807
+ tool: ToolSchema,
808
+ text: z10.string().min(1),
809
+ status: z10.enum(["queued", "sent"]).optional(),
810
+ sentAt: z10.string().datetime().optional(),
811
+ error: z10.string().optional()
812
+ });
813
+ var ChatCloseSchema = z10.object({
814
+ id: z10.string().min(1),
815
+ name: z10.string(),
816
+ tool: ToolSchema,
817
+ closedAt: z10.string().datetime().optional(),
818
+ how: z10.enum(["exit", "killed"]).optional(),
819
+ error: z10.string().optional()
820
+ });
821
+ var ChatClosesSchema = z10.array(ChatCloseSchema).min(1).max(MAX_PROPOSALS);
822
+ var ChatProjectProposalSchema = z10.object({
823
+ name: z10.string().min(1).max(80),
824
+ description: z10.string().max(500).optional(),
825
+ repos: z10.array(z10.object({ path: z10.string().min(1).max(4096), why: z10.string().min(1).max(200) })).min(1).max(50),
826
+ addTo: z10.string().min(1).max(80).optional()
827
+ });
828
+ var ChatTeamSeatSchema = z10.object({
829
+ role: z10.string().min(1),
830
+ label: z10.string(),
831
+ tool: ToolSchema,
832
+ model: z10.string().optional(),
833
+ effort: z10.string().optional(),
834
+ autonomy: AutonomySchema.optional(),
835
+ name: z10.string().optional()
836
+ });
837
+ var ChatTeamProposalSchema = z10.object({
838
+ cwd: z10.string().min(1),
839
+ project: z10.string(),
840
+ projectName: z10.string().optional(),
841
+ name: z10.string().min(1),
842
+ template: z10.string().optional(),
843
+ seats: z10.array(ChatTeamSeatSchema).min(1).max(8),
844
+ teamId: z10.string().optional(),
845
+ launchedAt: z10.string().datetime().optional(),
846
+ error: z10.string().optional(),
847
+ savedPath: z10.string().optional()
848
+ });
849
+ var ChatTaskProposalSchema = z10.object({
850
+ to: z10.string().min(1),
851
+ toName: z10.string().optional(),
852
+ team: z10.string().optional(),
853
+ title: z10.string().min(1).max(200),
854
+ text: z10.string().max(2e4).optional(),
855
+ taskId: z10.string().optional(),
856
+ ownerName: z10.string().optional(),
857
+ state: z10.enum(["open", "taken", "done", "refused", "dropped"]).optional(),
858
+ givenAt: z10.string().datetime().optional(),
859
+ error: z10.string().optional()
860
+ });
861
+ var ChatTasksSchema = z10.array(ChatTaskProposalSchema).min(1).max(MAX_PROPOSALS);
862
+ var ActionVarsSchema = z10.record(z10.string().max(40), z10.string().max(300));
863
+ var DETAIL_MAX = 4e3;
864
+ var ACTION_ERROR_CODES = [
865
+ "unknown",
866
+ "ambiguous",
867
+ "read_only",
868
+ "exited",
869
+ "running",
870
+ "archived",
871
+ "not_archived",
872
+ "no_terminal",
873
+ "same_model",
874
+ "no_request",
875
+ "in_session",
876
+ "no_note",
877
+ "member",
878
+ "not_member",
879
+ "unknown_role",
880
+ "not_resting",
881
+ "dissolved",
882
+ "no_seat",
883
+ "closed",
884
+ "holder",
885
+ "changed",
886
+ "cut",
887
+ "failed"
888
+ ];
889
+ var ChatActionStateSchema = z10.enum(["running", "done", "denied", "failed", "refused"]);
890
+ var ChatActionSchema = z10.object({
891
+ action: z10.string().min(1).max(40),
892
+ args: z10.record(z10.unknown()),
893
+ summary: z10.string().min(1).max(300),
894
+ vars: ActionVarsSchema.optional(),
895
+ detail: z10.string().max(DETAIL_MAX).optional(),
896
+ detailCut: z10.literal(true).optional(),
897
+ risk: ActionRiskSchema,
898
+ session: z10.string().optional(),
899
+ team: z10.string().optional(),
900
+ state: ChatActionStateSchema.optional(),
901
+ doneAt: z10.string().datetime().optional(),
902
+ error: z10.string().optional(),
903
+ code: z10.string().max(40).optional()
904
+ });
905
+ var ChatActionsSchema = z10.array(ChatActionSchema).min(1).max(MAX_PROPOSALS);
906
+ var ChatTurnSchema = z10.object({
907
+ machineId: z10.string().optional(),
908
+ machineName: z10.string().optional(),
909
+ choices: z10.array(z10.object({ id: z10.string(), label: z10.string() })).optional(),
910
+ qid: z10.string().min(1),
911
+ question: z10.string(),
912
+ askedAt: z10.string().datetime(),
913
+ steps: z10.array(ChatStepSchema),
914
+ answer: z10.string().optional(),
915
+ answeredAt: z10.string().datetime().optional(),
916
+ error: z10.string().optional(),
917
+ proposal: ChatProposalSchema.optional(),
918
+ proposals: ChatProposalsSchema.optional(),
919
+ relay: ChatRelaySchema.optional(),
920
+ closes: ChatClosesSchema.optional(),
921
+ projectProposal: ChatProjectProposalSchema.optional(),
922
+ team: ChatTeamProposalSchema.optional(),
923
+ tasks: ChatTasksSchema.optional(),
924
+ actions: ChatActionsSchema.optional(),
925
+ ignoredAt: z10.string().datetime().optional(),
926
+ attachments: z10.array(AttachmentRefSchema).optional()
927
+ });
928
+ var proposalRows = (t) => t?.proposals ?? (t?.proposal ? [t.proposal] : []);
929
+
930
+ // ../protocol/src/repos.ts
931
+ import { z as z11 } from "zod";
932
+ var ReposInspectSchema = z11.object({
933
+ type: z11.literal("repos.inspect"),
934
+ requestId: z11.string().min(1).max(100),
935
+ paths: z11.array(z11.string().min(1)).min(1).max(50),
936
+ machineId: z11.string().optional()
937
+ });
938
+ var RepoInfoSchema = z11.object({ path: z11.string(), exists: z11.boolean(), branch: z11.string().nullable() });
939
+ var ReposInfoSchema = z11.object({ type: z11.literal("repos.info"), requestId: z11.string(), repos: z11.array(RepoInfoSchema) });
940
+
941
+ // ../protocol/src/scan.ts
942
+ import { z as z12 } from "zod";
943
+ var ReposScanSchema = z12.object({ type: z12.literal("repos.scan"), requestId: z12.string().min(1).max(100), machineId: z12.string().optional() });
944
+ var FoundRepoSchema = z12.object({ path: z12.string(), branch: z12.string().nullable(), remote: z12.string().optional(), manifest: z12.string().max(200).optional() });
945
+ var ReposFoundSchema = z12.object({ type: z12.literal("repos.found"), requestId: z12.string(), repos: z12.array(FoundRepoSchema).max(500), truncated: z12.boolean(), error: z12.string().optional() });
946
+
947
+ // ../protocol/src/documents.ts
948
+ import { z as z13 } from "zod";
949
+ var DOCUMENT_MAX_BYTES = 1048576;
950
+ var path = z13.string().min(1).max(4096);
951
+ var requestId2 = z13.string().min(1);
952
+ var DocumentReadSchema = z13.object({
953
+ type: z13.literal("document.read"),
954
+ requestId: requestId2,
955
+ path,
956
+ machineId: z13.string().optional(),
957
+ sessionId: z13.string().min(1).optional(),
958
+ basePath: path.optional()
959
+ });
960
+ var DocumentContentSchema = z13.object({
961
+ type: z13.literal("document.content"),
962
+ requestId: requestId2,
963
+ path,
964
+ text: z13.string(),
965
+ bytes: z13.number().int().nonnegative(),
966
+ modifiedAt: z13.string().datetime()
967
+ });
968
+ var DocumentErrorCodeSchema = z13.enum([
969
+ "not_found",
970
+ "not_file",
971
+ "unsupported_path",
972
+ "unsupported_type",
973
+ "permission_denied",
974
+ "too_large",
975
+ "invalid_utf8",
976
+ "ambiguous_path",
977
+ "session_missing",
978
+ "read_failed"
979
+ ]);
980
+ var DocumentErrorSchema = z13.object({
981
+ type: z13.literal("document.error"),
982
+ requestId: requestId2,
983
+ path,
984
+ code: DocumentErrorCodeSchema
985
+ });
986
+
987
+ // ../protocol/src/routing.ts
988
+ import { z as z14 } from "zod";
989
+ var MachineInventorySchema = z14.object({
990
+ id: z14.string().min(1).max(80),
991
+ label: z14.string().min(1).max(120),
992
+ online: z14.boolean(),
993
+ local: z14.boolean(),
994
+ sessions: z14.array(z14.object({ id: z14.string(), name: z14.string(), project: z14.string(), cwd: z14.string(), topic: z14.string().optional(), state: z14.string(), readOnly: z14.boolean() })).max(500),
995
+ folders: z14.array(z14.object({ project: z14.string(), cwd: z14.string() })).max(100)
996
+ });
997
+ var RoutingContextSchema = z14.object({
998
+ machines: z14.array(MachineInventorySchema).min(1).max(20),
999
+ history: z14.array(z14.object({ question: z14.string().max(2e4), answer: z14.string().max(2e4).optional(), machineId: z14.string().optional() })).max(50)
1000
+ });
1001
+ var RouteResultSchema = z14.object({
1002
+ /** Null means an answer or a clarification, never an implicit local destination. */
1003
+ machineId: z14.string().nullable(),
1004
+ text: z14.string().min(1).max(2e4),
1005
+ choices: z14.array(z14.string()).max(20).optional()
1006
+ });
1007
+
1008
+ // ../protocol/src/ssh.ts
1009
+ import { z as z15 } from "zod";
1010
+ var SshAliasSchema = z15.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/);
1011
+
1012
+ // ../protocol/src/machines.ts
1013
+ import { z as z16 } from "zod";
1014
+ var MachineIdSchema = z16.string().regex(/^[a-zA-Z0-9_-]{1,64}$/);
1015
+ var MachineSchema = z16.object({
1016
+ id: MachineIdSchema,
1017
+ label: z16.string().trim().min(1).max(80),
1018
+ ssh: SshAliasSchema.optional()
1019
+ });
1020
+ var MachinesSchema = z16.array(MachineSchema).min(1).max(20).superRefine((list, ctx) => {
1021
+ if (list[0]?.id !== "local" || list[0].ssh !== void 0) ctx.addIssue({ code: "custom", message: "the first machine is local, without ssh", path: [0] });
1022
+ const ids = /* @__PURE__ */ new Set(), aliases = /* @__PURE__ */ new Set();
1023
+ list.forEach((m, i) => {
1024
+ if (i > 0 && !m.ssh) ctx.addIssue({ code: "custom", message: "a remote machine needs an ssh alias", path: [i, "ssh"] });
1025
+ if (ids.has(m.id)) ctx.addIssue({ code: "custom", message: `duplicate id ${m.id}`, path: [i, "id"] });
1026
+ ids.add(m.id);
1027
+ if (m.ssh) {
1028
+ if (aliases.has(m.ssh)) ctx.addIssue({ code: "custom", message: `duplicate ssh alias ${m.ssh}`, path: [i, "ssh"] });
1029
+ aliases.add(m.ssh);
1030
+ }
1031
+ });
1032
+ });
1033
+
1034
+ // ../protocol/src/island.ts
1035
+ import { z as z17 } from "zod";
1036
+ var IslandScaleSchema = z17.union([z17.literal("auto"), z17.literal(1), z17.literal(1.25), z17.literal(1.5), z17.literal(2)]);
1037
+ var IslandPrefsSchema = z17.object({
1038
+ /** Shown at the top of the screen; hidden, Desktop still notifies. */
1039
+ visible: z17.boolean(),
1040
+ /** `primary` (whichever it is after a reconfiguration), or a monitor's id as the shell lists it (`list_monitors`: the OS
1041
+ * name, `#index` without one). `second`, written by older fronts, is the first monitor that is not the primary. */
1042
+ screen: z17.string().trim().min(1).max(200),
1043
+ position: z17.enum(["center", "left", "right"]),
1044
+ /** A strip under a maximised window. */
1045
+ discreet: z17.boolean(),
1046
+ islandScale: IslandScaleSchema,
1047
+ panelScale: IslandScaleSchema,
1048
+ /** What the island shows besides asks and questions, which it always shows. */
1049
+ shows: z17.object({
1050
+ /** Allow, deny or answer in a line from the island; off, its cards only say « Open in Ledge ». */
1051
+ quick: z17.boolean(),
1052
+ /** The 6 s bubble when an agent finishes. */
1053
+ finished: z17.boolean(),
1054
+ /** An agent's sentence in a bubble. */
1055
+ say: z17.boolean(),
1056
+ /** The sub-agents next to their main agent. */
1057
+ helpers: z17.boolean()
1058
+ }),
1059
+ /** The global shortcut that opens the island, as shown (`Ctrl Alt L`). */
1060
+ shortcut: z17.string().trim().min(1).max(40)
1061
+ });
1062
+
1063
+ // ../protocol/src/projects.ts
1064
+ import { z as z18 } from "zod";
1065
+ var PROJECT_COLORS = { blue: "#3b82f6", green: "#10b981", amber: "#f59e0b", violet: "#8b5cf6", teal: "#14b8a6", pink: "#ec4899", red: "#ef4444", slate: "#64748b" };
1066
+ var ProjectColorSchema = z18.enum(["blue", "green", "amber", "violet", "teal", "pink", "red", "slate"]);
1067
+ var ProjectIdSchema = z18.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/);
1068
+ function normalizePath(raw) {
1069
+ const p = raw.trim().replace(/\/+/g, "/");
1070
+ return p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
1071
+ }
1072
+ var PathSchema = z18.string().max(4096).refine((p) => p.startsWith("/") && p === normalizePath(p), "an absolute, normalized path");
1073
+ var RepoSchema = z18.object({ machine: MachineIdSchema, path: PathSchema });
1074
+ var ProjectSchema = z18.object({
1075
+ id: ProjectIdSchema,
1076
+ name: z18.string().trim().min(1).max(80),
1077
+ color: ProjectColorSchema,
1078
+ description: z18.string().max(500).default(""),
1079
+ repos: z18.array(RepoSchema).max(50)
1080
+ });
1081
+ var RuleSchema = z18.object({ machine: MachineIdSchema, path: PathSchema, project: ProjectIdSchema });
1082
+ function projectsProblems(p) {
1083
+ const out = [];
1084
+ const ids = /* @__PURE__ */ new Set(), names = /* @__PURE__ */ new Set(), repos = /* @__PURE__ */ new Set(), rules = /* @__PURE__ */ new Set();
1085
+ p.projects.forEach((project, i) => {
1086
+ if (ids.has(project.id)) out.push({ code: "duplicate_id", path: ["projects", i, "id"] });
1087
+ ids.add(project.id);
1088
+ const name = project.name.trim().toLocaleLowerCase();
1089
+ if (names.has(name)) out.push({ code: "duplicate_name", path: ["projects", i, "name"] });
1090
+ names.add(name);
1091
+ project.repos.forEach((repo, j) => {
1092
+ const key = `${repo.machine}
1093
+ ${repo.path}`;
1094
+ if (repos.has(key)) out.push({ code: "duplicate_repo", path: ["projects", i, "repos", j] });
1095
+ repos.add(key);
1096
+ });
1097
+ });
1098
+ p.rules.forEach((rule, i) => {
1099
+ const key = `${rule.machine}
1100
+ ${rule.path}`;
1101
+ if (rules.has(key)) out.push({ code: "duplicate_rule", path: ["rules", i] });
1102
+ rules.add(key);
1103
+ if (!ids.has(rule.project)) out.push({ code: "unknown_project", path: ["rules", i, "project"] });
1104
+ });
1105
+ return out;
1106
+ }
1107
+ var ProjectsSchema = z18.object({ projects: z18.array(ProjectSchema).max(100), rules: z18.array(RuleSchema).max(1e3) }).superRefine((p, ctx) => {
1108
+ for (const problem of projectsProblems(p)) ctx.addIssue({ code: "custom", message: problem.code, path: problem.path });
1109
+ });
1110
+ var DEFAULT_PROJECTS = { projects: [], rules: [] };
1111
+ var under = (cwd, path2) => path2 === "/" || cwd === path2 || cwd.startsWith(`${path2}/`);
1112
+ function projectOf(session, state) {
1113
+ const machine = session.machineId ?? "local";
1114
+ const cwd = normalizePath(session.cwd);
1115
+ const known = new Set(state.projects.map((p) => p.id));
1116
+ const candidates = [
1117
+ ...state.projects.flatMap((p) => p.repos.filter((r) => r.machine === machine).map((r) => ({ project: p.id, path: r.path, rule: false }))),
1118
+ ...state.rules.filter((r) => r.machine === machine && known.has(r.project)).map((r) => ({ project: r.project, path: r.path, rule: true }))
1119
+ ].filter((c) => under(cwd, c.path));
1120
+ candidates.sort((x, y) => y.path.length - x.path.length || Number(y.rule) - Number(x.rule));
1121
+ return candidates[0]?.project ?? null;
1122
+ }
1123
+
1124
+ // ../protocol/src/thread.ts
1125
+ import { z as z19 } from "zod";
1126
+ var ToolStatusSchema = z19.enum(["running", "success", "failed", "unknown"]);
1127
+ var FileChangeSchema = z19.object({ path: z19.string().min(1), from: z19.string().min(1).optional(), change: z19.enum(["add", "edit", "delete", "write"]), diff: z19.string().optional() });
1128
+ var base = { id: z19.string().min(1), at: z19.string().datetime(), turn: z19.number().int().nonnegative(), helperId: z19.string().optional() };
1129
+ var ThreadItemSchema = z19.discriminatedUnion("kind", [
1130
+ z19.object({ kind: z19.literal("prompt"), ...base, text: z19.string(), images: z19.array(AttachmentRefSchema).optional() }),
1131
+ z19.object({ kind: z19.literal("message"), ...base, text: z19.string() }),
1132
+ /** `id` is the harness's tool_use_id when it gives one, so the result updates the same row; `byClose`: a question
1133
+ * dismissed by Ledge closing the session. */
1134
+ z19.object({ kind: z19.literal("tool"), ...base, tool: z19.string(), label: z19.string(), input: z19.string().optional(), output: z19.string().optional(), error: z19.string().optional(), status: ToolStatusSchema, durationMs: z19.number().nonnegative().optional(), files: z19.array(FileChangeSchema).default([]), byClose: z19.literal(true).optional() }),
1135
+ /** `unknown`: answered in the terminal, the daemon only saw the prompt go away; `always`: allowed and not asked again;
1136
+ * `byClose`: denied by Ledge closing the session. */
1137
+ z19.object({ kind: z19.literal("permission"), ...base, tool: z19.string(), text: z19.string(), decision: z19.enum(["accept", "deny", "always", "unknown"]).optional(), byClose: z19.literal(true).optional() }),
1138
+ z19.object({ kind: z19.literal("reply"), ...base, text: z19.string() }),
1139
+ z19.object({ kind: z19.literal("helper"), ...base, task: z19.string(), agentType: z19.string().optional(), state: z19.enum(["working", "exited"]) }),
1140
+ /** 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. */
1141
+ z19.object({ kind: z19.literal("sent"), ...base, to: z19.string(), peer: z19.string().optional(), summary: z19.string().optional(), text: z19.string(), status: z19.enum(["sending", "delivered", "read", "failed"]), received: z19.string().optional(), via: z19.literal("ledge").optional() }),
1142
+ /** 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. */
1143
+ z19.object({ kind: z19.literal("received"), ...base, from: z19.string(), peer: z19.string().optional(), text: z19.string(), sent: z19.string().optional() }),
1144
+ /** An automated notice of the agent's harness about messages between sessions (Claude Code's `[Cross-session idle
1145
+ * notice]` / `[Cross-session delivery notice]`): a system event, never the user's words. `name` is the session it is
1146
+ * about as quoted, `peer` its Ledge session id; a delivery notice says the outcome of `count` messages to `recipients`. */
1147
+ z19.object({
1148
+ kind: z19.literal("notice"),
1149
+ ...base,
1150
+ notice: z19.enum(["idle", "exited", "unavailable", "expired", "delivery", "other"]),
1151
+ /** An `other` notice from Ledge: `task`, the task a `<ledge-task>` is about; `team`, the team a `<ledge-notice team>` seated the
1152
+ * session in (roles spec §3.3). An older front, which knows neither, shows it as any other notice. */
1153
+ task: z19.string().optional(),
1154
+ team: z19.string().optional(),
1155
+ /** A task this session handed over (`outgoing`), and how (issue #92). */
1156
+ taskOp: z19.enum(TRANSMISSION_OPS).optional(),
1157
+ outgoing: z19.literal(true).optional(),
1158
+ name: z19.string().optional(),
1159
+ peer: z19.string().optional(),
1160
+ time: z19.string().optional(),
1161
+ summary: z19.string().optional(),
1162
+ status: z19.enum(["held", "denied", "expired", "delivered", "refused", "dropped", "unknown"]).optional(),
1163
+ count: z19.number().int().positive().optional(),
1164
+ recipients: z19.array(z19.string()).optional(),
1165
+ reason: z19.string().optional(),
1166
+ text: z19.string()
1167
+ })
1168
+ ]);
1169
+
1170
+ // ../protocol/src/mobile.ts
1171
+ import { z as z20 } from "zod";
1172
+ var DEFAULT_RELAY_URL = "https://app.kl-ledge.dev";
1173
+ var RelayUrlSchema = z20.string().transform((v, ctx) => {
1174
+ const bad = () => {
1175
+ ctx.addIssue({ code: z20.ZodIssueCode.custom, message: "invalid relay URL" });
1176
+ return z20.NEVER;
1177
+ };
1178
+ let u;
1179
+ try {
1180
+ u = new URL(v.replace(/\/$/, ""));
1181
+ } catch {
1182
+ return bad();
1183
+ }
1184
+ const local = u.hostname === "localhost" || u.hostname === "127.0.0.1";
1185
+ if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) return bad();
1186
+ if (u.username || u.password || u.pathname !== "/" || u.search || u.hash) return bad();
1187
+ return u.origin;
1188
+ });
1189
+ var MobileConfigSchema = z20.object({ enabled: z20.boolean(), relayUrl: RelayUrlSchema });
1190
+ var MobileLinkStateSchema = z20.discriminatedUnion("state", [
1191
+ z20.object({ state: z20.literal("off") }),
1192
+ z20.object({ state: z20.literal("connecting") }),
1193
+ z20.object({ state: z20.literal("online") }),
1194
+ z20.object({ state: z20.literal("offline"), retryAt: z20.string().datetime(), error: z20.string() }),
1195
+ z20.object({ state: z20.literal("signed_out"), reason: z20.enum(["other_pc", "session_invalid"]) }),
1196
+ /** The mobile service did not start (a broken mobile.json, a missing key store): nothing is linked until the daemon restarts. */
1197
+ z20.object({ state: z20.literal("unavailable"), error: z20.string().max(200) })
1198
+ ]);
1199
+ var MobilePairingSchema = z20.object({
1200
+ id: z20.string(),
1201
+ label: z20.string(),
1202
+ code: z20.string().nullable(),
1203
+ expiresAt: z20.string().datetime(),
1204
+ state: z20.enum(["waiting", "confirm", "approved", "linked", "expired", "refused"])
1205
+ });
1206
+ var MobileStateSchema = z20.object({
1207
+ enabled: z20.boolean(),
1208
+ relayUrl: z20.string(),
1209
+ account: z20.object({ login: z20.string(), accountId: z20.string() }).nullable(),
1210
+ signin: z20.object({ userCode: z20.string(), verificationUri: z20.string(), expiresAt: z20.string().datetime() }).nullable(),
1211
+ /** The last sign-in that did not end with a session; cleared by the next one. */
1212
+ signinError: z20.enum(["expired", "denied", "not_allowed", "failed"]).nullable(),
1213
+ link: MobileLinkStateSchema,
1214
+ devices: z20.array(z20.object({ id: z20.string(), label: z20.string(), approvedAt: z20.string(), online: z20.boolean(), lastSeenAt: z20.string().optional() })),
1215
+ pairings: z20.array(MobilePairingSchema)
1216
+ });
1217
+
1218
+ // ../protocol/src/messages.ts
1219
+ var PROTOCOL_PORT = 47831;
1220
+ var RefusalCodeSchema = z21.enum(["stale", "read_only", "no_prompt", "not_taken", "busy", "unknown_session", "nothing_to_decide", "not_offered", "free_text", "note_not_typed", "unavailable", "invalid", "attachment_missing", "attachments_not_accepted"]);
1221
+ var SessionEventSchema = z21.object({
1222
+ name: z21.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd", "PaneChanged", "PaneGone", "PanePrompt", "PaneIdle", "InjectFailed", "Decided", "PostToolUse", "PostToolUseFailure", "StopFailure", "SubagentStart", "SubagentStop", "PaneTitle", "Renamed", "NativeRequest", "AgentMessage", "QueueUpdate"]),
1223
+ at: z21.string().datetime(),
1224
+ source: z21.enum(["hook", "pane", "daemon"]),
1225
+ tool: z21.string().optional(),
1226
+ text: z21.string().optional(),
1227
+ options: z21.array(z21.string()).optional(),
1228
+ /** Each option's description, by index ('' when it has none); left out when no option has one. */
1229
+ optionDetails: z21.array(z21.string()).optional(),
1230
+ kind: z21.enum(["question", "permission", "plan", "diff"]).optional(),
1231
+ /** PanePrompt: what to review in full before deciding (a plan read in the terminal), when `text` is its summary. */
1232
+ body: z21.string().optional(),
1233
+ exitCode: z21.number().int().nullable().optional(),
1234
+ helperId: z21.string().optional(),
1235
+ /** The helper's description, once known (Claude Code: its `.meta.json`, written after SubagentStart). */
1236
+ task: z21.string().optional(),
1237
+ /** PostToolUseFailure: the user interrupted the turn (Esc); Claude then waits for input with no Stop. */
1238
+ interrupt: z21.boolean().optional(),
1239
+ /** UserPromptSubmit: the images the prompt carried, by attachment id. */
1240
+ attachments: z21.array(AttachmentIdSchema).max(ATTACHMENTS_MAX).optional(),
1241
+ /** QueueUpdate: OMP's queue as it stands. */
1242
+ queue: z21.array(z21.object({ text: z21.string(), mode: z21.enum(["steer", "follow_up"]) })).optional(),
1243
+ /** AgentMessage: the harness's own message, its text so far (`streaming`) or whole. `messageId` is unique for the session's life. */
1244
+ messageId: z21.string().optional(),
1245
+ streaming: z21.boolean().optional(),
1246
+ /** NativeRequest: the dialog's id, and OMP's `ask` questions when it is one. */
1247
+ requestId: z21.string().optional(),
1248
+ questions: z21.array(AskQuestionSchema).optional(),
1249
+ /** Stop: its text is already in the thread as AgentMessage cards, which the reply card must not repeat. */
1250
+ streamed: z21.boolean().optional(),
1251
+ /** Native tracking (spec 2026-10-13 §2): the harness's id of the tool call, so its result lands on the same row. */
1252
+ toolUseId: z21.string().optional(),
1253
+ /** The tool's input as the harness gave it, every string capped by the hook. */
1254
+ toolInput: z21.record(z21.string(), z21.unknown()).optional(),
1255
+ /** PostToolUse: the result as text; SubagentStop: nothing (the report travels in `text`). */
1256
+ output: z21.string().optional(),
1257
+ durationMs: z21.number().nonnegative().optional(),
1258
+ /** PostToolUseFailure: the harness's error message. */
1259
+ error: z21.string().optional(),
1260
+ /** The session's transcript, or the helper's on SubagentStop; the daemon reads its tail for the agent's messages. */
1261
+ transcriptPath: z21.string().optional(),
1262
+ /** Decided: what was chosen when the daemon knows it. */
1263
+ decision: DecisionSchema.optional(),
1264
+ /** Decided: the request was refused by Ledge closing the session (its interrupt keys), not by the user's answer. */
1265
+ byClose: z21.literal(true).optional(),
1266
+ /** The agent's own session id when the hook names it, so the daemon can resume that conversation. */
1267
+ agentSessionId: z21.string().optional()
1268
+ });
1269
+ var RecentSchema = z21.object({ cwd: z21.string().min(1), project: z21.string().min(1), lastAt: z21.string().datetime() });
1270
+ var SEMVER = /^\d+\.\d+\.\d+$/;
1271
+ var DaemonInfoSchema = z21.object({
1272
+ version: z21.string(),
1273
+ install: z21.enum(["npm", "git", "unknown"]),
1274
+ systemd: z21.boolean(),
1275
+ manual: z21.string().optional(),
1276
+ routing: z21.boolean().optional(),
1277
+ archive: z21.boolean().optional(),
1278
+ /** Takes `model` and `effort` on `session.launch` and answers `models.list` (#58). */
1279
+ models: z21.boolean().optional(),
1280
+ /** Drives OMP over its RPC: launching `omp` sessions is possible when the binary is there (`models.list` says so). */
1281
+ omp: z21.boolean().optional(),
1282
+ /** Takes `session.model`: a live session's model and effort changed (#58, lot 2). */
1283
+ modelSwitch: z21.boolean().optional(),
1284
+ /** Takes `approval` on an OMP `session.launch` (auto mode). */
1285
+ ompApproval: z21.boolean().optional(),
1286
+ /** Its home folder, so a client shows the folders under it as `~` (absent from older daemons). */
1287
+ home: z21.string().optional(),
1288
+ mobile: z21.boolean().optional(),
1289
+ /** Knows roles, teams and tasks given by Ledge (spec 2026-10-25 roles). */
1290
+ teams: z21.boolean().optional()
1291
+ });
1292
+ var UpdateFailureSchema = z21.enum(["git-install", "no-systemd", "sessions-at-risk", "npm-failed", "busy"]);
1293
+ var ServerMessageSchema = z21.discriminatedUnion("type", [
1294
+ z21.object({ type: z21.literal("chat.routed"), qid: z21.string(), result: RouteResultSchema }),
1295
+ DocumentContentSchema,
1296
+ DocumentErrorSchema,
1297
+ AttachmentStoredSchema,
1298
+ AttachmentDataSchema,
1299
+ AttachmentErrorSchema,
1300
+ ReposInfoSchema,
1301
+ ReposFoundSchema,
1302
+ ModelsListingSchema,
1303
+ /** `recents`: the folders of past sessions, most recent first. `launcher`: the command that runs `ledge` inside WSL
1304
+ * (`[node, cli]`), so the overlay can attach a terminal to a session with `<launcher> attach <id>`. `daemon`: absent
1305
+ * from daemons older than the auto-update. */
1306
+ z21.object({ type: z21.literal("snapshot"), sessions: z21.array(SessionSchema), recents: z21.array(RecentSchema).optional(), launcher: z21.array(z21.string()).optional(), daemon: DaemonInfoSchema.optional(), sshAlias: SshAliasSchema.optional() }),
1307
+ z21.object({ type: z21.literal("session.updated"), session: SessionSchema }),
1308
+ /** OMP: the instructions taken back from its queue by an interrupt that restores it; the asker puts them back in its draft. */
1309
+ z21.object({
1310
+ type: z21.literal("session.queue.restored"),
1311
+ id: z21.string(),
1312
+ texts: z21.array(z21.string()),
1313
+ /** The same instructions with the images they carried; `imagesDropped`: some images could not be put back. */
1314
+ entries: z21.array(z21.object({ text: z21.string(), attachments: z21.array(AttachmentIdSchema).optional() })).optional(),
1315
+ imagesDropped: z21.boolean().optional()
1316
+ }),
1317
+ z21.object({ type: z21.literal("session.removed"), id: z21.string() }),
1318
+ z21.object({ type: z21.literal("session.preview"), id: z21.string(), lines: z21.array(z21.string()) }),
1319
+ /** A session's thread, on request (spec 2026-10-13 §5); then each created or updated item, broadcast. */
1320
+ z21.object({ type: z21.literal("session.thread"), id: z21.string(), items: z21.array(ThreadItemSchema) }),
1321
+ z21.object({ type: z21.literal("session.thread.item"), id: z21.string(), item: ThreadItemSchema }),
1322
+ /** Answer to the requester of a `session.launch`: the session exists (its `session.updated` is broadcast as usual). */
1323
+ z21.object({ type: z21.literal("session.launched"), id: z21.string() }),
1324
+ /** Answer to the requester of a `session.close`, once per row closed: the session `id` and how it ended. */
1325
+ z21.object({ type: z21.literal("session.closed"), qid: z21.string(), row: z21.number().int().min(0), id: z21.string(), how: z21.enum(["exit", "killed"]) }),
1326
+ z21.object({ type: z21.literal("recents"), recents: z21.array(RecentSchema) }),
1327
+ /** Answer to the requester of a `folder.list` or `folder.create`: `path` resolved (`~` expanded), `parent` null at the
1328
+ * root, `entries` its subfolders by name (hidden ones left out). A failure keeps the asked path and says why in `error`,
1329
+ * never in an `error` message, which the launch form reads as a failed launch. */
1330
+ z21.object({
1331
+ type: z21.literal("folder.listing"),
1332
+ path: z21.string(),
1333
+ parent: z21.string().nullable(),
1334
+ entries: z21.array(z21.string()),
1335
+ error: z21.string().optional(),
1336
+ /** The request this answers, when it carried one. */
1337
+ requestId: z21.string().optional(),
1338
+ /** Those `entries` that are Git repositories, with their branch (the short commit when HEAD is detached). */
1339
+ repos: z21.array(z21.object({ name: z21.string(), branch: z21.string() })).optional()
1340
+ }),
1341
+ /** 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. */
1342
+ z21.object({ type: z21.literal("chat.step"), qid: z21.string(), step: ChatStepSchema }),
1343
+ z21.object({ type: z21.literal("chat.delta"), qid: z21.string(), text: z21.string() }),
1344
+ z21.object({ type: z21.literal("chat.done"), qid: z21.string(), text: z21.string(), at: z21.string().datetime(), proposal: ChatProposalSchema.optional(), proposals: ChatProposalsSchema.optional(), relay: ChatRelaySchema.optional(), closes: ChatClosesSchema.optional(), projectProposal: ChatProjectProposalSchema.optional(), team: ChatTeamProposalSchema.optional(), tasks: ChatTasksSchema.optional(), actions: ChatActionsSchema.optional() }),
1345
+ /** What a `chat.act` applied: the rows done, and the one that failed with why (the rows after it stay proposed). */
1346
+ z21.object({ type: z21.literal("chat.acted"), rid: z21.string(), qid: z21.string(), done: z21.array(z21.number().int().min(0)), failed: z21.array(z21.object({
1347
+ row: z21.number().int().min(0),
1348
+ message: z21.string(),
1349
+ /** nothing was sent */
1350
+ refused: z21.literal(true).optional()
1351
+ })) }),
1352
+ /** What an `action.run` came to: `row` the action as the daemon resolved it (its summary, its risk); `reason` why it was not
1353
+ * done, with `code` (`refused`: not possible now; `agent`: a session may not run it; `confirm_required`: destructive, ask
1354
+ * again; `failed`: the handler turned it down; a free string, as an `error`'s); `path` the template a `team.save` wrote. */
1355
+ z21.object({ type: z21.literal("action.result"), rid: z21.string(), action: z21.string().min(1).max(40), row: ChatActionSchema.optional(), reason: z21.string().optional(), code: z21.string().optional(), path: z21.string().optional() }),
1356
+ z21.object({ type: z21.literal("chat.error"), qid: z21.string(), message: z21.string() }),
1357
+ z21.object({ type: z21.literal("chat.history"), turns: z21.array(ChatTurnSchema) }),
1358
+ /** The daemon updating itself: npm is running, then the service restarts (the socket closes right after). */
1359
+ z21.object({ type: z21.literal("update.progress"), version: z21.string(), step: z21.enum(["installing", "restarting"]) }),
1360
+ /** Nothing was changed. `detail`: the useful end of npm's output; `command`: what the person can run instead. */
1361
+ z21.object({ type: z21.literal("update.failed"), version: z21.string(), reason: UpdateFailureSchema, detail: z21.string().optional(), command: z21.string().optional() }),
1362
+ /** The shared machines list (spec 2026-10-14 §5), sent to local clients only. */
1363
+ z21.object({ type: z21.literal("machines"), machines: MachinesSchema }),
1364
+ /** The local daemon's projects and folder rules (spec 2026-10-16 build §5), sent to local clients only. */
1365
+ z21.object({ type: z21.literal("projects"), projects: ProjectsSchema }),
1366
+ /** The island's settings shared by the local fronts; null until a front sets them (the island then sends its own). Local clients only. */
1367
+ z21.object({ type: z21.literal("island"), prefs: IslandPrefsSchema.nullable() }),
1368
+ /** Mobile access (lot 3), after the island in each snapshot and on every change. Local clients only. */
1369
+ z21.object({ type: z21.literal("mobile"), state: MobileStateSchema }),
1370
+ /** 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`. */
1371
+ z21.object({ type: z21.literal("message.ack"), rid: z21.string(), outcome: z21.enum(["accepted", "queued", "native", "refused"]), reason: z21.string().optional() }),
1372
+ /** The answer to a `message.read`: the body with its sender, target and time, or the refusal's `reason`. */
1373
+ z21.object({ type: z21.literal("message.body"), id: z21.string(), from: z21.string().optional(), to: z21.string().optional(), at: z21.string().optional(), text: z21.string().optional(), reason: z21.string().optional() }),
1374
+ TaskResultSchema,
1375
+ SessionHandoverResultSchema,
1376
+ ContextPrefsMessageSchema,
1377
+ HandoverResultSchema,
1378
+ TaskUpdatedSchema,
1379
+ RolesListingSchema,
1380
+ TeamsMessageSchema,
1381
+ TeamUpdatedSchema,
1382
+ TeamLaunchedSchema,
1383
+ TeamFailedSchema,
1384
+ TeamSavedSchema,
1385
+ z21.object({
1386
+ type: z21.literal("error"),
1387
+ message: z21.string(),
1388
+ id: z21.string().optional(),
1389
+ /** Le type du message client refusé. */
1390
+ request: z21.string().optional(),
1391
+ /** Ce que le front doit reconnaître au-delà du texte (NOTE_NOT_TYPED) ; une chaîne libre, qu'un front plus ancien ignore. */
1392
+ code: z21.string().optional()
1393
+ })
1394
+ ]);
1395
+ var MESSAGE_MAX = 2e4;
1396
+ var SUMMARY_MAX = 500;
1397
+ var launchRows = z21.array(z21.number().int().min(0).max(MAX_PROPOSALS - 1)).min(1).max(MAX_PROPOSALS).refine((r) => new Set(r).size === r.length, "a row twice");
1398
+ var ClientMessageSchema = z21.discriminatedUnion("type", [
1399
+ z21.object({ type: z21.literal("chat.route"), qid: z21.string().min(1).max(100), text: z21.string().min(1).max(2e4), tool: ToolSchema.optional(), context: RoutingContextSchema }),
1400
+ DocumentReadSchema,
1401
+ AttachmentPutSchema,
1402
+ AttachmentGetSchema,
1403
+ ReposInspectSchema,
1404
+ ReposScanSchema,
1405
+ ModelsListSchema,
1406
+ /** The overlay starts a session: `cwd` is a WSL path (`~` allowed), `name` a preferred bot name (the daemon picks a
1407
+ * free one otherwise), `prompt` the agent's first instruction, `continue` resumes the folder's last conversation.
1408
+ * With `qid` it is the chat card of that turn: the daemon launches the turn's proposal and ignores the other fields
1409
+ * but `rows`, `model` and `effort`. */
1410
+ z21.object({
1411
+ type: z21.literal("session.launch"),
1412
+ tool: ToolSchema,
1413
+ cwd: z21.string().min(1),
1414
+ name: z21.string().optional(),
1415
+ prompt: z21.string().optional(),
1416
+ continue: z21.boolean().optional(),
1417
+ qid: z21.string().min(1).optional(),
1418
+ /** With `qid`: the card's rows to launch (#24), indexes into the turn's `proposals`, each once; the first row without it. */
1419
+ rows: launchRows.optional(),
1420
+ /** Folders in addition to `cwd` (Claude: `--add-dir`); resolved like `cwd`. */
1421
+ dirs: z21.array(z21.string().min(1)).max(20).optional(),
1422
+ /** The agent's model and effort (#58), the agent's own default when left out; with `qid`, for every row launched. */
1423
+ model: ModelIdSchema.optional(),
1424
+ effort: EffortSchema.optional(),
1425
+ /** OMP only: its approval mode, `always-ask` when left out. */
1426
+ approval: OmpApprovalSchema.optional(),
1427
+ /** A role (spec 2026-10-25 roles §3.4): its prompt reaches the agent at every start, its defaults fill what is left out. */
1428
+ role: RoleIdSchema.optional(),
1429
+ /** A live team the new session joins at once, in `role`. */
1430
+ team: TeamIdSchema.optional(),
1431
+ /** Routing by the fleet, removed before the message reaches the daemon. */
1432
+ machineId: z21.string().optional()
1433
+ }),
1434
+ /** The launch form's folder browser: list a folder's subfolders (home when no `path`), or make `name` inside `parent`
1435
+ * and list it. Both are answered with a `folder.listing`. */
1436
+ z21.object({
1437
+ type: z21.literal("folder.list"),
1438
+ path: z21.string().optional(),
1439
+ /** Routing by the fleet, removed before the message reaches the daemon. */
1440
+ machineId: z21.string().optional(),
1441
+ /** Copied into the `folder.listing` that answers; the fleet relays only that listing, from the machine asked. */
1442
+ requestId: z21.string().min(1).max(100).optional()
1443
+ }),
1444
+ z21.object({
1445
+ type: z21.literal("folder.create"),
1446
+ parent: z21.string().min(1),
1447
+ name: z21.string().min(1),
1448
+ /** As for `folder.list`: routing by the fleet, and the request its `folder.listing` answers. */
1449
+ machineId: z21.string().optional(),
1450
+ requestId: z21.string().min(1).max(100).optional()
1451
+ }),
1452
+ /** `steer`: OMP only, redirect the turn running now instead of queueing the text after it. */
1453
+ /** OMP's `ask`: the answers to every question of the dialog `requestId`, in order (`selected` labels, and/or free `text`); `cancel` declines it. */
1454
+ z21.object({ type: z21.literal("session.answer"), id: z21.string(), requestId: z21.string().min(1).max(200), answers: z21.array(z21.object({ id: z21.string(), selected: z21.array(z21.string().max(2e3)).max(100), text: z21.string().max(2e4).optional() })).max(50), cancel: z21.boolean().optional() }),
1455
+ z21.object({ type: z21.literal("session.reply"), id: z21.string(), text: z21.string(), attachments: z21.array(AttachmentIdSchema).max(ATTACHMENTS_MAX).optional(), steer: z21.boolean().optional() }),
1456
+ /** The chat card's instruction (spec 2026-10-12 relay §4): sent now if the session is free, else queued until it is. */
1457
+ z21.object({ type: z21.literal("session.relay"), qid: z21.string().min(1) }),
1458
+ /** Takes the card's queued instruction back out of the queue. */
1459
+ z21.object({ type: z21.literal("session.relay.cancel"), qid: z21.string().min(1) }),
1460
+ /** The chat's generic card (action registry spec §5.1): applies its rows in order (all of them without `rows`), stopping at
1461
+ * the first failure. `confirmed` says the front asked again; refused without it when a row is destructive. */
1462
+ z21.object({
1463
+ type: z21.literal("chat.act"),
1464
+ rid: z21.string().min(1).max(100),
1465
+ qid: z21.string().min(1),
1466
+ rows: launchRows.optional(),
1467
+ confirmed: z21.literal(true).optional(),
1468
+ /** « Refuser »: each row (session.decide only) answers its session's request with deny, whatever its own decision; never
1469
+ * confirmed, and allowed on a request shown cut. The rows become `denied`. */
1470
+ deny: z21.literal(true).optional()
1471
+ }),
1472
+ /** « Ignorer » the turn's cards, for every device and for Ledge (who is told at the next question); `undo` opens them
1473
+ * again (« Revoir »). Refused while one of them is under way. Every client gets the turn's `ignoredAt` through chat.history. */
1474
+ z21.object({ type: z21.literal("chat.ignore"), qid: z21.string().min(1), undo: z21.literal(true).optional() }),
1475
+ /** A registry action run outside the chat (#93, the CLI): the daemon resolves `args` as it does a generic card's row, then
1476
+ * applies it through its own handler. `by` the session that asks, left out for the user; `confirmed` for a destructive one. */
1477
+ z21.object({
1478
+ type: z21.literal("action.run"),
1479
+ rid: z21.string().min(1).max(100),
1480
+ by: z21.string().min(1).optional(),
1481
+ /** `by`'s key (LEDGE_SESSION_KEY) */
1482
+ key: z21.string().max(128).optional(),
1483
+ action: ActionIdSchema,
1484
+ args: z21.record(z21.unknown()),
1485
+ confirmed: z21.literal(true).optional()
1486
+ }),
1487
+ /** The chat's close card: closes the turn's sessions, `rows` (indexes into its `closes`) or all of them. */
1488
+ z21.object({ type: z21.literal("session.close"), qid: z21.string().min(1), rows: launchRows.optional() }),
1489
+ /** `always` and `note` only where the harness offers them (`offers`); a note goes with `deny`, for the agent to read. */
1490
+ z21.object({
1491
+ type: z21.literal("session.decide"),
1492
+ id: z21.string(),
1493
+ decision: DecisionSchema,
1494
+ note: z21.string().trim().min(1).max(NOTE_MAX).optional(),
1495
+ /** 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.
1496
+ * `key` (pendingKey) covers its whole content; older fronts send kind and text only. */
1497
+ expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z21.string().max(64).optional() }).optional()
1498
+ }),
1499
+ z21.object({ type: z21.literal("session.focus"), id: z21.string() }),
1500
+ z21.object({ type: z21.literal("session.kill"), id: z21.string() }),
1501
+ /** `restoreQueue` (OMP): also take back what waits in its queue, returned as `session.queue.restored` to put in the draft. */
1502
+ z21.object({ type: z21.literal("session.interrupt"), id: z21.string(), restoreQueue: z21.boolean().optional() }),
1503
+ /** Removes a finished (exited) or read-only (hook_only) session from the list; refused for a live launcher session. */
1504
+ z21.object({ type: z21.literal("session.dismiss"), id: z21.string() }),
1505
+ /** Puts a session in the archive: it leaves the live list but is kept. */
1506
+ z21.object({ type: z21.literal("session.archive"), id: z21.string() }),
1507
+ /** Takes an archived session back out of the archive, as it was. */
1508
+ z21.object({ type: z21.literal("session.restore"), id: z21.string() }),
1509
+ /** Starts the agent again on an archived session's conversation. */
1510
+ z21.object({ type: z21.literal("session.resume"), id: z21.string() }),
1511
+ /** Changes a live session's model and effort (#58, lot 2), `null` for the agent's own default: the daemon restarts the
1512
+ * agent on the same conversation with them, at once when the session is free, else at its next free moment. */
1513
+ z21.object({ type: z21.literal("session.model"), id: z21.string(), model: ModelIdSchema.nullable(), effort: EffortSchema.nullable() }),
1514
+ /** Deletes a session for good, archived or not. */
1515
+ z21.object({ type: z21.literal("session.delete"), id: z21.string() }),
1516
+ /** 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. */
1517
+ z21.object({ type: z21.literal("session.rename"), id: z21.string(), topic: z21.string().max(500) }),
1518
+ z21.object({ type: z21.literal("session.thread"), id: z21.string() }),
1519
+ z21.object({ type: z21.literal("session.register"), session: SessionSchema }),
1520
+ /** `agent` and `cwd` describe the session when the daemon does not know it yet (a hook from an agent started without ledge). */
1521
+ z21.object({ type: z21.literal("session.event"), id: z21.string(), event: SessionEventSchema, agent: ToolSchema.optional(), cwd: z21.string().optional() }),
1522
+ z21.object({ type: z21.literal("session.unregister"), id: z21.string(), exitCode: z21.number().int().nullable() }),
1523
+ z21.object({ type: z21.literal("snapshot.request") }),
1524
+ z21.object({ type: z21.literal("chat.ask"), qid: z21.string().min(1), text: z21.string(), images: z21.array(ImageUploadSchema).max(ATTACHMENTS_MAX).optional(), tool: ToolSchema.optional() }),
1525
+ z21.object({ type: z21.literal("chat.cancel"), qid: z21.string().min(1) }),
1526
+ /** Starts a new conversation with Ledge: the day's turns are cleared and the next question opens a new harness session. */
1527
+ z21.object({ type: z21.literal("chat.reset") }),
1528
+ /** Installs `@paradow/ledge@version` and restarts the service (spec 2026-10-12 auto-update §6). */
1529
+ z21.object({ type: z21.literal("update.daemon"), version: z21.string().regex(SEMVER) }),
1530
+ /** Replaces the daemon's machines list (first connected front wins); refused unless valid. */
1531
+ z21.object({ type: z21.literal("machines.set"), machines: MachinesSchema }),
1532
+ /** Replaces the local daemon's projects and rules as a whole; refused unless valid. */
1533
+ z21.object({ type: z21.literal("projects.set"), projects: ProjectsSchema }),
1534
+ /** Replaces the island's settings as a whole; refused unless valid. */
1535
+ z21.object({ type: z21.literal("island.set"), prefs: IslandPrefsSchema }),
1536
+ /** Mobile access (lot 3), local fronts only. A changed `relayUrl` drops the relay session. */
1537
+ z21.object({ type: z21.literal("mobile.set"), enabled: z21.boolean(), relayUrl: RelayUrlSchema }),
1538
+ z21.object({ type: z21.literal("mobile.signin") }),
1539
+ z21.object({ type: z21.literal("mobile.signin.cancel") }),
1540
+ z21.object({ type: z21.literal("mobile.signout") }),
1541
+ /** While the link is offline: try again now. */
1542
+ z21.object({ type: z21.literal("mobile.retry") }),
1543
+ z21.object({ type: z21.literal("mobile.pairing.approve"), id: z21.string().min(1).max(100) }),
1544
+ z21.object({ type: z21.literal("mobile.pairing.deny"), id: z21.string().min(1).max(100) }),
1545
+ /** `id`: the device's fingerprint. */
1546
+ z21.object({ type: z21.literal("mobile.device.revoke"), id: z21.string().min(1).max(100) }),
1547
+ /** 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. */
1548
+ z21.object({ type: z21.literal("session.message"), rid: z21.string().min(1), from: z21.string().min(1), to: z21.string().min(1), text: z21.string().min(1).max(MESSAGE_MAX), summary: z21.string().max(SUMMARY_MAX).optional(), intercept: z21.boolean().optional() }),
1549
+ /** `ledge read <id>`: a message's whole body, for its receiver or its sender (`by`, a session id). */
1550
+ z21.object({ type: z21.literal("message.read"), id: z21.string().min(1).max(40), by: z21.string().min(1) }),
1551
+ TaskCmdSchema,
1552
+ SessionHandoverSchema,
1553
+ ContextSetSchema,
1554
+ HandoverBriefSchema,
1555
+ HandoverShowSchema,
1556
+ RolesListSchema,
1557
+ TeamLaunchSchema,
1558
+ TeamJoinSchema,
1559
+ TeamLeaveSchema,
1560
+ TeamResumeSchema,
1561
+ TeamDissolveSchema,
1562
+ TeamSaveSchema
1563
+ ]);
1564
+ function parseClientMessage(raw) {
1565
+ return ClientMessageSchema.parse(raw);
1566
+ }
1567
+ function parseServerMessage(raw) {
1568
+ return ServerMessageSchema.parse(raw);
1569
+ }
1570
+
1571
+ // ../protocol/src/names.ts
1572
+ var NAMES = [
1573
+ "Otto",
1574
+ "Pim",
1575
+ "Nova",
1576
+ "Bix",
1577
+ "Lu",
1578
+ "Max",
1579
+ "Ida",
1580
+ "Rex",
1581
+ "Zoe",
1582
+ "Kit",
1583
+ "Ari",
1584
+ "Bo",
1585
+ "Cal",
1586
+ "Dee",
1587
+ "Eli",
1588
+ "Fay",
1589
+ "Gus",
1590
+ "Hal",
1591
+ "Ivy",
1592
+ "Jax",
1593
+ "Kai",
1594
+ "Lio",
1595
+ "Mo",
1596
+ "Nia",
1597
+ "Oz",
1598
+ "Pax",
1599
+ "Quin",
1600
+ "Rae",
1601
+ "Sol",
1602
+ "Tad",
1603
+ "Uma",
1604
+ "Vic",
1605
+ "Wyn",
1606
+ "Xen",
1607
+ "Yan",
1608
+ "Zia",
1609
+ "Abe",
1610
+ "Bea",
1611
+ "Cy",
1612
+ "Dot",
1613
+ "Eva",
1614
+ "Fin",
1615
+ "Gil",
1616
+ "Hen",
1617
+ "Iro",
1618
+ "Jo",
1619
+ "Kip",
1620
+ "Lex",
1621
+ "Mia",
1622
+ "Ned",
1623
+ "Ola",
1624
+ "Pat",
1625
+ "Rio",
1626
+ "Sam",
1627
+ "Tia",
1628
+ "Ulf",
1629
+ "Val",
1630
+ "Wes",
1631
+ "Yui",
1632
+ "Zed",
1633
+ "Ash",
1634
+ "Bry",
1635
+ "Cleo",
1636
+ "Dax"
1637
+ ];
1638
+ var cap = (s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
1639
+ function pickName(taken, preferred) {
1640
+ const used = new Set(taken);
1641
+ const free = (n) => !used.has(n);
1642
+ const wanted = preferred?.trim();
1643
+ if (wanted) {
1644
+ const base2 = cap(wanted);
1645
+ if (free(base2)) return base2;
1646
+ for (let i = 2; ; i++) if (free(`${base2}-${i}`)) return `${base2}-${i}`;
1647
+ }
1648
+ const first = NAMES.find(free);
1649
+ if (first) return first;
1650
+ for (let i = 2; ; i++) if (free(`${NAMES[0]}-${i}`)) return `${NAMES[0]}-${i}`;
1651
+ }
1652
+
1653
+ // ../protocol/src/topic.ts
1654
+ var TOPIC_MAX = 80;
1655
+ function cleanTopic(text) {
1656
+ const line = text.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0) ?? "";
1657
+ const flat = line.replace(/\s+/g, " ");
1658
+ return flat.length > TOPIC_MAX ? `${flat.slice(0, TOPIC_MAX - 1).trimEnd()}\u2026` : flat;
1659
+ }
1660
+ function sessionTitle(s) {
1661
+ return `${s.name} \xB7 ${s.topic || s.project}`;
1662
+ }
1663
+
1664
+ // ../protocol/src/relay.ts
1665
+ import { z as z22 } from "zod";
1666
+ var SEALED_MAX = 1048576;
1667
+ var RelayPeerSchema = z22.union([z22.literal("pc"), z22.string().regex(/^d_[A-Za-z0-9_-]{16,40}$/)]);
1668
+ var RelayClientMessageSchema = z22.object({
1669
+ type: z22.literal("send"),
1670
+ to: RelayPeerSchema,
1671
+ body: z22.string().max(SEALED_MAX)
1672
+ });
1673
+ var RelayServerMessageSchema = z22.discriminatedUnion("type", [
1674
+ z22.object({ type: z22.literal("welcome"), accountId: z22.string(), self: RelayPeerSchema, online: z22.array(RelayPeerSchema) }),
1675
+ z22.object({ type: z22.literal("deliver"), from: RelayPeerSchema, body: z22.string() }),
1676
+ z22.object({ type: z22.literal("presence"), peer: RelayPeerSchema, online: z22.boolean() }),
1677
+ /**
1678
+ * A frame was refused; `to` names the target when it is known.
1679
+ * Contract: an error concerning a sealed body (`offline`, `too_large`, or a socket closed with 4008
1680
+ * `rate_limited`) means that body was not delivered, so the sequence on that channel is lost: the
1681
+ * sender must re-handshake with that peer before sending to it again.
1682
+ */
1683
+ z22.object({
1684
+ type: z22.literal("error"),
1685
+ code: z22.enum(["bad_frame", "too_large", "bad_target", "offline", "rate_limited"]),
1686
+ to: RelayPeerSchema.optional()
1687
+ })
1688
+ ]);
1689
+ var PAIR_LABEL_MAX = 40;
1690
+ var KeyB64 = z22.string().regex(/^[A-Za-z0-9_-]{87}$/);
1691
+ var Nonce = z22.string().regex(/^[A-Za-z0-9_-]{43}$/);
1692
+ var Fingerprint = z22.string().regex(/^[A-Za-z0-9_-]{22}$/);
1693
+ var SealedFrameSchema = z22.discriminatedUnion("k", [
1694
+ /** `e` is the ephemeral key, `f` the fingerprint of the sender's static key. */
1695
+ z22.object({ k: z22.literal("hello"), e: KeyB64, f: Fingerprint }),
1696
+ z22.object({ k: z22.literal("msg"), s: z22.string() }),
1697
+ z22.object({ k: z22.literal("pair_commit"), c: Nonce, label: z22.string().trim().min(1).max(PAIR_LABEL_MAX) }),
1698
+ z22.object({ k: z22.literal("pair_offer"), pc: KeyB64, n: Nonce }),
1699
+ z22.object({ k: z22.literal("pair_reveal"), d: KeyB64, n: Nonce }),
1700
+ z22.object({ k: z22.literal("pair_ok") }),
1701
+ z22.object({ k: z22.literal("pair_no") }),
1702
+ z22.object({ k: z22.literal("denied") }),
1703
+ z22.object({ k: z22.literal("rekey") })
1704
+ ]);
1705
+
1706
+ // ../protocol/src/relayAuth.ts
1707
+ import { z as z23 } from "zod";
1708
+ var RELAY_CLOSE_SIGNED_OUT = 4001;
1709
+ var DeviceStartResponseSchema = z23.object({
1710
+ deviceCode: z23.string().min(1),
1711
+ userCode: z23.string().min(1),
1712
+ verificationUri: z23.string().min(1),
1713
+ expiresIn: z23.number().int().positive(),
1714
+ interval: z23.number().int().positive()
1715
+ });
1716
+ var DevicePollResponseSchema = z23.discriminatedUnion("status", [
1717
+ z23.object({ status: z23.literal("pending") }),
1718
+ z23.object({ status: z23.literal("slow_down"), interval: z23.number().int().positive() }),
1719
+ /** `token` is the relay session token, shown this once only. */
1720
+ z23.object({ status: z23.literal("ok"), token: z23.string().min(1), accountId: z23.string().min(1), login: z23.string().min(1) }),
1721
+ z23.object({ status: z23.literal("expired") }),
1722
+ z23.object({ status: z23.literal("denied") }),
1723
+ z23.object({ status: z23.literal("not_allowed") })
1724
+ ]);
1725
+ var DeviceRevokeResponseSchema = z23.object({ revoked: z23.number().int().min(0) });
1726
+ var MeResponseSchema = z23.object({
1727
+ accountId: z23.string().min(1),
1728
+ login: z23.string().min(1),
1729
+ peer: RelayPeerSchema,
1730
+ kind: z23.enum(["pc", "device"])
1731
+ });
1732
+
1733
+ // ../protocol/src/phone.ts
1734
+ import { z as z25 } from "zod";
1735
+
1736
+ // ../protocol/src/push.ts
1737
+ import { z as z24 } from "zod";
1738
+ var B64U = z24.string().regex(/^[A-Za-z0-9_-]*$/);
1739
+ var PUSH_HOSTS = ["*.push.apple.com", "fcm.googleapis.com", "updates.push.services.mozilla.com", "*.notify.windows.com"];
1740
+ function pushEndpointAllowed(url) {
1741
+ let u;
1742
+ try {
1743
+ u = new URL(url);
1744
+ } catch {
1745
+ return false;
1746
+ }
1747
+ if (u.protocol !== "https:" || u.port !== "" || u.username !== "" || u.password !== "") return false;
1748
+ const host = u.hostname.toLowerCase();
1749
+ return PUSH_HOSTS.some((h) => {
1750
+ if (!h.startsWith("*.")) return host === h;
1751
+ const suffix = h.slice(1);
1752
+ return host.length > suffix.length && host.endsWith(suffix);
1753
+ });
1754
+ }
1755
+ var PushSendSchema = z24.object({
1756
+ endpoint: z24.string().url().max(1024),
1757
+ body: B64U.max(5462),
1758
+ ttl: z24.number().int().min(0).max(86400),
1759
+ urgency: z24.enum(["normal", "high"]),
1760
+ topic: z24.string().regex(/^[A-Za-z0-9_-]{1,32}$/)
1761
+ });
1762
+ var PushSendResultSchema = z24.object({
1763
+ result: z24.enum(["sent", "gone", "failed"]),
1764
+ status: z24.number().int().optional()
1765
+ });
1766
+ var PushSubscriptionSchema = z24.object({
1767
+ endpoint: z24.string().url().max(1024).refine(pushEndpointAllowed),
1768
+ keys: z24.object({ p256dh: B64U.length(87), auth: B64U.length(22) })
1769
+ });
1770
+ var PushPayloadSchema = z24.object({
1771
+ v: z24.literal(1),
1772
+ kind: z24.enum(["request", "question", "finished", "chat"]),
1773
+ title: z24.string().max(80),
1774
+ body: z24.string().max(120),
1775
+ url: z24.string().regex(/^\/#\/[A-Za-z0-9/%._~-]*$/).max(300),
1776
+ badge: z24.number().int().min(0).max(999),
1777
+ tag: z24.string().regex(/^[A-Za-z0-9_-]{1,32}$/)
1778
+ });
1779
+
1780
+ // ../protocol/src/phone.ts
1781
+ var PHONE_PROTOCOL = 1;
1782
+ var PAD_TIERS = [1024, 4096, 16384, 65536, 262144, 655360];
1783
+ var PLAIN_MAX = 655360;
1784
+ var utf8 = new TextEncoder();
1785
+ function padPlain(json) {
1786
+ const bytes = utf8.encode(json).length;
1787
+ if (bytes > PLAIN_MAX) return null;
1788
+ const tier = PAD_TIERS.find((t) => t >= bytes);
1789
+ if (tier === void 0) return null;
1790
+ return json + " ".repeat(tier - bytes);
1791
+ }
1792
+ var THREAD_ITEMS_MAX = 200;
1793
+ var THREAD_BUDGET = 524288;
1794
+ var THREAD_TEXT_MAX = 8192;
1795
+ var TOOL_TEXT_MAX = 2048;
1796
+ var PENDING_BODY_MAX = 65536;
1797
+ var SNAPSHOT_BODY_MAX = 2048;
1798
+ var RiskSchema = z25.enum(["low", "high"]);
1799
+ var RiskReasonSchema = z25.enum(["question", "read_only_command", "edit_inside_folder", "pushes", "destroys", "deploys", "publishes", "pipes_to_shell", "network", "writes_outside_folder", "sensitive_path", "unsafe_flag", "shell_syntax", "script", "plan", "unknown"]);
1800
+ var OperationIdSchema = z25.string().uuid();
1801
+ var OperationStatusSchema = z25.enum(["received", "applied", "rejected", "failed"]);
1802
+ var OperationReasonSchema = z25.enum([...RefusalCodeSchema.options, "untypable", "confirm_required", "conflict", "assertion_invalid", "assertion_expired"]);
1803
+ var CapabilitiesSchema = z25.object({
1804
+ /** A question can be answered in free text. */
1805
+ freeTextAnswer: z25.boolean(),
1806
+ /** The session's accept/deny can be typed; absent = true. */
1807
+ decide: z25.boolean().optional(),
1808
+ /** OMP's dialog questions can be answered from the phone; absent = false. */
1809
+ questions: z25.boolean().optional()
1810
+ });
1811
+ var UNTYPABLE = /[\u0000-\u001f\u007f-\u009f\u200b\u200e\u200f\u2028-\u202e\u2066-\u2069\ufeff]/;
1812
+ var CapabilitiesByToolSchema = z25.object({ claude: CapabilitiesSchema, codex: CapabilitiesSchema, cursor: CapabilitiesSchema, omp: CapabilitiesSchema });
1813
+ var PhonePendingSchema = PendingSchema.extend({ key: z25.string().max(64), risk: RiskSchema, riskReason: RiskReasonSchema, cut: z25.literal(true).optional() });
1814
+ var PhoneSessionSchema = SessionSchema.pick({
1815
+ id: true,
1816
+ name: true,
1817
+ tool: true,
1818
+ project: true,
1819
+ title: true,
1820
+ topic: true,
1821
+ repo: true,
1822
+ branch: true,
1823
+ worktree: true,
1824
+ state: true,
1825
+ helpers: true,
1826
+ lastReply: true,
1827
+ startedAt: true,
1828
+ lastActivityAt: true,
1829
+ /** Its role and its live team (ledge home spec §2.4): the agents screen groups a project's sessions by table. */
1830
+ role: true,
1831
+ roleLabel: true,
1832
+ team: true,
1833
+ /** Its context and its relief (context handover spec §3.2): the phone shows the gauge; the relief is started from the PC. */
1834
+ context: true,
1835
+ relieves: true
1836
+ }).extend({ pending: PhonePendingSchema.optional(), unread: z25.boolean(), handover: z25.object({ state: HandoverStateSchema, toName: z25.string().max(80).optional() }).strict().optional() });
1837
+ var B64U2 = z25.string().regex(/^[A-Za-z0-9_-]*$/);
1838
+ var CredentialSchema = z25.object({
1839
+ id: B64U2.max(1366),
1840
+ publicKey: B64U2.max(2048),
1841
+ alg: z25.union([z25.literal(-7), z25.literal(-257)]),
1842
+ clientDataJSON: B64U2.max(4096),
1843
+ authenticatorData: B64U2.max(4096),
1844
+ transports: z25.array(z25.string().max(20)).max(8).optional()
1845
+ });
1846
+ var AssertionSchema = z25.object({
1847
+ id: B64U2.max(1366),
1848
+ clientDataJSON: B64U2.max(4096),
1849
+ authenticatorData: B64U2.max(4096),
1850
+ signature: B64U2.max(1024)
1851
+ });
1852
+ var ConfirmPurposeSchema = z25.enum(["register", "decide", "launch", "close", "act"]);
1853
+ var PhoneChatStepSchema = ChatStepSchema.pick({ kind: true, id: true, name: true }).strict();
1854
+ var PhoneChatProposalSchema = z25.object({
1855
+ project: z25.string().max(80),
1856
+ tool: ToolSchema,
1857
+ prompt: z25.string().max(TOOL_TEXT_MAX).optional(),
1858
+ launchedId: z25.string().optional(),
1859
+ failed: z25.literal(true).optional()
1860
+ }).strict();
1861
+ var PhoneChatRelaySchema = z25.object({
1862
+ id: z25.string().min(1),
1863
+ name: z25.string(),
1864
+ tool: ToolSchema,
1865
+ text: z25.string().max(TOOL_TEXT_MAX),
1866
+ status: z25.enum(["queued", "sent"]).optional(),
1867
+ sentAt: z25.string().datetime().optional(),
1868
+ failed: z25.literal(true).optional()
1869
+ }).strict();
1870
+ var PhoneChatCloseSchema = z25.object({
1871
+ id: z25.string().min(1),
1872
+ name: z25.string(),
1873
+ tool: ToolSchema,
1874
+ how: z25.enum(["exit", "killed"]).optional(),
1875
+ closedAt: z25.string().datetime().optional(),
1876
+ failed: z25.literal(true).optional()
1877
+ }).strict();
1878
+ var PhoneChatTeamSchema = z25.object({
1879
+ project: z25.string().max(80),
1880
+ name: z25.string().max(60),
1881
+ template: z25.string().max(64).optional(),
1882
+ seats: z25.array(z25.object({ role: z25.string().max(40), label: z25.string().max(40), tool: ToolSchema, model: z25.string().max(200).optional(), effort: z25.string().max(20).optional(), autonomy: AutonomySchema.optional() }).strict()).min(1).max(8),
1883
+ teamId: z25.string().max(40).optional(),
1884
+ launchedAt: z25.string().datetime().optional(),
1885
+ failed: z25.literal(true).optional()
1886
+ }).strict();
1887
+ var PhoneChatTaskSchema = z25.object({
1888
+ to: z25.string().max(80),
1889
+ toName: z25.string().max(80).optional(),
1890
+ title: z25.string().max(200),
1891
+ text: z25.string().max(TOOL_TEXT_MAX).optional(),
1892
+ taskId: z25.string().max(40).optional(),
1893
+ ownerName: z25.string().max(80).optional(),
1894
+ state: z25.enum(["open", "taken", "done", "refused", "dropped"]).optional(),
1895
+ failed: z25.literal(true).optional()
1896
+ }).strict();
1897
+ var PhoneChatActionSchema = z25.object({
1898
+ action: z25.string().min(1).max(40),
1899
+ summary: z25.string().max(300),
1900
+ vars: ActionVarsSchema.optional(),
1901
+ detail: z25.string().max(TOOL_TEXT_MAX).optional(),
1902
+ detailCut: z25.literal(true).optional(),
1903
+ risk: ActionRiskSchema,
1904
+ session: z25.string().max(100).optional(),
1905
+ team: z25.string().max(40).optional(),
1906
+ state: ChatActionStateSchema.optional(),
1907
+ doneAt: z25.string().datetime().optional(),
1908
+ code: z25.string().max(40).optional()
1909
+ }).strict();
1910
+ var PhoneTeamSchema = z25.object({
1911
+ id: z25.string().max(40),
1912
+ name: z25.string().max(60),
1913
+ project: z25.string().max(80).nullable(),
1914
+ state: z25.enum(["active", "resting"]),
1915
+ seats: z25.array(z25.object({ role: z25.string().max(40), sessionId: z25.string().max(100).nullable() }).strict()).max(16),
1916
+ done: z25.number().int().min(0),
1917
+ given: z25.number().int().min(0)
1918
+ }).strict();
1919
+ var PhoneTaskSchema = z25.object({
1920
+ id: z25.string().max(40),
1921
+ title: z25.string().max(200),
1922
+ owner: z25.string().max(100),
1923
+ ownerName: z25.string().max(80),
1924
+ ownerRole: z25.string().max(40).optional(),
1925
+ team: z25.string().max(40).optional(),
1926
+ teamName: z25.string().max(60).optional(),
1927
+ state: z25.enum(["open", "taken", "done", "refused", "dropped"]),
1928
+ result: z25.string().max(TOOL_TEXT_MAX).optional(),
1929
+ cut: z25.literal(true).optional(),
1930
+ at: z25.string(),
1931
+ updatedAt: z25.string(),
1932
+ seen: z25.literal(true).optional()
1933
+ }).strict();
1934
+ var ChatErrorCodeSchema = z25.enum(["busy", "timeout", "cancelled", "failed"]);
1935
+ var PhoneChatTurnSchema = z25.object({
1936
+ qid: OperationIdSchema,
1937
+ question: z25.string().max(MESSAGE_MAX),
1938
+ askedAt: z25.string().datetime(),
1939
+ steps: z25.array(PhoneChatStepSchema).max(50),
1940
+ answer: z25.string().max(THREAD_TEXT_MAX).optional(),
1941
+ answeredAt: z25.string().datetime().optional(),
1942
+ error: ChatErrorCodeSchema.optional(),
1943
+ proposals: z25.array(PhoneChatProposalSchema).max(MAX_PROPOSALS).optional(),
1944
+ relay: PhoneChatRelaySchema.optional(),
1945
+ closes: z25.array(PhoneChatCloseSchema).min(1).max(MAX_PROPOSALS).optional(),
1946
+ cut: z25.literal(true).optional(),
1947
+ team: PhoneChatTeamSchema.optional(),
1948
+ tasks: z25.array(PhoneChatTaskSchema).min(1).max(MAX_PROPOSALS).optional(),
1949
+ actions: z25.array(PhoneChatActionSchema).min(1).max(MAX_PROPOSALS).optional(),
1950
+ ignoredAt: z25.string().datetime().optional(),
1951
+ attachments: z25.array(AttachmentRefSchema).max(ATTACHMENTS_MAX).optional()
1952
+ }).strict();
1953
+ var CHAT_HISTORY_BUDGET = 393216;
1954
+ var sessionId = z25.string().min(1).max(100);
1955
+ var PhoneExpectSchema = PendingSchema.pick({ kind: true, text: true }).extend({ key: z25.string().min(1).max(64) });
1956
+ var CardRowsSchema = z25.array(z25.number().int().min(0).max(MAX_PROPOSALS - 1)).min(1).max(MAX_PROPOSALS).refine((r) => new Set(r).size === r.length, "a row twice");
1957
+ var PhoneClientMessageSchema = z25.discriminatedUnion("type", [
1958
+ z25.object({ type: z25.literal("snapshot.request") }),
1959
+ z25.object({ type: z25.literal("session.thread"), id: sessionId }),
1960
+ /** The session's request with its whole body (up to `PENDING_BODY_MAX`). */
1961
+ z25.object({ type: z25.literal("session.pending"), id: sessionId }),
1962
+ /** The session whose terminal the phone shows; null when it shows none. */
1963
+ z25.object({ type: z25.literal("session.watch"), id: sessionId.nullable() }),
1964
+ z25.object({ type: z25.literal("session.read"), id: sessionId, at: z25.string().datetime() }),
1965
+ /** Asks for a challenge. One object for both purposes: the gateway refuses a `decide` without `id` and `expect`. */
1966
+ z25.object({ type: z25.literal("confirm.prepare"), purpose: ConfirmPurposeSchema, id: sessionId.optional(), expect: PhoneExpectSchema.optional(), qid: OperationIdSchema.optional() }),
1967
+ z25.object({ type: z25.literal("credential.register"), op: OperationIdSchema, credential: CredentialSchema }),
1968
+ z25.object({
1969
+ type: z25.literal("session.decide"),
1970
+ op: OperationIdSchema,
1971
+ id: sessionId,
1972
+ decision: DecisionSchema,
1973
+ note: z25.string().trim().min(1).max(NOTE_MAX).optional(),
1974
+ /** The request the phone showed: refused unless it is still the pending one. */
1975
+ expect: PhoneExpectSchema,
1976
+ /** The device check over the challenge of `confirm.prepare`; required by the daemon for a high-risk accept. */
1977
+ assertion: AssertionSchema.optional()
1978
+ }),
1979
+ z25.object({
1980
+ type: z25.literal("session.reply"),
1981
+ op: OperationIdSchema,
1982
+ id: sessionId,
1983
+ text: z25.string().max(MESSAGE_MAX),
1984
+ attachments: z25.array(AttachmentIdSchema).max(ATTACHMENTS_MAX).optional(),
1985
+ /** The request the reply answers, or null when the phone showed none: refused if the session's request is no longer that one. */
1986
+ expect: PhoneExpectSchema.nullable(),
1987
+ assertion: AssertionSchema.optional()
1988
+ }),
1989
+ /** OMP's `ask`: the answers to every question of the dialog `requestId`, in order; `cancel` declines it. Bounds as the local `session.answer`. */
1990
+ z25.object({
1991
+ type: z25.literal("session.answer"),
1992
+ op: OperationIdSchema,
1993
+ id: sessionId,
1994
+ requestId: z25.string().min(1).max(200),
1995
+ answers: z25.array(z25.object({ id: z25.string().max(200), selected: z25.array(z25.string().max(2e3)).max(100), text: z25.string().max(2e4).optional() })).max(50),
1996
+ cancel: z25.literal(true).optional(),
1997
+ expect: PhoneExpectSchema
1998
+ }),
1999
+ z25.object({ type: z25.literal("session.interrupt"), op: OperationIdSchema, id: sessionId }),
2000
+ /** The phone drops its passkey from the daemon. */
2001
+ z25.object({ type: z25.literal("device.forget"), op: OperationIdSchema }),
2002
+ /** The phone's push subscription, with the VAPID key it was made with (so a key change is seen). */
2003
+ z25.object({ type: z25.literal("push.subscribe"), op: OperationIdSchema, subscription: PushSubscriptionSchema, key: B64U2.length(87) }),
2004
+ z25.object({ type: z25.literal("push.unsubscribe"), op: OperationIdSchema }),
2005
+ /** The phone's own chat with Ledge. */
2006
+ z25.object({ type: z25.literal("chat.ask"), qid: OperationIdSchema, text: z25.string().trim().max(MESSAGE_MAX), attachments: z25.array(AttachmentIdSchema).max(ATTACHMENTS_MAX).optional() }),
2007
+ z25.object({ type: z25.literal("chat.cancel"), qid: OperationIdSchema }),
2008
+ z25.object({ type: z25.literal("chat.reset") }),
2009
+ z25.object({ type: z25.literal("chat.history") }),
2010
+ /** An image to store, sent alone before the message that names it; no `id`, and at most `ATTACHMENT_MAX` bytes. */
2011
+ AttachmentPutSchema.pick({ type: true, requestId: true }).extend({ image: ImageUploadSchema.extend({ data: z25.string().min(1).max(b64Max(ATTACHMENT_MAX)) }) }),
2012
+ AttachmentGetSchema.pick({ type: true, requestId: true, attachment: true, preview: true }),
2013
+ z25.object({ type: z25.literal("chat.relay"), op: OperationIdSchema, qid: OperationIdSchema }),
2014
+ z25.object({ type: z25.literal("chat.launch"), op: OperationIdSchema, qid: OperationIdSchema, rows: CardRowsSchema.optional(), assertion: AssertionSchema.optional() }),
2015
+ z25.object({ type: z25.literal("chat.close"), op: OperationIdSchema, qid: OperationIdSchema, rows: CardRowsSchema.optional(), assertion: AssertionSchema.optional() }),
2016
+ /** The turn's team card (roles spec §3.8): launched like a session card, with the device check (purpose `launch`, the turn). */
2017
+ z25.object({ type: z25.literal("chat.team"), op: OperationIdSchema, qid: OperationIdSchema, assertion: AssertionSchema.optional() }),
2018
+ /** A row of the turn's task card: given by Ledge. */
2019
+ z25.object({ type: z25.literal("chat.task"), op: OperationIdSchema, qid: OperationIdSchema, row: z25.number().int().min(0).max(MAX_PROPOSALS - 1) }),
2020
+ /** Rows of the turn's generic card (action registry spec §5): `confirmed` as the local `chat.act`; a row whose action asks
2021
+ * for a device check (`phone`) needs one assertion, purpose `act`, over the turn. */
2022
+ z25.object({
2023
+ type: z25.literal("chat.act"),
2024
+ op: OperationIdSchema,
2025
+ qid: OperationIdSchema,
2026
+ rows: CardRowsSchema.optional(),
2027
+ confirmed: z25.literal(true).optional(),
2028
+ assertion: AssertionSchema.optional(),
2029
+ /** « Refuser », as the local `chat.act`: no passkey, no confirmation. */
2030
+ deny: z25.literal(true).optional()
2031
+ }),
2032
+ /** « Ignorer » the turn's cards, or `undo` (« Revoir »), as the local `chat.ignore`. */
2033
+ z25.object({ type: z25.literal("chat.ignore"), op: OperationIdSchema, qid: OperationIdSchema, undo: z25.literal(true).optional() }),
2034
+ /** « Reprendre » a resting team. */
2035
+ z25.object({ type: z25.literal("team.resume"), op: OperationIdSchema, team: z25.string().min(1).max(40) }),
2036
+ /** Puts a closed task Ledge gave away from « À traiter ». */
2037
+ z25.object({ type: z25.literal("task.seen"), op: OperationIdSchema, task: z25.string().min(1).max(40) }),
2038
+ /** Whether the phone's app is in front: a device that is visible gets no push. */
2039
+ z25.object({ type: z25.literal("presence"), visible: z25.boolean() })
2040
+ ]);
2041
+ var PhoneServerMessageSchema = z25.discriminatedUnion("type", [
2042
+ z25.object({ type: z25.literal("snapshot"), protocol: z25.literal(PHONE_PROTOCOL), sessions: z25.array(PhoneSessionSchema), capabilities: CapabilitiesByToolSchema, daemon: z25.object({ version: z25.string(), name: z25.string().max(40), boot: z25.string().uuid().optional() }), device: z25.object({ confirm: z25.boolean(), push: z25.enum(["on", "off", "stale", "unavailable"]).optional(), chat: z25.literal(true).optional(), contextNear: ContextPrefsSchema.shape.near.optional() }).optional() }),
2043
+ z25.object({ type: z25.literal("confirm.challenge"), purpose: ConfirmPurposeSchema, challenge: B64U2.max(64), rpId: z25.string().max(253), credentialId: B64U2.max(1366).optional(), expiresInMs: z25.number().int().positive() }),
2044
+ z25.object({ type: z25.literal("session.updated"), session: PhoneSessionSchema }),
2045
+ z25.object({ type: z25.literal("session.removed"), id: z25.string() }),
2046
+ /** `pending.cut` is set when even this body was shortened. */
2047
+ z25.object({ type: z25.literal("session.pending"), id: z25.string(), pending: PhonePendingSchema }),
2048
+ z25.object({ type: z25.literal("session.preview"), id: z25.string(), lines: z25.array(z25.string()) }),
2049
+ /** `cut`: the ids of the items shortened; `more`: older items were left out. */
2050
+ z25.object({ type: z25.literal("session.thread"), id: z25.string(), items: z25.array(ThreadItemSchema), cut: z25.array(z25.string()), more: z25.boolean() }),
2051
+ z25.object({ type: z25.literal("session.thread.item"), id: z25.string(), item: ThreadItemSchema, cut: z25.literal(true).optional() }),
2052
+ z25.object({ type: z25.literal("operation.result"), op: OperationIdSchema, status: OperationStatusSchema, reason: OperationReasonSchema.optional() }),
2053
+ z25.object({ type: z25.literal("chat.history"), turns: z25.array(PhoneChatTurnSchema).max(50) }),
2054
+ z25.object({ type: z25.literal("chat.step"), qid: OperationIdSchema, step: PhoneChatStepSchema }),
2055
+ /** The whole text so far, not an increment. */
2056
+ z25.object({ type: z25.literal("chat.delta"), qid: OperationIdSchema, text: z25.string().max(THREAD_TEXT_MAX) }),
2057
+ z25.object({ type: z25.literal("chat.done"), qid: OperationIdSchema, turn: PhoneChatTurnSchema }),
2058
+ z25.object({ type: z25.literal("chat.error"), qid: OperationIdSchema, code: ChatErrorCodeSchema }),
2059
+ /** The live teams, after each snapshot and on every change. */
2060
+ z25.object({ type: z25.literal("teams"), teams: z25.array(PhoneTeamSchema).max(50) }),
2061
+ /** The tasks Ledge gave, open or closed lately, after each snapshot; then each change. */
2062
+ z25.object({ type: z25.literal("tasks"), tasks: z25.array(PhoneTaskSchema).max(200) }),
2063
+ z25.object({ type: z25.literal("task.updated"), task: PhoneTaskSchema }),
2064
+ AttachmentStoredSchema,
2065
+ AttachmentDataSchema,
2066
+ AttachmentErrorSchema,
2067
+ /** Never carries free text. */
2068
+ z25.object({ type: z25.literal("error"), code: z25.enum(["invalid", "unknown_session", "too_large"]), request: z25.string().max(40).optional() })
2069
+ ]);
2070
+
2071
+ // src/auth.ts
2072
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
2073
+ import { chmod, link, mkdir, readFile, unlink, writeFile } from "fs/promises";
2074
+ import { readFileSync } from "fs";
2075
+ import { dirname } from "path";
2076
+ var TOKEN_RE = /^[0-9a-f]{64}$/;
2077
+ var DAEMON_PROTOCOL_PREFIX = "ledge.";
2078
+ function isDaemonToken(value) {
2079
+ return TOKEN_RE.test(value);
2080
+ }
2081
+ function tokenEquals(presented, expected) {
2082
+ const a = createHash("sha256").update(presented.slice(0, 256), "utf8").digest();
2083
+ const b = createHash("sha256").update(expected, "utf8").digest();
2084
+ return timingSafeEqual(a, b);
2085
+ }
2086
+ function hostAllowed(hostHeader) {
2087
+ const raw = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
2088
+ if (!raw || raw.length > 255) return false;
2089
+ const host = raw.trim().toLowerCase();
2090
+ if (!host || host.includes(" ") || host.endsWith(".")) return false;
2091
+ let name;
2092
+ if (host.startsWith("[")) {
2093
+ const end = host.indexOf("]");
2094
+ if (end < 0) return false;
2095
+ name = host.slice(1, end);
2096
+ const rest = host.slice(end + 1);
2097
+ if (rest !== "" && !/^:\d{1,5}$/.test(rest)) return false;
2098
+ } else {
2099
+ const colon = host.lastIndexOf(":");
2100
+ if (colon >= 0) {
2101
+ if (!/^\d{1,5}$/.test(host.slice(colon + 1))) return false;
2102
+ name = host.slice(0, colon);
2103
+ } else name = host;
2104
+ }
2105
+ return name === "localhost" || name === "127.0.0.1" || name === "::1";
2106
+ }
2107
+ function headerValue(value, max) {
2108
+ const raw = Array.isArray(value) ? value.join(",") : value;
2109
+ return !raw || raw.length > max ? "" : raw;
2110
+ }
2111
+ function bearerToken(authorization) {
2112
+ if (!authorization || authorization.length > 512) return "";
2113
+ return /^Bearer[ \t]+(\S+)$/i.exec(authorization)?.[1] ?? "";
2114
+ }
2115
+ function protocolToken(header) {
2116
+ const raw = headerValue(header, 512);
2117
+ if (!raw) return "";
2118
+ for (const part of raw.split(",").slice(0, 8)) {
2119
+ const protocol = part.trim();
2120
+ if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX)) return protocol.slice(DAEMON_PROTOCOL_PREFIX.length);
2121
+ }
2122
+ return "";
2123
+ }
2124
+ function requestAuthorized(headers, expected) {
2125
+ const bearerOk = tokenEquals(bearerToken(typeof headers.authorization === "string" ? headers.authorization : void 0), expected);
2126
+ const presented = protocolToken(headers["sec-websocket-protocol"]);
2127
+ return bearerOk || presented !== "" && tokenEquals(presented, expected);
2128
+ }
2129
+ function selectDaemonProtocol(protocols, expected) {
2130
+ for (const protocol of protocols) {
2131
+ if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX) && tokenEquals(protocol.slice(DAEMON_PROTOCOL_PREFIX.length), expected)) return protocol;
2132
+ }
2133
+ return false;
2134
+ }
2135
+ async function readTokenFile(file) {
2136
+ try {
2137
+ const raw = (await readFile(file, "utf8")).trim();
2138
+ return isDaemonToken(raw) ? raw : void 0;
2139
+ } catch (e) {
2140
+ if (e.code === "ENOENT") return void 0;
2141
+ throw e;
2142
+ }
2143
+ }
2144
+ async function lockTokenFile(file) {
2145
+ await chmod(file, 384);
2146
+ }
2147
+ async function ensureDaemonToken() {
2148
+ const file = paths().token;
2149
+ await mkdir(dirname(file), { recursive: true });
2150
+ const current = await readTokenFile(file);
2151
+ if (current) {
2152
+ await lockTokenFile(file);
2153
+ return current;
2154
+ }
2155
+ const token = randomBytes(32).toString("hex");
2156
+ const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
2157
+ await writeFile(tmp, `${token}
2158
+ `, { mode: 384, flag: "wx" });
2159
+ await chmod(tmp, 384);
2160
+ try {
2161
+ await link(tmp, file);
2162
+ } catch (e) {
2163
+ await unlink(tmp).catch(() => {
2164
+ });
2165
+ if (e.code !== "EEXIST") throw e;
2166
+ const raced = await readTokenFile(file);
2167
+ if (raced) {
2168
+ await lockTokenFile(file);
2169
+ return raced;
2170
+ }
2171
+ await writeFile(file, `${token}
2172
+ `, { mode: 384 });
2173
+ }
2174
+ await unlink(tmp).catch(() => {
2175
+ });
2176
+ await lockTokenFile(file);
2177
+ const stored = await readTokenFile(file);
2178
+ if (!stored) throw new Error("daemon token was not stored");
2179
+ return stored;
2180
+ }
2181
+ var SESSION_KEY_VAR = "LEDGE_SESSION_KEY";
2182
+ function sessionKey(id, token = readDaemonToken()) {
2183
+ return token ? createHmac("sha256", token).update(`ledge-session:${id}`).digest("hex") : void 0;
2184
+ }
2185
+ function readDaemonToken() {
2186
+ try {
2187
+ const raw = readFileSync(paths().token, "utf8").trim();
2188
+ return isDaemonToken(raw) ? raw : void 0;
2189
+ } catch {
2190
+ return void 0;
2191
+ }
2192
+ }
2193
+
2194
+ export {
2195
+ ATTACHMENT_ID,
2196
+ ATTACHMENT_MAX,
2197
+ ATTACHMENT_LOCAL_MAX,
2198
+ PREVIEW_MAX,
2199
+ ATTACHMENTS_MAX,
2200
+ AttachmentRefSchema,
2201
+ CONTEXT_NEAR,
2202
+ CONTEXT_NEAR_MIN,
2203
+ CONTEXT_NEAR_MAX,
2204
+ ContextPrefsSchema,
2205
+ contextNear,
2206
+ BRIEF_MAX,
2207
+ ToolSchema,
2208
+ autonomyArgs,
2209
+ ompApprovalFor,
2210
+ SessionSchema,
2211
+ backendOf,
2212
+ isArchived,
2213
+ ModelIdSchema,
2214
+ EffortSchema,
2215
+ extractProposal,
2216
+ parseRelayBlock,
2217
+ parseCloseBlock,
2218
+ parseTeamBlock,
2219
+ parseTaskBlock,
2220
+ parseProjectBlock,
2221
+ RoleIdSchema,
2222
+ ROLE_LABEL_MAX,
2223
+ ROLE_PROMPT_MAX,
2224
+ RoleSchema,
2225
+ SeatsSchema,
2226
+ TeamNameSchema,
2227
+ TeamSchema,
2228
+ isTeamLive,
2229
+ NOTE_MAX,
2230
+ NOTE_NOT_TYPED,
2231
+ offers,
2232
+ pendingKey,
2233
+ TASK_TITLE_MAX,
2234
+ TASK_TEXT_MAX,
2235
+ LEDGE_GIVER,
2236
+ transmissions,
2237
+ givenByLedge,
2238
+ taskClosed,
2239
+ ACTIONS,
2240
+ actionById,
2241
+ riskOf,
2242
+ LEGACY_FENCE_ACTION,
2243
+ parseActionBlock,
2244
+ NEVER_PROPOSED,
2245
+ MAX_PROPOSALS,
2246
+ DETAIL_MAX,
2247
+ ACTION_ERROR_CODES,
2248
+ ChatTurnSchema,
2249
+ proposalRows,
2250
+ DOCUMENT_MAX_BYTES,
2251
+ SshAliasSchema,
2252
+ MachinesSchema,
2253
+ IslandPrefsSchema,
2254
+ PROJECT_COLORS,
2255
+ ProjectsSchema,
2256
+ DEFAULT_PROJECTS,
2257
+ projectOf,
2258
+ RouteResultSchema,
2259
+ DEFAULT_RELAY_URL,
2260
+ MobileConfigSchema,
2261
+ PROTOCOL_PORT,
2262
+ RefusalCodeSchema,
2263
+ RecentSchema,
2264
+ MESSAGE_MAX,
2265
+ SUMMARY_MAX,
2266
+ parseClientMessage,
2267
+ parseServerMessage,
2268
+ NAMES,
2269
+ pickName,
2270
+ TOPIC_MAX,
2271
+ cleanTopic,
2272
+ sessionTitle,
2273
+ RelayServerMessageSchema,
2274
+ PAIR_LABEL_MAX,
2275
+ SealedFrameSchema,
2276
+ RELAY_CLOSE_SIGNED_OUT,
2277
+ DeviceStartResponseSchema,
2278
+ DevicePollResponseSchema,
2279
+ pushEndpointAllowed,
2280
+ PushSendResultSchema,
2281
+ PushPayloadSchema,
2282
+ PHONE_PROTOCOL,
2283
+ padPlain,
2284
+ THREAD_ITEMS_MAX,
2285
+ THREAD_BUDGET,
2286
+ THREAD_TEXT_MAX,
2287
+ TOOL_TEXT_MAX,
2288
+ PENDING_BODY_MAX,
2289
+ SNAPSHOT_BODY_MAX,
2290
+ OperationIdSchema,
2291
+ UNTYPABLE,
2292
+ CHAT_HISTORY_BUDGET,
2293
+ PhoneClientMessageSchema,
2294
+ tokenEquals,
2295
+ hostAllowed,
2296
+ requestAuthorized,
2297
+ selectDaemonProtocol,
2298
+ ensureDaemonToken,
2299
+ SESSION_KEY_VAR,
2300
+ sessionKey,
2301
+ readDaemonToken
2302
+ };