@paradow/ledge 0.12.1 → 0.14.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/dist/cli/{attach-UWADG6NX.js → attach-3U4A44ZE.js} +22 -9
- package/dist/cli/{bridge-53ZTDSST.js → bridge-7JT6P53L.js} +2 -2
- package/dist/cli/{chunk-A7YEHYLY.js → chunk-2P2SUT7A.js} +2 -2
- package/dist/cli/{chunk-YUSY3E47.js → chunk-5DDBCJX2.js} +1 -1
- package/dist/cli/{chunk-VY45MACP.js → chunk-5LVPB3ET.js} +436 -135
- package/dist/cli/{chunk-XIO4OIHR.js → chunk-6A7JPTHV.js} +2 -2
- package/dist/cli/{chunk-ZNRXLDSH.js → chunk-D3JQV5AA.js} +6 -6
- package/dist/cli/{chunk-ZEHTNFPV.js → chunk-FPW7MF5X.js} +2 -2
- package/dist/cli/{chunk-FRGNPUDO.js → chunk-IIXALRFM.js} +2 -2
- package/dist/cli/{chunk-XXFUYME7.js → chunk-PFDZ6VYG.js} +1 -1
- package/dist/cli/{chunk-FGOFRWIB.js → chunk-QBJEUZYW.js} +1 -1
- package/dist/cli/chunk-QUUZSAPI.js +6 -0
- package/dist/cli/{chunk-X34LPKG7.js → chunk-ULWVO3TH.js} +4 -3
- package/dist/cli/{chunk-QAEH5TJB.js → chunk-UWANRALT.js} +93 -4
- package/dist/cli/{daemon-LV7V2VP5.js → daemon-KHGAO6BC.js} +3340 -323
- package/dist/cli/{hook-OLZ2I4GM.js → hook-R6OWCFAQ.js} +8 -8
- package/dist/cli/index.js +11 -11
- package/dist/cli/{launch-ZGICGFVU.js → launch-PPTQEWYD.js} +7 -7
- package/dist/cli/send-74LTIRSN.js +14 -0
- package/dist/cli/{setup-TULNBDFB.js → setup-FPQK3NA7.js} +4 -4
- package/dist/cli/{status-LLITXNOW.js → status-QLTB6T3U.js} +3 -3
- package/dist/cli/{uninstall-OFSLCP4T.js → uninstall-QC5WEWDH.js} +4 -4
- package/dist/cli/{version-BGEWDJNC.js → version-AELAEF6T.js} +1 -1
- package/dist/cli/who-5F6CHRA3.js +11 -0
- package/package.json +2 -1
- package/dist/cli/chunk-DKL4DFNI.js +0 -6
- package/dist/cli/send-VV25P46X.js +0 -14
- package/dist/cli/who-K6VK7EXV.js +0 -11
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
paths
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-QBJEUZYW.js";
|
|
4
4
|
|
|
5
5
|
// ../protocol/src/session.ts
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
var ToolSchema = z.enum(["claude", "codex", "cursor"]);
|
|
7
|
+
var ToolSchema = z.enum(["claude", "codex", "cursor", "omp"]);
|
|
8
|
+
var OmpApprovalSchema = z.enum(["always-ask", "write", "yolo"]);
|
|
9
|
+
var BackendSchema = z.enum(["tmux", "omp-rpc"]);
|
|
8
10
|
var SessionStateSchema = z.enum(["starting", "working", "waiting_input", "waiting_permission", "idle", "exited", "error"]);
|
|
11
|
+
var AskQuestionSchema = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
question: z.string(),
|
|
14
|
+
header: z.string().optional(),
|
|
15
|
+
multi: z.boolean().optional(),
|
|
16
|
+
options: z.array(z.object({ label: z.string(), description: z.string().optional() }))
|
|
17
|
+
});
|
|
9
18
|
var PendingSchema = z.object({
|
|
10
19
|
kind: z.enum(["question", "permission", "plan", "diff"]),
|
|
11
20
|
text: z.string(),
|
|
@@ -15,7 +24,11 @@ var PendingSchema = z.object({
|
|
|
15
24
|
options: z.array(z.string()).optional(),
|
|
16
25
|
/** Each option's description, by index ('' when it has none); left out when no option has one. */
|
|
17
26
|
optionDetails: z.array(z.string()).optional(),
|
|
18
|
-
|
|
27
|
+
/** `rpc`: the harness's own protocol asked (OMP): answered on its request id, never by typing. */
|
|
28
|
+
source: z.enum(["hook", "pane", "rpc"]),
|
|
29
|
+
/** OMP: every question of one `ask` call, answered together with `session.answer`; `requestId` is the dialog it answers. */
|
|
30
|
+
questions: z.array(AskQuestionSchema).optional(),
|
|
31
|
+
requestId: z.string().optional(),
|
|
19
32
|
/** Set when the request comes from a helper (subagent): it is routed through its parent. */
|
|
20
33
|
helperId: z.string().optional(),
|
|
21
34
|
/** For a helper-routed request: the state the parent returns to once it is answered (main thread may stop meanwhile). */
|
|
@@ -60,9 +73,13 @@ var SessionSchema = z.object({
|
|
|
60
73
|
/** The folder the agent works in now, as its last hook reported it: `cwd` stays where it started, this follows a worktree it entered. */
|
|
61
74
|
workdir: z.string().optional(),
|
|
62
75
|
tmuxSession: z.string().nullable(),
|
|
76
|
+
/** Left out for a tmux session. `omp-rpc`: no pane, nothing to attach to; the daemon holds the process (see `agentSessionFile`). */
|
|
77
|
+
backend: BackendSchema.optional(),
|
|
63
78
|
state: SessionStateSchema,
|
|
64
79
|
pending: PendingSchema.optional(),
|
|
65
80
|
helpers: z.array(HelperSchema).default([]),
|
|
81
|
+
/** OMP: the instructions waiting in its own queue (typed while it works), as it reports them; left out when none. */
|
|
82
|
+
queue: z.array(z.object({ text: z.string(), mode: z.enum(["steer", "follow_up"]) })).optional(),
|
|
66
83
|
lastReply: ReplySchema.optional(),
|
|
67
84
|
/** The last message this session sent to another agent: its tool_use_id, the address, the peer's Ledge session id, a one-line summary. */
|
|
68
85
|
lastMessage: z.object({ id: z.string(), to: z.string(), peer: z.string().optional(), summary: z.string(), at: z.string().datetime() }).optional(),
|
|
@@ -72,8 +89,14 @@ var SessionSchema = z.object({
|
|
|
72
89
|
/** The model and effort Ledge launched the agent with (#58); left out, the agent's own default. */
|
|
73
90
|
model: z.string().optional(),
|
|
74
91
|
effort: z.string().optional(),
|
|
92
|
+
/** OMP: the approval mode it was launched with, kept by a resume; left out, `always-ask`. */
|
|
93
|
+
approval: OmpApprovalSchema.optional(),
|
|
94
|
+
/** A model change asked while the agent worked (`session.model`), applied at its next free moment; a field left out is the agent's default. */
|
|
95
|
+
modelNext: z.object({ model: z.string().optional(), effort: z.string().optional() }).optional(),
|
|
75
96
|
/** The agent's own session id when Ledge chose it at launch (Claude Code `--session-id`). */
|
|
76
97
|
agentSessionId: z.string().optional(),
|
|
98
|
+
/** OMP: the exact journal file of the conversation, what a resume names (never "the latest of the folder"). */
|
|
99
|
+
agentSessionFile: z.string().optional(),
|
|
77
100
|
/** The agent's transcript: the latest one a hook named (it follows /clear and --resume), else the one the launch implies. */
|
|
78
101
|
transcriptPath: z.string().optional(),
|
|
79
102
|
exitCode: z.number().int().nullable().optional(),
|
|
@@ -85,12 +108,15 @@ var SessionSchema = z.object({
|
|
|
85
108
|
/** When the session was last taken out of the archive: the poller's retirement clock restarts from it, not from `exitedAt`. */
|
|
86
109
|
restoredAt: z.string().datetime().optional()
|
|
87
110
|
});
|
|
111
|
+
function backendOf(s) {
|
|
112
|
+
return s.backend ?? "tmux";
|
|
113
|
+
}
|
|
88
114
|
function isArchived(s) {
|
|
89
115
|
return s.archivedAt !== void 0;
|
|
90
116
|
}
|
|
91
117
|
|
|
92
118
|
// ../protocol/src/messages.ts
|
|
93
|
-
import { z as
|
|
119
|
+
import { z as z15 } from "zod";
|
|
94
120
|
|
|
95
121
|
// ../protocol/src/chat.ts
|
|
96
122
|
import { z as z2 } from "zod";
|
|
@@ -279,8 +305,9 @@ var IslandScaleSchema = z10.union([z10.literal("auto"), z10.literal(1), z10.lite
|
|
|
279
305
|
var IslandPrefsSchema = z10.object({
|
|
280
306
|
/** Shown at the top of the screen; hidden, Desktop still notifies. */
|
|
281
307
|
visible: z10.boolean(),
|
|
282
|
-
/**
|
|
283
|
-
|
|
308
|
+
/** `primary` (whichever it is after a reconfiguration), or a monitor's id as the shell lists it (`list_monitors`: the OS
|
|
309
|
+
* name, `#index` without one). `second`, written by older fronts, is the first monitor that is not the primary. */
|
|
310
|
+
screen: z10.string().trim().min(1).max(200),
|
|
284
311
|
position: z10.enum(["center", "left", "right"]),
|
|
285
312
|
/** A strip under a maximised window. */
|
|
286
313
|
discreet: z10.boolean(),
|
|
@@ -406,7 +433,9 @@ var NOTE_NOT_TYPED = "note_not_typed";
|
|
|
406
433
|
var DECISION_OFFERS = {
|
|
407
434
|
claude: { always: ["permission", "diff", "plan"], note: ["permission", "diff", "plan"] },
|
|
408
435
|
codex: { always: ["permission", "diff"], note: ["permission", "diff", "plan"] },
|
|
409
|
-
cursor: { always: [], note: ["permission", "plan"] }
|
|
436
|
+
cursor: { always: [], note: ["permission", "plan"] },
|
|
437
|
+
/** OMP answers its own native requests (spec 2026-10-20 §3.3): no « always », no note until those are typed. */
|
|
438
|
+
omp: { always: [], note: [] }
|
|
410
439
|
};
|
|
411
440
|
function offers(tool, kind, what) {
|
|
412
441
|
return DECISION_OFFERS[tool][what].includes(kind);
|
|
@@ -427,59 +456,125 @@ function pendingKey(p) {
|
|
|
427
456
|
return cyrb53(s, 0) + cyrb53(s, 1);
|
|
428
457
|
}
|
|
429
458
|
|
|
459
|
+
// ../protocol/src/mobile.ts
|
|
460
|
+
import { z as z14 } from "zod";
|
|
461
|
+
var DEFAULT_RELAY_URL = "https://server.ledge.paradow.dev";
|
|
462
|
+
var RelayUrlSchema = z14.string().transform((v, ctx) => {
|
|
463
|
+
const bad = () => {
|
|
464
|
+
ctx.addIssue({ code: z14.ZodIssueCode.custom, message: "invalid relay URL" });
|
|
465
|
+
return z14.NEVER;
|
|
466
|
+
};
|
|
467
|
+
let u;
|
|
468
|
+
try {
|
|
469
|
+
u = new URL(v.replace(/\/$/, ""));
|
|
470
|
+
} catch {
|
|
471
|
+
return bad();
|
|
472
|
+
}
|
|
473
|
+
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1";
|
|
474
|
+
if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) return bad();
|
|
475
|
+
if (u.username || u.password || u.pathname !== "/" || u.search || u.hash) return bad();
|
|
476
|
+
return u.origin;
|
|
477
|
+
});
|
|
478
|
+
var MobileConfigSchema = z14.object({ enabled: z14.boolean(), relayUrl: RelayUrlSchema });
|
|
479
|
+
var MobileLinkStateSchema = z14.discriminatedUnion("state", [
|
|
480
|
+
z14.object({ state: z14.literal("off") }),
|
|
481
|
+
z14.object({ state: z14.literal("connecting") }),
|
|
482
|
+
z14.object({ state: z14.literal("online") }),
|
|
483
|
+
z14.object({ state: z14.literal("offline"), retryAt: z14.string().datetime(), error: z14.string() }),
|
|
484
|
+
z14.object({ state: z14.literal("signed_out"), reason: z14.enum(["other_pc", "session_invalid"]) }),
|
|
485
|
+
/** The mobile service did not start (a broken mobile.json, a missing key store): nothing is linked until the daemon restarts. */
|
|
486
|
+
z14.object({ state: z14.literal("unavailable"), error: z14.string().max(200) })
|
|
487
|
+
]);
|
|
488
|
+
var MobilePairingSchema = z14.object({
|
|
489
|
+
id: z14.string(),
|
|
490
|
+
label: z14.string(),
|
|
491
|
+
code: z14.string().nullable(),
|
|
492
|
+
expiresAt: z14.string().datetime(),
|
|
493
|
+
state: z14.enum(["waiting", "confirm", "approved", "linked", "expired", "refused"])
|
|
494
|
+
});
|
|
495
|
+
var MobileStateSchema = z14.object({
|
|
496
|
+
enabled: z14.boolean(),
|
|
497
|
+
relayUrl: z14.string(),
|
|
498
|
+
account: z14.object({ login: z14.string(), accountId: z14.string() }).nullable(),
|
|
499
|
+
signin: z14.object({ userCode: z14.string(), verificationUri: z14.string(), expiresAt: z14.string().datetime() }).nullable(),
|
|
500
|
+
/** The last sign-in that did not end with a session; cleared by the next one. */
|
|
501
|
+
signinError: z14.enum(["expired", "denied", "not_allowed", "failed"]).nullable(),
|
|
502
|
+
link: MobileLinkStateSchema,
|
|
503
|
+
devices: z14.array(z14.object({ id: z14.string(), label: z14.string(), approvedAt: z14.string(), online: z14.boolean(), lastSeenAt: z14.string().optional() })),
|
|
504
|
+
pairings: z14.array(MobilePairingSchema)
|
|
505
|
+
});
|
|
506
|
+
|
|
430
507
|
// ../protocol/src/messages.ts
|
|
431
508
|
var PROTOCOL_PORT = 47831;
|
|
432
|
-
var
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
509
|
+
var RefusalCodeSchema = z15.enum(["stale", "read_only", "no_prompt", "not_taken", "busy", "unknown_session", "nothing_to_decide", "not_offered", "free_text", "note_not_typed", "unavailable", "invalid"]);
|
|
510
|
+
var SessionEventSchema = z15.object({
|
|
511
|
+
name: z15.enum(["SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest", "Notification", "Stop", "SessionEnd", "PaneChanged", "PaneGone", "PanePrompt", "PaneIdle", "InjectFailed", "Decided", "PostToolUse", "PostToolUseFailure", "StopFailure", "SubagentStart", "SubagentStop", "PaneTitle", "Renamed", "NativeRequest", "AgentMessage", "QueueUpdate"]),
|
|
512
|
+
at: z15.string().datetime(),
|
|
513
|
+
source: z15.enum(["hook", "pane", "daemon"]),
|
|
514
|
+
tool: z15.string().optional(),
|
|
515
|
+
text: z15.string().optional(),
|
|
516
|
+
options: z15.array(z15.string()).optional(),
|
|
439
517
|
/** Each option's description, by index ('' when it has none); left out when no option has one. */
|
|
440
|
-
optionDetails:
|
|
441
|
-
kind:
|
|
518
|
+
optionDetails: z15.array(z15.string()).optional(),
|
|
519
|
+
kind: z15.enum(["question", "permission", "plan", "diff"]).optional(),
|
|
442
520
|
/** PanePrompt: what to review in full before deciding (a plan read in the terminal), when `text` is its summary. */
|
|
443
|
-
body:
|
|
444
|
-
exitCode:
|
|
445
|
-
helperId:
|
|
521
|
+
body: z15.string().optional(),
|
|
522
|
+
exitCode: z15.number().int().nullable().optional(),
|
|
523
|
+
helperId: z15.string().optional(),
|
|
446
524
|
/** The helper's description, once known (Claude Code: its `.meta.json`, written after SubagentStart). */
|
|
447
|
-
task:
|
|
525
|
+
task: z15.string().optional(),
|
|
448
526
|
/** PostToolUseFailure: the user interrupted the turn (Esc); Claude then waits for input with no Stop. */
|
|
449
|
-
interrupt:
|
|
527
|
+
interrupt: z15.boolean().optional(),
|
|
528
|
+
/** QueueUpdate: OMP's queue as it stands. */
|
|
529
|
+
queue: z15.array(z15.object({ text: z15.string(), mode: z15.enum(["steer", "follow_up"]) })).optional(),
|
|
530
|
+
/** AgentMessage: the harness's own message, its text so far (`streaming`) or whole. `messageId` is unique for the session's life. */
|
|
531
|
+
messageId: z15.string().optional(),
|
|
532
|
+
streaming: z15.boolean().optional(),
|
|
533
|
+
/** NativeRequest: the dialog's id, and OMP's `ask` questions when it is one. */
|
|
534
|
+
requestId: z15.string().optional(),
|
|
535
|
+
questions: z15.array(AskQuestionSchema).optional(),
|
|
536
|
+
/** Stop: its text is already in the thread as AgentMessage cards, which the reply card must not repeat. */
|
|
537
|
+
streamed: z15.boolean().optional(),
|
|
450
538
|
/** Native tracking (spec 2026-10-13 §2): the harness's id of the tool call, so its result lands on the same row. */
|
|
451
|
-
toolUseId:
|
|
539
|
+
toolUseId: z15.string().optional(),
|
|
452
540
|
/** The tool's input as the harness gave it, every string capped by the hook. */
|
|
453
|
-
toolInput:
|
|
541
|
+
toolInput: z15.record(z15.string(), z15.unknown()).optional(),
|
|
454
542
|
/** PostToolUse: the result as text; SubagentStop: nothing (the report travels in `text`). */
|
|
455
|
-
output:
|
|
456
|
-
durationMs:
|
|
543
|
+
output: z15.string().optional(),
|
|
544
|
+
durationMs: z15.number().nonnegative().optional(),
|
|
457
545
|
/** PostToolUseFailure: the harness's error message. */
|
|
458
|
-
error:
|
|
546
|
+
error: z15.string().optional(),
|
|
459
547
|
/** The session's transcript, or the helper's on SubagentStop; the daemon reads its tail for the agent's messages. */
|
|
460
|
-
transcriptPath:
|
|
548
|
+
transcriptPath: z15.string().optional(),
|
|
461
549
|
/** Decided: what was chosen when the daemon knows it. */
|
|
462
550
|
decision: DecisionSchema.optional(),
|
|
463
551
|
/** The agent's own session id when the hook names it, so the daemon can resume that conversation. */
|
|
464
|
-
agentSessionId:
|
|
552
|
+
agentSessionId: z15.string().optional()
|
|
465
553
|
});
|
|
466
|
-
var RecentSchema =
|
|
554
|
+
var RecentSchema = z15.object({ cwd: z15.string().min(1), project: z15.string().min(1), lastAt: z15.string().datetime() });
|
|
467
555
|
var SEMVER = /^\d+\.\d+\.\d+$/;
|
|
468
|
-
var DaemonInfoSchema =
|
|
469
|
-
version:
|
|
470
|
-
install:
|
|
471
|
-
systemd:
|
|
472
|
-
manual:
|
|
473
|
-
routing:
|
|
474
|
-
archive:
|
|
556
|
+
var DaemonInfoSchema = z15.object({
|
|
557
|
+
version: z15.string(),
|
|
558
|
+
install: z15.enum(["npm", "git", "unknown"]),
|
|
559
|
+
systemd: z15.boolean(),
|
|
560
|
+
manual: z15.string().optional(),
|
|
561
|
+
routing: z15.boolean().optional(),
|
|
562
|
+
archive: z15.boolean().optional(),
|
|
475
563
|
/** Takes `model` and `effort` on `session.launch` and answers `models.list` (#58). */
|
|
476
|
-
models:
|
|
564
|
+
models: z15.boolean().optional(),
|
|
565
|
+
/** Drives OMP over its RPC: launching `omp` sessions is possible when the binary is there (`models.list` says so). */
|
|
566
|
+
omp: z15.boolean().optional(),
|
|
567
|
+
/** Takes `session.model`: a live session's model and effort changed (#58, lot 2). */
|
|
568
|
+
modelSwitch: z15.boolean().optional(),
|
|
569
|
+
/** Takes `approval` on an OMP `session.launch` (auto mode). */
|
|
570
|
+
ompApproval: z15.boolean().optional(),
|
|
477
571
|
/** Its home folder, so a client shows the folders under it as `~` (absent from older daemons). */
|
|
478
|
-
home:
|
|
572
|
+
home: z15.string().optional(),
|
|
573
|
+
mobile: z15.boolean().optional()
|
|
479
574
|
});
|
|
480
|
-
var UpdateFailureSchema =
|
|
481
|
-
var ServerMessageSchema =
|
|
482
|
-
|
|
575
|
+
var UpdateFailureSchema = z15.enum(["git-install", "no-systemd", "sessions-at-risk", "npm-failed", "busy"]);
|
|
576
|
+
var ServerMessageSchema = z15.discriminatedUnion("type", [
|
|
577
|
+
z15.object({ type: z15.literal("chat.routed"), qid: z15.string(), result: RouteResultSchema }),
|
|
483
578
|
DocumentContentSchema,
|
|
484
579
|
DocumentErrorSchema,
|
|
485
580
|
ReposInfoSchema,
|
|
@@ -488,62 +583,66 @@ var ServerMessageSchema = z14.discriminatedUnion("type", [
|
|
|
488
583
|
/** `recents`: the folders of past sessions, most recent first. `launcher`: the command that runs `ledge` inside WSL
|
|
489
584
|
* (`[node, cli]`), so the overlay can attach a terminal to a session with `<launcher> attach <id>`. `daemon`: absent
|
|
490
585
|
* from daemons older than the auto-update. */
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
586
|
+
z15.object({ type: z15.literal("snapshot"), sessions: z15.array(SessionSchema), recents: z15.array(RecentSchema).optional(), launcher: z15.array(z15.string()).optional(), daemon: DaemonInfoSchema.optional(), sshAlias: SshAliasSchema.optional() }),
|
|
587
|
+
z15.object({ type: z15.literal("session.updated"), session: SessionSchema }),
|
|
588
|
+
/** OMP: the instructions taken back from its queue by an interrupt that restores it; the asker puts them back in its draft. */
|
|
589
|
+
z15.object({ type: z15.literal("session.queue.restored"), id: z15.string(), texts: z15.array(z15.string()) }),
|
|
590
|
+
z15.object({ type: z15.literal("session.removed"), id: z15.string() }),
|
|
591
|
+
z15.object({ type: z15.literal("session.preview"), id: z15.string(), lines: z15.array(z15.string()) }),
|
|
495
592
|
/** A session's thread, on request (spec 2026-10-13 §5); then each created or updated item, broadcast. */
|
|
496
|
-
|
|
497
|
-
|
|
593
|
+
z15.object({ type: z15.literal("session.thread"), id: z15.string(), items: z15.array(ThreadItemSchema) }),
|
|
594
|
+
z15.object({ type: z15.literal("session.thread.item"), id: z15.string(), item: ThreadItemSchema }),
|
|
498
595
|
/** Answer to the requester of a `session.launch`: the session exists (its `session.updated` is broadcast as usual). */
|
|
499
|
-
|
|
500
|
-
|
|
596
|
+
z15.object({ type: z15.literal("session.launched"), id: z15.string() }),
|
|
597
|
+
z15.object({ type: z15.literal("recents"), recents: z15.array(RecentSchema) }),
|
|
501
598
|
/** Answer to the requester of a `folder.list` or `folder.create`: `path` resolved (`~` expanded), `parent` null at the
|
|
502
599
|
* root, `entries` its subfolders by name (hidden ones left out). A failure keeps the asked path and says why in `error`,
|
|
503
600
|
* never in an `error` message, which the launch form reads as a failed launch. */
|
|
504
|
-
|
|
505
|
-
type:
|
|
506
|
-
path:
|
|
507
|
-
parent:
|
|
508
|
-
entries:
|
|
509
|
-
error:
|
|
601
|
+
z15.object({
|
|
602
|
+
type: z15.literal("folder.listing"),
|
|
603
|
+
path: z15.string(),
|
|
604
|
+
parent: z15.string().nullable(),
|
|
605
|
+
entries: z15.array(z15.string()),
|
|
606
|
+
error: z15.string().optional(),
|
|
510
607
|
/** The request this answers, when it carried one. */
|
|
511
|
-
requestId:
|
|
608
|
+
requestId: z15.string().optional(),
|
|
512
609
|
/** Those `entries` that are Git repositories, with their branch (the short commit when HEAD is detached). */
|
|
513
|
-
repos:
|
|
610
|
+
repos: z15.array(z15.object({ name: z15.string(), branch: z15.string() })).optional()
|
|
514
611
|
}),
|
|
515
612
|
/** 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. */
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
613
|
+
z15.object({ type: z15.literal("chat.step"), qid: z15.string(), step: ChatStepSchema }),
|
|
614
|
+
z15.object({ type: z15.literal("chat.delta"), qid: z15.string(), text: z15.string() }),
|
|
615
|
+
z15.object({ type: z15.literal("chat.done"), qid: z15.string(), text: z15.string(), at: z15.string().datetime(), proposal: ChatProposalSchema.optional(), proposals: ChatProposalsSchema.optional(), relay: ChatRelaySchema.optional(), projectProposal: ChatProjectProposalSchema.optional() }),
|
|
616
|
+
z15.object({ type: z15.literal("chat.error"), qid: z15.string(), message: z15.string() }),
|
|
617
|
+
z15.object({ type: z15.literal("chat.history"), turns: z15.array(ChatTurnSchema) }),
|
|
521
618
|
/** The daemon updating itself: npm is running, then the service restarts (the socket closes right after). */
|
|
522
|
-
|
|
619
|
+
z15.object({ type: z15.literal("update.progress"), version: z15.string(), step: z15.enum(["installing", "restarting"]) }),
|
|
523
620
|
/** Nothing was changed. `detail`: the useful end of npm's output; `command`: what the person can run instead. */
|
|
524
|
-
|
|
621
|
+
z15.object({ type: z15.literal("update.failed"), version: z15.string(), reason: UpdateFailureSchema, detail: z15.string().optional(), command: z15.string().optional() }),
|
|
525
622
|
/** The shared machines list (spec 2026-10-14 §5), sent to local clients only. */
|
|
526
|
-
|
|
623
|
+
z15.object({ type: z15.literal("machines"), machines: MachinesSchema }),
|
|
527
624
|
/** The local daemon's projects and folder rules (spec 2026-10-16 build §5), sent to local clients only. */
|
|
528
|
-
|
|
625
|
+
z15.object({ type: z15.literal("projects"), projects: ProjectsSchema }),
|
|
529
626
|
/** The island's settings shared by the local fronts; null until a front sets them (the island then sends its own). Local clients only. */
|
|
530
|
-
|
|
627
|
+
z15.object({ type: z15.literal("island"), prefs: IslandPrefsSchema.nullable() }),
|
|
628
|
+
/** Mobile access (lot 3), after the island in each snapshot and on every change. Local clients only. */
|
|
629
|
+
z15.object({ type: z15.literal("mobile"), state: MobileStateSchema }),
|
|
531
630
|
/** The daemon's immediate answer to a `session.message`: delivered (`accepted`), held for later (`queued`), left to Claude's own SendMessage (`native`), or refused with a `reason`. */
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
type:
|
|
535
|
-
message:
|
|
536
|
-
id:
|
|
631
|
+
z15.object({ type: z15.literal("message.ack"), rid: z15.string(), outcome: z15.enum(["accepted", "queued", "native", "refused"]), reason: z15.string().optional() }),
|
|
632
|
+
z15.object({
|
|
633
|
+
type: z15.literal("error"),
|
|
634
|
+
message: z15.string(),
|
|
635
|
+
id: z15.string().optional(),
|
|
537
636
|
/** Le type du message client refusé. */
|
|
538
|
-
request:
|
|
637
|
+
request: z15.string().optional(),
|
|
539
638
|
/** Ce que le front doit reconnaître au-delà du texte (NOTE_NOT_TYPED) ; une chaîne libre, qu'un front plus ancien ignore. */
|
|
540
|
-
code:
|
|
639
|
+
code: z15.string().optional()
|
|
541
640
|
})
|
|
542
641
|
]);
|
|
543
642
|
var MESSAGE_MAX = 2e4;
|
|
544
643
|
var SUMMARY_MAX = 500;
|
|
545
|
-
var ClientMessageSchema =
|
|
546
|
-
|
|
644
|
+
var ClientMessageSchema = z15.discriminatedUnion("type", [
|
|
645
|
+
z15.object({ type: z15.literal("chat.route"), qid: z15.string().min(1).max(100), text: z15.string().min(1).max(2e4), tool: ToolSchema.optional(), context: RoutingContextSchema }),
|
|
547
646
|
DocumentReadSchema,
|
|
548
647
|
ReposInspectSchema,
|
|
549
648
|
ReposScanSchema,
|
|
@@ -552,92 +651,112 @@ var ClientMessageSchema = z14.discriminatedUnion("type", [
|
|
|
552
651
|
* free one otherwise), `prompt` the agent's first instruction, `continue` resumes the folder's last conversation.
|
|
553
652
|
* With `qid` it is the chat card of that turn: the daemon launches the turn's proposal and ignores the other fields
|
|
554
653
|
* but `rows`, `model` and `effort`. */
|
|
555
|
-
|
|
556
|
-
type:
|
|
654
|
+
z15.object({
|
|
655
|
+
type: z15.literal("session.launch"),
|
|
557
656
|
tool: ToolSchema,
|
|
558
|
-
cwd:
|
|
559
|
-
name:
|
|
560
|
-
prompt:
|
|
561
|
-
continue:
|
|
562
|
-
qid:
|
|
657
|
+
cwd: z15.string().min(1),
|
|
658
|
+
name: z15.string().optional(),
|
|
659
|
+
prompt: z15.string().optional(),
|
|
660
|
+
continue: z15.boolean().optional(),
|
|
661
|
+
qid: z15.string().min(1).optional(),
|
|
563
662
|
/** With `qid`: the card's rows to launch (#24), indexes into the turn's `proposals`, each once; the first row without it. */
|
|
564
|
-
rows:
|
|
663
|
+
rows: z15.array(z15.number().int().min(0).max(MAX_PROPOSALS - 1)).min(1).max(MAX_PROPOSALS).refine((r) => new Set(r).size === r.length, "a row twice").optional(),
|
|
565
664
|
/** Folders in addition to `cwd` (Claude: `--add-dir`); resolved like `cwd`. */
|
|
566
|
-
dirs:
|
|
665
|
+
dirs: z15.array(z15.string().min(1)).max(20).optional(),
|
|
567
666
|
/** The agent's model and effort (#58), the agent's own default when left out; with `qid`, for every row launched. */
|
|
568
667
|
model: ModelIdSchema.optional(),
|
|
569
668
|
effort: EffortSchema.optional(),
|
|
669
|
+
/** OMP only: its approval mode, `always-ask` when left out. */
|
|
670
|
+
approval: OmpApprovalSchema.optional(),
|
|
570
671
|
/** Routing by the fleet, removed before the message reaches the daemon. */
|
|
571
|
-
machineId:
|
|
672
|
+
machineId: z15.string().optional()
|
|
572
673
|
}),
|
|
573
674
|
/** The launch form's folder browser: list a folder's subfolders (home when no `path`), or make `name` inside `parent`
|
|
574
675
|
* and list it. Both are answered with a `folder.listing`. */
|
|
575
|
-
|
|
576
|
-
type:
|
|
577
|
-
path:
|
|
676
|
+
z15.object({
|
|
677
|
+
type: z15.literal("folder.list"),
|
|
678
|
+
path: z15.string().optional(),
|
|
578
679
|
/** Routing by the fleet, removed before the message reaches the daemon. */
|
|
579
|
-
machineId:
|
|
680
|
+
machineId: z15.string().optional(),
|
|
580
681
|
/** Copied into the `folder.listing` that answers; the fleet relays only that listing, from the machine asked. */
|
|
581
|
-
requestId:
|
|
682
|
+
requestId: z15.string().min(1).max(100).optional()
|
|
582
683
|
}),
|
|
583
|
-
|
|
584
|
-
type:
|
|
585
|
-
parent:
|
|
586
|
-
name:
|
|
684
|
+
z15.object({
|
|
685
|
+
type: z15.literal("folder.create"),
|
|
686
|
+
parent: z15.string().min(1),
|
|
687
|
+
name: z15.string().min(1),
|
|
587
688
|
/** As for `folder.list`: routing by the fleet, and the request its `folder.listing` answers. */
|
|
588
|
-
machineId:
|
|
589
|
-
requestId:
|
|
689
|
+
machineId: z15.string().optional(),
|
|
690
|
+
requestId: z15.string().min(1).max(100).optional()
|
|
590
691
|
}),
|
|
591
|
-
|
|
692
|
+
/** `steer`: OMP only, redirect the turn running now instead of queueing the text after it. */
|
|
693
|
+
/** OMP's `ask`: the answers to every question of the dialog `requestId`, in order (`selected` labels, and/or free `text`); `cancel` declines it. */
|
|
694
|
+
z15.object({ type: z15.literal("session.answer"), id: z15.string(), requestId: z15.string().min(1).max(200), answers: z15.array(z15.object({ id: z15.string(), selected: z15.array(z15.string().max(2e3)).max(100), text: z15.string().max(2e4).optional() })).max(50), cancel: z15.boolean().optional() }),
|
|
695
|
+
z15.object({ type: z15.literal("session.reply"), id: z15.string(), text: z15.string().min(1), steer: z15.boolean().optional() }),
|
|
592
696
|
/** The chat card's instruction (spec 2026-10-12 relay §4): sent now if the session is free, else queued until it is. */
|
|
593
|
-
|
|
697
|
+
z15.object({ type: z15.literal("session.relay"), qid: z15.string().min(1) }),
|
|
594
698
|
/** Takes the card's queued instruction back out of the queue. */
|
|
595
|
-
|
|
699
|
+
z15.object({ type: z15.literal("session.relay.cancel"), qid: z15.string().min(1) }),
|
|
596
700
|
/** `always` and `note` only where the harness offers them (`offers`); a note goes with `deny`, for the agent to read. */
|
|
597
|
-
|
|
598
|
-
type:
|
|
599
|
-
id:
|
|
701
|
+
z15.object({
|
|
702
|
+
type: z15.literal("session.decide"),
|
|
703
|
+
id: z15.string(),
|
|
600
704
|
decision: DecisionSchema,
|
|
601
|
-
note:
|
|
705
|
+
note: z15.string().trim().min(1).max(NOTE_MAX).optional(),
|
|
602
706
|
/** 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.
|
|
603
707
|
* `key` (pendingKey) covers its whole content; older fronts send kind and text only. */
|
|
604
|
-
expect: PendingSchema.pick({ kind: true, text: true }).extend({ key:
|
|
708
|
+
expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z15.string().max(64).optional() }).optional()
|
|
605
709
|
}),
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
710
|
+
z15.object({ type: z15.literal("session.focus"), id: z15.string() }),
|
|
711
|
+
z15.object({ type: z15.literal("session.kill"), id: z15.string() }),
|
|
712
|
+
/** `restoreQueue` (OMP): also take back what waits in its queue, returned as `session.queue.restored` to put in the draft. */
|
|
713
|
+
z15.object({ type: z15.literal("session.interrupt"), id: z15.string(), restoreQueue: z15.boolean().optional() }),
|
|
609
714
|
/** Removes a finished (exited) or read-only (hook_only) session from the list; refused for a live launcher session. */
|
|
610
|
-
|
|
715
|
+
z15.object({ type: z15.literal("session.dismiss"), id: z15.string() }),
|
|
611
716
|
/** Puts a session in the archive: it leaves the live list but is kept. */
|
|
612
|
-
|
|
717
|
+
z15.object({ type: z15.literal("session.archive"), id: z15.string() }),
|
|
613
718
|
/** Takes an archived session back out of the archive, as it was. */
|
|
614
|
-
|
|
719
|
+
z15.object({ type: z15.literal("session.restore"), id: z15.string() }),
|
|
615
720
|
/** Starts the agent again on an archived session's conversation. */
|
|
616
|
-
|
|
721
|
+
z15.object({ type: z15.literal("session.resume"), id: z15.string() }),
|
|
722
|
+
/** Changes a live session's model and effort (#58, lot 2), `null` for the agent's own default: the daemon restarts the
|
|
723
|
+
* agent on the same conversation with them, at once when the session is free, else at its next free moment. */
|
|
724
|
+
z15.object({ type: z15.literal("session.model"), id: z15.string(), model: ModelIdSchema.nullable(), effort: EffortSchema.nullable() }),
|
|
617
725
|
/** Deletes a session for good, archived or not. */
|
|
618
|
-
|
|
726
|
+
z15.object({ type: z15.literal("session.delete"), id: z15.string() }),
|
|
619
727
|
/** 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. */
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
728
|
+
z15.object({ type: z15.literal("session.rename"), id: z15.string(), topic: z15.string().max(500) }),
|
|
729
|
+
z15.object({ type: z15.literal("session.thread"), id: z15.string() }),
|
|
730
|
+
z15.object({ type: z15.literal("session.register"), session: SessionSchema }),
|
|
623
731
|
/** `agent` and `cwd` describe the session when the daemon does not know it yet (a hook from an agent started without ledge). */
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
732
|
+
z15.object({ type: z15.literal("session.event"), id: z15.string(), event: SessionEventSchema, agent: ToolSchema.optional(), cwd: z15.string().optional() }),
|
|
733
|
+
z15.object({ type: z15.literal("session.unregister"), id: z15.string(), exitCode: z15.number().int().nullable() }),
|
|
734
|
+
z15.object({ type: z15.literal("snapshot.request") }),
|
|
735
|
+
z15.object({ type: z15.literal("chat.ask"), qid: z15.string().min(1), text: z15.string().min(1), tool: ToolSchema.optional() }),
|
|
736
|
+
z15.object({ type: z15.literal("chat.cancel"), qid: z15.string().min(1) }),
|
|
629
737
|
/** Starts a new conversation with Ledge: the day's turns are cleared and the next question opens a new harness session. */
|
|
630
|
-
|
|
738
|
+
z15.object({ type: z15.literal("chat.reset") }),
|
|
631
739
|
/** Installs `@paradow/ledge@version` and restarts the service (spec 2026-10-12 auto-update §6). */
|
|
632
|
-
|
|
740
|
+
z15.object({ type: z15.literal("update.daemon"), version: z15.string().regex(SEMVER) }),
|
|
633
741
|
/** Replaces the daemon's machines list (first connected front wins); refused unless valid. */
|
|
634
|
-
|
|
742
|
+
z15.object({ type: z15.literal("machines.set"), machines: MachinesSchema }),
|
|
635
743
|
/** Replaces the local daemon's projects and rules as a whole; refused unless valid. */
|
|
636
|
-
|
|
744
|
+
z15.object({ type: z15.literal("projects.set"), projects: ProjectsSchema }),
|
|
637
745
|
/** Replaces the island's settings as a whole; refused unless valid. */
|
|
638
|
-
|
|
746
|
+
z15.object({ type: z15.literal("island.set"), prefs: IslandPrefsSchema }),
|
|
747
|
+
/** Mobile access (lot 3), local fronts only. A changed `relayUrl` drops the relay session. */
|
|
748
|
+
z15.object({ type: z15.literal("mobile.set"), enabled: z15.boolean(), relayUrl: RelayUrlSchema }),
|
|
749
|
+
z15.object({ type: z15.literal("mobile.signin") }),
|
|
750
|
+
z15.object({ type: z15.literal("mobile.signin.cancel") }),
|
|
751
|
+
z15.object({ type: z15.literal("mobile.signout") }),
|
|
752
|
+
/** While the link is offline: try again now. */
|
|
753
|
+
z15.object({ type: z15.literal("mobile.retry") }),
|
|
754
|
+
z15.object({ type: z15.literal("mobile.pairing.approve"), id: z15.string().min(1).max(100) }),
|
|
755
|
+
z15.object({ type: z15.literal("mobile.pairing.deny"), id: z15.string().min(1).max(100) }),
|
|
756
|
+
/** `id`: the device's fingerprint. */
|
|
757
|
+
z15.object({ type: z15.literal("mobile.device.revoke"), id: z15.string().min(1).max(100) }),
|
|
639
758
|
/** One session writing to another by Ledge name (`to` may end in ` [ref]`); `intercept` (the Claude hook): answer `native` unless `to` is a live non-Claude session. */
|
|
640
|
-
|
|
759
|
+
z15.object({ type: z15.literal("session.message"), rid: z15.string().min(1), from: z15.string().min(1), to: z15.string().min(1), text: z15.string().min(1).max(MESSAGE_MAX), summary: z15.string().max(SUMMARY_MAX).optional(), intercept: z15.boolean().optional() })
|
|
641
760
|
]);
|
|
642
761
|
function parseClientMessage(raw) {
|
|
643
762
|
return ClientMessageSchema.parse(raw);
|
|
@@ -729,14 +848,14 @@ function pickName(taken, preferred) {
|
|
|
729
848
|
}
|
|
730
849
|
|
|
731
850
|
// ../protocol/src/proposal.ts
|
|
732
|
-
import { z as
|
|
733
|
-
var ProposalBlockSchema =
|
|
734
|
-
var RelayBlockSchema =
|
|
735
|
-
var ProjectBlockSchema =
|
|
736
|
-
name:
|
|
737
|
-
description:
|
|
738
|
-
repos:
|
|
739
|
-
addTo:
|
|
851
|
+
import { z as z16 } from "zod";
|
|
852
|
+
var ProposalBlockSchema = z16.object({ cwd: z16.string().trim().min(1), prompt: z16.string().optional() });
|
|
853
|
+
var RelayBlockSchema = z16.object({ session: z16.string().trim().min(1), text: z16.string().trim().min(1) });
|
|
854
|
+
var ProjectBlockSchema = z16.object({
|
|
855
|
+
name: z16.string().trim().min(1).max(80),
|
|
856
|
+
description: z16.string().max(500).optional(),
|
|
857
|
+
repos: z16.array(z16.object({ path: z16.string().min(1).max(4096), why: z16.string().trim().min(1).max(200) })).min(1).max(50),
|
|
858
|
+
addTo: z16.string().trim().max(80).optional()
|
|
740
859
|
});
|
|
741
860
|
var BLOCK = /```ledge-(session|relay|project)[ \t]*\n?([\s\S]*?)(?:```|$)/g;
|
|
742
861
|
function extractProposal(answer) {
|
|
@@ -784,6 +903,166 @@ function sessionTitle(s) {
|
|
|
784
903
|
return `${s.name} \xB7 ${s.topic || s.project}`;
|
|
785
904
|
}
|
|
786
905
|
|
|
906
|
+
// ../protocol/src/relay.ts
|
|
907
|
+
import { z as z17 } from "zod";
|
|
908
|
+
var SEALED_MAX = 1048576;
|
|
909
|
+
var RelayPeerSchema = z17.union([z17.literal("pc"), z17.string().regex(/^d_[A-Za-z0-9_-]{16,40}$/)]);
|
|
910
|
+
var RelayClientMessageSchema = z17.object({
|
|
911
|
+
type: z17.literal("send"),
|
|
912
|
+
to: RelayPeerSchema,
|
|
913
|
+
body: z17.string().max(SEALED_MAX)
|
|
914
|
+
});
|
|
915
|
+
var RelayServerMessageSchema = z17.discriminatedUnion("type", [
|
|
916
|
+
z17.object({ type: z17.literal("welcome"), accountId: z17.string(), self: RelayPeerSchema, online: z17.array(RelayPeerSchema) }),
|
|
917
|
+
z17.object({ type: z17.literal("deliver"), from: RelayPeerSchema, body: z17.string() }),
|
|
918
|
+
z17.object({ type: z17.literal("presence"), peer: RelayPeerSchema, online: z17.boolean() }),
|
|
919
|
+
/**
|
|
920
|
+
* A frame was refused; `to` names the target when it is known.
|
|
921
|
+
* Contract: an error concerning a sealed body (`offline`, `too_large`, or a socket closed with 4008
|
|
922
|
+
* `rate_limited`) means that body was not delivered, so the sequence on that channel is lost: the
|
|
923
|
+
* sender must re-handshake with that peer before sending to it again.
|
|
924
|
+
*/
|
|
925
|
+
z17.object({
|
|
926
|
+
type: z17.literal("error"),
|
|
927
|
+
code: z17.enum(["bad_frame", "too_large", "bad_target", "offline", "rate_limited"]),
|
|
928
|
+
to: RelayPeerSchema.optional()
|
|
929
|
+
})
|
|
930
|
+
]);
|
|
931
|
+
var PAIR_LABEL_MAX = 40;
|
|
932
|
+
var KeyB64 = z17.string().regex(/^[A-Za-z0-9_-]{87}$/);
|
|
933
|
+
var Nonce = z17.string().regex(/^[A-Za-z0-9_-]{43}$/);
|
|
934
|
+
var Fingerprint = z17.string().regex(/^[A-Za-z0-9_-]{22}$/);
|
|
935
|
+
var SealedFrameSchema = z17.discriminatedUnion("k", [
|
|
936
|
+
/** `e` is the ephemeral key, `f` the fingerprint of the sender's static key. */
|
|
937
|
+
z17.object({ k: z17.literal("hello"), e: KeyB64, f: Fingerprint }),
|
|
938
|
+
z17.object({ k: z17.literal("msg"), s: z17.string() }),
|
|
939
|
+
z17.object({ k: z17.literal("pair_commit"), c: Nonce, label: z17.string().trim().min(1).max(PAIR_LABEL_MAX) }),
|
|
940
|
+
z17.object({ k: z17.literal("pair_offer"), pc: KeyB64, n: Nonce }),
|
|
941
|
+
z17.object({ k: z17.literal("pair_reveal"), d: KeyB64, n: Nonce }),
|
|
942
|
+
z17.object({ k: z17.literal("pair_ok") }),
|
|
943
|
+
z17.object({ k: z17.literal("pair_no") }),
|
|
944
|
+
z17.object({ k: z17.literal("denied") }),
|
|
945
|
+
z17.object({ k: z17.literal("rekey") })
|
|
946
|
+
]);
|
|
947
|
+
|
|
948
|
+
// ../protocol/src/relayAuth.ts
|
|
949
|
+
import { z as z18 } from "zod";
|
|
950
|
+
var RELAY_CLOSE_SIGNED_OUT = 4001;
|
|
951
|
+
var DeviceStartResponseSchema = z18.object({
|
|
952
|
+
deviceCode: z18.string().min(1),
|
|
953
|
+
userCode: z18.string().min(1),
|
|
954
|
+
verificationUri: z18.string().min(1),
|
|
955
|
+
expiresIn: z18.number().int().positive(),
|
|
956
|
+
interval: z18.number().int().positive()
|
|
957
|
+
});
|
|
958
|
+
var DevicePollResponseSchema = z18.discriminatedUnion("status", [
|
|
959
|
+
z18.object({ status: z18.literal("pending") }),
|
|
960
|
+
z18.object({ status: z18.literal("slow_down"), interval: z18.number().int().positive() }),
|
|
961
|
+
/** `token` is the relay session token, shown this once only. */
|
|
962
|
+
z18.object({ status: z18.literal("ok"), token: z18.string().min(1), accountId: z18.string().min(1), login: z18.string().min(1) }),
|
|
963
|
+
z18.object({ status: z18.literal("expired") }),
|
|
964
|
+
z18.object({ status: z18.literal("denied") }),
|
|
965
|
+
z18.object({ status: z18.literal("not_allowed") })
|
|
966
|
+
]);
|
|
967
|
+
var DeviceRevokeResponseSchema = z18.object({ revoked: z18.number().int().min(0) });
|
|
968
|
+
var MeResponseSchema = z18.object({
|
|
969
|
+
accountId: z18.string().min(1),
|
|
970
|
+
login: z18.string().min(1),
|
|
971
|
+
peer: RelayPeerSchema,
|
|
972
|
+
kind: z18.enum(["pc", "device"])
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
// ../protocol/src/phone.ts
|
|
976
|
+
import { z as z19 } from "zod";
|
|
977
|
+
var PHONE_PROTOCOL = 1;
|
|
978
|
+
var PAD_TIERS = [1024, 4096, 16384, 65536, 262144, 655360];
|
|
979
|
+
var PLAIN_MAX = 655360;
|
|
980
|
+
var utf8 = new TextEncoder();
|
|
981
|
+
function padPlain(json) {
|
|
982
|
+
const bytes = utf8.encode(json).length;
|
|
983
|
+
if (bytes > PLAIN_MAX) return null;
|
|
984
|
+
const tier = PAD_TIERS.find((t) => t >= bytes);
|
|
985
|
+
if (tier === void 0) return null;
|
|
986
|
+
return json + " ".repeat(tier - bytes);
|
|
987
|
+
}
|
|
988
|
+
var THREAD_ITEMS_MAX = 200;
|
|
989
|
+
var THREAD_BUDGET = 524288;
|
|
990
|
+
var THREAD_TEXT_MAX = 8192;
|
|
991
|
+
var TOOL_TEXT_MAX = 2048;
|
|
992
|
+
var PENDING_BODY_MAX = 65536;
|
|
993
|
+
var SNAPSHOT_BODY_MAX = 2048;
|
|
994
|
+
var RiskSchema = z19.enum(["low", "high"]);
|
|
995
|
+
var RiskReasonSchema = z19.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"]);
|
|
996
|
+
var OperationIdSchema = z19.string().uuid();
|
|
997
|
+
var OperationStatusSchema = z19.enum(["received", "applied", "rejected", "failed"]);
|
|
998
|
+
var OperationReasonSchema = z19.enum([...RefusalCodeSchema.options, "untypable", "confirm_required", "conflict"]);
|
|
999
|
+
var CapabilitiesSchema = z19.object({
|
|
1000
|
+
/** A question can be answered in free text. */
|
|
1001
|
+
freeTextAnswer: z19.boolean()
|
|
1002
|
+
});
|
|
1003
|
+
var UNTYPABLE = /[\u0000-\u001f\u007f-\u009f\u200b\u200e\u200f\u2028-\u202e\u2066-\u2069\ufeff]/;
|
|
1004
|
+
var CapabilitiesByToolSchema = z19.object({ claude: CapabilitiesSchema, codex: CapabilitiesSchema, cursor: CapabilitiesSchema, omp: CapabilitiesSchema });
|
|
1005
|
+
var PhonePendingSchema = PendingSchema.extend({ key: z19.string().max(64), risk: RiskSchema, riskReason: RiskReasonSchema, cut: z19.literal(true).optional() });
|
|
1006
|
+
var PhoneSessionSchema = SessionSchema.pick({
|
|
1007
|
+
id: true,
|
|
1008
|
+
name: true,
|
|
1009
|
+
tool: true,
|
|
1010
|
+
project: true,
|
|
1011
|
+
title: true,
|
|
1012
|
+
topic: true,
|
|
1013
|
+
repo: true,
|
|
1014
|
+
branch: true,
|
|
1015
|
+
worktree: true,
|
|
1016
|
+
state: true,
|
|
1017
|
+
helpers: true,
|
|
1018
|
+
lastReply: true,
|
|
1019
|
+
startedAt: true,
|
|
1020
|
+
lastActivityAt: true
|
|
1021
|
+
}).extend({ pending: PhonePendingSchema.optional(), unread: z19.boolean() });
|
|
1022
|
+
var sessionId = z19.string().min(1).max(100);
|
|
1023
|
+
var PhoneExpectSchema = PendingSchema.pick({ kind: true, text: true }).extend({ key: z19.string().min(1).max(64) });
|
|
1024
|
+
var PhoneClientMessageSchema = z19.discriminatedUnion("type", [
|
|
1025
|
+
z19.object({ type: z19.literal("snapshot.request") }),
|
|
1026
|
+
z19.object({ type: z19.literal("session.thread"), id: sessionId }),
|
|
1027
|
+
/** The session's request with its whole body (up to `PENDING_BODY_MAX`). */
|
|
1028
|
+
z19.object({ type: z19.literal("session.pending"), id: sessionId }),
|
|
1029
|
+
/** The session whose terminal the phone shows; null when it shows none. */
|
|
1030
|
+
z19.object({ type: z19.literal("session.watch"), id: sessionId.nullable() }),
|
|
1031
|
+
z19.object({ type: z19.literal("session.read"), id: sessionId, at: z19.string().datetime() }),
|
|
1032
|
+
z19.object({
|
|
1033
|
+
type: z19.literal("session.decide"),
|
|
1034
|
+
op: OperationIdSchema,
|
|
1035
|
+
id: sessionId,
|
|
1036
|
+
decision: DecisionSchema,
|
|
1037
|
+
note: z19.string().trim().min(1).max(NOTE_MAX).optional(),
|
|
1038
|
+
/** The request the phone showed: refused unless it is still the pending one. */
|
|
1039
|
+
expect: PhoneExpectSchema
|
|
1040
|
+
}),
|
|
1041
|
+
z19.object({
|
|
1042
|
+
type: z19.literal("session.reply"),
|
|
1043
|
+
op: OperationIdSchema,
|
|
1044
|
+
id: sessionId,
|
|
1045
|
+
text: z19.string().min(1).max(MESSAGE_MAX),
|
|
1046
|
+
/** The request the reply answers, or null when the phone showed none: refused if the session's request is no longer that one. */
|
|
1047
|
+
expect: PhoneExpectSchema.nullable()
|
|
1048
|
+
}),
|
|
1049
|
+
z19.object({ type: z19.literal("session.interrupt"), op: OperationIdSchema, id: sessionId })
|
|
1050
|
+
]);
|
|
1051
|
+
var PhoneServerMessageSchema = z19.discriminatedUnion("type", [
|
|
1052
|
+
z19.object({ type: z19.literal("snapshot"), protocol: z19.literal(PHONE_PROTOCOL), sessions: z19.array(PhoneSessionSchema), capabilities: CapabilitiesByToolSchema, daemon: z19.object({ version: z19.string(), name: z19.string().max(40) }) }),
|
|
1053
|
+
z19.object({ type: z19.literal("session.updated"), session: PhoneSessionSchema }),
|
|
1054
|
+
z19.object({ type: z19.literal("session.removed"), id: z19.string() }),
|
|
1055
|
+
/** `pending.cut` is set when even this body was shortened. */
|
|
1056
|
+
z19.object({ type: z19.literal("session.pending"), id: z19.string(), pending: PhonePendingSchema }),
|
|
1057
|
+
z19.object({ type: z19.literal("session.preview"), id: z19.string(), lines: z19.array(z19.string()) }),
|
|
1058
|
+
/** `cut`: the ids of the items shortened; `more`: older items were left out. */
|
|
1059
|
+
z19.object({ type: z19.literal("session.thread"), id: z19.string(), items: z19.array(ThreadItemSchema), cut: z19.array(z19.string()), more: z19.boolean() }),
|
|
1060
|
+
z19.object({ type: z19.literal("session.thread.item"), id: z19.string(), item: ThreadItemSchema, cut: z19.literal(true).optional() }),
|
|
1061
|
+
z19.object({ type: z19.literal("operation.result"), op: OperationIdSchema, status: OperationStatusSchema, reason: OperationReasonSchema.optional() }),
|
|
1062
|
+
/** Never carries free text. */
|
|
1063
|
+
z19.object({ type: z19.literal("error"), code: z19.enum(["invalid", "unknown_session", "too_large"]), request: z19.string().max(40).optional() })
|
|
1064
|
+
]);
|
|
1065
|
+
|
|
787
1066
|
// src/auth.ts
|
|
788
1067
|
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
789
1068
|
import { chmod, link, mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
@@ -904,7 +1183,9 @@ function readDaemonToken() {
|
|
|
904
1183
|
}
|
|
905
1184
|
|
|
906
1185
|
export {
|
|
1186
|
+
ToolSchema,
|
|
907
1187
|
SessionSchema,
|
|
1188
|
+
backendOf,
|
|
908
1189
|
isArchived,
|
|
909
1190
|
MAX_PROPOSALS,
|
|
910
1191
|
proposalRows,
|
|
@@ -921,7 +1202,10 @@ export {
|
|
|
921
1202
|
ModelIdSchema,
|
|
922
1203
|
EffortSchema,
|
|
923
1204
|
RouteResultSchema,
|
|
1205
|
+
DEFAULT_RELAY_URL,
|
|
1206
|
+
MobileConfigSchema,
|
|
924
1207
|
PROTOCOL_PORT,
|
|
1208
|
+
RefusalCodeSchema,
|
|
925
1209
|
RecentSchema,
|
|
926
1210
|
MESSAGE_MAX,
|
|
927
1211
|
SUMMARY_MAX,
|
|
@@ -932,8 +1216,25 @@ export {
|
|
|
932
1216
|
parseProposalBlock,
|
|
933
1217
|
parseRelayBlock,
|
|
934
1218
|
parseProjectBlock,
|
|
1219
|
+
TOPIC_MAX,
|
|
935
1220
|
cleanTopic,
|
|
936
1221
|
sessionTitle,
|
|
1222
|
+
RelayServerMessageSchema,
|
|
1223
|
+
PAIR_LABEL_MAX,
|
|
1224
|
+
SealedFrameSchema,
|
|
1225
|
+
RELAY_CLOSE_SIGNED_OUT,
|
|
1226
|
+
DeviceStartResponseSchema,
|
|
1227
|
+
DevicePollResponseSchema,
|
|
1228
|
+
PHONE_PROTOCOL,
|
|
1229
|
+
padPlain,
|
|
1230
|
+
THREAD_ITEMS_MAX,
|
|
1231
|
+
THREAD_BUDGET,
|
|
1232
|
+
THREAD_TEXT_MAX,
|
|
1233
|
+
TOOL_TEXT_MAX,
|
|
1234
|
+
PENDING_BODY_MAX,
|
|
1235
|
+
SNAPSHOT_BODY_MAX,
|
|
1236
|
+
UNTYPABLE,
|
|
1237
|
+
PhoneClientMessageSchema,
|
|
937
1238
|
hostAllowed,
|
|
938
1239
|
requestAuthorized,
|
|
939
1240
|
selectDaemonProtocol,
|