@paradow/ledge 0.16.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli/{attach-5H727FCW.js → attach-EYIX7FZN.js} +7 -7
- package/dist/cli/{bridge-LXMOQN6Q.js → bridge-6UHCD76N.js} +2 -2
- package/dist/cli/{chunk-MQICEVKR.js → chunk-36KJUV62.js} +6 -6
- package/dist/cli/chunk-5ZCHFGCV.js +115 -0
- package/dist/cli/{chunk-QJVODYJ5.js → chunk-AQ25HFBP.js} +40 -2
- package/dist/cli/{chunk-R5462SQM.js → chunk-ELLXOV4Q.js} +2 -1
- package/dist/cli/{chunk-BNKPXSUY.js → chunk-JQZHXVIZ.js} +1 -1
- package/dist/cli/{chunk-2H5FJH4Q.js → chunk-LAUNHDTX.js} +2 -2
- package/dist/cli/{chunk-DBSCO3HI.js → chunk-NC5YAEJB.js} +56 -7
- package/dist/cli/{chunk-S4ANPQGA.js → chunk-OVJL2QTN.js} +2 -2
- package/dist/cli/{chunk-TB47EHML.js → chunk-Q7YNLP66.js} +2 -2
- package/dist/cli/{chunk-Q5WWH7FG.js → chunk-QD27W6W5.js} +681 -630
- package/dist/cli/chunk-V2H7UD2N.js +6 -0
- package/dist/cli/{chunk-H36JD4E3.js → chunk-VE4LGNXX.js} +2 -2
- package/dist/cli/{chunk-DEXDXDO4.js → chunk-VVK4I6B5.js} +1 -1
- package/dist/cli/{daemon-DKH63BGV.js → daemon-QEJLAIBT.js} +355 -40
- package/dist/cli/{hook-LY2YTK5U.js → hook-AE5HC6AS.js} +18 -15
- package/dist/cli/index.js +30 -11
- package/dist/cli/{launch-UQEFFPBN.js → launch-B4G5OFBV.js} +7 -7
- package/dist/cli/send-2OZ3YZRV.js +18 -0
- package/dist/cli/{setup-3N2EOJXS.js → setup-2VM4GCDJ.js} +4 -4
- package/dist/cli/{status-ZQSYXILE.js → status-3VJAO6UL.js} +3 -3
- package/dist/cli/task-PMKNYP6M.js +18 -0
- package/dist/cli/tui-DWA2EDC2.js +3561 -0
- package/dist/cli/{uninstall-W2OMKU3B.js → uninstall-AKUWE33D.js} +4 -4
- package/dist/cli/{version-6BMBIXHN.js → version-WOGCIZY3.js} +1 -1
- package/dist/cli/who-37TNDYVA.js +11 -0
- package/package.json +7 -1
- package/dist/cli/chunk-J4ICZEGP.js +0 -6
- package/dist/cli/send-EY63XQXI.js +0 -14
- package/dist/cli/who-VGIMGQNS.js +0 -11
|
@@ -1,6 +1,125 @@
|
|
|
1
1
|
import {
|
|
2
2
|
paths
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ELLXOV4Q.js";
|
|
4
|
+
|
|
5
|
+
// src/auth.ts
|
|
6
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
7
|
+
import { chmod, link, mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { dirname } from "path";
|
|
10
|
+
var TOKEN_RE = /^[0-9a-f]{64}$/;
|
|
11
|
+
var DAEMON_PROTOCOL_PREFIX = "ledge.";
|
|
12
|
+
function isDaemonToken(value) {
|
|
13
|
+
return TOKEN_RE.test(value);
|
|
14
|
+
}
|
|
15
|
+
function tokenEquals(presented, expected) {
|
|
16
|
+
const a = createHash("sha256").update(presented.slice(0, 256), "utf8").digest();
|
|
17
|
+
const b = createHash("sha256").update(expected, "utf8").digest();
|
|
18
|
+
return timingSafeEqual(a, b);
|
|
19
|
+
}
|
|
20
|
+
function hostAllowed(hostHeader) {
|
|
21
|
+
const raw = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
|
|
22
|
+
if (!raw || raw.length > 255) return false;
|
|
23
|
+
const host = raw.trim().toLowerCase();
|
|
24
|
+
if (!host || host.includes(" ") || host.endsWith(".")) return false;
|
|
25
|
+
let name;
|
|
26
|
+
if (host.startsWith("[")) {
|
|
27
|
+
const end = host.indexOf("]");
|
|
28
|
+
if (end < 0) return false;
|
|
29
|
+
name = host.slice(1, end);
|
|
30
|
+
const rest = host.slice(end + 1);
|
|
31
|
+
if (rest !== "" && !/^:\d{1,5}$/.test(rest)) return false;
|
|
32
|
+
} else {
|
|
33
|
+
const colon = host.lastIndexOf(":");
|
|
34
|
+
if (colon >= 0) {
|
|
35
|
+
if (!/^\d{1,5}$/.test(host.slice(colon + 1))) return false;
|
|
36
|
+
name = host.slice(0, colon);
|
|
37
|
+
} else name = host;
|
|
38
|
+
}
|
|
39
|
+
return name === "localhost" || name === "127.0.0.1" || name === "::1";
|
|
40
|
+
}
|
|
41
|
+
function headerValue(value, max) {
|
|
42
|
+
const raw = Array.isArray(value) ? value.join(",") : value;
|
|
43
|
+
return !raw || raw.length > max ? "" : raw;
|
|
44
|
+
}
|
|
45
|
+
function bearerToken(authorization) {
|
|
46
|
+
if (!authorization || authorization.length > 512) return "";
|
|
47
|
+
return /^Bearer[ \t]+(\S+)$/i.exec(authorization)?.[1] ?? "";
|
|
48
|
+
}
|
|
49
|
+
function protocolToken(header) {
|
|
50
|
+
const raw = headerValue(header, 512);
|
|
51
|
+
if (!raw) return "";
|
|
52
|
+
for (const part of raw.split(",").slice(0, 8)) {
|
|
53
|
+
const protocol = part.trim();
|
|
54
|
+
if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX)) return protocol.slice(DAEMON_PROTOCOL_PREFIX.length);
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
function requestAuthorized(headers, expected) {
|
|
59
|
+
const bearerOk = tokenEquals(bearerToken(typeof headers.authorization === "string" ? headers.authorization : void 0), expected);
|
|
60
|
+
const presented = protocolToken(headers["sec-websocket-protocol"]);
|
|
61
|
+
return bearerOk || presented !== "" && tokenEquals(presented, expected);
|
|
62
|
+
}
|
|
63
|
+
function selectDaemonProtocol(protocols, expected) {
|
|
64
|
+
for (const protocol of protocols) {
|
|
65
|
+
if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX) && tokenEquals(protocol.slice(DAEMON_PROTOCOL_PREFIX.length), expected)) return protocol;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
async function readTokenFile(file) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = (await readFile(file, "utf8")).trim();
|
|
72
|
+
return isDaemonToken(raw) ? raw : void 0;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
if (e.code === "ENOENT") return void 0;
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function lockTokenFile(file) {
|
|
79
|
+
await chmod(file, 384);
|
|
80
|
+
}
|
|
81
|
+
async function ensureDaemonToken() {
|
|
82
|
+
const file = paths().token;
|
|
83
|
+
await mkdir(dirname(file), { recursive: true });
|
|
84
|
+
const current = await readTokenFile(file);
|
|
85
|
+
if (current) {
|
|
86
|
+
await lockTokenFile(file);
|
|
87
|
+
return current;
|
|
88
|
+
}
|
|
89
|
+
const token = randomBytes(32).toString("hex");
|
|
90
|
+
const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
91
|
+
await writeFile(tmp, `${token}
|
|
92
|
+
`, { mode: 384, flag: "wx" });
|
|
93
|
+
await chmod(tmp, 384);
|
|
94
|
+
try {
|
|
95
|
+
await link(tmp, file);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
await unlink(tmp).catch(() => {
|
|
98
|
+
});
|
|
99
|
+
if (e.code !== "EEXIST") throw e;
|
|
100
|
+
const raced = await readTokenFile(file);
|
|
101
|
+
if (raced) {
|
|
102
|
+
await lockTokenFile(file);
|
|
103
|
+
return raced;
|
|
104
|
+
}
|
|
105
|
+
await writeFile(file, `${token}
|
|
106
|
+
`, { mode: 384 });
|
|
107
|
+
}
|
|
108
|
+
await unlink(tmp).catch(() => {
|
|
109
|
+
});
|
|
110
|
+
await lockTokenFile(file);
|
|
111
|
+
const stored = await readTokenFile(file);
|
|
112
|
+
if (!stored) throw new Error("daemon token was not stored");
|
|
113
|
+
return stored;
|
|
114
|
+
}
|
|
115
|
+
function readDaemonToken() {
|
|
116
|
+
try {
|
|
117
|
+
const raw = readFileSync(paths().token, "utf8").trim();
|
|
118
|
+
return isDaemonToken(raw) ? raw : void 0;
|
|
119
|
+
} catch {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
4
123
|
|
|
5
124
|
// ../protocol/src/session.ts
|
|
6
125
|
import { z } from "zod";
|
|
@@ -115,62 +234,100 @@ function isArchived(s) {
|
|
|
115
234
|
return s.archivedAt !== void 0;
|
|
116
235
|
}
|
|
117
236
|
|
|
237
|
+
// ../protocol/src/tasks.ts
|
|
238
|
+
import { z as z2 } from "zod";
|
|
239
|
+
var TASK_TITLE_MAX = 200;
|
|
240
|
+
var TASK_TEXT_MAX = 2e4;
|
|
241
|
+
var TaskSchema = z2.object({
|
|
242
|
+
id: z2.string(),
|
|
243
|
+
title: z2.string(),
|
|
244
|
+
text: z2.string().optional(),
|
|
245
|
+
giver: z2.string(),
|
|
246
|
+
giverName: z2.string(),
|
|
247
|
+
owner: z2.string(),
|
|
248
|
+
ownerName: z2.string(),
|
|
249
|
+
state: z2.enum(["open", "taken", "done", "refused", "dropped"]),
|
|
250
|
+
result: z2.string().optional(),
|
|
251
|
+
at: z2.string(),
|
|
252
|
+
updatedAt: z2.string(),
|
|
253
|
+
log: z2.array(z2.object({ at: z2.string(), by: z2.string(), what: z2.string() }))
|
|
254
|
+
});
|
|
255
|
+
var taskClosed = (t) => t.state === "done" || t.state === "refused" || t.state === "dropped";
|
|
256
|
+
var TaskCmdSchema = z2.object({
|
|
257
|
+
type: z2.literal("task.cmd"),
|
|
258
|
+
rid: z2.string().min(1),
|
|
259
|
+
by: z2.string().min(1),
|
|
260
|
+
op: z2.enum(["give", "take", "done", "refuse", "pass", "show", "list"]),
|
|
261
|
+
task: z2.string().min(1).max(40).optional(),
|
|
262
|
+
to: z2.string().min(1).optional(),
|
|
263
|
+
title: z2.string().min(1).max(TASK_TITLE_MAX).optional(),
|
|
264
|
+
text: z2.string().max(TASK_TEXT_MAX).optional(),
|
|
265
|
+
all: z2.boolean().optional()
|
|
266
|
+
});
|
|
267
|
+
var TaskResultSchema = z2.object({
|
|
268
|
+
type: z2.literal("task.result"),
|
|
269
|
+
rid: z2.string(),
|
|
270
|
+
task: TaskSchema.optional(),
|
|
271
|
+
tasks: z2.array(TaskSchema).optional(),
|
|
272
|
+
reason: z2.string().optional()
|
|
273
|
+
});
|
|
274
|
+
|
|
118
275
|
// ../protocol/src/messages.ts
|
|
119
|
-
import { z as
|
|
276
|
+
import { z as z16 } from "zod";
|
|
120
277
|
|
|
121
278
|
// ../protocol/src/chat.ts
|
|
122
|
-
import { z as
|
|
123
|
-
var ChatStepSchema =
|
|
124
|
-
kind:
|
|
125
|
-
id:
|
|
126
|
-
name:
|
|
127
|
-
path:
|
|
279
|
+
import { z as z3 } from "zod";
|
|
280
|
+
var ChatStepSchema = z3.object({
|
|
281
|
+
kind: z3.enum(["sessions", "pane", "reply", "git", "repo"]),
|
|
282
|
+
id: z3.string().optional(),
|
|
283
|
+
name: z3.string().optional(),
|
|
284
|
+
path: z3.string().optional()
|
|
128
285
|
});
|
|
129
|
-
var ChatProposalSchema =
|
|
130
|
-
cwd:
|
|
131
|
-
project:
|
|
132
|
-
prompt:
|
|
286
|
+
var ChatProposalSchema = z3.object({
|
|
287
|
+
cwd: z3.string().min(1),
|
|
288
|
+
project: z3.string(),
|
|
289
|
+
prompt: z3.string().optional(),
|
|
133
290
|
tool: ToolSchema,
|
|
134
|
-
launchedId:
|
|
135
|
-
error:
|
|
291
|
+
launchedId: z3.string().optional(),
|
|
292
|
+
error: z3.string().optional()
|
|
136
293
|
});
|
|
137
294
|
var MAX_PROPOSALS = 8;
|
|
138
|
-
var ChatProposalsSchema =
|
|
139
|
-
var ChatRelaySchema =
|
|
140
|
-
id:
|
|
141
|
-
name:
|
|
295
|
+
var ChatProposalsSchema = z3.array(ChatProposalSchema).min(1).max(MAX_PROPOSALS);
|
|
296
|
+
var ChatRelaySchema = z3.object({
|
|
297
|
+
id: z3.string().min(1),
|
|
298
|
+
name: z3.string(),
|
|
142
299
|
tool: ToolSchema,
|
|
143
|
-
text:
|
|
144
|
-
status:
|
|
145
|
-
sentAt:
|
|
146
|
-
error:
|
|
300
|
+
text: z3.string().min(1),
|
|
301
|
+
status: z3.enum(["queued", "sent"]).optional(),
|
|
302
|
+
sentAt: z3.string().datetime().optional(),
|
|
303
|
+
error: z3.string().optional()
|
|
147
304
|
});
|
|
148
|
-
var ChatCloseSchema =
|
|
149
|
-
id:
|
|
150
|
-
name:
|
|
305
|
+
var ChatCloseSchema = z3.object({
|
|
306
|
+
id: z3.string().min(1),
|
|
307
|
+
name: z3.string(),
|
|
151
308
|
tool: ToolSchema,
|
|
152
|
-
closedAt:
|
|
153
|
-
how:
|
|
154
|
-
error:
|
|
309
|
+
closedAt: z3.string().datetime().optional(),
|
|
310
|
+
how: z3.enum(["exit", "killed"]).optional(),
|
|
311
|
+
error: z3.string().optional()
|
|
155
312
|
});
|
|
156
|
-
var ChatClosesSchema =
|
|
157
|
-
var ChatProjectProposalSchema =
|
|
158
|
-
name:
|
|
159
|
-
description:
|
|
160
|
-
repos:
|
|
161
|
-
addTo:
|
|
313
|
+
var ChatClosesSchema = z3.array(ChatCloseSchema).min(1).max(MAX_PROPOSALS);
|
|
314
|
+
var ChatProjectProposalSchema = z3.object({
|
|
315
|
+
name: z3.string().min(1).max(80),
|
|
316
|
+
description: z3.string().max(500).optional(),
|
|
317
|
+
repos: z3.array(z3.object({ path: z3.string().min(1).max(4096), why: z3.string().min(1).max(200) })).min(1).max(50),
|
|
318
|
+
addTo: z3.string().min(1).max(80).optional()
|
|
162
319
|
});
|
|
163
|
-
var ChatTurnSchema =
|
|
164
|
-
machineId:
|
|
165
|
-
machineName:
|
|
166
|
-
choices:
|
|
167
|
-
qid:
|
|
168
|
-
question:
|
|
169
|
-
askedAt:
|
|
170
|
-
steps:
|
|
171
|
-
answer:
|
|
172
|
-
answeredAt:
|
|
173
|
-
error:
|
|
320
|
+
var ChatTurnSchema = z3.object({
|
|
321
|
+
machineId: z3.string().optional(),
|
|
322
|
+
machineName: z3.string().optional(),
|
|
323
|
+
choices: z3.array(z3.object({ id: z3.string(), label: z3.string() })).optional(),
|
|
324
|
+
qid: z3.string().min(1),
|
|
325
|
+
question: z3.string(),
|
|
326
|
+
askedAt: z3.string().datetime(),
|
|
327
|
+
steps: z3.array(ChatStepSchema),
|
|
328
|
+
answer: z3.string().optional(),
|
|
329
|
+
answeredAt: z3.string().datetime().optional(),
|
|
330
|
+
error: z3.string().optional(),
|
|
174
331
|
proposal: ChatProposalSchema.optional(),
|
|
175
332
|
proposals: ChatProposalsSchema.optional(),
|
|
176
333
|
relay: ChatRelaySchema.optional(),
|
|
@@ -180,70 +337,70 @@ var ChatTurnSchema = z2.object({
|
|
|
180
337
|
var proposalRows = (t) => t?.proposals ?? (t?.proposal ? [t.proposal] : []);
|
|
181
338
|
|
|
182
339
|
// ../protocol/src/repos.ts
|
|
183
|
-
import { z as
|
|
184
|
-
var ReposInspectSchema =
|
|
185
|
-
type:
|
|
186
|
-
requestId:
|
|
187
|
-
paths:
|
|
188
|
-
machineId:
|
|
340
|
+
import { z as z4 } from "zod";
|
|
341
|
+
var ReposInspectSchema = z4.object({
|
|
342
|
+
type: z4.literal("repos.inspect"),
|
|
343
|
+
requestId: z4.string().min(1).max(100),
|
|
344
|
+
paths: z4.array(z4.string().min(1)).min(1).max(50),
|
|
345
|
+
machineId: z4.string().optional()
|
|
189
346
|
});
|
|
190
|
-
var RepoInfoSchema =
|
|
191
|
-
var ReposInfoSchema =
|
|
347
|
+
var RepoInfoSchema = z4.object({ path: z4.string(), exists: z4.boolean(), branch: z4.string().nullable() });
|
|
348
|
+
var ReposInfoSchema = z4.object({ type: z4.literal("repos.info"), requestId: z4.string(), repos: z4.array(RepoInfoSchema) });
|
|
192
349
|
|
|
193
350
|
// ../protocol/src/models.ts
|
|
194
|
-
import { z as
|
|
195
|
-
var ModelIdSchema =
|
|
196
|
-
var EffortSchema =
|
|
197
|
-
var ModelChoiceSchema =
|
|
351
|
+
import { z as z5 } from "zod";
|
|
352
|
+
var ModelIdSchema = z5.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:/@[\]=,+-]*$/, "not a model id");
|
|
353
|
+
var EffortSchema = z5.string().min(1).max(20).regex(/^[a-z]+$/, "not an effort");
|
|
354
|
+
var ModelChoiceSchema = z5.object({
|
|
198
355
|
id: ModelIdSchema,
|
|
199
|
-
label:
|
|
200
|
-
description:
|
|
201
|
-
efforts:
|
|
356
|
+
label: z5.string(),
|
|
357
|
+
description: z5.string().optional(),
|
|
358
|
+
efforts: z5.array(EffortSchema).optional(),
|
|
202
359
|
defaultEffort: EffortSchema.optional(),
|
|
203
|
-
isDefault:
|
|
360
|
+
isDefault: z5.boolean().optional()
|
|
204
361
|
});
|
|
205
|
-
var ModelsListSchema =
|
|
206
|
-
type:
|
|
207
|
-
requestId:
|
|
362
|
+
var ModelsListSchema = z5.object({
|
|
363
|
+
type: z5.literal("models.list"),
|
|
364
|
+
requestId: z5.string().min(1).max(100),
|
|
208
365
|
tool: ToolSchema,
|
|
209
|
-
machineId:
|
|
366
|
+
machineId: z5.string().optional()
|
|
210
367
|
});
|
|
211
|
-
var ModelsListingSchema =
|
|
212
|
-
type:
|
|
213
|
-
requestId:
|
|
368
|
+
var ModelsListingSchema = z5.object({
|
|
369
|
+
type: z5.literal("models.listing"),
|
|
370
|
+
requestId: z5.string(),
|
|
214
371
|
tool: ToolSchema,
|
|
215
|
-
models:
|
|
216
|
-
error:
|
|
372
|
+
models: z5.array(ModelChoiceSchema),
|
|
373
|
+
error: z5.string().optional()
|
|
217
374
|
});
|
|
218
375
|
|
|
219
376
|
// ../protocol/src/scan.ts
|
|
220
|
-
import { z as
|
|
221
|
-
var ReposScanSchema =
|
|
222
|
-
var FoundRepoSchema =
|
|
223
|
-
var ReposFoundSchema =
|
|
377
|
+
import { z as z6 } from "zod";
|
|
378
|
+
var ReposScanSchema = z6.object({ type: z6.literal("repos.scan"), requestId: z6.string().min(1).max(100), machineId: z6.string().optional() });
|
|
379
|
+
var FoundRepoSchema = z6.object({ path: z6.string(), branch: z6.string().nullable(), remote: z6.string().optional(), manifest: z6.string().max(200).optional() });
|
|
380
|
+
var ReposFoundSchema = z6.object({ type: z6.literal("repos.found"), requestId: z6.string(), repos: z6.array(FoundRepoSchema).max(500), truncated: z6.boolean(), error: z6.string().optional() });
|
|
224
381
|
|
|
225
382
|
// ../protocol/src/documents.ts
|
|
226
|
-
import { z as
|
|
383
|
+
import { z as z7 } from "zod";
|
|
227
384
|
var DOCUMENT_MAX_BYTES = 1048576;
|
|
228
|
-
var path =
|
|
229
|
-
var requestId =
|
|
230
|
-
var DocumentReadSchema =
|
|
231
|
-
type:
|
|
385
|
+
var path = z7.string().min(1).max(4096);
|
|
386
|
+
var requestId = z7.string().min(1);
|
|
387
|
+
var DocumentReadSchema = z7.object({
|
|
388
|
+
type: z7.literal("document.read"),
|
|
232
389
|
requestId,
|
|
233
390
|
path,
|
|
234
|
-
machineId:
|
|
235
|
-
sessionId:
|
|
391
|
+
machineId: z7.string().optional(),
|
|
392
|
+
sessionId: z7.string().min(1).optional(),
|
|
236
393
|
basePath: path.optional()
|
|
237
394
|
});
|
|
238
|
-
var DocumentContentSchema =
|
|
239
|
-
type:
|
|
395
|
+
var DocumentContentSchema = z7.object({
|
|
396
|
+
type: z7.literal("document.content"),
|
|
240
397
|
requestId,
|
|
241
398
|
path,
|
|
242
|
-
text:
|
|
243
|
-
bytes:
|
|
244
|
-
modifiedAt:
|
|
399
|
+
text: z7.string(),
|
|
400
|
+
bytes: z7.number().int().nonnegative(),
|
|
401
|
+
modifiedAt: z7.string().datetime()
|
|
245
402
|
});
|
|
246
|
-
var DocumentErrorCodeSchema =
|
|
403
|
+
var DocumentErrorCodeSchema = z7.enum([
|
|
247
404
|
"not_found",
|
|
248
405
|
"not_file",
|
|
249
406
|
"unsupported_path",
|
|
@@ -255,47 +412,47 @@ var DocumentErrorCodeSchema = z6.enum([
|
|
|
255
412
|
"session_missing",
|
|
256
413
|
"read_failed"
|
|
257
414
|
]);
|
|
258
|
-
var DocumentErrorSchema =
|
|
259
|
-
type:
|
|
415
|
+
var DocumentErrorSchema = z7.object({
|
|
416
|
+
type: z7.literal("document.error"),
|
|
260
417
|
requestId,
|
|
261
418
|
path,
|
|
262
419
|
code: DocumentErrorCodeSchema
|
|
263
420
|
});
|
|
264
421
|
|
|
265
422
|
// ../protocol/src/routing.ts
|
|
266
|
-
import { z as
|
|
267
|
-
var MachineInventorySchema =
|
|
268
|
-
id:
|
|
269
|
-
label:
|
|
270
|
-
online:
|
|
271
|
-
local:
|
|
272
|
-
sessions:
|
|
273
|
-
folders:
|
|
423
|
+
import { z as z8 } from "zod";
|
|
424
|
+
var MachineInventorySchema = z8.object({
|
|
425
|
+
id: z8.string().min(1).max(80),
|
|
426
|
+
label: z8.string().min(1).max(120),
|
|
427
|
+
online: z8.boolean(),
|
|
428
|
+
local: z8.boolean(),
|
|
429
|
+
sessions: z8.array(z8.object({ id: z8.string(), name: z8.string(), project: z8.string(), cwd: z8.string(), topic: z8.string().optional(), state: z8.string(), readOnly: z8.boolean() })).max(500),
|
|
430
|
+
folders: z8.array(z8.object({ project: z8.string(), cwd: z8.string() })).max(100)
|
|
274
431
|
});
|
|
275
|
-
var RoutingContextSchema =
|
|
276
|
-
machines:
|
|
277
|
-
history:
|
|
432
|
+
var RoutingContextSchema = z8.object({
|
|
433
|
+
machines: z8.array(MachineInventorySchema).min(1).max(20),
|
|
434
|
+
history: z8.array(z8.object({ question: z8.string().max(2e4), answer: z8.string().max(2e4).optional(), machineId: z8.string().optional() })).max(50)
|
|
278
435
|
});
|
|
279
|
-
var RouteResultSchema =
|
|
436
|
+
var RouteResultSchema = z8.object({
|
|
280
437
|
/** Null means an answer or a clarification, never an implicit local destination. */
|
|
281
|
-
machineId:
|
|
282
|
-
text:
|
|
283
|
-
choices:
|
|
438
|
+
machineId: z8.string().nullable(),
|
|
439
|
+
text: z8.string().min(1).max(2e4),
|
|
440
|
+
choices: z8.array(z8.string()).max(20).optional()
|
|
284
441
|
});
|
|
285
442
|
|
|
286
443
|
// ../protocol/src/ssh.ts
|
|
287
|
-
import { z as
|
|
288
|
-
var SshAliasSchema =
|
|
444
|
+
import { z as z9 } from "zod";
|
|
445
|
+
var SshAliasSchema = z9.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/);
|
|
289
446
|
|
|
290
447
|
// ../protocol/src/machines.ts
|
|
291
|
-
import { z as
|
|
292
|
-
var MachineIdSchema =
|
|
293
|
-
var MachineSchema =
|
|
448
|
+
import { z as z10 } from "zod";
|
|
449
|
+
var MachineIdSchema = z10.string().regex(/^[a-zA-Z0-9_-]{1,64}$/);
|
|
450
|
+
var MachineSchema = z10.object({
|
|
294
451
|
id: MachineIdSchema,
|
|
295
|
-
label:
|
|
452
|
+
label: z10.string().trim().min(1).max(80),
|
|
296
453
|
ssh: SshAliasSchema.optional()
|
|
297
454
|
});
|
|
298
|
-
var MachinesSchema =
|
|
455
|
+
var MachinesSchema = z10.array(MachineSchema).min(1).max(20).superRefine((list, ctx) => {
|
|
299
456
|
if (list[0]?.id !== "local" || list[0].ssh !== void 0) ctx.addIssue({ code: "custom", message: "the first machine is local, without ssh", path: [0] });
|
|
300
457
|
const ids = /* @__PURE__ */ new Set(), aliases = /* @__PURE__ */ new Set();
|
|
301
458
|
list.forEach((m, i) => {
|
|
@@ -310,52 +467,53 @@ var MachinesSchema = z9.array(MachineSchema).min(1).max(20).superRefine((list, c
|
|
|
310
467
|
});
|
|
311
468
|
|
|
312
469
|
// ../protocol/src/island.ts
|
|
313
|
-
import { z as
|
|
314
|
-
var IslandScaleSchema =
|
|
315
|
-
var IslandPrefsSchema =
|
|
470
|
+
import { z as z11 } from "zod";
|
|
471
|
+
var IslandScaleSchema = z11.union([z11.literal("auto"), z11.literal(1), z11.literal(1.25), z11.literal(1.5), z11.literal(2)]);
|
|
472
|
+
var IslandPrefsSchema = z11.object({
|
|
316
473
|
/** Shown at the top of the screen; hidden, Desktop still notifies. */
|
|
317
|
-
visible:
|
|
474
|
+
visible: z11.boolean(),
|
|
318
475
|
/** `primary` (whichever it is after a reconfiguration), or a monitor's id as the shell lists it (`list_monitors`: the OS
|
|
319
476
|
* name, `#index` without one). `second`, written by older fronts, is the first monitor that is not the primary. */
|
|
320
|
-
screen:
|
|
321
|
-
position:
|
|
477
|
+
screen: z11.string().trim().min(1).max(200),
|
|
478
|
+
position: z11.enum(["center", "left", "right"]),
|
|
322
479
|
/** A strip under a maximised window. */
|
|
323
|
-
discreet:
|
|
480
|
+
discreet: z11.boolean(),
|
|
324
481
|
islandScale: IslandScaleSchema,
|
|
325
482
|
panelScale: IslandScaleSchema,
|
|
326
483
|
/** What the island shows besides asks and questions, which it always shows. */
|
|
327
|
-
shows:
|
|
484
|
+
shows: z11.object({
|
|
328
485
|
/** Allow, deny or answer in a line from the island; off, its cards only say « Open in Ledge ». */
|
|
329
|
-
quick:
|
|
486
|
+
quick: z11.boolean(),
|
|
330
487
|
/** The 6 s bubble when an agent finishes. */
|
|
331
|
-
finished:
|
|
488
|
+
finished: z11.boolean(),
|
|
332
489
|
/** An agent's sentence in a bubble. */
|
|
333
|
-
say:
|
|
490
|
+
say: z11.boolean(),
|
|
334
491
|
/** The sub-agents next to their main agent. */
|
|
335
|
-
helpers:
|
|
492
|
+
helpers: z11.boolean()
|
|
336
493
|
}),
|
|
337
494
|
/** The global shortcut that opens the island, as shown (`Ctrl Alt L`). */
|
|
338
|
-
shortcut:
|
|
495
|
+
shortcut: z11.string().trim().min(1).max(40)
|
|
339
496
|
});
|
|
340
497
|
|
|
341
498
|
// ../protocol/src/projects.ts
|
|
342
|
-
import { z as
|
|
343
|
-
var
|
|
344
|
-
var
|
|
499
|
+
import { z as z12 } from "zod";
|
|
500
|
+
var PROJECT_COLORS = { blue: "#3b82f6", green: "#10b981", amber: "#f59e0b", violet: "#8b5cf6", teal: "#14b8a6", pink: "#ec4899", red: "#ef4444", slate: "#64748b" };
|
|
501
|
+
var ProjectColorSchema = z12.enum(["blue", "green", "amber", "violet", "teal", "pink", "red", "slate"]);
|
|
502
|
+
var ProjectIdSchema = z12.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/);
|
|
345
503
|
function normalizePath(raw) {
|
|
346
504
|
const p = raw.trim().replace(/\/+/g, "/");
|
|
347
505
|
return p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
|
|
348
506
|
}
|
|
349
|
-
var PathSchema =
|
|
350
|
-
var RepoSchema =
|
|
351
|
-
var ProjectSchema =
|
|
507
|
+
var PathSchema = z12.string().max(4096).refine((p) => p.startsWith("/") && p === normalizePath(p), "an absolute, normalized path");
|
|
508
|
+
var RepoSchema = z12.object({ machine: MachineIdSchema, path: PathSchema });
|
|
509
|
+
var ProjectSchema = z12.object({
|
|
352
510
|
id: ProjectIdSchema,
|
|
353
|
-
name:
|
|
511
|
+
name: z12.string().trim().min(1).max(80),
|
|
354
512
|
color: ProjectColorSchema,
|
|
355
|
-
description:
|
|
356
|
-
repos:
|
|
513
|
+
description: z12.string().max(500).default(""),
|
|
514
|
+
repos: z12.array(RepoSchema).max(50)
|
|
357
515
|
});
|
|
358
|
-
var RuleSchema =
|
|
516
|
+
var RuleSchema = z12.object({ machine: MachineIdSchema, path: PathSchema, project: ProjectIdSchema });
|
|
359
517
|
function projectsProblems(p) {
|
|
360
518
|
const out = [];
|
|
361
519
|
const ids = /* @__PURE__ */ new Set(), names = /* @__PURE__ */ new Set(), repos = /* @__PURE__ */ new Set(), rules = /* @__PURE__ */ new Set();
|
|
@@ -381,7 +539,7 @@ ${rule.path}`;
|
|
|
381
539
|
});
|
|
382
540
|
return out;
|
|
383
541
|
}
|
|
384
|
-
var ProjectsSchema =
|
|
542
|
+
var ProjectsSchema = z12.object({ projects: z12.array(ProjectSchema).max(100), rules: z12.array(RuleSchema).max(1e3) }).superRefine((p, ctx) => {
|
|
385
543
|
for (const problem of projectsProblems(p)) ctx.addIssue({ code: "custom", message: problem.code, path: problem.path });
|
|
386
544
|
});
|
|
387
545
|
var DEFAULT_PROJECTS = { projects: [], rules: [] };
|
|
@@ -399,47 +557,47 @@ function projectOf(session, state) {
|
|
|
399
557
|
}
|
|
400
558
|
|
|
401
559
|
// ../protocol/src/thread.ts
|
|
402
|
-
import { z as
|
|
403
|
-
var ToolStatusSchema =
|
|
404
|
-
var FileChangeSchema =
|
|
405
|
-
var base = { id:
|
|
406
|
-
var ThreadItemSchema =
|
|
407
|
-
|
|
408
|
-
|
|
560
|
+
import { z as z13 } from "zod";
|
|
561
|
+
var ToolStatusSchema = z13.enum(["running", "success", "failed", "unknown"]);
|
|
562
|
+
var FileChangeSchema = z13.object({ path: z13.string().min(1), from: z13.string().min(1).optional(), change: z13.enum(["add", "edit", "delete", "write"]), diff: z13.string().optional() });
|
|
563
|
+
var base = { id: z13.string().min(1), at: z13.string().datetime(), turn: z13.number().int().nonnegative(), helperId: z13.string().optional() };
|
|
564
|
+
var ThreadItemSchema = z13.discriminatedUnion("kind", [
|
|
565
|
+
z13.object({ kind: z13.literal("prompt"), ...base, text: z13.string() }),
|
|
566
|
+
z13.object({ kind: z13.literal("message"), ...base, text: z13.string() }),
|
|
409
567
|
/** `id` is the harness's tool_use_id when it gives one, so the result updates the same row; `byClose`: a question
|
|
410
568
|
* dismissed by Ledge closing the session. */
|
|
411
|
-
|
|
569
|
+
z13.object({ kind: z13.literal("tool"), ...base, tool: z13.string(), label: z13.string(), input: z13.string().optional(), output: z13.string().optional(), error: z13.string().optional(), status: ToolStatusSchema, durationMs: z13.number().nonnegative().optional(), files: z13.array(FileChangeSchema).default([]), byClose: z13.literal(true).optional() }),
|
|
412
570
|
/** `unknown`: answered in the terminal, the daemon only saw the prompt go away; `always`: allowed and not asked again;
|
|
413
571
|
* `byClose`: denied by Ledge closing the session. */
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
572
|
+
z13.object({ kind: z13.literal("permission"), ...base, tool: z13.string(), text: z13.string(), decision: z13.enum(["accept", "deny", "always", "unknown"]).optional(), byClose: z13.literal(true).optional() }),
|
|
573
|
+
z13.object({ kind: z13.literal("reply"), ...base, text: z13.string() }),
|
|
574
|
+
z13.object({ kind: z13.literal("helper"), ...base, task: z13.string(), agentType: z13.string().optional(), state: z13.enum(["working", "exited"]) }),
|
|
417
575
|
/** 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. */
|
|
418
|
-
|
|
576
|
+
z13.object({ kind: z13.literal("sent"), ...base, to: z13.string(), peer: z13.string().optional(), summary: z13.string().optional(), text: z13.string(), status: z13.enum(["sending", "delivered", "read", "failed"]), received: z13.string().optional(), via: z13.literal("ledge").optional() }),
|
|
419
577
|
/** 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. */
|
|
420
|
-
|
|
578
|
+
z13.object({ kind: z13.literal("received"), ...base, from: z13.string(), peer: z13.string().optional(), text: z13.string(), sent: z13.string().optional() }),
|
|
421
579
|
/** An automated notice of the agent's harness about messages between sessions (Claude Code's `[Cross-session idle
|
|
422
580
|
* notice]` / `[Cross-session delivery notice]`): a system event, never the user's words. `name` is the session it is
|
|
423
581
|
* about as quoted, `peer` its Ledge session id; a delivery notice says the outcome of `count` messages to `recipients`. */
|
|
424
|
-
|
|
425
|
-
kind:
|
|
582
|
+
z13.object({
|
|
583
|
+
kind: z13.literal("notice"),
|
|
426
584
|
...base,
|
|
427
|
-
notice:
|
|
428
|
-
name:
|
|
429
|
-
peer:
|
|
430
|
-
time:
|
|
431
|
-
summary:
|
|
432
|
-
status:
|
|
433
|
-
count:
|
|
434
|
-
recipients:
|
|
435
|
-
reason:
|
|
436
|
-
text:
|
|
585
|
+
notice: z13.enum(["idle", "exited", "unavailable", "expired", "delivery", "other"]),
|
|
586
|
+
name: z13.string().optional(),
|
|
587
|
+
peer: z13.string().optional(),
|
|
588
|
+
time: z13.string().optional(),
|
|
589
|
+
summary: z13.string().optional(),
|
|
590
|
+
status: z13.enum(["held", "denied", "expired", "delivered", "refused", "dropped", "unknown"]).optional(),
|
|
591
|
+
count: z13.number().int().positive().optional(),
|
|
592
|
+
recipients: z13.array(z13.string()).optional(),
|
|
593
|
+
reason: z13.string().optional(),
|
|
594
|
+
text: z13.string()
|
|
437
595
|
})
|
|
438
596
|
]);
|
|
439
597
|
|
|
440
598
|
// ../protocol/src/decisions.ts
|
|
441
|
-
import { z as
|
|
442
|
-
var DecisionSchema =
|
|
599
|
+
import { z as z14 } from "zod";
|
|
600
|
+
var DecisionSchema = z14.enum(["accept", "deny", "always"]);
|
|
443
601
|
var NOTE_MAX = 4e3;
|
|
444
602
|
var NOTE_NOT_TYPED = "note_not_typed";
|
|
445
603
|
var DECISION_OFFERS = {
|
|
@@ -469,12 +627,12 @@ function pendingKey(p) {
|
|
|
469
627
|
}
|
|
470
628
|
|
|
471
629
|
// ../protocol/src/mobile.ts
|
|
472
|
-
import { z as
|
|
630
|
+
import { z as z15 } from "zod";
|
|
473
631
|
var DEFAULT_RELAY_URL = "https://app.kl-ledge.dev";
|
|
474
|
-
var RelayUrlSchema =
|
|
632
|
+
var RelayUrlSchema = z15.string().transform((v, ctx) => {
|
|
475
633
|
const bad = () => {
|
|
476
|
-
ctx.addIssue({ code:
|
|
477
|
-
return
|
|
634
|
+
ctx.addIssue({ code: z15.ZodIssueCode.custom, message: "invalid relay URL" });
|
|
635
|
+
return z15.NEVER;
|
|
478
636
|
};
|
|
479
637
|
let u;
|
|
480
638
|
try {
|
|
@@ -487,108 +645,108 @@ var RelayUrlSchema = z14.string().transform((v, ctx) => {
|
|
|
487
645
|
if (u.username || u.password || u.pathname !== "/" || u.search || u.hash) return bad();
|
|
488
646
|
return u.origin;
|
|
489
647
|
});
|
|
490
|
-
var MobileConfigSchema =
|
|
491
|
-
var MobileLinkStateSchema =
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
648
|
+
var MobileConfigSchema = z15.object({ enabled: z15.boolean(), relayUrl: RelayUrlSchema });
|
|
649
|
+
var MobileLinkStateSchema = z15.discriminatedUnion("state", [
|
|
650
|
+
z15.object({ state: z15.literal("off") }),
|
|
651
|
+
z15.object({ state: z15.literal("connecting") }),
|
|
652
|
+
z15.object({ state: z15.literal("online") }),
|
|
653
|
+
z15.object({ state: z15.literal("offline"), retryAt: z15.string().datetime(), error: z15.string() }),
|
|
654
|
+
z15.object({ state: z15.literal("signed_out"), reason: z15.enum(["other_pc", "session_invalid"]) }),
|
|
497
655
|
/** The mobile service did not start (a broken mobile.json, a missing key store): nothing is linked until the daemon restarts. */
|
|
498
|
-
|
|
656
|
+
z15.object({ state: z15.literal("unavailable"), error: z15.string().max(200) })
|
|
499
657
|
]);
|
|
500
|
-
var MobilePairingSchema =
|
|
501
|
-
id:
|
|
502
|
-
label:
|
|
503
|
-
code:
|
|
504
|
-
expiresAt:
|
|
505
|
-
state:
|
|
658
|
+
var MobilePairingSchema = z15.object({
|
|
659
|
+
id: z15.string(),
|
|
660
|
+
label: z15.string(),
|
|
661
|
+
code: z15.string().nullable(),
|
|
662
|
+
expiresAt: z15.string().datetime(),
|
|
663
|
+
state: z15.enum(["waiting", "confirm", "approved", "linked", "expired", "refused"])
|
|
506
664
|
});
|
|
507
|
-
var MobileStateSchema =
|
|
508
|
-
enabled:
|
|
509
|
-
relayUrl:
|
|
510
|
-
account:
|
|
511
|
-
signin:
|
|
665
|
+
var MobileStateSchema = z15.object({
|
|
666
|
+
enabled: z15.boolean(),
|
|
667
|
+
relayUrl: z15.string(),
|
|
668
|
+
account: z15.object({ login: z15.string(), accountId: z15.string() }).nullable(),
|
|
669
|
+
signin: z15.object({ userCode: z15.string(), verificationUri: z15.string(), expiresAt: z15.string().datetime() }).nullable(),
|
|
512
670
|
/** The last sign-in that did not end with a session; cleared by the next one. */
|
|
513
|
-
signinError:
|
|
671
|
+
signinError: z15.enum(["expired", "denied", "not_allowed", "failed"]).nullable(),
|
|
514
672
|
link: MobileLinkStateSchema,
|
|
515
|
-
devices:
|
|
516
|
-
pairings:
|
|
673
|
+
devices: z15.array(z15.object({ id: z15.string(), label: z15.string(), approvedAt: z15.string(), online: z15.boolean(), lastSeenAt: z15.string().optional() })),
|
|
674
|
+
pairings: z15.array(MobilePairingSchema)
|
|
517
675
|
});
|
|
518
676
|
|
|
519
677
|
// ../protocol/src/messages.ts
|
|
520
678
|
var PROTOCOL_PORT = 47831;
|
|
521
|
-
var RefusalCodeSchema =
|
|
522
|
-
var SessionEventSchema =
|
|
523
|
-
name:
|
|
524
|
-
at:
|
|
525
|
-
source:
|
|
526
|
-
tool:
|
|
527
|
-
text:
|
|
528
|
-
options:
|
|
679
|
+
var RefusalCodeSchema = z16.enum(["stale", "read_only", "no_prompt", "not_taken", "busy", "unknown_session", "nothing_to_decide", "not_offered", "free_text", "note_not_typed", "unavailable", "invalid"]);
|
|
680
|
+
var SessionEventSchema = z16.object({
|
|
681
|
+
name: z16.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd", "PaneChanged", "PaneGone", "PanePrompt", "PaneIdle", "InjectFailed", "Decided", "PostToolUse", "PostToolUseFailure", "StopFailure", "SubagentStart", "SubagentStop", "PaneTitle", "Renamed", "NativeRequest", "AgentMessage", "QueueUpdate"]),
|
|
682
|
+
at: z16.string().datetime(),
|
|
683
|
+
source: z16.enum(["hook", "pane", "daemon"]),
|
|
684
|
+
tool: z16.string().optional(),
|
|
685
|
+
text: z16.string().optional(),
|
|
686
|
+
options: z16.array(z16.string()).optional(),
|
|
529
687
|
/** Each option's description, by index ('' when it has none); left out when no option has one. */
|
|
530
|
-
optionDetails:
|
|
531
|
-
kind:
|
|
688
|
+
optionDetails: z16.array(z16.string()).optional(),
|
|
689
|
+
kind: z16.enum(["question", "permission", "plan", "diff"]).optional(),
|
|
532
690
|
/** PanePrompt: what to review in full before deciding (a plan read in the terminal), when `text` is its summary. */
|
|
533
|
-
body:
|
|
534
|
-
exitCode:
|
|
535
|
-
helperId:
|
|
691
|
+
body: z16.string().optional(),
|
|
692
|
+
exitCode: z16.number().int().nullable().optional(),
|
|
693
|
+
helperId: z16.string().optional(),
|
|
536
694
|
/** The helper's description, once known (Claude Code: its `.meta.json`, written after SubagentStart). */
|
|
537
|
-
task:
|
|
695
|
+
task: z16.string().optional(),
|
|
538
696
|
/** PostToolUseFailure: the user interrupted the turn (Esc); Claude then waits for input with no Stop. */
|
|
539
|
-
interrupt:
|
|
697
|
+
interrupt: z16.boolean().optional(),
|
|
540
698
|
/** QueueUpdate: OMP's queue as it stands. */
|
|
541
|
-
queue:
|
|
699
|
+
queue: z16.array(z16.object({ text: z16.string(), mode: z16.enum(["steer", "follow_up"]) })).optional(),
|
|
542
700
|
/** AgentMessage: the harness's own message, its text so far (`streaming`) or whole. `messageId` is unique for the session's life. */
|
|
543
|
-
messageId:
|
|
544
|
-
streaming:
|
|
701
|
+
messageId: z16.string().optional(),
|
|
702
|
+
streaming: z16.boolean().optional(),
|
|
545
703
|
/** NativeRequest: the dialog's id, and OMP's `ask` questions when it is one. */
|
|
546
|
-
requestId:
|
|
547
|
-
questions:
|
|
704
|
+
requestId: z16.string().optional(),
|
|
705
|
+
questions: z16.array(AskQuestionSchema).optional(),
|
|
548
706
|
/** Stop: its text is already in the thread as AgentMessage cards, which the reply card must not repeat. */
|
|
549
|
-
streamed:
|
|
707
|
+
streamed: z16.boolean().optional(),
|
|
550
708
|
/** Native tracking (spec 2026-10-13 §2): the harness's id of the tool call, so its result lands on the same row. */
|
|
551
|
-
toolUseId:
|
|
709
|
+
toolUseId: z16.string().optional(),
|
|
552
710
|
/** The tool's input as the harness gave it, every string capped by the hook. */
|
|
553
|
-
toolInput:
|
|
711
|
+
toolInput: z16.record(z16.string(), z16.unknown()).optional(),
|
|
554
712
|
/** PostToolUse: the result as text; SubagentStop: nothing (the report travels in `text`). */
|
|
555
|
-
output:
|
|
556
|
-
durationMs:
|
|
713
|
+
output: z16.string().optional(),
|
|
714
|
+
durationMs: z16.number().nonnegative().optional(),
|
|
557
715
|
/** PostToolUseFailure: the harness's error message. */
|
|
558
|
-
error:
|
|
716
|
+
error: z16.string().optional(),
|
|
559
717
|
/** The session's transcript, or the helper's on SubagentStop; the daemon reads its tail for the agent's messages. */
|
|
560
|
-
transcriptPath:
|
|
718
|
+
transcriptPath: z16.string().optional(),
|
|
561
719
|
/** Decided: what was chosen when the daemon knows it. */
|
|
562
720
|
decision: DecisionSchema.optional(),
|
|
563
721
|
/** Decided: the request was refused by Ledge closing the session (its interrupt keys), not by the user's answer. */
|
|
564
|
-
byClose:
|
|
722
|
+
byClose: z16.literal(true).optional(),
|
|
565
723
|
/** The agent's own session id when the hook names it, so the daemon can resume that conversation. */
|
|
566
|
-
agentSessionId:
|
|
724
|
+
agentSessionId: z16.string().optional()
|
|
567
725
|
});
|
|
568
|
-
var RecentSchema =
|
|
726
|
+
var RecentSchema = z16.object({ cwd: z16.string().min(1), project: z16.string().min(1), lastAt: z16.string().datetime() });
|
|
569
727
|
var SEMVER = /^\d+\.\d+\.\d+$/;
|
|
570
|
-
var DaemonInfoSchema =
|
|
571
|
-
version:
|
|
572
|
-
install:
|
|
573
|
-
systemd:
|
|
574
|
-
manual:
|
|
575
|
-
routing:
|
|
576
|
-
archive:
|
|
728
|
+
var DaemonInfoSchema = z16.object({
|
|
729
|
+
version: z16.string(),
|
|
730
|
+
install: z16.enum(["npm", "git", "unknown"]),
|
|
731
|
+
systemd: z16.boolean(),
|
|
732
|
+
manual: z16.string().optional(),
|
|
733
|
+
routing: z16.boolean().optional(),
|
|
734
|
+
archive: z16.boolean().optional(),
|
|
577
735
|
/** Takes `model` and `effort` on `session.launch` and answers `models.list` (#58). */
|
|
578
|
-
models:
|
|
736
|
+
models: z16.boolean().optional(),
|
|
579
737
|
/** Drives OMP over its RPC: launching `omp` sessions is possible when the binary is there (`models.list` says so). */
|
|
580
|
-
omp:
|
|
738
|
+
omp: z16.boolean().optional(),
|
|
581
739
|
/** Takes `session.model`: a live session's model and effort changed (#58, lot 2). */
|
|
582
|
-
modelSwitch:
|
|
740
|
+
modelSwitch: z16.boolean().optional(),
|
|
583
741
|
/** Takes `approval` on an OMP `session.launch` (auto mode). */
|
|
584
|
-
ompApproval:
|
|
742
|
+
ompApproval: z16.boolean().optional(),
|
|
585
743
|
/** Its home folder, so a client shows the folders under it as `~` (absent from older daemons). */
|
|
586
|
-
home:
|
|
587
|
-
mobile:
|
|
744
|
+
home: z16.string().optional(),
|
|
745
|
+
mobile: z16.boolean().optional()
|
|
588
746
|
});
|
|
589
|
-
var UpdateFailureSchema =
|
|
590
|
-
var ServerMessageSchema =
|
|
591
|
-
|
|
747
|
+
var UpdateFailureSchema = z16.enum(["git-install", "no-systemd", "sessions-at-risk", "npm-failed", "busy"]);
|
|
748
|
+
var ServerMessageSchema = z16.discriminatedUnion("type", [
|
|
749
|
+
z16.object({ type: z16.literal("chat.routed"), qid: z16.string(), result: RouteResultSchema }),
|
|
592
750
|
DocumentContentSchema,
|
|
593
751
|
DocumentErrorSchema,
|
|
594
752
|
ReposInfoSchema,
|
|
@@ -597,69 +755,72 @@ var ServerMessageSchema = z15.discriminatedUnion("type", [
|
|
|
597
755
|
/** `recents`: the folders of past sessions, most recent first. `launcher`: the command that runs `ledge` inside WSL
|
|
598
756
|
* (`[node, cli]`), so the overlay can attach a terminal to a session with `<launcher> attach <id>`. `daemon`: absent
|
|
599
757
|
* from daemons older than the auto-update. */
|
|
600
|
-
|
|
601
|
-
|
|
758
|
+
z16.object({ type: z16.literal("snapshot"), sessions: z16.array(SessionSchema), recents: z16.array(RecentSchema).optional(), launcher: z16.array(z16.string()).optional(), daemon: DaemonInfoSchema.optional(), sshAlias: SshAliasSchema.optional() }),
|
|
759
|
+
z16.object({ type: z16.literal("session.updated"), session: SessionSchema }),
|
|
602
760
|
/** OMP: the instructions taken back from its queue by an interrupt that restores it; the asker puts them back in its draft. */
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
761
|
+
z16.object({ type: z16.literal("session.queue.restored"), id: z16.string(), texts: z16.array(z16.string()) }),
|
|
762
|
+
z16.object({ type: z16.literal("session.removed"), id: z16.string() }),
|
|
763
|
+
z16.object({ type: z16.literal("session.preview"), id: z16.string(), lines: z16.array(z16.string()) }),
|
|
606
764
|
/** A session's thread, on request (spec 2026-10-13 §5); then each created or updated item, broadcast. */
|
|
607
|
-
|
|
608
|
-
|
|
765
|
+
z16.object({ type: z16.literal("session.thread"), id: z16.string(), items: z16.array(ThreadItemSchema) }),
|
|
766
|
+
z16.object({ type: z16.literal("session.thread.item"), id: z16.string(), item: ThreadItemSchema }),
|
|
609
767
|
/** Answer to the requester of a `session.launch`: the session exists (its `session.updated` is broadcast as usual). */
|
|
610
|
-
|
|
768
|
+
z16.object({ type: z16.literal("session.launched"), id: z16.string() }),
|
|
611
769
|
/** Answer to the requester of a `session.close`, once per row closed: the session `id` and how it ended. */
|
|
612
|
-
|
|
613
|
-
|
|
770
|
+
z16.object({ type: z16.literal("session.closed"), qid: z16.string(), row: z16.number().int().min(0), id: z16.string(), how: z16.enum(["exit", "killed"]) }),
|
|
771
|
+
z16.object({ type: z16.literal("recents"), recents: z16.array(RecentSchema) }),
|
|
614
772
|
/** Answer to the requester of a `folder.list` or `folder.create`: `path` resolved (`~` expanded), `parent` null at the
|
|
615
773
|
* root, `entries` its subfolders by name (hidden ones left out). A failure keeps the asked path and says why in `error`,
|
|
616
774
|
* never in an `error` message, which the launch form reads as a failed launch. */
|
|
617
|
-
|
|
618
|
-
type:
|
|
619
|
-
path:
|
|
620
|
-
parent:
|
|
621
|
-
entries:
|
|
622
|
-
error:
|
|
775
|
+
z16.object({
|
|
776
|
+
type: z16.literal("folder.listing"),
|
|
777
|
+
path: z16.string(),
|
|
778
|
+
parent: z16.string().nullable(),
|
|
779
|
+
entries: z16.array(z16.string()),
|
|
780
|
+
error: z16.string().optional(),
|
|
623
781
|
/** The request this answers, when it carried one. */
|
|
624
|
-
requestId:
|
|
782
|
+
requestId: z16.string().optional(),
|
|
625
783
|
/** Those `entries` that are Git repositories, with their branch (the short commit when HEAD is detached). */
|
|
626
|
-
repos:
|
|
784
|
+
repos: z16.array(z16.object({ name: z16.string(), branch: z16.string() })).optional()
|
|
627
785
|
}),
|
|
628
786
|
/** 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. */
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
787
|
+
z16.object({ type: z16.literal("chat.step"), qid: z16.string(), step: ChatStepSchema }),
|
|
788
|
+
z16.object({ type: z16.literal("chat.delta"), qid: z16.string(), text: z16.string() }),
|
|
789
|
+
z16.object({ type: z16.literal("chat.done"), qid: z16.string(), text: z16.string(), at: z16.string().datetime(), proposal: ChatProposalSchema.optional(), proposals: ChatProposalsSchema.optional(), relay: ChatRelaySchema.optional(), closes: ChatClosesSchema.optional(), projectProposal: ChatProjectProposalSchema.optional() }),
|
|
790
|
+
z16.object({ type: z16.literal("chat.error"), qid: z16.string(), message: z16.string() }),
|
|
791
|
+
z16.object({ type: z16.literal("chat.history"), turns: z16.array(ChatTurnSchema) }),
|
|
634
792
|
/** The daemon updating itself: npm is running, then the service restarts (the socket closes right after). */
|
|
635
|
-
|
|
793
|
+
z16.object({ type: z16.literal("update.progress"), version: z16.string(), step: z16.enum(["installing", "restarting"]) }),
|
|
636
794
|
/** Nothing was changed. `detail`: the useful end of npm's output; `command`: what the person can run instead. */
|
|
637
|
-
|
|
795
|
+
z16.object({ type: z16.literal("update.failed"), version: z16.string(), reason: UpdateFailureSchema, detail: z16.string().optional(), command: z16.string().optional() }),
|
|
638
796
|
/** The shared machines list (spec 2026-10-14 §5), sent to local clients only. */
|
|
639
|
-
|
|
797
|
+
z16.object({ type: z16.literal("machines"), machines: MachinesSchema }),
|
|
640
798
|
/** The local daemon's projects and folder rules (spec 2026-10-16 build §5), sent to local clients only. */
|
|
641
|
-
|
|
799
|
+
z16.object({ type: z16.literal("projects"), projects: ProjectsSchema }),
|
|
642
800
|
/** The island's settings shared by the local fronts; null until a front sets them (the island then sends its own). Local clients only. */
|
|
643
|
-
|
|
801
|
+
z16.object({ type: z16.literal("island"), prefs: IslandPrefsSchema.nullable() }),
|
|
644
802
|
/** Mobile access (lot 3), after the island in each snapshot and on every change. Local clients only. */
|
|
645
|
-
|
|
803
|
+
z16.object({ type: z16.literal("mobile"), state: MobileStateSchema }),
|
|
646
804
|
/** 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`. */
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
805
|
+
z16.object({ type: z16.literal("message.ack"), rid: z16.string(), outcome: z16.enum(["accepted", "queued", "native", "refused"]), reason: z16.string().optional() }),
|
|
806
|
+
/** The answer to a `message.read`: the body with its sender, target and time, or the refusal's `reason`. */
|
|
807
|
+
z16.object({ type: z16.literal("message.body"), id: z16.string(), from: z16.string().optional(), to: z16.string().optional(), at: z16.string().optional(), text: z16.string().optional(), reason: z16.string().optional() }),
|
|
808
|
+
TaskResultSchema,
|
|
809
|
+
z16.object({
|
|
810
|
+
type: z16.literal("error"),
|
|
811
|
+
message: z16.string(),
|
|
812
|
+
id: z16.string().optional(),
|
|
652
813
|
/** Le type du message client refusé. */
|
|
653
|
-
request:
|
|
814
|
+
request: z16.string().optional(),
|
|
654
815
|
/** Ce que le front doit reconnaître au-delà du texte (NOTE_NOT_TYPED) ; une chaîne libre, qu'un front plus ancien ignore. */
|
|
655
|
-
code:
|
|
816
|
+
code: z16.string().optional()
|
|
656
817
|
})
|
|
657
818
|
]);
|
|
658
819
|
var MESSAGE_MAX = 2e4;
|
|
659
820
|
var SUMMARY_MAX = 500;
|
|
660
|
-
var launchRows =
|
|
661
|
-
var ClientMessageSchema =
|
|
662
|
-
|
|
821
|
+
var launchRows = z16.array(z16.number().int().min(0).max(MAX_PROPOSALS - 1)).min(1).max(MAX_PROPOSALS).refine((r) => new Set(r).size === r.length, "a row twice");
|
|
822
|
+
var ClientMessageSchema = z16.discriminatedUnion("type", [
|
|
823
|
+
z16.object({ type: z16.literal("chat.route"), qid: z16.string().min(1).max(100), text: z16.string().min(1).max(2e4), tool: ToolSchema.optional(), context: RoutingContextSchema }),
|
|
663
824
|
DocumentReadSchema,
|
|
664
825
|
ReposInspectSchema,
|
|
665
826
|
ReposScanSchema,
|
|
@@ -668,114 +829,117 @@ var ClientMessageSchema = z15.discriminatedUnion("type", [
|
|
|
668
829
|
* free one otherwise), `prompt` the agent's first instruction, `continue` resumes the folder's last conversation.
|
|
669
830
|
* With `qid` it is the chat card of that turn: the daemon launches the turn's proposal and ignores the other fields
|
|
670
831
|
* but `rows`, `model` and `effort`. */
|
|
671
|
-
|
|
672
|
-
type:
|
|
832
|
+
z16.object({
|
|
833
|
+
type: z16.literal("session.launch"),
|
|
673
834
|
tool: ToolSchema,
|
|
674
|
-
cwd:
|
|
675
|
-
name:
|
|
676
|
-
prompt:
|
|
677
|
-
continue:
|
|
678
|
-
qid:
|
|
835
|
+
cwd: z16.string().min(1),
|
|
836
|
+
name: z16.string().optional(),
|
|
837
|
+
prompt: z16.string().optional(),
|
|
838
|
+
continue: z16.boolean().optional(),
|
|
839
|
+
qid: z16.string().min(1).optional(),
|
|
679
840
|
/** With `qid`: the card's rows to launch (#24), indexes into the turn's `proposals`, each once; the first row without it. */
|
|
680
841
|
rows: launchRows.optional(),
|
|
681
842
|
/** Folders in addition to `cwd` (Claude: `--add-dir`); resolved like `cwd`. */
|
|
682
|
-
dirs:
|
|
843
|
+
dirs: z16.array(z16.string().min(1)).max(20).optional(),
|
|
683
844
|
/** The agent's model and effort (#58), the agent's own default when left out; with `qid`, for every row launched. */
|
|
684
845
|
model: ModelIdSchema.optional(),
|
|
685
846
|
effort: EffortSchema.optional(),
|
|
686
847
|
/** OMP only: its approval mode, `always-ask` when left out. */
|
|
687
848
|
approval: OmpApprovalSchema.optional(),
|
|
688
849
|
/** Routing by the fleet, removed before the message reaches the daemon. */
|
|
689
|
-
machineId:
|
|
850
|
+
machineId: z16.string().optional()
|
|
690
851
|
}),
|
|
691
852
|
/** The launch form's folder browser: list a folder's subfolders (home when no `path`), or make `name` inside `parent`
|
|
692
853
|
* and list it. Both are answered with a `folder.listing`. */
|
|
693
|
-
|
|
694
|
-
type:
|
|
695
|
-
path:
|
|
854
|
+
z16.object({
|
|
855
|
+
type: z16.literal("folder.list"),
|
|
856
|
+
path: z16.string().optional(),
|
|
696
857
|
/** Routing by the fleet, removed before the message reaches the daemon. */
|
|
697
|
-
machineId:
|
|
858
|
+
machineId: z16.string().optional(),
|
|
698
859
|
/** Copied into the `folder.listing` that answers; the fleet relays only that listing, from the machine asked. */
|
|
699
|
-
requestId:
|
|
860
|
+
requestId: z16.string().min(1).max(100).optional()
|
|
700
861
|
}),
|
|
701
|
-
|
|
702
|
-
type:
|
|
703
|
-
parent:
|
|
704
|
-
name:
|
|
862
|
+
z16.object({
|
|
863
|
+
type: z16.literal("folder.create"),
|
|
864
|
+
parent: z16.string().min(1),
|
|
865
|
+
name: z16.string().min(1),
|
|
705
866
|
/** As for `folder.list`: routing by the fleet, and the request its `folder.listing` answers. */
|
|
706
|
-
machineId:
|
|
707
|
-
requestId:
|
|
867
|
+
machineId: z16.string().optional(),
|
|
868
|
+
requestId: z16.string().min(1).max(100).optional()
|
|
708
869
|
}),
|
|
709
870
|
/** `steer`: OMP only, redirect the turn running now instead of queueing the text after it. */
|
|
710
871
|
/** OMP's `ask`: the answers to every question of the dialog `requestId`, in order (`selected` labels, and/or free `text`); `cancel` declines it. */
|
|
711
|
-
|
|
712
|
-
|
|
872
|
+
z16.object({ type: z16.literal("session.answer"), id: z16.string(), requestId: z16.string().min(1).max(200), answers: z16.array(z16.object({ id: z16.string(), selected: z16.array(z16.string().max(2e3)).max(100), text: z16.string().max(2e4).optional() })).max(50), cancel: z16.boolean().optional() }),
|
|
873
|
+
z16.object({ type: z16.literal("session.reply"), id: z16.string(), text: z16.string().min(1), steer: z16.boolean().optional() }),
|
|
713
874
|
/** The chat card's instruction (spec 2026-10-12 relay §4): sent now if the session is free, else queued until it is. */
|
|
714
|
-
|
|
875
|
+
z16.object({ type: z16.literal("session.relay"), qid: z16.string().min(1) }),
|
|
715
876
|
/** Takes the card's queued instruction back out of the queue. */
|
|
716
|
-
|
|
877
|
+
z16.object({ type: z16.literal("session.relay.cancel"), qid: z16.string().min(1) }),
|
|
717
878
|
/** The chat's close card: closes the turn's sessions, `rows` (indexes into its `closes`) or all of them. */
|
|
718
|
-
|
|
879
|
+
z16.object({ type: z16.literal("session.close"), qid: z16.string().min(1), rows: launchRows.optional() }),
|
|
719
880
|
/** `always` and `note` only where the harness offers them (`offers`); a note goes with `deny`, for the agent to read. */
|
|
720
|
-
|
|
721
|
-
type:
|
|
722
|
-
id:
|
|
881
|
+
z16.object({
|
|
882
|
+
type: z16.literal("session.decide"),
|
|
883
|
+
id: z16.string(),
|
|
723
884
|
decision: DecisionSchema,
|
|
724
|
-
note:
|
|
885
|
+
note: z16.string().trim().min(1).max(NOTE_MAX).optional(),
|
|
725
886
|
/** 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.
|
|
726
887
|
* `key` (pendingKey) covers its whole content; older fronts send kind and text only. */
|
|
727
|
-
expect: PendingSchema.pick({ kind: true, text: true }).extend({ key:
|
|
888
|
+
expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z16.string().max(64).optional() }).optional()
|
|
728
889
|
}),
|
|
729
|
-
|
|
730
|
-
|
|
890
|
+
z16.object({ type: z16.literal("session.focus"), id: z16.string() }),
|
|
891
|
+
z16.object({ type: z16.literal("session.kill"), id: z16.string() }),
|
|
731
892
|
/** `restoreQueue` (OMP): also take back what waits in its queue, returned as `session.queue.restored` to put in the draft. */
|
|
732
|
-
|
|
893
|
+
z16.object({ type: z16.literal("session.interrupt"), id: z16.string(), restoreQueue: z16.boolean().optional() }),
|
|
733
894
|
/** Removes a finished (exited) or read-only (hook_only) session from the list; refused for a live launcher session. */
|
|
734
|
-
|
|
895
|
+
z16.object({ type: z16.literal("session.dismiss"), id: z16.string() }),
|
|
735
896
|
/** Puts a session in the archive: it leaves the live list but is kept. */
|
|
736
|
-
|
|
897
|
+
z16.object({ type: z16.literal("session.archive"), id: z16.string() }),
|
|
737
898
|
/** Takes an archived session back out of the archive, as it was. */
|
|
738
|
-
|
|
899
|
+
z16.object({ type: z16.literal("session.restore"), id: z16.string() }),
|
|
739
900
|
/** Starts the agent again on an archived session's conversation. */
|
|
740
|
-
|
|
901
|
+
z16.object({ type: z16.literal("session.resume"), id: z16.string() }),
|
|
741
902
|
/** Changes a live session's model and effort (#58, lot 2), `null` for the agent's own default: the daemon restarts the
|
|
742
903
|
* agent on the same conversation with them, at once when the session is free, else at its next free moment. */
|
|
743
|
-
|
|
904
|
+
z16.object({ type: z16.literal("session.model"), id: z16.string(), model: ModelIdSchema.nullable(), effort: EffortSchema.nullable() }),
|
|
744
905
|
/** Deletes a session for good, archived or not. */
|
|
745
|
-
|
|
906
|
+
z16.object({ type: z16.literal("session.delete"), id: z16.string() }),
|
|
746
907
|
/** 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. */
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
908
|
+
z16.object({ type: z16.literal("session.rename"), id: z16.string(), topic: z16.string().max(500) }),
|
|
909
|
+
z16.object({ type: z16.literal("session.thread"), id: z16.string() }),
|
|
910
|
+
z16.object({ type: z16.literal("session.register"), session: SessionSchema }),
|
|
750
911
|
/** `agent` and `cwd` describe the session when the daemon does not know it yet (a hook from an agent started without ledge). */
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
912
|
+
z16.object({ type: z16.literal("session.event"), id: z16.string(), event: SessionEventSchema, agent: ToolSchema.optional(), cwd: z16.string().optional() }),
|
|
913
|
+
z16.object({ type: z16.literal("session.unregister"), id: z16.string(), exitCode: z16.number().int().nullable() }),
|
|
914
|
+
z16.object({ type: z16.literal("snapshot.request") }),
|
|
915
|
+
z16.object({ type: z16.literal("chat.ask"), qid: z16.string().min(1), text: z16.string().min(1), tool: ToolSchema.optional() }),
|
|
916
|
+
z16.object({ type: z16.literal("chat.cancel"), qid: z16.string().min(1) }),
|
|
756
917
|
/** Starts a new conversation with Ledge: the day's turns are cleared and the next question opens a new harness session. */
|
|
757
|
-
|
|
918
|
+
z16.object({ type: z16.literal("chat.reset") }),
|
|
758
919
|
/** Installs `@paradow/ledge@version` and restarts the service (spec 2026-10-12 auto-update §6). */
|
|
759
|
-
|
|
920
|
+
z16.object({ type: z16.literal("update.daemon"), version: z16.string().regex(SEMVER) }),
|
|
760
921
|
/** Replaces the daemon's machines list (first connected front wins); refused unless valid. */
|
|
761
|
-
|
|
922
|
+
z16.object({ type: z16.literal("machines.set"), machines: MachinesSchema }),
|
|
762
923
|
/** Replaces the local daemon's projects and rules as a whole; refused unless valid. */
|
|
763
|
-
|
|
924
|
+
z16.object({ type: z16.literal("projects.set"), projects: ProjectsSchema }),
|
|
764
925
|
/** Replaces the island's settings as a whole; refused unless valid. */
|
|
765
|
-
|
|
926
|
+
z16.object({ type: z16.literal("island.set"), prefs: IslandPrefsSchema }),
|
|
766
927
|
/** Mobile access (lot 3), local fronts only. A changed `relayUrl` drops the relay session. */
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
928
|
+
z16.object({ type: z16.literal("mobile.set"), enabled: z16.boolean(), relayUrl: RelayUrlSchema }),
|
|
929
|
+
z16.object({ type: z16.literal("mobile.signin") }),
|
|
930
|
+
z16.object({ type: z16.literal("mobile.signin.cancel") }),
|
|
931
|
+
z16.object({ type: z16.literal("mobile.signout") }),
|
|
771
932
|
/** While the link is offline: try again now. */
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
933
|
+
z16.object({ type: z16.literal("mobile.retry") }),
|
|
934
|
+
z16.object({ type: z16.literal("mobile.pairing.approve"), id: z16.string().min(1).max(100) }),
|
|
935
|
+
z16.object({ type: z16.literal("mobile.pairing.deny"), id: z16.string().min(1).max(100) }),
|
|
775
936
|
/** `id`: the device's fingerprint. */
|
|
776
|
-
|
|
937
|
+
z16.object({ type: z16.literal("mobile.device.revoke"), id: z16.string().min(1).max(100) }),
|
|
777
938
|
/** 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. */
|
|
778
|
-
|
|
939
|
+
z16.object({ type: z16.literal("session.message"), rid: z16.string().min(1), from: z16.string().min(1), to: z16.string().min(1), text: z16.string().min(1).max(MESSAGE_MAX), summary: z16.string().max(SUMMARY_MAX).optional(), intercept: z16.boolean().optional() }),
|
|
940
|
+
/** `ledge read <id>`: a message's whole body, for its receiver or its sender (`by`, a session id). */
|
|
941
|
+
z16.object({ type: z16.literal("message.read"), id: z16.string().min(1).max(40), by: z16.string().min(1) }),
|
|
942
|
+
TaskCmdSchema
|
|
779
943
|
]);
|
|
780
944
|
function parseClientMessage(raw) {
|
|
781
945
|
return ClientMessageSchema.parse(raw);
|
|
@@ -867,16 +1031,16 @@ function pickName(taken, preferred) {
|
|
|
867
1031
|
}
|
|
868
1032
|
|
|
869
1033
|
// ../protocol/src/proposal.ts
|
|
870
|
-
import { z as
|
|
871
|
-
var ProposalBlockSchema =
|
|
872
|
-
var RelayBlockSchema =
|
|
873
|
-
var ProjectBlockSchema =
|
|
874
|
-
name:
|
|
875
|
-
description:
|
|
876
|
-
repos:
|
|
877
|
-
addTo:
|
|
1034
|
+
import { z as z17 } from "zod";
|
|
1035
|
+
var ProposalBlockSchema = z17.object({ cwd: z17.string().trim().min(1), prompt: z17.string().optional() });
|
|
1036
|
+
var RelayBlockSchema = z17.object({ session: z17.string().trim().min(1), text: z17.string().trim().min(1) });
|
|
1037
|
+
var ProjectBlockSchema = z17.object({
|
|
1038
|
+
name: z17.string().trim().min(1).max(80),
|
|
1039
|
+
description: z17.string().max(500).optional(),
|
|
1040
|
+
repos: z17.array(z17.object({ path: z17.string().min(1).max(4096), why: z17.string().trim().min(1).max(200) })).min(1).max(50),
|
|
1041
|
+
addTo: z17.string().trim().max(80).optional()
|
|
878
1042
|
});
|
|
879
|
-
var CloseBlockSchema =
|
|
1043
|
+
var CloseBlockSchema = z17.object({ session: z17.string().trim().min(1) });
|
|
880
1044
|
var BLOCK = /```ledge-(session|relay|project|close)[ \t]*\n?([\s\S]*?)(?:```|$)/g;
|
|
881
1045
|
function extractProposal(answer) {
|
|
882
1046
|
const found = [];
|
|
@@ -932,80 +1096,80 @@ function sessionTitle(s) {
|
|
|
932
1096
|
}
|
|
933
1097
|
|
|
934
1098
|
// ../protocol/src/relay.ts
|
|
935
|
-
import { z as
|
|
1099
|
+
import { z as z18 } from "zod";
|
|
936
1100
|
var SEALED_MAX = 1048576;
|
|
937
|
-
var RelayPeerSchema =
|
|
938
|
-
var RelayClientMessageSchema =
|
|
939
|
-
type:
|
|
1101
|
+
var RelayPeerSchema = z18.union([z18.literal("pc"), z18.string().regex(/^d_[A-Za-z0-9_-]{16,40}$/)]);
|
|
1102
|
+
var RelayClientMessageSchema = z18.object({
|
|
1103
|
+
type: z18.literal("send"),
|
|
940
1104
|
to: RelayPeerSchema,
|
|
941
|
-
body:
|
|
1105
|
+
body: z18.string().max(SEALED_MAX)
|
|
942
1106
|
});
|
|
943
|
-
var RelayServerMessageSchema =
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1107
|
+
var RelayServerMessageSchema = z18.discriminatedUnion("type", [
|
|
1108
|
+
z18.object({ type: z18.literal("welcome"), accountId: z18.string(), self: RelayPeerSchema, online: z18.array(RelayPeerSchema) }),
|
|
1109
|
+
z18.object({ type: z18.literal("deliver"), from: RelayPeerSchema, body: z18.string() }),
|
|
1110
|
+
z18.object({ type: z18.literal("presence"), peer: RelayPeerSchema, online: z18.boolean() }),
|
|
947
1111
|
/**
|
|
948
1112
|
* A frame was refused; `to` names the target when it is known.
|
|
949
1113
|
* Contract: an error concerning a sealed body (`offline`, `too_large`, or a socket closed with 4008
|
|
950
1114
|
* `rate_limited`) means that body was not delivered, so the sequence on that channel is lost: the
|
|
951
1115
|
* sender must re-handshake with that peer before sending to it again.
|
|
952
1116
|
*/
|
|
953
|
-
|
|
954
|
-
type:
|
|
955
|
-
code:
|
|
1117
|
+
z18.object({
|
|
1118
|
+
type: z18.literal("error"),
|
|
1119
|
+
code: z18.enum(["bad_frame", "too_large", "bad_target", "offline", "rate_limited"]),
|
|
956
1120
|
to: RelayPeerSchema.optional()
|
|
957
1121
|
})
|
|
958
1122
|
]);
|
|
959
1123
|
var PAIR_LABEL_MAX = 40;
|
|
960
|
-
var KeyB64 =
|
|
961
|
-
var Nonce =
|
|
962
|
-
var Fingerprint =
|
|
963
|
-
var SealedFrameSchema =
|
|
1124
|
+
var KeyB64 = z18.string().regex(/^[A-Za-z0-9_-]{87}$/);
|
|
1125
|
+
var Nonce = z18.string().regex(/^[A-Za-z0-9_-]{43}$/);
|
|
1126
|
+
var Fingerprint = z18.string().regex(/^[A-Za-z0-9_-]{22}$/);
|
|
1127
|
+
var SealedFrameSchema = z18.discriminatedUnion("k", [
|
|
964
1128
|
/** `e` is the ephemeral key, `f` the fingerprint of the sender's static key. */
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1129
|
+
z18.object({ k: z18.literal("hello"), e: KeyB64, f: Fingerprint }),
|
|
1130
|
+
z18.object({ k: z18.literal("msg"), s: z18.string() }),
|
|
1131
|
+
z18.object({ k: z18.literal("pair_commit"), c: Nonce, label: z18.string().trim().min(1).max(PAIR_LABEL_MAX) }),
|
|
1132
|
+
z18.object({ k: z18.literal("pair_offer"), pc: KeyB64, n: Nonce }),
|
|
1133
|
+
z18.object({ k: z18.literal("pair_reveal"), d: KeyB64, n: Nonce }),
|
|
1134
|
+
z18.object({ k: z18.literal("pair_ok") }),
|
|
1135
|
+
z18.object({ k: z18.literal("pair_no") }),
|
|
1136
|
+
z18.object({ k: z18.literal("denied") }),
|
|
1137
|
+
z18.object({ k: z18.literal("rekey") })
|
|
974
1138
|
]);
|
|
975
1139
|
|
|
976
1140
|
// ../protocol/src/relayAuth.ts
|
|
977
|
-
import { z as
|
|
1141
|
+
import { z as z19 } from "zod";
|
|
978
1142
|
var RELAY_CLOSE_SIGNED_OUT = 4001;
|
|
979
|
-
var DeviceStartResponseSchema =
|
|
980
|
-
deviceCode:
|
|
981
|
-
userCode:
|
|
982
|
-
verificationUri:
|
|
983
|
-
expiresIn:
|
|
984
|
-
interval:
|
|
1143
|
+
var DeviceStartResponseSchema = z19.object({
|
|
1144
|
+
deviceCode: z19.string().min(1),
|
|
1145
|
+
userCode: z19.string().min(1),
|
|
1146
|
+
verificationUri: z19.string().min(1),
|
|
1147
|
+
expiresIn: z19.number().int().positive(),
|
|
1148
|
+
interval: z19.number().int().positive()
|
|
985
1149
|
});
|
|
986
|
-
var DevicePollResponseSchema =
|
|
987
|
-
|
|
988
|
-
|
|
1150
|
+
var DevicePollResponseSchema = z19.discriminatedUnion("status", [
|
|
1151
|
+
z19.object({ status: z19.literal("pending") }),
|
|
1152
|
+
z19.object({ status: z19.literal("slow_down"), interval: z19.number().int().positive() }),
|
|
989
1153
|
/** `token` is the relay session token, shown this once only. */
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1154
|
+
z19.object({ status: z19.literal("ok"), token: z19.string().min(1), accountId: z19.string().min(1), login: z19.string().min(1) }),
|
|
1155
|
+
z19.object({ status: z19.literal("expired") }),
|
|
1156
|
+
z19.object({ status: z19.literal("denied") }),
|
|
1157
|
+
z19.object({ status: z19.literal("not_allowed") })
|
|
994
1158
|
]);
|
|
995
|
-
var DeviceRevokeResponseSchema =
|
|
996
|
-
var MeResponseSchema =
|
|
997
|
-
accountId:
|
|
998
|
-
login:
|
|
1159
|
+
var DeviceRevokeResponseSchema = z19.object({ revoked: z19.number().int().min(0) });
|
|
1160
|
+
var MeResponseSchema = z19.object({
|
|
1161
|
+
accountId: z19.string().min(1),
|
|
1162
|
+
login: z19.string().min(1),
|
|
999
1163
|
peer: RelayPeerSchema,
|
|
1000
|
-
kind:
|
|
1164
|
+
kind: z19.enum(["pc", "device"])
|
|
1001
1165
|
});
|
|
1002
1166
|
|
|
1003
1167
|
// ../protocol/src/phone.ts
|
|
1004
|
-
import { z as
|
|
1168
|
+
import { z as z21 } from "zod";
|
|
1005
1169
|
|
|
1006
1170
|
// ../protocol/src/push.ts
|
|
1007
|
-
import { z as
|
|
1008
|
-
var B64U =
|
|
1171
|
+
import { z as z20 } from "zod";
|
|
1172
|
+
var B64U = z20.string().regex(/^[A-Za-z0-9_-]*$/);
|
|
1009
1173
|
var PUSH_HOSTS = ["*.push.apple.com", "fcm.googleapis.com", "updates.push.services.mozilla.com", "*.notify.windows.com"];
|
|
1010
1174
|
function pushEndpointAllowed(url) {
|
|
1011
1175
|
let u;
|
|
@@ -1022,29 +1186,29 @@ function pushEndpointAllowed(url) {
|
|
|
1022
1186
|
return host.length > suffix.length && host.endsWith(suffix);
|
|
1023
1187
|
});
|
|
1024
1188
|
}
|
|
1025
|
-
var PushSendSchema =
|
|
1026
|
-
endpoint:
|
|
1189
|
+
var PushSendSchema = z20.object({
|
|
1190
|
+
endpoint: z20.string().url().max(1024),
|
|
1027
1191
|
body: B64U.max(5462),
|
|
1028
|
-
ttl:
|
|
1029
|
-
urgency:
|
|
1030
|
-
topic:
|
|
1192
|
+
ttl: z20.number().int().min(0).max(86400),
|
|
1193
|
+
urgency: z20.enum(["normal", "high"]),
|
|
1194
|
+
topic: z20.string().regex(/^[A-Za-z0-9_-]{1,32}$/)
|
|
1031
1195
|
});
|
|
1032
|
-
var PushSendResultSchema =
|
|
1033
|
-
result:
|
|
1034
|
-
status:
|
|
1196
|
+
var PushSendResultSchema = z20.object({
|
|
1197
|
+
result: z20.enum(["sent", "gone", "failed"]),
|
|
1198
|
+
status: z20.number().int().optional()
|
|
1035
1199
|
});
|
|
1036
|
-
var PushSubscriptionSchema =
|
|
1037
|
-
endpoint:
|
|
1038
|
-
keys:
|
|
1200
|
+
var PushSubscriptionSchema = z20.object({
|
|
1201
|
+
endpoint: z20.string().url().max(1024).refine(pushEndpointAllowed),
|
|
1202
|
+
keys: z20.object({ p256dh: B64U.length(87), auth: B64U.length(22) })
|
|
1039
1203
|
});
|
|
1040
|
-
var PushPayloadSchema =
|
|
1041
|
-
v:
|
|
1042
|
-
kind:
|
|
1043
|
-
title:
|
|
1044
|
-
body:
|
|
1045
|
-
url:
|
|
1046
|
-
badge:
|
|
1047
|
-
tag:
|
|
1204
|
+
var PushPayloadSchema = z20.object({
|
|
1205
|
+
v: z20.literal(1),
|
|
1206
|
+
kind: z20.enum(["request", "question", "finished", "chat"]),
|
|
1207
|
+
title: z20.string().max(80),
|
|
1208
|
+
body: z20.string().max(120),
|
|
1209
|
+
url: z20.string().regex(/^\/#\/[A-Za-z0-9/%._~-]*$/).max(300),
|
|
1210
|
+
badge: z20.number().int().min(0).max(999),
|
|
1211
|
+
tag: z20.string().regex(/^[A-Za-z0-9_-]{1,32}$/)
|
|
1048
1212
|
});
|
|
1049
1213
|
|
|
1050
1214
|
// ../protocol/src/phone.ts
|
|
@@ -1065,22 +1229,22 @@ var THREAD_TEXT_MAX = 8192;
|
|
|
1065
1229
|
var TOOL_TEXT_MAX = 2048;
|
|
1066
1230
|
var PENDING_BODY_MAX = 65536;
|
|
1067
1231
|
var SNAPSHOT_BODY_MAX = 2048;
|
|
1068
|
-
var RiskSchema =
|
|
1069
|
-
var RiskReasonSchema =
|
|
1070
|
-
var OperationIdSchema =
|
|
1071
|
-
var OperationStatusSchema =
|
|
1072
|
-
var OperationReasonSchema =
|
|
1073
|
-
var CapabilitiesSchema =
|
|
1232
|
+
var RiskSchema = z21.enum(["low", "high"]);
|
|
1233
|
+
var RiskReasonSchema = z21.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"]);
|
|
1234
|
+
var OperationIdSchema = z21.string().uuid();
|
|
1235
|
+
var OperationStatusSchema = z21.enum(["received", "applied", "rejected", "failed"]);
|
|
1236
|
+
var OperationReasonSchema = z21.enum([...RefusalCodeSchema.options, "untypable", "confirm_required", "conflict", "assertion_invalid", "assertion_expired"]);
|
|
1237
|
+
var CapabilitiesSchema = z21.object({
|
|
1074
1238
|
/** A question can be answered in free text. */
|
|
1075
|
-
freeTextAnswer:
|
|
1239
|
+
freeTextAnswer: z21.boolean(),
|
|
1076
1240
|
/** The session's accept/deny can be typed; absent = true. */
|
|
1077
|
-
decide:
|
|
1241
|
+
decide: z21.boolean().optional(),
|
|
1078
1242
|
/** OMP's dialog questions can be answered from the phone; absent = false. */
|
|
1079
|
-
questions:
|
|
1243
|
+
questions: z21.boolean().optional()
|
|
1080
1244
|
});
|
|
1081
1245
|
var UNTYPABLE = /[\u0000-\u001f\u007f-\u009f\u200b\u200e\u200f\u2028-\u202e\u2066-\u2069\ufeff]/;
|
|
1082
|
-
var CapabilitiesByToolSchema =
|
|
1083
|
-
var PhonePendingSchema = PendingSchema.extend({ key:
|
|
1246
|
+
var CapabilitiesByToolSchema = z21.object({ claude: CapabilitiesSchema, codex: CapabilitiesSchema, cursor: CapabilitiesSchema, omp: CapabilitiesSchema });
|
|
1247
|
+
var PhonePendingSchema = PendingSchema.extend({ key: z21.string().max(64), risk: RiskSchema, riskReason: RiskReasonSchema, cut: z21.literal(true).optional() });
|
|
1084
1248
|
var PhoneSessionSchema = SessionSchema.pick({
|
|
1085
1249
|
id: true,
|
|
1086
1250
|
name: true,
|
|
@@ -1096,272 +1260,160 @@ var PhoneSessionSchema = SessionSchema.pick({
|
|
|
1096
1260
|
lastReply: true,
|
|
1097
1261
|
startedAt: true,
|
|
1098
1262
|
lastActivityAt: true
|
|
1099
|
-
}).extend({ pending: PhonePendingSchema.optional(), unread:
|
|
1100
|
-
var B64U2 =
|
|
1101
|
-
var CredentialSchema =
|
|
1263
|
+
}).extend({ pending: PhonePendingSchema.optional(), unread: z21.boolean() });
|
|
1264
|
+
var B64U2 = z21.string().regex(/^[A-Za-z0-9_-]*$/);
|
|
1265
|
+
var CredentialSchema = z21.object({
|
|
1102
1266
|
id: B64U2.max(1366),
|
|
1103
1267
|
publicKey: B64U2.max(2048),
|
|
1104
|
-
alg:
|
|
1268
|
+
alg: z21.union([z21.literal(-7), z21.literal(-257)]),
|
|
1105
1269
|
clientDataJSON: B64U2.max(4096),
|
|
1106
1270
|
authenticatorData: B64U2.max(4096),
|
|
1107
|
-
transports:
|
|
1271
|
+
transports: z21.array(z21.string().max(20)).max(8).optional()
|
|
1108
1272
|
});
|
|
1109
|
-
var AssertionSchema =
|
|
1273
|
+
var AssertionSchema = z21.object({
|
|
1110
1274
|
id: B64U2.max(1366),
|
|
1111
1275
|
clientDataJSON: B64U2.max(4096),
|
|
1112
1276
|
authenticatorData: B64U2.max(4096),
|
|
1113
1277
|
signature: B64U2.max(1024)
|
|
1114
1278
|
});
|
|
1115
|
-
var ConfirmPurposeSchema =
|
|
1279
|
+
var ConfirmPurposeSchema = z21.enum(["register", "decide", "launch", "close"]);
|
|
1116
1280
|
var PhoneChatStepSchema = ChatStepSchema.pick({ kind: true, id: true, name: true }).strict();
|
|
1117
|
-
var PhoneChatProposalSchema =
|
|
1118
|
-
project:
|
|
1281
|
+
var PhoneChatProposalSchema = z21.object({
|
|
1282
|
+
project: z21.string().max(80),
|
|
1119
1283
|
tool: ToolSchema,
|
|
1120
|
-
prompt:
|
|
1121
|
-
launchedId:
|
|
1122
|
-
failed:
|
|
1284
|
+
prompt: z21.string().max(TOOL_TEXT_MAX).optional(),
|
|
1285
|
+
launchedId: z21.string().optional(),
|
|
1286
|
+
failed: z21.literal(true).optional()
|
|
1123
1287
|
}).strict();
|
|
1124
|
-
var PhoneChatRelaySchema =
|
|
1125
|
-
id:
|
|
1126
|
-
name:
|
|
1288
|
+
var PhoneChatRelaySchema = z21.object({
|
|
1289
|
+
id: z21.string().min(1),
|
|
1290
|
+
name: z21.string(),
|
|
1127
1291
|
tool: ToolSchema,
|
|
1128
|
-
text:
|
|
1129
|
-
status:
|
|
1130
|
-
sentAt:
|
|
1131
|
-
failed:
|
|
1292
|
+
text: z21.string().max(TOOL_TEXT_MAX),
|
|
1293
|
+
status: z21.enum(["queued", "sent"]).optional(),
|
|
1294
|
+
sentAt: z21.string().datetime().optional(),
|
|
1295
|
+
failed: z21.literal(true).optional()
|
|
1132
1296
|
}).strict();
|
|
1133
|
-
var PhoneChatCloseSchema =
|
|
1134
|
-
id:
|
|
1135
|
-
name:
|
|
1297
|
+
var PhoneChatCloseSchema = z21.object({
|
|
1298
|
+
id: z21.string().min(1),
|
|
1299
|
+
name: z21.string(),
|
|
1136
1300
|
tool: ToolSchema,
|
|
1137
|
-
how:
|
|
1138
|
-
closedAt:
|
|
1139
|
-
failed:
|
|
1301
|
+
how: z21.enum(["exit", "killed"]).optional(),
|
|
1302
|
+
closedAt: z21.string().datetime().optional(),
|
|
1303
|
+
failed: z21.literal(true).optional()
|
|
1140
1304
|
}).strict();
|
|
1141
|
-
var ChatErrorCodeSchema =
|
|
1142
|
-
var PhoneChatTurnSchema =
|
|
1305
|
+
var ChatErrorCodeSchema = z21.enum(["busy", "timeout", "cancelled", "failed"]);
|
|
1306
|
+
var PhoneChatTurnSchema = z21.object({
|
|
1143
1307
|
qid: OperationIdSchema,
|
|
1144
|
-
question:
|
|
1145
|
-
askedAt:
|
|
1146
|
-
steps:
|
|
1147
|
-
answer:
|
|
1148
|
-
answeredAt:
|
|
1308
|
+
question: z21.string().max(MESSAGE_MAX),
|
|
1309
|
+
askedAt: z21.string().datetime(),
|
|
1310
|
+
steps: z21.array(PhoneChatStepSchema).max(50),
|
|
1311
|
+
answer: z21.string().max(THREAD_TEXT_MAX).optional(),
|
|
1312
|
+
answeredAt: z21.string().datetime().optional(),
|
|
1149
1313
|
error: ChatErrorCodeSchema.optional(),
|
|
1150
|
-
proposals:
|
|
1314
|
+
proposals: z21.array(PhoneChatProposalSchema).max(MAX_PROPOSALS).optional(),
|
|
1151
1315
|
relay: PhoneChatRelaySchema.optional(),
|
|
1152
|
-
closes:
|
|
1153
|
-
cut:
|
|
1316
|
+
closes: z21.array(PhoneChatCloseSchema).min(1).max(MAX_PROPOSALS).optional(),
|
|
1317
|
+
cut: z21.literal(true).optional()
|
|
1154
1318
|
}).strict();
|
|
1155
1319
|
var CHAT_HISTORY_BUDGET = 393216;
|
|
1156
|
-
var sessionId =
|
|
1157
|
-
var PhoneExpectSchema = PendingSchema.pick({ kind: true, text: true }).extend({ key:
|
|
1158
|
-
var CardRowsSchema =
|
|
1159
|
-
var PhoneClientMessageSchema =
|
|
1160
|
-
|
|
1161
|
-
|
|
1320
|
+
var sessionId = z21.string().min(1).max(100);
|
|
1321
|
+
var PhoneExpectSchema = PendingSchema.pick({ kind: true, text: true }).extend({ key: z21.string().min(1).max(64) });
|
|
1322
|
+
var CardRowsSchema = 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");
|
|
1323
|
+
var PhoneClientMessageSchema = z21.discriminatedUnion("type", [
|
|
1324
|
+
z21.object({ type: z21.literal("snapshot.request") }),
|
|
1325
|
+
z21.object({ type: z21.literal("session.thread"), id: sessionId }),
|
|
1162
1326
|
/** The session's request with its whole body (up to `PENDING_BODY_MAX`). */
|
|
1163
|
-
|
|
1327
|
+
z21.object({ type: z21.literal("session.pending"), id: sessionId }),
|
|
1164
1328
|
/** The session whose terminal the phone shows; null when it shows none. */
|
|
1165
|
-
|
|
1166
|
-
|
|
1329
|
+
z21.object({ type: z21.literal("session.watch"), id: sessionId.nullable() }),
|
|
1330
|
+
z21.object({ type: z21.literal("session.read"), id: sessionId, at: z21.string().datetime() }),
|
|
1167
1331
|
/** Asks for a challenge. One object for both purposes: the gateway refuses a `decide` without `id` and `expect`. */
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
type:
|
|
1332
|
+
z21.object({ type: z21.literal("confirm.prepare"), purpose: ConfirmPurposeSchema, id: sessionId.optional(), expect: PhoneExpectSchema.optional(), qid: OperationIdSchema.optional() }),
|
|
1333
|
+
z21.object({ type: z21.literal("credential.register"), op: OperationIdSchema, credential: CredentialSchema }),
|
|
1334
|
+
z21.object({
|
|
1335
|
+
type: z21.literal("session.decide"),
|
|
1172
1336
|
op: OperationIdSchema,
|
|
1173
1337
|
id: sessionId,
|
|
1174
1338
|
decision: DecisionSchema,
|
|
1175
|
-
note:
|
|
1339
|
+
note: z21.string().trim().min(1).max(NOTE_MAX).optional(),
|
|
1176
1340
|
/** The request the phone showed: refused unless it is still the pending one. */
|
|
1177
1341
|
expect: PhoneExpectSchema,
|
|
1178
1342
|
/** The device check over the challenge of `confirm.prepare`; required by the daemon for a high-risk accept. */
|
|
1179
1343
|
assertion: AssertionSchema.optional()
|
|
1180
1344
|
}),
|
|
1181
|
-
|
|
1182
|
-
type:
|
|
1345
|
+
z21.object({
|
|
1346
|
+
type: z21.literal("session.reply"),
|
|
1183
1347
|
op: OperationIdSchema,
|
|
1184
1348
|
id: sessionId,
|
|
1185
|
-
text:
|
|
1349
|
+
text: z21.string().min(1).max(MESSAGE_MAX),
|
|
1186
1350
|
/** The request the reply answers, or null when the phone showed none: refused if the session's request is no longer that one. */
|
|
1187
1351
|
expect: PhoneExpectSchema.nullable(),
|
|
1188
1352
|
assertion: AssertionSchema.optional()
|
|
1189
1353
|
}),
|
|
1190
1354
|
/** OMP's `ask`: the answers to every question of the dialog `requestId`, in order; `cancel` declines it. Bounds as the local `session.answer`. */
|
|
1191
|
-
|
|
1192
|
-
type:
|
|
1355
|
+
z21.object({
|
|
1356
|
+
type: z21.literal("session.answer"),
|
|
1193
1357
|
op: OperationIdSchema,
|
|
1194
1358
|
id: sessionId,
|
|
1195
|
-
requestId:
|
|
1196
|
-
answers:
|
|
1197
|
-
cancel:
|
|
1359
|
+
requestId: z21.string().min(1).max(200),
|
|
1360
|
+
answers: z21.array(z21.object({ id: z21.string().max(200), selected: z21.array(z21.string().max(2e3)).max(100), text: z21.string().max(2e4).optional() })).max(50),
|
|
1361
|
+
cancel: z21.literal(true).optional(),
|
|
1198
1362
|
expect: PhoneExpectSchema
|
|
1199
1363
|
}),
|
|
1200
|
-
|
|
1364
|
+
z21.object({ type: z21.literal("session.interrupt"), op: OperationIdSchema, id: sessionId }),
|
|
1201
1365
|
/** The phone drops its passkey from the daemon. */
|
|
1202
|
-
|
|
1366
|
+
z21.object({ type: z21.literal("device.forget"), op: OperationIdSchema }),
|
|
1203
1367
|
/** The phone's push subscription, with the VAPID key it was made with (so a key change is seen). */
|
|
1204
|
-
|
|
1205
|
-
|
|
1368
|
+
z21.object({ type: z21.literal("push.subscribe"), op: OperationIdSchema, subscription: PushSubscriptionSchema, key: B64U2.length(87) }),
|
|
1369
|
+
z21.object({ type: z21.literal("push.unsubscribe"), op: OperationIdSchema }),
|
|
1206
1370
|
/** The phone's own chat with Ledge. */
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1371
|
+
z21.object({ type: z21.literal("chat.ask"), qid: OperationIdSchema, text: z21.string().trim().min(1).max(MESSAGE_MAX) }),
|
|
1372
|
+
z21.object({ type: z21.literal("chat.cancel"), qid: OperationIdSchema }),
|
|
1373
|
+
z21.object({ type: z21.literal("chat.reset") }),
|
|
1374
|
+
z21.object({ type: z21.literal("chat.history") }),
|
|
1375
|
+
z21.object({ type: z21.literal("chat.relay"), op: OperationIdSchema, qid: OperationIdSchema }),
|
|
1376
|
+
z21.object({ type: z21.literal("chat.launch"), op: OperationIdSchema, qid: OperationIdSchema, rows: CardRowsSchema.optional(), assertion: AssertionSchema.optional() }),
|
|
1377
|
+
z21.object({ type: z21.literal("chat.close"), op: OperationIdSchema, qid: OperationIdSchema, rows: CardRowsSchema.optional(), assertion: AssertionSchema.optional() }),
|
|
1214
1378
|
/** Whether the phone's app is in front: a device that is visible gets no push. */
|
|
1215
|
-
|
|
1379
|
+
z21.object({ type: z21.literal("presence"), visible: z21.boolean() })
|
|
1216
1380
|
]);
|
|
1217
|
-
var PhoneServerMessageSchema =
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1381
|
+
var PhoneServerMessageSchema = z21.discriminatedUnion("type", [
|
|
1382
|
+
z21.object({ type: z21.literal("snapshot"), protocol: z21.literal(PHONE_PROTOCOL), sessions: z21.array(PhoneSessionSchema), capabilities: CapabilitiesByToolSchema, daemon: z21.object({ version: z21.string(), name: z21.string().max(40), boot: z21.string().uuid().optional() }), device: z21.object({ confirm: z21.boolean(), push: z21.enum(["on", "off", "stale", "unavailable"]).optional(), chat: z21.literal(true).optional() }).optional() }),
|
|
1383
|
+
z21.object({ type: z21.literal("confirm.challenge"), purpose: ConfirmPurposeSchema, challenge: B64U2.max(64), rpId: z21.string().max(253), credentialId: B64U2.max(1366).optional(), expiresInMs: z21.number().int().positive() }),
|
|
1384
|
+
z21.object({ type: z21.literal("session.updated"), session: PhoneSessionSchema }),
|
|
1385
|
+
z21.object({ type: z21.literal("session.removed"), id: z21.string() }),
|
|
1222
1386
|
/** `pending.cut` is set when even this body was shortened. */
|
|
1223
|
-
|
|
1224
|
-
|
|
1387
|
+
z21.object({ type: z21.literal("session.pending"), id: z21.string(), pending: PhonePendingSchema }),
|
|
1388
|
+
z21.object({ type: z21.literal("session.preview"), id: z21.string(), lines: z21.array(z21.string()) }),
|
|
1225
1389
|
/** `cut`: the ids of the items shortened; `more`: older items were left out. */
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1390
|
+
z21.object({ type: z21.literal("session.thread"), id: z21.string(), items: z21.array(ThreadItemSchema), cut: z21.array(z21.string()), more: z21.boolean() }),
|
|
1391
|
+
z21.object({ type: z21.literal("session.thread.item"), id: z21.string(), item: ThreadItemSchema, cut: z21.literal(true).optional() }),
|
|
1392
|
+
z21.object({ type: z21.literal("operation.result"), op: OperationIdSchema, status: OperationStatusSchema, reason: OperationReasonSchema.optional() }),
|
|
1393
|
+
z21.object({ type: z21.literal("chat.history"), turns: z21.array(PhoneChatTurnSchema).max(50) }),
|
|
1394
|
+
z21.object({ type: z21.literal("chat.step"), qid: OperationIdSchema, step: PhoneChatStepSchema }),
|
|
1231
1395
|
/** The whole text so far, not an increment. */
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1396
|
+
z21.object({ type: z21.literal("chat.delta"), qid: OperationIdSchema, text: z21.string().max(THREAD_TEXT_MAX) }),
|
|
1397
|
+
z21.object({ type: z21.literal("chat.done"), qid: OperationIdSchema, turn: PhoneChatTurnSchema }),
|
|
1398
|
+
z21.object({ type: z21.literal("chat.error"), qid: OperationIdSchema, code: ChatErrorCodeSchema }),
|
|
1235
1399
|
/** Never carries free text. */
|
|
1236
|
-
|
|
1400
|
+
z21.object({ type: z21.literal("error"), code: z21.enum(["invalid", "unknown_session", "too_large"]), request: z21.string().max(40).optional() })
|
|
1237
1401
|
]);
|
|
1238
1402
|
|
|
1239
|
-
// src/auth.ts
|
|
1240
|
-
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
1241
|
-
import { chmod, link, mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
1242
|
-
import { readFileSync } from "fs";
|
|
1243
|
-
import { dirname } from "path";
|
|
1244
|
-
var TOKEN_RE = /^[0-9a-f]{64}$/;
|
|
1245
|
-
var DAEMON_PROTOCOL_PREFIX = "ledge.";
|
|
1246
|
-
function isDaemonToken(value) {
|
|
1247
|
-
return TOKEN_RE.test(value);
|
|
1248
|
-
}
|
|
1249
|
-
function tokenEquals(presented, expected) {
|
|
1250
|
-
const a = createHash("sha256").update(presented.slice(0, 256), "utf8").digest();
|
|
1251
|
-
const b = createHash("sha256").update(expected, "utf8").digest();
|
|
1252
|
-
return timingSafeEqual(a, b);
|
|
1253
|
-
}
|
|
1254
|
-
function hostAllowed(hostHeader) {
|
|
1255
|
-
const raw = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
|
|
1256
|
-
if (!raw || raw.length > 255) return false;
|
|
1257
|
-
const host = raw.trim().toLowerCase();
|
|
1258
|
-
if (!host || host.includes(" ") || host.endsWith(".")) return false;
|
|
1259
|
-
let name;
|
|
1260
|
-
if (host.startsWith("[")) {
|
|
1261
|
-
const end = host.indexOf("]");
|
|
1262
|
-
if (end < 0) return false;
|
|
1263
|
-
name = host.slice(1, end);
|
|
1264
|
-
const rest = host.slice(end + 1);
|
|
1265
|
-
if (rest !== "" && !/^:\d{1,5}$/.test(rest)) return false;
|
|
1266
|
-
} else {
|
|
1267
|
-
const colon = host.lastIndexOf(":");
|
|
1268
|
-
if (colon >= 0) {
|
|
1269
|
-
if (!/^\d{1,5}$/.test(host.slice(colon + 1))) return false;
|
|
1270
|
-
name = host.slice(0, colon);
|
|
1271
|
-
} else name = host;
|
|
1272
|
-
}
|
|
1273
|
-
return name === "localhost" || name === "127.0.0.1" || name === "::1";
|
|
1274
|
-
}
|
|
1275
|
-
function headerValue(value, max) {
|
|
1276
|
-
const raw = Array.isArray(value) ? value.join(",") : value;
|
|
1277
|
-
return !raw || raw.length > max ? "" : raw;
|
|
1278
|
-
}
|
|
1279
|
-
function bearerToken(authorization) {
|
|
1280
|
-
if (!authorization || authorization.length > 512) return "";
|
|
1281
|
-
return /^Bearer[ \t]+(\S+)$/i.exec(authorization)?.[1] ?? "";
|
|
1282
|
-
}
|
|
1283
|
-
function protocolToken(header) {
|
|
1284
|
-
const raw = headerValue(header, 512);
|
|
1285
|
-
if (!raw) return "";
|
|
1286
|
-
for (const part of raw.split(",").slice(0, 8)) {
|
|
1287
|
-
const protocol = part.trim();
|
|
1288
|
-
if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX)) return protocol.slice(DAEMON_PROTOCOL_PREFIX.length);
|
|
1289
|
-
}
|
|
1290
|
-
return "";
|
|
1291
|
-
}
|
|
1292
|
-
function requestAuthorized(headers, expected) {
|
|
1293
|
-
const bearerOk = tokenEquals(bearerToken(typeof headers.authorization === "string" ? headers.authorization : void 0), expected);
|
|
1294
|
-
const presented = protocolToken(headers["sec-websocket-protocol"]);
|
|
1295
|
-
return bearerOk || presented !== "" && tokenEquals(presented, expected);
|
|
1296
|
-
}
|
|
1297
|
-
function selectDaemonProtocol(protocols, expected) {
|
|
1298
|
-
for (const protocol of protocols) {
|
|
1299
|
-
if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX) && tokenEquals(protocol.slice(DAEMON_PROTOCOL_PREFIX.length), expected)) return protocol;
|
|
1300
|
-
}
|
|
1301
|
-
return false;
|
|
1302
|
-
}
|
|
1303
|
-
async function readTokenFile(file) {
|
|
1304
|
-
try {
|
|
1305
|
-
const raw = (await readFile(file, "utf8")).trim();
|
|
1306
|
-
return isDaemonToken(raw) ? raw : void 0;
|
|
1307
|
-
} catch (e) {
|
|
1308
|
-
if (e.code === "ENOENT") return void 0;
|
|
1309
|
-
throw e;
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
async function lockTokenFile(file) {
|
|
1313
|
-
await chmod(file, 384);
|
|
1314
|
-
}
|
|
1315
|
-
async function ensureDaemonToken() {
|
|
1316
|
-
const file = paths().token;
|
|
1317
|
-
await mkdir(dirname(file), { recursive: true });
|
|
1318
|
-
const current = await readTokenFile(file);
|
|
1319
|
-
if (current) {
|
|
1320
|
-
await lockTokenFile(file);
|
|
1321
|
-
return current;
|
|
1322
|
-
}
|
|
1323
|
-
const token = randomBytes(32).toString("hex");
|
|
1324
|
-
const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
1325
|
-
await writeFile(tmp, `${token}
|
|
1326
|
-
`, { mode: 384, flag: "wx" });
|
|
1327
|
-
await chmod(tmp, 384);
|
|
1328
|
-
try {
|
|
1329
|
-
await link(tmp, file);
|
|
1330
|
-
} catch (e) {
|
|
1331
|
-
await unlink(tmp).catch(() => {
|
|
1332
|
-
});
|
|
1333
|
-
if (e.code !== "EEXIST") throw e;
|
|
1334
|
-
const raced = await readTokenFile(file);
|
|
1335
|
-
if (raced) {
|
|
1336
|
-
await lockTokenFile(file);
|
|
1337
|
-
return raced;
|
|
1338
|
-
}
|
|
1339
|
-
await writeFile(file, `${token}
|
|
1340
|
-
`, { mode: 384 });
|
|
1341
|
-
}
|
|
1342
|
-
await unlink(tmp).catch(() => {
|
|
1343
|
-
});
|
|
1344
|
-
await lockTokenFile(file);
|
|
1345
|
-
const stored = await readTokenFile(file);
|
|
1346
|
-
if (!stored) throw new Error("daemon token was not stored");
|
|
1347
|
-
return stored;
|
|
1348
|
-
}
|
|
1349
|
-
function readDaemonToken() {
|
|
1350
|
-
try {
|
|
1351
|
-
const raw = readFileSync(paths().token, "utf8").trim();
|
|
1352
|
-
return isDaemonToken(raw) ? raw : void 0;
|
|
1353
|
-
} catch {
|
|
1354
|
-
return void 0;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
1403
|
export {
|
|
1404
|
+
hostAllowed,
|
|
1405
|
+
requestAuthorized,
|
|
1406
|
+
selectDaemonProtocol,
|
|
1407
|
+
ensureDaemonToken,
|
|
1408
|
+
readDaemonToken,
|
|
1359
1409
|
ToolSchema,
|
|
1360
1410
|
SessionSchema,
|
|
1361
1411
|
backendOf,
|
|
1362
1412
|
isArchived,
|
|
1363
1413
|
MAX_PROPOSALS,
|
|
1414
|
+
ChatTurnSchema,
|
|
1364
1415
|
proposalRows,
|
|
1416
|
+
NOTE_MAX,
|
|
1365
1417
|
NOTE_NOT_TYPED,
|
|
1366
1418
|
offers,
|
|
1367
1419
|
pendingKey,
|
|
@@ -1369,6 +1421,7 @@ export {
|
|
|
1369
1421
|
SshAliasSchema,
|
|
1370
1422
|
MachinesSchema,
|
|
1371
1423
|
IslandPrefsSchema,
|
|
1424
|
+
PROJECT_COLORS,
|
|
1372
1425
|
ProjectsSchema,
|
|
1373
1426
|
DEFAULT_PROJECTS,
|
|
1374
1427
|
projectOf,
|
|
@@ -1377,6 +1430,9 @@ export {
|
|
|
1377
1430
|
RouteResultSchema,
|
|
1378
1431
|
DEFAULT_RELAY_URL,
|
|
1379
1432
|
MobileConfigSchema,
|
|
1433
|
+
TASK_TITLE_MAX,
|
|
1434
|
+
TASK_TEXT_MAX,
|
|
1435
|
+
taskClosed,
|
|
1380
1436
|
PROTOCOL_PORT,
|
|
1381
1437
|
RefusalCodeSchema,
|
|
1382
1438
|
RecentSchema,
|
|
@@ -1414,10 +1470,5 @@ export {
|
|
|
1414
1470
|
OperationIdSchema,
|
|
1415
1471
|
UNTYPABLE,
|
|
1416
1472
|
CHAT_HISTORY_BUDGET,
|
|
1417
|
-
PhoneClientMessageSchema
|
|
1418
|
-
hostAllowed,
|
|
1419
|
-
requestAuthorized,
|
|
1420
|
-
selectDaemonProtocol,
|
|
1421
|
-
ensureDaemonToken,
|
|
1422
|
-
readDaemonToken
|
|
1473
|
+
PhoneClientMessageSchema
|
|
1423
1474
|
};
|