@paradow/ledge 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  // ../protocol/src/messages.ts
2
- import { z as z4 } from "zod";
2
+ import { z as z7 } from "zod";
3
3
 
4
4
  // ../protocol/src/session.ts
5
5
  import { z } from "zod";
@@ -10,6 +10,8 @@ var PendingSchema = z.object({
10
10
  text: z.string(),
11
11
  tool: z.string().optional(),
12
12
  options: z.array(z.string()).optional(),
13
+ /** Each option's description, by index ('' when it has none); left out when no option has one. */
14
+ optionDetails: z.array(z.string()).optional(),
13
15
  source: z.enum(["hook", "pane"]),
14
16
  /** Set when the request comes from a helper (subagent): it is routed through its parent. */
15
17
  helperId: z.string().optional(),
@@ -28,6 +30,10 @@ var HelperSchema = z.object({
28
30
  });
29
31
  var TopicSourceSchema = z.enum(["user", "pane", "prompt"]);
30
32
  var SessionSchema = z.object({
33
+ /** Origin is annotated by the overlay, never used as an unqualified session id on the wire. */
34
+ machineId: z.string().optional(),
35
+ machineName: z.string().optional(),
36
+ repoId: z.string().optional(),
31
37
  id: z.string().min(1),
32
38
  name: z.string().min(1),
33
39
  tool: ToolSchema,
@@ -82,6 +88,9 @@ var ChatRelaySchema = z2.object({
82
88
  error: z2.string().optional()
83
89
  });
84
90
  var ChatTurnSchema = z2.object({
91
+ machineId: z2.string().optional(),
92
+ machineName: z2.string().optional(),
93
+ choices: z2.array(z2.object({ id: z2.string(), label: z2.string() })).optional(),
85
94
  qid: z2.string().min(1),
86
95
  question: z2.string(),
87
96
  askedAt: z2.string().datetime(),
@@ -102,6 +111,7 @@ var DocumentReadSchema = z3.object({
102
111
  type: z3.literal("document.read"),
103
112
  requestId,
104
113
  path,
114
+ machineId: z3.string().optional(),
105
115
  sessionId: z3.string().min(1).optional(),
106
116
  basePath: path.optional()
107
117
  });
@@ -132,90 +142,152 @@ var DocumentErrorSchema = z3.object({
132
142
  code: DocumentErrorCodeSchema
133
143
  });
134
144
 
145
+ // ../protocol/src/routing.ts
146
+ import { z as z4 } from "zod";
147
+ var MachineInventorySchema = z4.object({
148
+ id: z4.string().min(1).max(80),
149
+ label: z4.string().min(1).max(120),
150
+ online: z4.boolean(),
151
+ local: z4.boolean(),
152
+ sessions: z4.array(z4.object({ id: z4.string(), name: z4.string(), project: z4.string(), cwd: z4.string(), topic: z4.string().optional(), state: z4.string(), readOnly: z4.boolean() })).max(500),
153
+ folders: z4.array(z4.object({ project: z4.string(), cwd: z4.string() })).max(100)
154
+ });
155
+ var RoutingContextSchema = z4.object({
156
+ machines: z4.array(MachineInventorySchema).min(1).max(20),
157
+ history: z4.array(z4.object({ question: z4.string().max(2e4), answer: z4.string().max(2e4).optional(), machineId: z4.string().optional() })).max(50)
158
+ });
159
+ var RouteResultSchema = z4.object({
160
+ /** Null means an answer or a clarification, never an implicit local destination. */
161
+ machineId: z4.string().nullable(),
162
+ text: z4.string().min(1).max(2e4),
163
+ choices: z4.array(z4.string()).max(20).optional()
164
+ });
165
+
166
+ // ../protocol/src/ssh.ts
167
+ import { z as z5 } from "zod";
168
+ var SshAliasSchema = z5.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/);
169
+
170
+ // ../protocol/src/thread.ts
171
+ import { z as z6 } from "zod";
172
+ var ToolStatusSchema = z6.enum(["running", "success", "failed", "unknown"]);
173
+ var FileChangeSchema = z6.object({ path: z6.string().min(1), change: z6.enum(["add", "edit", "delete", "write"]), diff: z6.string().optional() });
174
+ var base = { id: z6.string().min(1), at: z6.string().datetime(), turn: z6.number().int().nonnegative(), helperId: z6.string().optional() };
175
+ var ThreadItemSchema = z6.discriminatedUnion("kind", [
176
+ z6.object({ kind: z6.literal("prompt"), ...base, text: z6.string() }),
177
+ z6.object({ kind: z6.literal("message"), ...base, text: z6.string() }),
178
+ /** `id` is the harness's tool_use_id when it gives one, so the result updates the same row. */
179
+ z6.object({ kind: z6.literal("tool"), ...base, tool: z6.string(), label: z6.string(), input: z6.string().optional(), output: z6.string().optional(), error: z6.string().optional(), status: ToolStatusSchema, durationMs: z6.number().nonnegative().optional(), files: z6.array(FileChangeSchema).default([]) }),
180
+ /** `unknown`: answered in the terminal, the daemon only saw the prompt go away. */
181
+ z6.object({ kind: z6.literal("permission"), ...base, tool: z6.string(), text: z6.string(), decision: z6.enum(["accept", "deny", "unknown"]).optional() }),
182
+ z6.object({ kind: z6.literal("reply"), ...base, text: z6.string() }),
183
+ z6.object({ kind: z6.literal("helper"), ...base, task: z6.string(), agentType: z6.string().optional(), state: z6.enum(["working", "exited"]) })
184
+ ]);
185
+
135
186
  // ../protocol/src/messages.ts
136
187
  var PROTOCOL_PORT = 47831;
137
- var SessionEventSchema = z4.object({
138
- name: z4.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd", "PaneChanged", "PaneGone", "PanePrompt", "PaneIdle", "InjectFailed", "Decided", "PostToolUse", "PostToolUseFailure", "StopFailure", "SubagentStart", "SubagentStop", "PaneTitle", "Renamed"]),
139
- at: z4.string().datetime(),
140
- source: z4.enum(["hook", "pane", "daemon"]),
141
- tool: z4.string().optional(),
142
- text: z4.string().optional(),
143
- options: z4.array(z4.string()).optional(),
144
- kind: z4.enum(["question", "permission", "plan", "diff"]).optional(),
145
- exitCode: z4.number().int().nullable().optional(),
146
- helperId: z4.string().optional(),
188
+ var SessionEventSchema = z7.object({
189
+ name: z7.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd", "PaneChanged", "PaneGone", "PanePrompt", "PaneIdle", "InjectFailed", "Decided", "PostToolUse", "PostToolUseFailure", "StopFailure", "SubagentStart", "SubagentStop", "PaneTitle", "Renamed"]),
190
+ at: z7.string().datetime(),
191
+ source: z7.enum(["hook", "pane", "daemon"]),
192
+ tool: z7.string().optional(),
193
+ text: z7.string().optional(),
194
+ options: z7.array(z7.string()).optional(),
195
+ /** Each option's description, by index ('' when it has none); left out when no option has one. */
196
+ optionDetails: z7.array(z7.string()).optional(),
197
+ kind: z7.enum(["question", "permission", "plan", "diff"]).optional(),
198
+ exitCode: z7.number().int().nullable().optional(),
199
+ helperId: z7.string().optional(),
147
200
  /** The helper's description, once known (Claude Code: its `.meta.json`, written after SubagentStart). */
148
- task: z4.string().optional(),
201
+ task: z7.string().optional(),
149
202
  /** PostToolUseFailure: the user interrupted the turn (Esc); Claude then waits for input with no Stop. */
150
- interrupt: z4.boolean().optional()
203
+ interrupt: z7.boolean().optional(),
204
+ /** Native tracking (spec 2026-10-13 §2): the harness's id of the tool call, so its result lands on the same row. */
205
+ toolUseId: z7.string().optional(),
206
+ /** The tool's input as the harness gave it, every string capped by the hook. */
207
+ toolInput: z7.record(z7.string(), z7.unknown()).optional(),
208
+ /** PostToolUse: the result as text; SubagentStop: nothing (the report travels in `text`). */
209
+ output: z7.string().optional(),
210
+ durationMs: z7.number().nonnegative().optional(),
211
+ /** PostToolUseFailure: the harness's error message. */
212
+ error: z7.string().optional(),
213
+ /** The session's transcript, or the helper's on SubagentStop; the daemon reads its tail for the agent's messages. */
214
+ transcriptPath: z7.string().optional(),
215
+ /** Decided: what was chosen when the daemon knows it. */
216
+ decision: z7.enum(["accept", "deny"]).optional()
151
217
  });
152
- var RecentSchema = z4.object({ cwd: z4.string().min(1), project: z4.string().min(1), lastAt: z4.string().datetime() });
218
+ var RecentSchema = z7.object({ cwd: z7.string().min(1), project: z7.string().min(1), lastAt: z7.string().datetime() });
153
219
  var SEMVER = /^\d+\.\d+\.\d+$/;
154
- var DaemonInfoSchema = z4.object({ version: z4.string(), install: z4.enum(["npm", "git", "unknown"]), systemd: z4.boolean(), manual: z4.string().optional() });
155
- var UpdateFailureSchema = z4.enum(["git-install", "no-systemd", "sessions-at-risk", "npm-failed", "busy"]);
156
- var ServerMessageSchema = z4.discriminatedUnion("type", [
220
+ var DaemonInfoSchema = z7.object({ version: z7.string(), install: z7.enum(["npm", "git", "unknown"]), systemd: z7.boolean(), manual: z7.string().optional(), routing: z7.boolean().optional() });
221
+ var UpdateFailureSchema = z7.enum(["git-install", "no-systemd", "sessions-at-risk", "npm-failed", "busy"]);
222
+ var ServerMessageSchema = z7.discriminatedUnion("type", [
223
+ z7.object({ type: z7.literal("chat.routed"), qid: z7.string(), result: RouteResultSchema }),
157
224
  DocumentContentSchema,
158
225
  DocumentErrorSchema,
159
226
  /** `recents`: the folders of past sessions, most recent first. `launcher`: the command that runs `ledge` inside WSL
160
227
  * (`[node, cli]`), so the overlay can attach a terminal to a session with `<launcher> attach <id>`. `daemon`: absent
161
228
  * from daemons older than the auto-update. */
162
- z4.object({ type: z4.literal("snapshot"), sessions: z4.array(SessionSchema), recents: z4.array(RecentSchema).optional(), launcher: z4.array(z4.string()).optional(), daemon: DaemonInfoSchema.optional() }),
163
- z4.object({ type: z4.literal("session.updated"), session: SessionSchema }),
164
- z4.object({ type: z4.literal("session.removed"), id: z4.string() }),
165
- z4.object({ type: z4.literal("session.preview"), id: z4.string(), lines: z4.array(z4.string()) }),
229
+ z7.object({ type: z7.literal("snapshot"), sessions: z7.array(SessionSchema), recents: z7.array(RecentSchema).optional(), launcher: z7.array(z7.string()).optional(), daemon: DaemonInfoSchema.optional(), sshAlias: SshAliasSchema.optional() }),
230
+ z7.object({ type: z7.literal("session.updated"), session: SessionSchema }),
231
+ z7.object({ type: z7.literal("session.removed"), id: z7.string() }),
232
+ z7.object({ type: z7.literal("session.preview"), id: z7.string(), lines: z7.array(z7.string()) }),
233
+ /** A session's thread, on request (spec 2026-10-13 §5); then each created or updated item, broadcast. */
234
+ z7.object({ type: z7.literal("session.thread"), id: z7.string(), items: z7.array(ThreadItemSchema) }),
235
+ z7.object({ type: z7.literal("session.thread.item"), id: z7.string(), item: ThreadItemSchema }),
166
236
  /** Answer to the requester of a `session.launch`: the session exists (its `session.updated` is broadcast as usual). */
167
- z4.object({ type: z4.literal("session.launched"), id: z4.string() }),
168
- z4.object({ type: z4.literal("recents"), recents: z4.array(RecentSchema) }),
237
+ z7.object({ type: z7.literal("session.launched"), id: z7.string() }),
238
+ z7.object({ type: z7.literal("recents"), recents: z7.array(RecentSchema) }),
169
239
  /** Answer to the requester of a `folder.list` or `folder.create`: `path` resolved (`~` expanded), `parent` null at the
170
240
  * root, `entries` its subfolders by name (hidden ones left out). A failure keeps the asked path and says why in `error`,
171
241
  * never in an `error` message, which the launch form reads as a failed launch. */
172
- z4.object({ type: z4.literal("folder.listing"), path: z4.string(), parent: z4.string().nullable(), entries: z4.array(z4.string()), error: z4.string().optional() }),
242
+ z7.object({ type: z7.literal("folder.listing"), path: z7.string(), parent: z7.string().nullable(), entries: z7.array(z7.string()), error: z7.string().optional() }),
173
243
  /** 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. */
174
- z4.object({ type: z4.literal("chat.step"), qid: z4.string(), step: ChatStepSchema }),
175
- z4.object({ type: z4.literal("chat.delta"), qid: z4.string(), text: z4.string() }),
176
- z4.object({ type: z4.literal("chat.done"), qid: z4.string(), text: z4.string(), at: z4.string().datetime(), proposal: ChatProposalSchema.optional(), relay: ChatRelaySchema.optional() }),
177
- z4.object({ type: z4.literal("chat.error"), qid: z4.string(), message: z4.string() }),
178
- z4.object({ type: z4.literal("chat.history"), turns: z4.array(ChatTurnSchema) }),
244
+ z7.object({ type: z7.literal("chat.step"), qid: z7.string(), step: ChatStepSchema }),
245
+ z7.object({ type: z7.literal("chat.delta"), qid: z7.string(), text: z7.string() }),
246
+ z7.object({ type: z7.literal("chat.done"), qid: z7.string(), text: z7.string(), at: z7.string().datetime(), proposal: ChatProposalSchema.optional(), relay: ChatRelaySchema.optional() }),
247
+ z7.object({ type: z7.literal("chat.error"), qid: z7.string(), message: z7.string() }),
248
+ z7.object({ type: z7.literal("chat.history"), turns: z7.array(ChatTurnSchema) }),
179
249
  /** The daemon updating itself: npm is running, then the service restarts (the socket closes right after). */
180
- z4.object({ type: z4.literal("update.progress"), version: z4.string(), step: z4.enum(["installing", "restarting"]) }),
250
+ z7.object({ type: z7.literal("update.progress"), version: z7.string(), step: z7.enum(["installing", "restarting"]) }),
181
251
  /** Nothing was changed. `detail`: the useful end of npm's output; `command`: what the person can run instead. */
182
- z4.object({ type: z4.literal("update.failed"), version: z4.string(), reason: UpdateFailureSchema, detail: z4.string().optional(), command: z4.string().optional() }),
183
- z4.object({ type: z4.literal("error"), message: z4.string(), id: z4.string().optional() })
252
+ z7.object({ type: z7.literal("update.failed"), version: z7.string(), reason: UpdateFailureSchema, detail: z7.string().optional(), command: z7.string().optional() }),
253
+ z7.object({ type: z7.literal("error"), message: z7.string(), id: z7.string().optional() })
184
254
  ]);
185
- var ClientMessageSchema = z4.discriminatedUnion("type", [
255
+ var ClientMessageSchema = z7.discriminatedUnion("type", [
256
+ z7.object({ type: z7.literal("chat.route"), qid: z7.string().min(1).max(100), text: z7.string().min(1).max(2e4), tool: ToolSchema.optional(), context: RoutingContextSchema }),
186
257
  DocumentReadSchema,
187
258
  /** The overlay starts a session: `cwd` is a WSL path (`~` allowed), `name` a preferred bot name (the daemon picks a
188
259
  * free one otherwise), `prompt` the agent's first instruction, `continue` resumes the folder's last conversation.
189
260
  * With `qid` it is the chat card of that turn: the daemon launches the turn's proposal and ignores the other fields. */
190
- z4.object({ type: z4.literal("session.launch"), tool: ToolSchema, cwd: z4.string().min(1), name: z4.string().optional(), prompt: z4.string().optional(), continue: z4.boolean().optional(), qid: z4.string().min(1).optional() }),
261
+ z7.object({ type: z7.literal("session.launch"), tool: ToolSchema, cwd: z7.string().min(1), name: z7.string().optional(), prompt: z7.string().optional(), continue: z7.boolean().optional(), qid: z7.string().min(1).optional() }),
191
262
  /** The launch form's folder browser: list a folder's subfolders (home when no `path`), or make `name` inside `parent`
192
263
  * and list it. Both are answered with a `folder.listing`. */
193
- z4.object({ type: z4.literal("folder.list"), path: z4.string().optional() }),
194
- z4.object({ type: z4.literal("folder.create"), parent: z4.string().min(1), name: z4.string().min(1) }),
195
- z4.object({ type: z4.literal("session.reply"), id: z4.string(), text: z4.string().min(1) }),
264
+ z7.object({ type: z7.literal("folder.list"), path: z7.string().optional() }),
265
+ z7.object({ type: z7.literal("folder.create"), parent: z7.string().min(1), name: z7.string().min(1) }),
266
+ z7.object({ type: z7.literal("session.reply"), id: z7.string(), text: z7.string().min(1) }),
196
267
  /** The chat card's instruction (spec 2026-10-12 relay §4): sent now if the session is free, else queued until it is. */
197
- z4.object({ type: z4.literal("session.relay"), qid: z4.string().min(1) }),
268
+ z7.object({ type: z7.literal("session.relay"), qid: z7.string().min(1) }),
198
269
  /** Takes the card's queued instruction back out of the queue. */
199
- z4.object({ type: z4.literal("session.relay.cancel"), qid: z4.string().min(1) }),
200
- z4.object({ type: z4.literal("session.decide"), id: z4.string(), decision: z4.enum(["accept", "deny"]) }),
201
- z4.object({ type: z4.literal("session.focus"), id: z4.string() }),
202
- z4.object({ type: z4.literal("session.kill"), id: z4.string() }),
203
- z4.object({ type: z4.literal("session.interrupt"), id: z4.string() }),
270
+ z7.object({ type: z7.literal("session.relay.cancel"), qid: z7.string().min(1) }),
271
+ z7.object({ type: z7.literal("session.decide"), id: z7.string(), decision: z7.enum(["accept", "deny"]) }),
272
+ z7.object({ type: z7.literal("session.focus"), id: z7.string() }),
273
+ z7.object({ type: z7.literal("session.kill"), id: z7.string() }),
274
+ z7.object({ type: z7.literal("session.interrupt"), id: z7.string() }),
204
275
  /** Removes a finished (exited) or read-only (hook_only) session from the list; refused for a live launcher session. */
205
- z4.object({ type: z4.literal("session.dismiss"), id: z4.string() }),
276
+ z7.object({ type: z7.literal("session.dismiss"), id: z7.string() }),
206
277
  /** 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. */
207
- z4.object({ type: z4.literal("session.rename"), id: z4.string(), topic: z4.string().max(500) }),
208
- z4.object({ type: z4.literal("session.register"), session: SessionSchema }),
278
+ z7.object({ type: z7.literal("session.rename"), id: z7.string(), topic: z7.string().max(500) }),
279
+ z7.object({ type: z7.literal("session.thread"), id: z7.string() }),
280
+ z7.object({ type: z7.literal("session.register"), session: SessionSchema }),
209
281
  /** `agent` and `cwd` describe the session when the daemon does not know it yet (a hook from an agent started without ledge). */
210
- z4.object({ type: z4.literal("session.event"), id: z4.string(), event: SessionEventSchema, agent: ToolSchema.optional(), cwd: z4.string().optional() }),
211
- z4.object({ type: z4.literal("session.unregister"), id: z4.string(), exitCode: z4.number().int().nullable() }),
212
- z4.object({ type: z4.literal("snapshot.request") }),
213
- z4.object({ type: z4.literal("chat.ask"), qid: z4.string().min(1), text: z4.string().min(1), tool: ToolSchema.optional() }),
214
- z4.object({ type: z4.literal("chat.cancel"), qid: z4.string().min(1) }),
282
+ z7.object({ type: z7.literal("session.event"), id: z7.string(), event: SessionEventSchema, agent: ToolSchema.optional(), cwd: z7.string().optional() }),
283
+ z7.object({ type: z7.literal("session.unregister"), id: z7.string(), exitCode: z7.number().int().nullable() }),
284
+ z7.object({ type: z7.literal("snapshot.request") }),
285
+ z7.object({ type: z7.literal("chat.ask"), qid: z7.string().min(1), text: z7.string().min(1), tool: ToolSchema.optional() }),
286
+ z7.object({ type: z7.literal("chat.cancel"), qid: z7.string().min(1) }),
215
287
  /** Starts a new conversation with Ledge: the day's turns are cleared and the next question opens a new harness session. */
216
- z4.object({ type: z4.literal("chat.reset") }),
288
+ z7.object({ type: z7.literal("chat.reset") }),
217
289
  /** Installs `@paradow/ledge@version` and restarts the service (spec 2026-10-12 auto-update §6). */
218
- z4.object({ type: z4.literal("update.daemon"), version: z4.string().regex(SEMVER) })
290
+ z7.object({ type: z7.literal("update.daemon"), version: z7.string().regex(SEMVER) })
219
291
  ]);
220
292
  function parseClientMessage(raw) {
221
293
  return ClientMessageSchema.parse(raw);
@@ -297,9 +369,9 @@ function pickName(taken, preferred) {
297
369
  const free = (n) => !used.has(n);
298
370
  const wanted = preferred?.trim();
299
371
  if (wanted) {
300
- const base = cap(wanted);
301
- if (free(base)) return base;
302
- for (let i = 2; ; i++) if (free(`${base}-${i}`)) return `${base}-${i}`;
372
+ const base2 = cap(wanted);
373
+ if (free(base2)) return base2;
374
+ for (let i = 2; ; i++) if (free(`${base2}-${i}`)) return `${base2}-${i}`;
303
375
  }
304
376
  const first = NAMES.find(free);
305
377
  if (first) return first;
@@ -307,9 +379,9 @@ function pickName(taken, preferred) {
307
379
  }
308
380
 
309
381
  // ../protocol/src/proposal.ts
310
- import { z as z5 } from "zod";
311
- var ProposalBlockSchema = z5.object({ cwd: z5.string().trim().min(1), prompt: z5.string().optional() });
312
- var RelayBlockSchema = z5.object({ session: z5.string().trim().min(1), text: z5.string().trim().min(1) });
382
+ import { z as z8 } from "zod";
383
+ var ProposalBlockSchema = z8.object({ cwd: z8.string().trim().min(1), prompt: z8.string().optional() });
384
+ var RelayBlockSchema = z8.object({ session: z8.string().trim().min(1), text: z8.string().trim().min(1) });
313
385
  var BLOCK = /```ledge-(session|relay)[ \t]*\n?([\s\S]*?)(?:```|$)/g;
314
386
  function extractProposal(answer) {
315
387
  const found = [];
@@ -351,6 +423,8 @@ function sessionTitle(s) {
351
423
  export {
352
424
  SessionSchema,
353
425
  DOCUMENT_MAX_BYTES,
426
+ RouteResultSchema,
427
+ SshAliasSchema,
354
428
  PROTOCOL_PORT,
355
429
  RecentSchema,
356
430
  parseClientMessage,