@paradow/ledge 0.41.0 → 0.42.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/index.js +166 -39
- package/dist/cli/reduceWorker.js +46 -9
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -365,11 +365,15 @@ function pendingKey(p) {
|
|
|
365
365
|
const s = JSON.stringify([p.kind, p.text, p.body ?? null, p.tool ?? null, p.options ?? null, p.optionDetails ?? null, p.helperId ?? null]);
|
|
366
366
|
return cyrb53(s, 0) + cyrb53(s, 1);
|
|
367
367
|
}
|
|
368
|
-
var DecisionSchema, NOTE_MAX, NOTE_NOT_TYPED, DECISION_OFFERS;
|
|
368
|
+
var DecisionSchema, DecidedBySchema, NOTE_MAX, NOTE_NOT_TYPED, DECISION_OFFERS;
|
|
369
369
|
var init_decisions = __esm({
|
|
370
370
|
"../protocol/src/decisions.ts"() {
|
|
371
371
|
"use strict";
|
|
372
372
|
DecisionSchema = z6.enum(["accept", "deny", "always"]);
|
|
373
|
+
DecidedBySchema = z6.discriminatedUnion("kind", [
|
|
374
|
+
z6.object({ kind: z6.literal("pc") }),
|
|
375
|
+
z6.object({ kind: z6.literal("device"), id: z6.string().min(1).max(100), name: z6.string().min(1).max(60) })
|
|
376
|
+
]);
|
|
373
377
|
NOTE_MAX = 4e3;
|
|
374
378
|
NOTE_NOT_TYPED = "note_not_typed";
|
|
375
379
|
DECISION_OFFERS = {
|
|
@@ -2056,6 +2060,7 @@ var init_thread = __esm({
|
|
|
2056
2060
|
init_tasks();
|
|
2057
2061
|
init_attachments();
|
|
2058
2062
|
init_rigMessages();
|
|
2063
|
+
init_decisions();
|
|
2059
2064
|
ToolStatusSchema = z22.enum(["running", "success", "failed", "unknown"]);
|
|
2060
2065
|
FileChangeSchema = z22.object({ path: z22.string().min(1), from: z22.string().min(1).optional(), change: z22.enum(["add", "edit", "delete", "write"]), diff: z22.string().optional() });
|
|
2061
2066
|
base = { id: z22.string().min(1), at: z22.string().datetime(), turn: z22.number().int().nonnegative(), helperId: z22.string().optional() };
|
|
@@ -2081,8 +2086,8 @@ var init_thread = __esm({
|
|
|
2081
2086
|
imagesDropped: z22.literal(true).optional()
|
|
2082
2087
|
}),
|
|
2083
2088
|
/** `unknown`: answered in the terminal, the daemon only saw the prompt go away; `always`: allowed and not asked again;
|
|
2084
|
-
* `byClose`: denied by Ledge closing the session. */
|
|
2085
|
-
z22.object({ kind: z22.literal("permission"), ...base, tool: z22.string(), text: z22.string(), decision: z22.enum(["accept", "deny", "always", "unknown"]).optional(), byClose: z22.literal(true).optional() }),
|
|
2089
|
+
* `byClose`: denied by Ledge closing the session; `by`: who answered it from Ledge (the PC or a device, its name then). */
|
|
2090
|
+
z22.object({ kind: z22.literal("permission"), ...base, tool: z22.string(), text: z22.string(), decision: z22.enum(["accept", "deny", "always", "unknown"]).optional(), byClose: z22.literal(true).optional(), by: DecidedBySchema.optional() }),
|
|
2086
2091
|
z22.object({ kind: z22.literal("reply"), ...base, text: z22.string() }),
|
|
2087
2092
|
z22.object({ kind: z22.literal("helper"), ...base, task: z22.string(), agentType: z22.string().optional(), state: z22.enum(["working", "exited"]) }),
|
|
2088
2093
|
/** 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. */
|
|
@@ -2141,7 +2146,10 @@ var init_thread = __esm({
|
|
|
2141
2146
|
|
|
2142
2147
|
// ../protocol/src/mobile.ts
|
|
2143
2148
|
import { z as z23 } from "zod";
|
|
2144
|
-
|
|
2149
|
+
function deviceKindOfLabel(label2) {
|
|
2150
|
+
return /\b(iPad|tablet|tablette)\b/i.test(label2) ? "tablet" : "phone";
|
|
2151
|
+
}
|
|
2152
|
+
var DEFAULT_RELAY_URL, RelayUrlSchema, MobileConfigSchema, MobileLinkStateSchema, DeviceKindSchema, DEVICE_NAME_MAX, DeviceNameSchema, MobilePairingSchema, MobileStateSchema;
|
|
2145
2153
|
var init_mobile = __esm({
|
|
2146
2154
|
"../protocol/src/mobile.ts"() {
|
|
2147
2155
|
"use strict";
|
|
@@ -2172,6 +2180,9 @@ var init_mobile = __esm({
|
|
|
2172
2180
|
/** The mobile service did not start (a broken mobile.json, a missing key store): nothing is linked until the daemon restarts. */
|
|
2173
2181
|
z23.object({ state: z23.literal("unavailable"), error: z23.string().max(200) })
|
|
2174
2182
|
]);
|
|
2183
|
+
DeviceKindSchema = z23.enum(["phone", "tablet"]);
|
|
2184
|
+
DEVICE_NAME_MAX = 40;
|
|
2185
|
+
DeviceNameSchema = z23.string().trim().min(1).max(DEVICE_NAME_MAX).regex(/^[^\u0000-\u001f\u007f]+$/);
|
|
2175
2186
|
MobilePairingSchema = z23.object({
|
|
2176
2187
|
id: z23.string(),
|
|
2177
2188
|
label: z23.string(),
|
|
@@ -2187,7 +2198,16 @@ var init_mobile = __esm({
|
|
|
2187
2198
|
/** The last sign-in that did not end with a session; cleared by the next one. */
|
|
2188
2199
|
signinError: z23.enum(["expired", "denied", "not_allowed", "failed"]).nullable(),
|
|
2189
2200
|
link: MobileLinkStateSchema,
|
|
2190
|
-
devices: z23.array(z23.object({
|
|
2201
|
+
devices: z23.array(z23.object({
|
|
2202
|
+
id: z23.string(),
|
|
2203
|
+
label: z23.string(),
|
|
2204
|
+
approvedAt: z23.string(),
|
|
2205
|
+
online: z23.boolean(),
|
|
2206
|
+
lastSeenAt: z23.string().optional(),
|
|
2207
|
+
/** Absent from an older daemon; `name` absent: the user gave none, the front derives one from the label. */
|
|
2208
|
+
kind: DeviceKindSchema.optional(),
|
|
2209
|
+
name: z23.string().optional()
|
|
2210
|
+
})),
|
|
2191
2211
|
pairings: z23.array(MobilePairingSchema),
|
|
2192
2212
|
/** « Taper dans les terminaux depuis le téléphone »: absent from an older daemon, which has no phone terminal. */
|
|
2193
2213
|
terminalInput: z23.boolean().optional()
|
|
@@ -2279,6 +2299,8 @@ var init_messages = __esm({
|
|
|
2279
2299
|
decision: DecisionSchema.optional(),
|
|
2280
2300
|
/** Decided: the request was refused by Ledge closing the session (its interrupt keys), not by the user's answer. */
|
|
2281
2301
|
byClose: z24.literal(true).optional(),
|
|
2302
|
+
/** Decided: who answered, when the daemon typed it (absent: answered in the terminal, or an older daemon). */
|
|
2303
|
+
by: DecidedBySchema.optional(),
|
|
2282
2304
|
/** The agent's own session id when the hook names it, so the daemon can resume that conversation. */
|
|
2283
2305
|
agentSessionId: z24.string().optional()
|
|
2284
2306
|
});
|
|
@@ -2555,7 +2577,9 @@ var init_messages = __esm({
|
|
|
2555
2577
|
note: z24.string().trim().min(1).max(NOTE_MAX).optional(),
|
|
2556
2578
|
/** 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.
|
|
2557
2579
|
* `key` (pendingKey) covers its whole content; older fronts send kind and text only. */
|
|
2558
|
-
expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z24.string().max(64).optional() }).optional()
|
|
2580
|
+
expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z24.string().max(64).optional() }).optional(),
|
|
2581
|
+
/** Who decides: set by the phone gateway for a device (over anything the device sent); left out by a local front, which means the PC. */
|
|
2582
|
+
by: DecidedBySchema.optional()
|
|
2559
2583
|
}),
|
|
2560
2584
|
z24.object({ type: z24.literal("session.focus"), id: z24.string() }),
|
|
2561
2585
|
z24.object({ type: z24.literal("session.kill"), id: z24.string() }),
|
|
@@ -2609,10 +2633,13 @@ var init_messages = __esm({
|
|
|
2609
2633
|
z24.object({ type: z24.literal("mobile.signout") }),
|
|
2610
2634
|
/** While the link is offline: try again now. */
|
|
2611
2635
|
z24.object({ type: z24.literal("mobile.retry") }),
|
|
2612
|
-
|
|
2636
|
+
/** `replaces`: an approved device this one takes the place of (the same app reinstalled), revoked once this one is stored. */
|
|
2637
|
+
z24.object({ type: z24.literal("mobile.pairing.approve"), id: z24.string().min(1).max(100), replaces: z24.string().min(1).max(100).optional() }),
|
|
2613
2638
|
z24.object({ type: z24.literal("mobile.pairing.deny"), id: z24.string().min(1).max(100) }),
|
|
2614
2639
|
/** `id`: the device's fingerprint. */
|
|
2615
2640
|
z24.object({ type: z24.literal("mobile.device.revoke"), id: z24.string().min(1).max(100) }),
|
|
2641
|
+
/** Names a device (`id`: its fingerprint); the device sees its new name in its next snapshot. */
|
|
2642
|
+
z24.object({ type: z24.literal("mobile.device.rename"), id: z24.string().min(1).max(100), name: DeviceNameSchema }),
|
|
2616
2643
|
/** « Taper dans les terminaux depuis le téléphone »; turned off, every phone loses the hand. */
|
|
2617
2644
|
z24.object({ type: z24.literal("mobile.terminal"), input: z24.boolean() }),
|
|
2618
2645
|
/** « Reprendre la main »: the phone typing into the session's terminal goes back to reading it. */
|
|
@@ -2869,7 +2896,9 @@ var init_push = __esm({
|
|
|
2869
2896
|
});
|
|
2870
2897
|
PushPayloadSchema = z27.object({
|
|
2871
2898
|
v: z27.literal(1),
|
|
2872
|
-
|
|
2899
|
+
/** `resolved`: a request or question answered (anywhere), replacing its notification under the same tag; only sent to a
|
|
2900
|
+
* device whose subscription says its service worker knows it (`push.subscribe` `resolved`). */
|
|
2901
|
+
kind: z27.enum(["request", "question", "finished", "chat", "decision", "resolved"]),
|
|
2873
2902
|
title: z27.string().max(80),
|
|
2874
2903
|
body: z27.string().max(120),
|
|
2875
2904
|
url: z27.string().regex(/^\/#\/[A-Za-z0-9/%._~-]*$/).max(300),
|
|
@@ -2902,6 +2931,7 @@ var init_phone = __esm({
|
|
|
2902
2931
|
init_messages();
|
|
2903
2932
|
init_broadcast();
|
|
2904
2933
|
init_push();
|
|
2934
|
+
init_mobile();
|
|
2905
2935
|
init_models();
|
|
2906
2936
|
init_commands();
|
|
2907
2937
|
init_openrig();
|
|
@@ -3173,8 +3203,11 @@ var init_phone = __esm({
|
|
|
3173
3203
|
z28.object({ type: z28.literal("openrig.row"), qitem: QitemIdSchema }),
|
|
3174
3204
|
/** The phone drops its passkey from the daemon. */
|
|
3175
3205
|
z28.object({ type: z28.literal("device.forget"), op: OperationIdSchema }),
|
|
3176
|
-
/** The
|
|
3177
|
-
z28.object({ type: z28.literal("
|
|
3206
|
+
/** The device names itself (on the PC and in its next snapshot); applied to the sender only. */
|
|
3207
|
+
z28.object({ type: z28.literal("device.rename"), op: OperationIdSchema, name: DeviceNameSchema }),
|
|
3208
|
+
/** The phone's push subscription, with the VAPID key it was made with (so a key change is seen). `resolved`: its service
|
|
3209
|
+
* worker shows a `resolved` push, which replaces a request's notification once it is answered elsewhere. */
|
|
3210
|
+
z28.object({ type: z28.literal("push.subscribe"), op: OperationIdSchema, subscription: PushSubscriptionSchema, key: B64U2.length(87), resolved: z28.literal(true).optional() }),
|
|
3178
3211
|
z28.object({ type: z28.literal("push.unsubscribe"), op: OperationIdSchema }),
|
|
3179
3212
|
/** The phone's own chat with Ledge. */
|
|
3180
3213
|
z28.object({ type: z28.literal("chat.ask"), qid: OperationIdSchema, text: z28.string().trim().max(MESSAGE_MAX), attachments: z28.array(AttachmentIdSchema).max(ATTACHMENTS_MAX).optional() }),
|
|
@@ -3266,7 +3299,16 @@ var init_phone = __esm({
|
|
|
3266
3299
|
openrig: z28.literal(true).optional(),
|
|
3267
3300
|
/** The daemon takes `session.broadcast` (« Écrire au groupe »). */
|
|
3268
3301
|
broadcast: z28.literal(true).optional()
|
|
3269
|
-
}), device: z28.object({
|
|
3302
|
+
}), device: z28.object({
|
|
3303
|
+
confirm: z28.boolean(),
|
|
3304
|
+
push: z28.enum(["on", "off", "stale", "unavailable"]).optional(),
|
|
3305
|
+
chat: z28.literal(true).optional(),
|
|
3306
|
+
contextNear: ContextPrefsSchema.shape.near.optional(),
|
|
3307
|
+
/** This device's id (its fingerprint, as decisions name it in `by`), its kind, and the name the user gave it (absent: none given). */
|
|
3308
|
+
id: z28.string().optional(),
|
|
3309
|
+
kind: DeviceKindSchema.optional(),
|
|
3310
|
+
name: z28.string().optional()
|
|
3311
|
+
}).optional() }),
|
|
3270
3312
|
/** « À toi » on this PC: the answer to `openrig.asks`, then each change, whole. */
|
|
3271
3313
|
z28.object({ type: z28.literal("openrig.asks"), channel: HumanChannelSchema, asks: z28.array(HumanAskSchema), partial: z28.literal(true).optional(), truncated: z28.literal(true).optional() }),
|
|
3272
3314
|
/** An answer's end, as on the desktop (`openrig.answered`), under the phone's `op`. `rejected`: the device check refused it (`reason`). */
|
|
@@ -6563,7 +6605,7 @@ var init_history = __esm({
|
|
|
6563
6605
|
return q ? this.put(id, { ...q, status: "unknown", byClose: true }) : void 0;
|
|
6564
6606
|
}
|
|
6565
6607
|
const perm = [...this.items(id)].reverse().find((i) => i.kind === "permission" && i.decision === void 0);
|
|
6566
|
-
return perm && perm.kind === "permission" ? this.put(id, { ...perm, decision: ev.decision, ...ev.byClose ? { byClose: true } : {} }) : void 0;
|
|
6608
|
+
return perm && perm.kind === "permission" ? this.put(id, { ...perm, decision: ev.decision, ...ev.byClose ? { byClose: true } : {}, ...ev.by ? { by: ev.by } : {} }) : void 0;
|
|
6567
6609
|
}
|
|
6568
6610
|
case "AgentMessage": {
|
|
6569
6611
|
if (!ev.messageId || !ev.text) return void 0;
|
|
@@ -9936,7 +9978,7 @@ var LEDGE_VERSION;
|
|
|
9936
9978
|
var init_version = __esm({
|
|
9937
9979
|
"src/version.ts"() {
|
|
9938
9980
|
"use strict";
|
|
9939
|
-
LEDGE_VERSION = true ? "0.
|
|
9981
|
+
LEDGE_VERSION = true ? "0.42.0" : "0.0.0-dev";
|
|
9940
9982
|
}
|
|
9941
9983
|
});
|
|
9942
9984
|
|
|
@@ -15604,7 +15646,7 @@ function toPhoneItem(item) {
|
|
|
15604
15646
|
break;
|
|
15605
15647
|
}
|
|
15606
15648
|
case "permission":
|
|
15607
|
-
out = { kind: "permission", ...base3, tool: item.tool, text: text7(item.text), ...pickSet(item, ["decision"]) };
|
|
15649
|
+
out = { kind: "permission", ...base3, tool: item.tool, text: text7(item.text), ...pickSet(item, ["decision", "by"]) };
|
|
15608
15650
|
break;
|
|
15609
15651
|
case "helper":
|
|
15610
15652
|
out = { kind: "helper", ...base3, task: text7(item.task), state: item.state, ...pickSet(item, ["agentType"]) };
|
|
@@ -15962,6 +16004,15 @@ function createPhoneGateway(deps) {
|
|
|
15962
16004
|
if (!p) return "off";
|
|
15963
16005
|
return key !== void 0 && p.key !== key ? "stale" : "on";
|
|
15964
16006
|
};
|
|
16007
|
+
const ownInfo = (deviceId) => {
|
|
16008
|
+
const d = mobile.deviceInfo?.(deviceId);
|
|
16009
|
+
return d ? { kind: d.kind, ...d.name ? { name: d.name } : {} } : {};
|
|
16010
|
+
};
|
|
16011
|
+
const decidedBy = (deviceId) => {
|
|
16012
|
+
const d = mobile.deviceInfo?.(deviceId);
|
|
16013
|
+
const name = (d?.name ?? d?.label ?? "").trim();
|
|
16014
|
+
return { kind: "device", id: deviceId, name: name ? clip(name, 60).text : "Appareil" };
|
|
16015
|
+
};
|
|
15965
16016
|
const snapshot = (deviceId) => {
|
|
15966
16017
|
const push3 = pushStatus(deviceId);
|
|
15967
16018
|
const f = followers.get(deviceId);
|
|
@@ -15986,7 +16037,7 @@ function createPhoneGateway(deps) {
|
|
|
15986
16037
|
...deps.openrig ? { openrig: true } : {},
|
|
15987
16038
|
broadcast: true
|
|
15988
16039
|
},
|
|
15989
|
-
device: { confirm: deps.credential(deviceId) !== null, push: push3, ...deps.chats ? { chat: true } : {}, ...deps.contextNear ? { contextNear: deps.contextNear() } : {} }
|
|
16040
|
+
device: { confirm: deps.credential(deviceId) !== null, push: push3, ...deps.chats ? { chat: true } : {}, ...deps.contextNear ? { contextNear: deps.contextNear() } : {}, id: deviceId, ...ownInfo(deviceId) }
|
|
15990
16041
|
};
|
|
15991
16042
|
return deps.models ? { ...message, daemon: { ...message.daemon, models: true } } : message;
|
|
15992
16043
|
};
|
|
@@ -16116,10 +16167,10 @@ function createPhoneGateway(deps) {
|
|
|
16116
16167
|
return { status: "rejected", reason: s.state === "exited" ? "ended" : "not_launched" };
|
|
16117
16168
|
}
|
|
16118
16169
|
};
|
|
16119
|
-
const local = (msg) => {
|
|
16170
|
+
const local = (deviceId, msg) => {
|
|
16120
16171
|
switch (msg.type) {
|
|
16121
16172
|
case "session.decide":
|
|
16122
|
-
return { type: "session.decide", id: msg.id, decision: msg.decision, ...msg.note !== void 0 ? { note: msg.note } : {}, expect: { kind: msg.expect.kind, text: msg.expect.text, key: msg.expect.key } };
|
|
16173
|
+
return { type: "session.decide", id: msg.id, decision: msg.decision, ...msg.note !== void 0 ? { note: msg.note } : {}, expect: { kind: msg.expect.kind, text: msg.expect.text, key: msg.expect.key }, by: decidedBy(deviceId) };
|
|
16123
16174
|
case "session.reply":
|
|
16124
16175
|
return { type: "session.reply", id: msg.id, text: msg.text, ...msg.attachments?.length ? { attachments: msg.attachments } : {} };
|
|
16125
16176
|
case "session.answer":
|
|
@@ -16495,9 +16546,14 @@ function createPhoneGateway(deps) {
|
|
|
16495
16546
|
}
|
|
16496
16547
|
if (msg.type === "credential.register") return register(deviceId, msg);
|
|
16497
16548
|
if (msg.type === "device.forget") return { status: "applied" };
|
|
16549
|
+
if (msg.type === "device.rename") {
|
|
16550
|
+
if (!mobile.rename) return { status: "rejected", reason: "unavailable" };
|
|
16551
|
+
mobile.rename(deviceId, msg.name);
|
|
16552
|
+
return { status: "applied" };
|
|
16553
|
+
}
|
|
16498
16554
|
if (msg.type === "push.subscribe") {
|
|
16499
16555
|
if (!pushEndpointAllowed(msg.subscription.endpoint)) return { status: "rejected", reason: "invalid" };
|
|
16500
|
-
mobile.setPush(deviceId, { endpoint: msg.subscription.endpoint, p256dh: msg.subscription.keys.p256dh, auth: msg.subscription.keys.auth, key: msg.key });
|
|
16556
|
+
mobile.setPush(deviceId, { endpoint: msg.subscription.endpoint, p256dh: msg.subscription.keys.p256dh, auth: msg.subscription.keys.auth, key: msg.key, ...msg.resolved ? { resolved: true } : {} });
|
|
16501
16557
|
return { status: "applied" };
|
|
16502
16558
|
}
|
|
16503
16559
|
if (msg.type === "push.unsubscribe") {
|
|
@@ -16516,7 +16572,7 @@ function createPhoneGateway(deps) {
|
|
|
16516
16572
|
if (msg.type === "openrig.answer") return rigAnswer(deviceId, msg);
|
|
16517
16573
|
const refused = precheck(deviceId, msg);
|
|
16518
16574
|
if (refused) return refused;
|
|
16519
|
-
const out = local(msg);
|
|
16575
|
+
const out = local(deviceId, msg);
|
|
16520
16576
|
if (out.type === "session.model") return modelChange(out);
|
|
16521
16577
|
let error;
|
|
16522
16578
|
await deps.dispatch(out, (m) => {
|
|
@@ -16542,7 +16598,7 @@ function createPhoneGateway(deps) {
|
|
|
16542
16598
|
}
|
|
16543
16599
|
registry.finish(msg.op, outcome.status, outcome.reason, outcome.how, outcome.report, outcome.detail);
|
|
16544
16600
|
result(deviceId, msg.op, outcome.status, outcome.reason, outcome.how, outcome.report);
|
|
16545
|
-
if (msg.type === "credential.register" && outcome.status === "applied" && approved(deviceId)) push2(deviceId, snapshot(deviceId), "snapshot");
|
|
16601
|
+
if ((msg.type === "credential.register" || msg.type === "device.rename") && outcome.status === "applied" && approved(deviceId)) push2(deviceId, snapshot(deviceId), "snapshot");
|
|
16546
16602
|
if (msg.type === "device.forget" && outcome.status === "applied") {
|
|
16547
16603
|
let timer2;
|
|
16548
16604
|
await Promise.race([outbox.flush(deviceId), new Promise((r) => {
|
|
@@ -17038,6 +17094,12 @@ function pushFor(before2, after2, all2) {
|
|
|
17038
17094
|
}
|
|
17039
17095
|
return null;
|
|
17040
17096
|
}
|
|
17097
|
+
function resolvedFor(before2, after2, all2, by) {
|
|
17098
|
+
if (!before2?.pending || after2.pending) return null;
|
|
17099
|
+
const badge = Math.min(999, all2.filter((s) => s.pending).length);
|
|
17100
|
+
const body = !by ? "" : by.kind === "pc" ? "sur le PC" : cut(`sur ${by.name}`, BODY_MAX2);
|
|
17101
|
+
return { v: 1, kind: "resolved", title: cut(`${after2.name} \xB7 trait\xE9e`, TITLE_MAX), body, url: `/#/thread/${encodeURIComponent(after2.id)}`, badge, tag: safeTag(after2.id) };
|
|
17102
|
+
}
|
|
17041
17103
|
function chatEndPayload(code3, badge) {
|
|
17042
17104
|
return {
|
|
17043
17105
|
v: 1,
|
|
@@ -17094,6 +17156,15 @@ function createNotifier(deps) {
|
|
|
17094
17156
|
const { mobile, log: log2 } = deps;
|
|
17095
17157
|
const now2 = deps.now ?? Date.now;
|
|
17096
17158
|
const lastFinished = /* @__PURE__ */ new Map();
|
|
17159
|
+
const told = /* @__PURE__ */ new Map();
|
|
17160
|
+
const tell = (id, tag) => {
|
|
17161
|
+
let s = told.get(id);
|
|
17162
|
+
if (!s) {
|
|
17163
|
+
s = /* @__PURE__ */ new Set();
|
|
17164
|
+
told.set(id, s);
|
|
17165
|
+
}
|
|
17166
|
+
s.add(tag);
|
|
17167
|
+
};
|
|
17097
17168
|
return {
|
|
17098
17169
|
async notify(payload, sessionId2, only) {
|
|
17099
17170
|
if (!mobile.state().enabled) return;
|
|
@@ -17118,6 +17189,7 @@ function createNotifier(deps) {
|
|
|
17118
17189
|
if (only !== void 0 && d.id !== only) return [];
|
|
17119
17190
|
const sub = mobile.pushOf(d.id);
|
|
17120
17191
|
if (!sub || relayKey !== void 0 && sub.key !== relayKey || deps.presence(d.id)) return [];
|
|
17192
|
+
if (p.kind === "resolved" && (!sub.resolved || !told.get(d.id)?.has(p.tag))) return [];
|
|
17121
17193
|
return [{ id: d.id, online: d.online, sub }];
|
|
17122
17194
|
});
|
|
17123
17195
|
if (targets.length === 0) return;
|
|
@@ -17128,10 +17200,12 @@ function createNotifier(deps) {
|
|
|
17128
17200
|
endpoint: sub.endpoint,
|
|
17129
17201
|
body: Buffer.from(encryptPush(plain4, { p256dh: sub.p256dh, auth: sub.auth }, { padTo: PAD_TO })).toString("base64url"),
|
|
17130
17202
|
ttl: TTL_S,
|
|
17131
|
-
urgency: p.kind === "finished" || p.kind === "chat" ? "normal" : "high",
|
|
17203
|
+
urgency: p.kind === "finished" || p.kind === "chat" || p.kind === "resolved" ? "normal" : "high",
|
|
17132
17204
|
topic: p.tag
|
|
17133
17205
|
};
|
|
17134
17206
|
const r = await mobile.sendPush(msg);
|
|
17207
|
+
if (p.kind === "resolved") told.get(id)?.delete(p.tag);
|
|
17208
|
+
else if ((p.kind === "request" || p.kind === "question") && r.result === "sent") tell(id, p.tag);
|
|
17135
17209
|
if (r.result === "gone") {
|
|
17136
17210
|
log2(`push: subscription of device ${id} is gone, removed`);
|
|
17137
17211
|
mobile.setPush(id, null);
|
|
@@ -17437,7 +17511,7 @@ async function startServer(opts) {
|
|
|
17437
17511
|
}
|
|
17438
17512
|
});
|
|
17439
17513
|
const terminalHolders = /* @__PURE__ */ new Map();
|
|
17440
|
-
const holdersMsg = () => ({ type: "terminal.holders", holds: [...terminalHolders].map(([id, deviceId]) => ({ id, device: mobile?.state().devices.find((d) => d.id === deviceId)
|
|
17514
|
+
const holdersMsg = () => ({ type: "terminal.holders", holds: [...terminalHolders].map(([id, deviceId]) => ({ id, device: ((d) => d?.name ?? d?.label)(mobile?.state().devices.find((d) => d.id === deviceId)) ?? deviceId })) });
|
|
17441
17515
|
const typing = /* @__PURE__ */ new Set();
|
|
17442
17516
|
const closing = /* @__PURE__ */ new Set();
|
|
17443
17517
|
const cardThreads = opts.chat ? {
|
|
@@ -17586,9 +17660,9 @@ async function startServer(opts) {
|
|
|
17586
17660
|
if (!a || a.settled || !answeredBy(a.pending, ev) || !cur || pendingKey(cur) !== pendingKey(a.pending)) return;
|
|
17587
17661
|
a.settled = true;
|
|
17588
17662
|
a.byHook = true;
|
|
17589
|
-
store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...a.decision ? { decision: a.decision } : {} });
|
|
17663
|
+
store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...a.decision ? { decision: a.decision } : {}, ...a.by ? { by: a.by } : {} });
|
|
17590
17664
|
}
|
|
17591
|
-
async function injectAndVerify(id, planFor, requirePrompt, decision) {
|
|
17665
|
+
async function injectAndVerify(id, planFor, requirePrompt, decision, by) {
|
|
17592
17666
|
const s = store.get(id);
|
|
17593
17667
|
if (!s) throw new Refusal(`unknown session ${id}`, "unknown_session");
|
|
17594
17668
|
if (inFlight.has(id)) throw new Refusal("a decision is already in progress", "busy");
|
|
@@ -17614,7 +17688,7 @@ async function startServer(opts) {
|
|
|
17614
17688
|
const plan2 = typeof planFor === "function" ? planFor(before2 ?? "") : planFor;
|
|
17615
17689
|
const stillShown = (p) => decided && adapter.promptGone ? !adapter.promptGone(p, decided) : adapter.promptStillVisible(bottomLines(p));
|
|
17616
17690
|
const tool = s.tool === "claude" && decided?.source === "hook" ? pendingTool(decided) : void 0;
|
|
17617
|
-
const answer = tool ? { pending: decided, ...decision ? { decision } : {}, settled: false, byHook: false } : void 0;
|
|
17691
|
+
const answer = tool ? { pending: decided, ...decision ? { decision } : {}, ...by ? { by } : {}, settled: false, byHook: false } : void 0;
|
|
17618
17692
|
const transcript = tool && !decided.helperId && s.transcriptPath ? s.transcriptPath : void 0;
|
|
17619
17693
|
const call = transcript ? claudeOpenCall(transcript, tool) : void 0;
|
|
17620
17694
|
const promptGone3 = async () => {
|
|
@@ -17635,7 +17709,7 @@ async function startServer(opts) {
|
|
|
17635
17709
|
if (e instanceof NoteNotTyped) {
|
|
17636
17710
|
if (await promptGone3()) {
|
|
17637
17711
|
if (answer) answer.settled = true;
|
|
17638
|
-
if (!answer?.byHook && stillTheSame()) store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...decision ? { decision } : {} });
|
|
17712
|
+
if (!answer?.byHook && stillTheSame()) store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...decision ? { decision } : {}, ...by ? { by } : {} });
|
|
17639
17713
|
throw e;
|
|
17640
17714
|
}
|
|
17641
17715
|
e = new Refusal(NOT_TAKEN, "not_taken");
|
|
@@ -17649,7 +17723,7 @@ async function startServer(opts) {
|
|
|
17649
17723
|
if (answer?.byHook) return true;
|
|
17650
17724
|
if (!stillTheSame()) return false;
|
|
17651
17725
|
if (gone) {
|
|
17652
|
-
store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...decision ? { decision } : {} });
|
|
17726
|
+
store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...decision ? { decision } : {}, ...by ? { by } : {} });
|
|
17653
17727
|
return true;
|
|
17654
17728
|
}
|
|
17655
17729
|
store.apply(id, { name: "InjectFailed", at: now2(), source: "daemon", text: NOT_TAKEN });
|
|
@@ -18344,7 +18418,7 @@ async function startServer(opts) {
|
|
|
18344
18418
|
if (msg.decision === "always" && !offers(s.tool, pending.kind, "always")) return refuse(new Refusal(NOT_OFFERED, "not_offered"), msg.id);
|
|
18345
18419
|
if (msg.note !== void 0 && (msg.decision !== "deny" || !offers(s.tool, pending.kind, "note"))) return refuse(new Refusal(NOT_OFFERED, "not_offered"), msg.id);
|
|
18346
18420
|
const { decision, note } = msg;
|
|
18347
|
-
await injectAndVerify(msg.id, (pane) => adapter.keysFor(decision, pending, pane, note), true, decision);
|
|
18421
|
+
await injectAndVerify(msg.id, (pane) => adapter.keysFor(decision, pending, pane, note), true, decision, msg.by ?? { kind: "pc" });
|
|
18348
18422
|
return;
|
|
18349
18423
|
}
|
|
18350
18424
|
case "session.reply": {
|
|
@@ -18680,10 +18754,22 @@ async function startServer(opts) {
|
|
|
18680
18754
|
const badge = store.all().filter((s) => s.pending).length + tasks.all().filter(openDecision).length;
|
|
18681
18755
|
void notifier.notify(decisionPayload(task, badge), void 0).catch((e) => log(`push failed (${e instanceof Error ? e.name : "error"})`));
|
|
18682
18756
|
};
|
|
18683
|
-
|
|
18757
|
+
const answeredBy2 = /* @__PURE__ */ new Map();
|
|
18758
|
+
const unsubscribeDecided = store.onEvent((id, ev) => {
|
|
18759
|
+
if (ev.name === "Decided") answeredBy2.set(id, ev.by ?? null);
|
|
18760
|
+
});
|
|
18761
|
+
const unsubscribeApply = store.onApply((before2, after2) => {
|
|
18762
|
+
const failed = (e) => log(`push failed (${e instanceof Error ? e.name : "error"})`);
|
|
18763
|
+
const r = resolvedFor(before2, after2, store.all(), answeredBy2.get(after2.id));
|
|
18764
|
+
if (before2?.pending && !after2.pending) answeredBy2.delete(after2.id);
|
|
18765
|
+
if (r) void notifier.notify(r, after2.id).catch(failed);
|
|
18684
18766
|
const p = pushFor(before2, after2, store.all());
|
|
18685
|
-
if (p) void notifier.notify(p, after2.id).catch(
|
|
18767
|
+
if (p) void notifier.notify(p, after2.id).catch(failed);
|
|
18686
18768
|
});
|
|
18769
|
+
unsubscribePush = () => {
|
|
18770
|
+
unsubscribeDecided();
|
|
18771
|
+
unsubscribeApply();
|
|
18772
|
+
};
|
|
18687
18773
|
}
|
|
18688
18774
|
wss.on("connection", (ws, req) => {
|
|
18689
18775
|
ws.on("error", (e) => log(`socket error: ${e.message}`));
|
|
@@ -19810,7 +19896,7 @@ function readRevocations() {
|
|
|
19810
19896
|
function writeRevocations(list3) {
|
|
19811
19897
|
writePrivate(paths().revocations, JSON.stringify(z31.array(RevocationSchema).parse(list3), null, 2) + "\n");
|
|
19812
19898
|
}
|
|
19813
|
-
var RelaySessionSchema, DeviceCredentialSchema, ApprovedDeviceSchema, DEFAULT_CONFIG, LEGACY_RELAY_URL, RevocationSchema;
|
|
19899
|
+
var RelaySessionSchema, DeviceCredentialSchema, MAX_DEVICE_PEERS, ApprovedDeviceSchema, DEFAULT_CONFIG, LEGACY_RELAY_URL, RevocationSchema;
|
|
19814
19900
|
var init_files2 = __esm({
|
|
19815
19901
|
"src/mobile/files.ts"() {
|
|
19816
19902
|
"use strict";
|
|
@@ -19827,6 +19913,7 @@ var init_files2 = __esm({
|
|
|
19827
19913
|
rpId: z31.string().min(1),
|
|
19828
19914
|
transports: z31.array(z31.string()).optional()
|
|
19829
19915
|
});
|
|
19916
|
+
MAX_DEVICE_PEERS = 8;
|
|
19830
19917
|
ApprovedDeviceSchema = z31.object({
|
|
19831
19918
|
id: z31.string().min(1),
|
|
19832
19919
|
publicKey: z31.string().min(1),
|
|
@@ -19835,8 +19922,14 @@ var init_files2 = __esm({
|
|
|
19835
19922
|
lastPeer: z31.string().optional(),
|
|
19836
19923
|
lastSeenAt: z31.string().optional(),
|
|
19837
19924
|
credential: DeviceCredentialSchema.optional(),
|
|
19838
|
-
/** The phone's push subscription and the relay's VAPID key it was made with. */
|
|
19839
|
-
push: z31.object({ endpoint: z31.string().min(1), p256dh: z31.string().min(1), auth: z31.string().min(1), key: z31.string().min(1) }).optional()
|
|
19925
|
+
/** The phone's push subscription and the relay's VAPID key it was made with; `resolved`: its service worker shows a `resolved` push. */
|
|
19926
|
+
push: z31.object({ endpoint: z31.string().min(1), p256dh: z31.string().min(1), auth: z31.string().min(1), key: z31.string().min(1), resolved: z31.literal(true).optional() }).optional(),
|
|
19927
|
+
/** Phone or tablet, from the pairing label; absent on a device approved before it was kept. */
|
|
19928
|
+
kind: DeviceKindSchema.optional(),
|
|
19929
|
+
/** The name the user gave it; absent means the default name the fronts derive. */
|
|
19930
|
+
name: DeviceNameSchema.optional(),
|
|
19931
|
+
/** Every relay peer that confirmed a channel for it, newest last (at most MAX_DEVICE_PEERS): all are revoked with it. */
|
|
19932
|
+
peers: z31.array(z31.string().min(1)).max(MAX_DEVICE_PEERS).optional()
|
|
19840
19933
|
});
|
|
19841
19934
|
DEFAULT_CONFIG = () => ({ enabled: false, relayUrl: DEFAULT_RELAY_URL });
|
|
19842
19935
|
LEGACY_RELAY_URL = "https://server.ledge.paradow.dev";
|
|
@@ -20480,6 +20573,10 @@ var init_channels = __esm({
|
|
|
20480
20573
|
});
|
|
20481
20574
|
|
|
20482
20575
|
// src/mobile/service.ts
|
|
20576
|
+
function peersOf(d) {
|
|
20577
|
+
const list3 = d.peers ?? [];
|
|
20578
|
+
return d.lastPeer && !list3.includes(d.lastPeer) ? [...list3, d.lastPeer] : list3;
|
|
20579
|
+
}
|
|
20483
20580
|
async function createMobileService(opts = {}) {
|
|
20484
20581
|
const now2 = opts.now ?? Date.now;
|
|
20485
20582
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
@@ -20510,6 +20607,13 @@ async function createMobileService(opts = {}) {
|
|
|
20510
20607
|
const sendFrame = (peer, frame) => {
|
|
20511
20608
|
link3?.send({ type: "send", to: peer, body: JSON.stringify(frame) });
|
|
20512
20609
|
};
|
|
20610
|
+
const withPeer = (d, peer) => [...peersOf(d).filter((p) => p !== peer), peer].slice(-MAX_DEVICE_PEERS);
|
|
20611
|
+
const withoutPeer = (d, peer) => {
|
|
20612
|
+
if (d.lastPeer !== peer && !d.peers?.includes(peer)) return d;
|
|
20613
|
+
const { lastPeer, peers, ...rest } = d;
|
|
20614
|
+
const left2 = (peers ?? []).filter((p) => p !== peer);
|
|
20615
|
+
return { ...rest, ...lastPeer && lastPeer !== peer ? { lastPeer } : {}, ...left2.length > 0 ? { peers: left2 } : {} };
|
|
20616
|
+
};
|
|
20513
20617
|
const saveDevices = (next4) => {
|
|
20514
20618
|
writeDevices(next4);
|
|
20515
20619
|
devices = next4;
|
|
@@ -20519,7 +20623,8 @@ async function createMobileService(opts = {}) {
|
|
|
20519
20623
|
send: sendFrame,
|
|
20520
20624
|
approve(d) {
|
|
20521
20625
|
const old = devices.find((x) => x.id === d.id);
|
|
20522
|
-
|
|
20626
|
+
const kind = deviceKindOfLabel(d.label);
|
|
20627
|
+
saveDevices(old ? devices.map((x) => x.id === d.id ? { ...x, label: d.label, kind } : x) : [...devices, { ...d, kind }]);
|
|
20523
20628
|
},
|
|
20524
20629
|
onChange: changed,
|
|
20525
20630
|
now: now2,
|
|
@@ -20533,7 +20638,7 @@ async function createMobileService(opts = {}) {
|
|
|
20533
20638
|
if (devices.some((d) => d.id === deviceId)) {
|
|
20534
20639
|
const at = new Date(now2()).toISOString();
|
|
20535
20640
|
try {
|
|
20536
|
-
saveDevices(devices.map((d) => d.id === deviceId ? { ...d, lastPeer: peer, lastSeenAt: at } : d));
|
|
20641
|
+
saveDevices(devices.map((d) => d.id === deviceId ? { ...d, lastPeer: peer, lastSeenAt: at, peers: withPeer(d, peer) } : withoutPeer(d, peer)));
|
|
20537
20642
|
} catch (e) {
|
|
20538
20643
|
log(`mobile: devices.json not written (${e.message})`);
|
|
20539
20644
|
}
|
|
@@ -20736,9 +20841,17 @@ async function createMobileService(opts = {}) {
|
|
|
20736
20841
|
saveDevices(devices.filter((x) => x.id !== id));
|
|
20737
20842
|
log(`mobile: revoked device ${id}`);
|
|
20738
20843
|
channels.dropDevice(id);
|
|
20739
|
-
|
|
20844
|
+
const others = new Set(devices.flatMap(peersOf));
|
|
20845
|
+
const peers = peersOf(d).filter((p) => !others.has(p));
|
|
20846
|
+
for (const p of peers) sendFrame(p, { k: "denied" });
|
|
20847
|
+
changed();
|
|
20848
|
+
await Promise.all(peers.map((p) => revokeOnRelay(p)));
|
|
20849
|
+
};
|
|
20850
|
+
const rename7 = (id, raw) => {
|
|
20851
|
+
const name = DeviceNameSchema.parse(raw);
|
|
20852
|
+
if (!devices.some((d) => d.id === id)) throw new Error("no such device");
|
|
20853
|
+
saveDevices(devices.map((d) => d.id === id ? { ...d, name } : d));
|
|
20740
20854
|
changed();
|
|
20741
|
-
if (d.lastPeer) await revokeOnRelay(d.lastPeer);
|
|
20742
20855
|
};
|
|
20743
20856
|
const pairingIn = (id, states) => {
|
|
20744
20857
|
const p = pairing.views().find((v2) => v2.id === id);
|
|
@@ -20760,13 +20873,20 @@ async function createMobileService(opts = {}) {
|
|
|
20760
20873
|
label: d.label,
|
|
20761
20874
|
approvedAt: d.approvedAt,
|
|
20762
20875
|
online: online.has(d.id),
|
|
20763
|
-
...d.lastSeenAt ? { lastSeenAt: d.lastSeenAt } : {}
|
|
20876
|
+
...d.lastSeenAt ? { lastSeenAt: d.lastSeenAt } : {},
|
|
20877
|
+
kind: d.kind ?? deviceKindOfLabel(d.label),
|
|
20878
|
+
...d.name ? { name: d.name } : {}
|
|
20764
20879
|
})),
|
|
20765
20880
|
pairings: pairing.views(),
|
|
20766
20881
|
terminalInput: config.terminalInput === true
|
|
20767
20882
|
};
|
|
20768
20883
|
},
|
|
20769
20884
|
revoke,
|
|
20885
|
+
rename: rename7,
|
|
20886
|
+
deviceInfo(deviceId) {
|
|
20887
|
+
const d = devices.find((x) => x.id === deviceId);
|
|
20888
|
+
return d ? { label: d.label, kind: d.kind ?? deviceKindOfLabel(d.label), ...d.name ? { name: d.name } : {} } : null;
|
|
20889
|
+
},
|
|
20770
20890
|
credential(deviceId) {
|
|
20771
20891
|
const c = devices.find((d) => d.id === deviceId)?.credential;
|
|
20772
20892
|
return c && c.rpId === rpOf(config.relayUrl)?.rpId ? c : null;
|
|
@@ -20867,17 +20987,24 @@ async function createMobileService(opts = {}) {
|
|
|
20867
20987
|
case "mobile.retry":
|
|
20868
20988
|
link3?.retryNow();
|
|
20869
20989
|
return;
|
|
20870
|
-
case "mobile.pairing.approve":
|
|
20990
|
+
case "mobile.pairing.approve": {
|
|
20871
20991
|
pairingIn(msg.id, ["confirm"]);
|
|
20872
20992
|
if (linkState.state !== "online") throw new Error("the relay is offline");
|
|
20993
|
+
const before2 = new Set(devices.map((d) => d.id));
|
|
20873
20994
|
pairing.approve(msg.id);
|
|
20995
|
+
const added = devices.some((d) => !before2.has(d.id));
|
|
20996
|
+
if (msg.replaces !== void 0 && added && before2.has(msg.replaces)) await revoke(msg.replaces);
|
|
20874
20997
|
return;
|
|
20998
|
+
}
|
|
20875
20999
|
case "mobile.pairing.deny":
|
|
20876
21000
|
pairingIn(msg.id, ["confirm", "waiting"]);
|
|
20877
21001
|
pairing.deny(msg.id);
|
|
20878
21002
|
return;
|
|
20879
21003
|
case "mobile.device.revoke":
|
|
20880
21004
|
return revoke(msg.id);
|
|
21005
|
+
case "mobile.device.rename":
|
|
21006
|
+
rename7(msg.id, msg.name);
|
|
21007
|
+
return;
|
|
20881
21008
|
}
|
|
20882
21009
|
},
|
|
20883
21010
|
send: (deviceId, text7) => channels.send(deviceId, text7),
|
package/dist/cli/reduceWorker.js
CHANGED
|
@@ -243,6 +243,10 @@ var TaskBlockSchema = z5.object({
|
|
|
243
243
|
// ../protocol/src/decisions.ts
|
|
244
244
|
import { z as z6 } from "zod";
|
|
245
245
|
var DecisionSchema = z6.enum(["accept", "deny", "always"]);
|
|
246
|
+
var DecidedBySchema = z6.discriminatedUnion("kind", [
|
|
247
|
+
z6.object({ kind: z6.literal("pc") }),
|
|
248
|
+
z6.object({ kind: z6.literal("device"), id: z6.string().min(1).max(100), name: z6.string().min(1).max(60) })
|
|
249
|
+
]);
|
|
246
250
|
var NOTE_MAX = 4e3;
|
|
247
251
|
|
|
248
252
|
// ../protocol/src/tasks.ts
|
|
@@ -1402,8 +1406,8 @@ var ThreadItemSchema = z22.discriminatedUnion("kind", [
|
|
|
1402
1406
|
imagesDropped: z22.literal(true).optional()
|
|
1403
1407
|
}),
|
|
1404
1408
|
/** `unknown`: answered in the terminal, the daemon only saw the prompt go away; `always`: allowed and not asked again;
|
|
1405
|
-
* `byClose`: denied by Ledge closing the session. */
|
|
1406
|
-
z22.object({ kind: z22.literal("permission"), ...base, tool: z22.string(), text: z22.string(), decision: z22.enum(["accept", "deny", "always", "unknown"]).optional(), byClose: z22.literal(true).optional() }),
|
|
1409
|
+
* `byClose`: denied by Ledge closing the session; `by`: who answered it from Ledge (the PC or a device, its name then). */
|
|
1410
|
+
z22.object({ kind: z22.literal("permission"), ...base, tool: z22.string(), text: z22.string(), decision: z22.enum(["accept", "deny", "always", "unknown"]).optional(), byClose: z22.literal(true).optional(), by: DecidedBySchema.optional() }),
|
|
1407
1411
|
z22.object({ kind: z22.literal("reply"), ...base, text: z22.string() }),
|
|
1408
1412
|
z22.object({ kind: z22.literal("helper"), ...base, task: z22.string(), agentType: z22.string().optional(), state: z22.enum(["working", "exited"]) }),
|
|
1409
1413
|
/** 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. */
|
|
@@ -1486,6 +1490,9 @@ var MobileLinkStateSchema = z23.discriminatedUnion("state", [
|
|
|
1486
1490
|
/** The mobile service did not start (a broken mobile.json, a missing key store): nothing is linked until the daemon restarts. */
|
|
1487
1491
|
z23.object({ state: z23.literal("unavailable"), error: z23.string().max(200) })
|
|
1488
1492
|
]);
|
|
1493
|
+
var DeviceKindSchema = z23.enum(["phone", "tablet"]);
|
|
1494
|
+
var DEVICE_NAME_MAX = 40;
|
|
1495
|
+
var DeviceNameSchema = z23.string().trim().min(1).max(DEVICE_NAME_MAX).regex(/^[^\u0000-\u001f\u007f]+$/);
|
|
1489
1496
|
var MobilePairingSchema = z23.object({
|
|
1490
1497
|
id: z23.string(),
|
|
1491
1498
|
label: z23.string(),
|
|
@@ -1501,7 +1508,16 @@ var MobileStateSchema = z23.object({
|
|
|
1501
1508
|
/** The last sign-in that did not end with a session; cleared by the next one. */
|
|
1502
1509
|
signinError: z23.enum(["expired", "denied", "not_allowed", "failed"]).nullable(),
|
|
1503
1510
|
link: MobileLinkStateSchema,
|
|
1504
|
-
devices: z23.array(z23.object({
|
|
1511
|
+
devices: z23.array(z23.object({
|
|
1512
|
+
id: z23.string(),
|
|
1513
|
+
label: z23.string(),
|
|
1514
|
+
approvedAt: z23.string(),
|
|
1515
|
+
online: z23.boolean(),
|
|
1516
|
+
lastSeenAt: z23.string().optional(),
|
|
1517
|
+
/** Absent from an older daemon; `name` absent: the user gave none, the front derives one from the label. */
|
|
1518
|
+
kind: DeviceKindSchema.optional(),
|
|
1519
|
+
name: z23.string().optional()
|
|
1520
|
+
})),
|
|
1505
1521
|
pairings: z23.array(MobilePairingSchema),
|
|
1506
1522
|
/** « Taper dans les terminaux depuis le téléphone »: absent from an older daemon, which has no phone terminal. */
|
|
1507
1523
|
terminalInput: z23.boolean().optional()
|
|
@@ -1556,6 +1572,8 @@ var SessionEventSchema = z24.object({
|
|
|
1556
1572
|
decision: DecisionSchema.optional(),
|
|
1557
1573
|
/** Decided: the request was refused by Ledge closing the session (its interrupt keys), not by the user's answer. */
|
|
1558
1574
|
byClose: z24.literal(true).optional(),
|
|
1575
|
+
/** Decided: who answered, when the daemon typed it (absent: answered in the terminal, or an older daemon). */
|
|
1576
|
+
by: DecidedBySchema.optional(),
|
|
1559
1577
|
/** The agent's own session id when the hook names it, so the daemon can resume that conversation. */
|
|
1560
1578
|
agentSessionId: z24.string().optional()
|
|
1561
1579
|
});
|
|
@@ -1832,7 +1850,9 @@ var ClientMessageSchema = z24.discriminatedUnion("type", [
|
|
|
1832
1850
|
note: z24.string().trim().min(1).max(NOTE_MAX).optional(),
|
|
1833
1851
|
/** 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.
|
|
1834
1852
|
* `key` (pendingKey) covers its whole content; older fronts send kind and text only. */
|
|
1835
|
-
expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z24.string().max(64).optional() }).optional()
|
|
1853
|
+
expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z24.string().max(64).optional() }).optional(),
|
|
1854
|
+
/** Who decides: set by the phone gateway for a device (over anything the device sent); left out by a local front, which means the PC. */
|
|
1855
|
+
by: DecidedBySchema.optional()
|
|
1836
1856
|
}),
|
|
1837
1857
|
z24.object({ type: z24.literal("session.focus"), id: z24.string() }),
|
|
1838
1858
|
z24.object({ type: z24.literal("session.kill"), id: z24.string() }),
|
|
@@ -1886,10 +1906,13 @@ var ClientMessageSchema = z24.discriminatedUnion("type", [
|
|
|
1886
1906
|
z24.object({ type: z24.literal("mobile.signout") }),
|
|
1887
1907
|
/** While the link is offline: try again now. */
|
|
1888
1908
|
z24.object({ type: z24.literal("mobile.retry") }),
|
|
1889
|
-
|
|
1909
|
+
/** `replaces`: an approved device this one takes the place of (the same app reinstalled), revoked once this one is stored. */
|
|
1910
|
+
z24.object({ type: z24.literal("mobile.pairing.approve"), id: z24.string().min(1).max(100), replaces: z24.string().min(1).max(100).optional() }),
|
|
1890
1911
|
z24.object({ type: z24.literal("mobile.pairing.deny"), id: z24.string().min(1).max(100) }),
|
|
1891
1912
|
/** `id`: the device's fingerprint. */
|
|
1892
1913
|
z24.object({ type: z24.literal("mobile.device.revoke"), id: z24.string().min(1).max(100) }),
|
|
1914
|
+
/** Names a device (`id`: its fingerprint); the device sees its new name in its next snapshot. */
|
|
1915
|
+
z24.object({ type: z24.literal("mobile.device.rename"), id: z24.string().min(1).max(100), name: DeviceNameSchema }),
|
|
1893
1916
|
/** « Taper dans les terminaux depuis le téléphone »; turned off, every phone loses the hand. */
|
|
1894
1917
|
z24.object({ type: z24.literal("mobile.terminal"), input: z24.boolean() }),
|
|
1895
1918
|
/** « Reprendre la main »: the phone typing into the session's terminal goes back to reading it. */
|
|
@@ -2022,7 +2045,9 @@ var PushSubscriptionSchema = z27.object({
|
|
|
2022
2045
|
});
|
|
2023
2046
|
var PushPayloadSchema = z27.object({
|
|
2024
2047
|
v: z27.literal(1),
|
|
2025
|
-
|
|
2048
|
+
/** `resolved`: a request or question answered (anywhere), replacing its notification under the same tag; only sent to a
|
|
2049
|
+
* device whose subscription says its service worker knows it (`push.subscribe` `resolved`). */
|
|
2050
|
+
kind: z27.enum(["request", "question", "finished", "chat", "decision", "resolved"]),
|
|
2026
2051
|
title: z27.string().max(80),
|
|
2027
2052
|
body: z27.string().max(120),
|
|
2028
2053
|
url: z27.string().regex(/^\/#\/[A-Za-z0-9/%._~-]*$/).max(300),
|
|
@@ -2289,8 +2314,11 @@ var PhoneClientMessageSchema = z28.discriminatedUnion("type", [
|
|
|
2289
2314
|
z28.object({ type: z28.literal("openrig.row"), qitem: QitemIdSchema }),
|
|
2290
2315
|
/** The phone drops its passkey from the daemon. */
|
|
2291
2316
|
z28.object({ type: z28.literal("device.forget"), op: OperationIdSchema }),
|
|
2292
|
-
/** The
|
|
2293
|
-
z28.object({ type: z28.literal("
|
|
2317
|
+
/** The device names itself (on the PC and in its next snapshot); applied to the sender only. */
|
|
2318
|
+
z28.object({ type: z28.literal("device.rename"), op: OperationIdSchema, name: DeviceNameSchema }),
|
|
2319
|
+
/** The phone's push subscription, with the VAPID key it was made with (so a key change is seen). `resolved`: its service
|
|
2320
|
+
* worker shows a `resolved` push, which replaces a request's notification once it is answered elsewhere. */
|
|
2321
|
+
z28.object({ type: z28.literal("push.subscribe"), op: OperationIdSchema, subscription: PushSubscriptionSchema, key: B64U2.length(87), resolved: z28.literal(true).optional() }),
|
|
2294
2322
|
z28.object({ type: z28.literal("push.unsubscribe"), op: OperationIdSchema }),
|
|
2295
2323
|
/** The phone's own chat with Ledge. */
|
|
2296
2324
|
z28.object({ type: z28.literal("chat.ask"), qid: OperationIdSchema, text: z28.string().trim().max(MESSAGE_MAX), attachments: z28.array(AttachmentIdSchema).max(ATTACHMENTS_MAX).optional() }),
|
|
@@ -2382,7 +2410,16 @@ var PhoneServerMessageSchema = z28.discriminatedUnion("type", [
|
|
|
2382
2410
|
openrig: z28.literal(true).optional(),
|
|
2383
2411
|
/** The daemon takes `session.broadcast` (« Écrire au groupe »). */
|
|
2384
2412
|
broadcast: z28.literal(true).optional()
|
|
2385
|
-
}), device: z28.object({
|
|
2413
|
+
}), device: z28.object({
|
|
2414
|
+
confirm: z28.boolean(),
|
|
2415
|
+
push: z28.enum(["on", "off", "stale", "unavailable"]).optional(),
|
|
2416
|
+
chat: z28.literal(true).optional(),
|
|
2417
|
+
contextNear: ContextPrefsSchema.shape.near.optional(),
|
|
2418
|
+
/** This device's id (its fingerprint, as decisions name it in `by`), its kind, and the name the user gave it (absent: none given). */
|
|
2419
|
+
id: z28.string().optional(),
|
|
2420
|
+
kind: DeviceKindSchema.optional(),
|
|
2421
|
+
name: z28.string().optional()
|
|
2422
|
+
}).optional() }),
|
|
2386
2423
|
/** « À toi » on this PC: the answer to `openrig.asks`, then each change, whole. */
|
|
2387
2424
|
z28.object({ type: z28.literal("openrig.asks"), channel: HumanChannelSchema, asks: z28.array(HumanAskSchema), partial: z28.literal(true).optional(), truncated: z28.literal(true).optional() }),
|
|
2388
2425
|
/** An answer's end, as on the desktop (`openrig.answered`), under the phone's `op`. `rejected`: the device check refused it (`reason`). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paradow/ledge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Follow and answer your WSL agent sessions (Claude Code, Codex, Cursor Agent) from the Ledge island on Windows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "ledge": "dist/cli/index.js" },
|