@paradow/ledge 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/{attach-3U4A44ZE.js → attach-Y4OMZXAH.js} +7 -7
  3. package/dist/cli/{bridge-7JT6P53L.js → bridge-6K523KUX.js} +2 -2
  4. package/dist/cli/{chunk-FPW7MF5X.js → chunk-2H5FJH4Q.js} +2 -2
  5. package/dist/cli/{chunk-5LVPB3ET.js → chunk-2LQITRTR.js} +178 -39
  6. package/dist/cli/{chunk-D3JQV5AA.js → chunk-4IBQ4PG6.js} +6 -6
  7. package/dist/cli/{chunk-5DDBCJX2.js → chunk-BNKPXSUY.js} +1 -1
  8. package/dist/cli/{chunk-2P2SUT7A.js → chunk-IMSPQUFC.js} +2 -2
  9. package/dist/cli/{chunk-ULWVO3TH.js → chunk-KGEF6OIU.js} +2 -2
  10. package/dist/cli/chunk-LKQ5DDQ4.js +6 -0
  11. package/dist/cli/{chunk-IIXALRFM.js → chunk-NKYJF3VT.js} +2 -2
  12. package/dist/cli/{chunk-PFDZ6VYG.js → chunk-PXO4FFJR.js} +1 -1
  13. package/dist/cli/{chunk-UWANRALT.js → chunk-QCEVJMXI.js} +4 -3
  14. package/dist/cli/{chunk-QBJEUZYW.js → chunk-R5462SQM.js} +1 -1
  15. package/dist/cli/{chunk-6A7JPTHV.js → chunk-ZANOMSLW.js} +22 -12
  16. package/dist/cli/{daemon-KHGAO6BC.js → daemon-TBYCUHC3.js} +1243 -189
  17. package/dist/cli/{hook-R6OWCFAQ.js → hook-PRPEB3T7.js} +8 -8
  18. package/dist/cli/index.js +11 -11
  19. package/dist/cli/{launch-PPTQEWYD.js → launch-JS74JGW6.js} +7 -7
  20. package/dist/cli/send-JX6O7AQR.js +14 -0
  21. package/dist/cli/{setup-FPQK3NA7.js → setup-LWYZEVNE.js} +10 -8
  22. package/dist/cli/{status-QLTB6T3U.js → status-Q6TQXVLF.js} +3 -3
  23. package/dist/cli/{uninstall-QC5WEWDH.js → uninstall-TFC4LALT.js} +7 -6
  24. package/dist/cli/{version-AELAEF6T.js → version-5BLH6OA6.js} +1 -1
  25. package/dist/cli/who-KAAEASAA.js +11 -0
  26. package/package.json +1 -1
  27. package/dist/cli/chunk-QUUZSAPI.js +0 -6
  28. package/dist/cli/send-74LTIRSN.js +0 -14
  29. package/dist/cli/who-5F6CHRA3.js +0 -11
package/README.md CHANGED
@@ -30,7 +30,7 @@ pnpm --filter @paradow/ledge build
30
30
  chmod +x packages/ledge/dist/cli/index.js
31
31
  mkdir -p ~/.local/bin
32
32
  ln -sf "$PWD/packages/ledge/dist/cli/index.js" ~/.local/bin/ledge # ~/.local/bin must be on the PATH Claude Code uses for hooks
33
- ledge setup
33
+ ledge setup # the link becomes a script that runs the CLI with this node (a session Ledge launches may have no node on its PATH: nvm)
34
34
  ```
35
35
 
36
36
  The daemon is spawned from the built CLI (`dist/cli/index.js`), so rebuild after changes (`pnpm --filter @paradow/ledge build`).
@@ -43,7 +43,7 @@ ledge claude --name max # pick the bot's name
43
43
  ledge status # list bots and what they wait for
44
44
  ledge attach <id> # put this terminal on a running session (what the overlay opens a tab with)
45
45
  ledge daemon # run the daemon in the foreground (normally run by the systemd service)
46
- ledge uninstall # undo ledge setup: hooks, service and the ~/.local/bin link (--purge: ~/.ledge too)
46
+ ledge uninstall # undo ledge setup: hooks, service and ~/.local/bin/ledge (--purge: ~/.ledge too)
47
47
  ```
48
48
 
49
49
  `ledge setup` installs `ledge-daemon.service` as a systemd user unit, starts it and enables linger, so the daemon runs from WSL boot and restarts after a crash. Rerun it after a node upgrade or a repository move: the unit is rewritten and the service restarted. Useful commands:
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  attachEnv
3
- } from "./chunk-D3JQV5AA.js";
3
+ } from "./chunk-4IBQ4PG6.js";
4
4
  import {
5
5
  connect
6
- } from "./chunk-PFDZ6VYG.js";
7
- import "./chunk-ULWVO3TH.js";
6
+ } from "./chunk-PXO4FFJR.js";
7
+ import "./chunk-KGEF6OIU.js";
8
8
  import {
9
9
  PROTOCOL_PORT,
10
10
  backendOf
11
- } from "./chunk-5LVPB3ET.js";
11
+ } from "./chunk-2LQITRTR.js";
12
12
  import {
13
13
  realTmux
14
- } from "./chunk-FPW7MF5X.js";
15
- import "./chunk-5DDBCJX2.js";
16
- import "./chunk-QBJEUZYW.js";
14
+ } from "./chunk-2H5FJH4Q.js";
15
+ import "./chunk-BNKPXSUY.js";
16
+ import "./chunk-R5462SQM.js";
17
17
 
18
18
  // src/cli/attach.ts
19
19
  import { spawn } from "child_process";
@@ -7,8 +7,8 @@ import {
7
7
  parseClientMessage,
8
8
  parseServerMessage,
9
9
  readDaemonToken
10
- } from "./chunk-5LVPB3ET.js";
11
- import "./chunk-QBJEUZYW.js";
10
+ } from "./chunk-2LQITRTR.js";
11
+ import "./chunk-R5462SQM.js";
12
12
 
13
13
  // src/cli/bridge.ts
14
14
  import WebSocket from "ws";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  log
3
- } from "./chunk-5DDBCJX2.js";
3
+ } from "./chunk-BNKPXSUY.js";
4
4
  import {
5
5
  paths
6
- } from "./chunk-QBJEUZYW.js";
6
+ } from "./chunk-R5462SQM.js";
7
7
 
8
8
  // src/tmux.ts
9
9
  import { execFile } from "child_process";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  paths
3
- } from "./chunk-QBJEUZYW.js";
3
+ } from "./chunk-R5462SQM.js";
4
4
 
5
5
  // ../protocol/src/session.ts
6
6
  import { z } from "zod";
@@ -458,7 +458,7 @@ function pendingKey(p) {
458
458
 
459
459
  // ../protocol/src/mobile.ts
460
460
  import { z as z14 } from "zod";
461
- var DEFAULT_RELAY_URL = "https://server.ledge.paradow.dev";
461
+ var DEFAULT_RELAY_URL = "https://app.kl-ledge.dev";
462
462
  var RelayUrlSchema = z14.string().transform((v, ctx) => {
463
463
  const bad = () => {
464
464
  ctx.addIssue({ code: z14.ZodIssueCode.custom, message: "invalid relay URL" });
@@ -973,7 +973,53 @@ var MeResponseSchema = z18.object({
973
973
  });
974
974
 
975
975
  // ../protocol/src/phone.ts
976
+ import { z as z20 } from "zod";
977
+
978
+ // ../protocol/src/push.ts
976
979
  import { z as z19 } from "zod";
980
+ var B64U = z19.string().regex(/^[A-Za-z0-9_-]*$/);
981
+ var PUSH_HOSTS = ["*.push.apple.com", "fcm.googleapis.com", "updates.push.services.mozilla.com", "*.notify.windows.com"];
982
+ function pushEndpointAllowed(url) {
983
+ let u;
984
+ try {
985
+ u = new URL(url);
986
+ } catch {
987
+ return false;
988
+ }
989
+ if (u.protocol !== "https:" || u.port !== "" || u.username !== "" || u.password !== "") return false;
990
+ const host = u.hostname.toLowerCase();
991
+ return PUSH_HOSTS.some((h) => {
992
+ if (!h.startsWith("*.")) return host === h;
993
+ const suffix = h.slice(1);
994
+ return host.length > suffix.length && host.endsWith(suffix);
995
+ });
996
+ }
997
+ var PushSendSchema = z19.object({
998
+ endpoint: z19.string().url().max(1024),
999
+ body: B64U.max(5462),
1000
+ ttl: z19.number().int().min(0).max(86400),
1001
+ urgency: z19.enum(["normal", "high"]),
1002
+ topic: z19.string().regex(/^[A-Za-z0-9_-]{1,32}$/)
1003
+ });
1004
+ var PushSendResultSchema = z19.object({
1005
+ result: z19.enum(["sent", "gone", "failed"]),
1006
+ status: z19.number().int().optional()
1007
+ });
1008
+ var PushSubscriptionSchema = z19.object({
1009
+ endpoint: z19.string().url().max(1024).refine(pushEndpointAllowed),
1010
+ keys: z19.object({ p256dh: B64U.length(87), auth: B64U.length(22) })
1011
+ });
1012
+ var PushPayloadSchema = z19.object({
1013
+ v: z19.literal(1),
1014
+ kind: z19.enum(["request", "question", "finished", "chat"]),
1015
+ title: z19.string().max(80),
1016
+ body: z19.string().max(120),
1017
+ url: z19.string().regex(/^\/#\/[A-Za-z0-9/%._~-]*$/).max(300),
1018
+ badge: z19.number().int().min(0).max(999),
1019
+ tag: z19.string().regex(/^[A-Za-z0-9_-]{1,32}$/)
1020
+ });
1021
+
1022
+ // ../protocol/src/phone.ts
977
1023
  var PHONE_PROTOCOL = 1;
978
1024
  var PAD_TIERS = [1024, 4096, 16384, 65536, 262144, 655360];
979
1025
  var PLAIN_MAX = 655360;
@@ -991,18 +1037,22 @@ var THREAD_TEXT_MAX = 8192;
991
1037
  var TOOL_TEXT_MAX = 2048;
992
1038
  var PENDING_BODY_MAX = 65536;
993
1039
  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({
1040
+ var RiskSchema = z20.enum(["low", "high"]);
1041
+ var RiskReasonSchema = z20.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"]);
1042
+ var OperationIdSchema = z20.string().uuid();
1043
+ var OperationStatusSchema = z20.enum(["received", "applied", "rejected", "failed"]);
1044
+ var OperationReasonSchema = z20.enum([...RefusalCodeSchema.options, "untypable", "confirm_required", "conflict", "assertion_invalid", "assertion_expired"]);
1045
+ var CapabilitiesSchema = z20.object({
1000
1046
  /** A question can be answered in free text. */
1001
- freeTextAnswer: z19.boolean()
1047
+ freeTextAnswer: z20.boolean(),
1048
+ /** The session's accept/deny can be typed; absent = true. */
1049
+ decide: z20.boolean().optional(),
1050
+ /** OMP's dialog questions can be answered from the phone; absent = false. */
1051
+ questions: z20.boolean().optional()
1002
1052
  });
1003
1053
  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() });
1054
+ var CapabilitiesByToolSchema = z20.object({ claude: CapabilitiesSchema, codex: CapabilitiesSchema, cursor: CapabilitiesSchema, omp: CapabilitiesSchema });
1055
+ var PhonePendingSchema = PendingSchema.extend({ key: z20.string().max(64), risk: RiskSchema, riskReason: RiskReasonSchema, cut: z20.literal(true).optional() });
1006
1056
  var PhoneSessionSchema = SessionSchema.pick({
1007
1057
  id: true,
1008
1058
  name: true,
@@ -1018,49 +1068,133 @@ var PhoneSessionSchema = SessionSchema.pick({
1018
1068
  lastReply: true,
1019
1069
  startedAt: true,
1020
1070
  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 }),
1071
+ }).extend({ pending: PhonePendingSchema.optional(), unread: z20.boolean() });
1072
+ var B64U2 = z20.string().regex(/^[A-Za-z0-9_-]*$/);
1073
+ var CredentialSchema = z20.object({
1074
+ id: B64U2.max(1366),
1075
+ publicKey: B64U2.max(2048),
1076
+ alg: z20.union([z20.literal(-7), z20.literal(-257)]),
1077
+ clientDataJSON: B64U2.max(4096),
1078
+ authenticatorData: B64U2.max(4096),
1079
+ transports: z20.array(z20.string().max(20)).max(8).optional()
1080
+ });
1081
+ var AssertionSchema = z20.object({
1082
+ id: B64U2.max(1366),
1083
+ clientDataJSON: B64U2.max(4096),
1084
+ authenticatorData: B64U2.max(4096),
1085
+ signature: B64U2.max(1024)
1086
+ });
1087
+ var ConfirmPurposeSchema = z20.enum(["register", "decide", "launch"]);
1088
+ var PhoneChatStepSchema = ChatStepSchema.pick({ kind: true, id: true, name: true }).strict();
1089
+ var PhoneChatProposalSchema = z20.object({
1090
+ project: z20.string().max(80),
1091
+ tool: ToolSchema,
1092
+ prompt: z20.string().max(TOOL_TEXT_MAX).optional(),
1093
+ launchedId: z20.string().optional(),
1094
+ failed: z20.literal(true).optional()
1095
+ }).strict();
1096
+ var PhoneChatRelaySchema = z20.object({
1097
+ id: z20.string().min(1),
1098
+ name: z20.string(),
1099
+ tool: ToolSchema,
1100
+ text: z20.string().max(TOOL_TEXT_MAX),
1101
+ status: z20.enum(["queued", "sent"]).optional(),
1102
+ sentAt: z20.string().datetime().optional(),
1103
+ failed: z20.literal(true).optional()
1104
+ }).strict();
1105
+ var ChatErrorCodeSchema = z20.enum(["busy", "timeout", "cancelled", "failed"]);
1106
+ var PhoneChatTurnSchema = z20.object({
1107
+ qid: OperationIdSchema,
1108
+ question: z20.string().max(MESSAGE_MAX),
1109
+ askedAt: z20.string().datetime(),
1110
+ steps: z20.array(PhoneChatStepSchema).max(50),
1111
+ answer: z20.string().max(THREAD_TEXT_MAX).optional(),
1112
+ answeredAt: z20.string().datetime().optional(),
1113
+ error: ChatErrorCodeSchema.optional(),
1114
+ proposals: z20.array(PhoneChatProposalSchema).max(MAX_PROPOSALS).optional(),
1115
+ relay: PhoneChatRelaySchema.optional(),
1116
+ cut: z20.literal(true).optional()
1117
+ }).strict();
1118
+ var CHAT_HISTORY_BUDGET = 393216;
1119
+ var sessionId = z20.string().min(1).max(100);
1120
+ var PhoneExpectSchema = PendingSchema.pick({ kind: true, text: true }).extend({ key: z20.string().min(1).max(64) });
1121
+ var PhoneClientMessageSchema = z20.discriminatedUnion("type", [
1122
+ z20.object({ type: z20.literal("snapshot.request") }),
1123
+ z20.object({ type: z20.literal("session.thread"), id: sessionId }),
1027
1124
  /** The session's request with its whole body (up to `PENDING_BODY_MAX`). */
1028
- z19.object({ type: z19.literal("session.pending"), id: sessionId }),
1125
+ z20.object({ type: z20.literal("session.pending"), id: sessionId }),
1029
1126
  /** 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"),
1127
+ z20.object({ type: z20.literal("session.watch"), id: sessionId.nullable() }),
1128
+ z20.object({ type: z20.literal("session.read"), id: sessionId, at: z20.string().datetime() }),
1129
+ /** Asks for a challenge. One object for both purposes: the gateway refuses a `decide` without `id` and `expect`. */
1130
+ z20.object({ type: z20.literal("confirm.prepare"), purpose: ConfirmPurposeSchema, id: sessionId.optional(), expect: PhoneExpectSchema.optional(), qid: OperationIdSchema.optional() }),
1131
+ z20.object({ type: z20.literal("credential.register"), op: OperationIdSchema, credential: CredentialSchema }),
1132
+ z20.object({
1133
+ type: z20.literal("session.decide"),
1034
1134
  op: OperationIdSchema,
1035
1135
  id: sessionId,
1036
1136
  decision: DecisionSchema,
1037
- note: z19.string().trim().min(1).max(NOTE_MAX).optional(),
1137
+ note: z20.string().trim().min(1).max(NOTE_MAX).optional(),
1038
1138
  /** The request the phone showed: refused unless it is still the pending one. */
1039
- expect: PhoneExpectSchema
1139
+ expect: PhoneExpectSchema,
1140
+ /** The device check over the challenge of `confirm.prepare`; required by the daemon for a high-risk accept. */
1141
+ assertion: AssertionSchema.optional()
1040
1142
  }),
1041
- z19.object({
1042
- type: z19.literal("session.reply"),
1143
+ z20.object({
1144
+ type: z20.literal("session.reply"),
1043
1145
  op: OperationIdSchema,
1044
1146
  id: sessionId,
1045
- text: z19.string().min(1).max(MESSAGE_MAX),
1147
+ text: z20.string().min(1).max(MESSAGE_MAX),
1046
1148
  /** 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()
1149
+ expect: PhoneExpectSchema.nullable(),
1150
+ assertion: AssertionSchema.optional()
1151
+ }),
1152
+ /** OMP's `ask`: the answers to every question of the dialog `requestId`, in order; `cancel` declines it. Bounds as the local `session.answer`. */
1153
+ z20.object({
1154
+ type: z20.literal("session.answer"),
1155
+ op: OperationIdSchema,
1156
+ id: sessionId,
1157
+ requestId: z20.string().min(1).max(200),
1158
+ answers: z20.array(z20.object({ id: z20.string().max(200), selected: z20.array(z20.string().max(2e3)).max(100), text: z20.string().max(2e4).optional() })).max(50),
1159
+ cancel: z20.literal(true).optional(),
1160
+ expect: PhoneExpectSchema
1048
1161
  }),
1049
- z19.object({ type: z19.literal("session.interrupt"), op: OperationIdSchema, id: sessionId })
1162
+ z20.object({ type: z20.literal("session.interrupt"), op: OperationIdSchema, id: sessionId }),
1163
+ /** The phone drops its passkey from the daemon. */
1164
+ z20.object({ type: z20.literal("device.forget"), op: OperationIdSchema }),
1165
+ /** The phone's push subscription, with the VAPID key it was made with (so a key change is seen). */
1166
+ z20.object({ type: z20.literal("push.subscribe"), op: OperationIdSchema, subscription: PushSubscriptionSchema, key: B64U2.length(87) }),
1167
+ z20.object({ type: z20.literal("push.unsubscribe"), op: OperationIdSchema }),
1168
+ /** The phone's own chat with Ledge. */
1169
+ z20.object({ type: z20.literal("chat.ask"), qid: OperationIdSchema, text: z20.string().trim().min(1).max(MESSAGE_MAX) }),
1170
+ z20.object({ type: z20.literal("chat.cancel"), qid: OperationIdSchema }),
1171
+ z20.object({ type: z20.literal("chat.reset") }),
1172
+ z20.object({ type: z20.literal("chat.history") }),
1173
+ z20.object({ type: z20.literal("chat.relay"), op: OperationIdSchema, qid: OperationIdSchema }),
1174
+ z20.object({ type: z20.literal("chat.launch"), op: OperationIdSchema, qid: OperationIdSchema, rows: z20.array(z20.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(), assertion: AssertionSchema.optional() }),
1175
+ /** Whether the phone's app is in front: a device that is visible gets no push. */
1176
+ z20.object({ type: z20.literal("presence"), visible: z20.boolean() })
1050
1177
  ]);
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() }),
1178
+ var PhoneServerMessageSchema = z20.discriminatedUnion("type", [
1179
+ z20.object({ type: z20.literal("snapshot"), protocol: z20.literal(PHONE_PROTOCOL), sessions: z20.array(PhoneSessionSchema), capabilities: CapabilitiesByToolSchema, daemon: z20.object({ version: z20.string(), name: z20.string().max(40), boot: z20.string().uuid().optional() }), device: z20.object({ confirm: z20.boolean(), push: z20.enum(["on", "off", "stale", "unavailable"]).optional(), chat: z20.literal(true).optional() }).optional() }),
1180
+ z20.object({ type: z20.literal("confirm.challenge"), purpose: ConfirmPurposeSchema, challenge: B64U2.max(64), rpId: z20.string().max(253), credentialId: B64U2.max(1366).optional(), expiresInMs: z20.number().int().positive() }),
1181
+ z20.object({ type: z20.literal("session.updated"), session: PhoneSessionSchema }),
1182
+ z20.object({ type: z20.literal("session.removed"), id: z20.string() }),
1055
1183
  /** `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()) }),
1184
+ z20.object({ type: z20.literal("session.pending"), id: z20.string(), pending: PhonePendingSchema }),
1185
+ z20.object({ type: z20.literal("session.preview"), id: z20.string(), lines: z20.array(z20.string()) }),
1058
1186
  /** `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() }),
1187
+ z20.object({ type: z20.literal("session.thread"), id: z20.string(), items: z20.array(ThreadItemSchema), cut: z20.array(z20.string()), more: z20.boolean() }),
1188
+ z20.object({ type: z20.literal("session.thread.item"), id: z20.string(), item: ThreadItemSchema, cut: z20.literal(true).optional() }),
1189
+ z20.object({ type: z20.literal("operation.result"), op: OperationIdSchema, status: OperationStatusSchema, reason: OperationReasonSchema.optional() }),
1190
+ z20.object({ type: z20.literal("chat.history"), turns: z20.array(PhoneChatTurnSchema).max(50) }),
1191
+ z20.object({ type: z20.literal("chat.step"), qid: OperationIdSchema, step: PhoneChatStepSchema }),
1192
+ /** The whole text so far, not an increment. */
1193
+ z20.object({ type: z20.literal("chat.delta"), qid: OperationIdSchema, text: z20.string().max(THREAD_TEXT_MAX) }),
1194
+ z20.object({ type: z20.literal("chat.done"), qid: OperationIdSchema, turn: PhoneChatTurnSchema }),
1195
+ z20.object({ type: z20.literal("chat.error"), qid: OperationIdSchema, code: ChatErrorCodeSchema }),
1062
1196
  /** 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() })
1197
+ z20.object({ type: z20.literal("error"), code: z20.enum(["invalid", "unknown_session", "too_large"]), request: z20.string().max(40).optional() })
1064
1198
  ]);
1065
1199
 
1066
1200
  // src/auth.ts
@@ -1225,6 +1359,9 @@ export {
1225
1359
  RELAY_CLOSE_SIGNED_OUT,
1226
1360
  DeviceStartResponseSchema,
1227
1361
  DevicePollResponseSchema,
1362
+ pushEndpointAllowed,
1363
+ PushSendResultSchema,
1364
+ PushPayloadSchema,
1228
1365
  PHONE_PROTOCOL,
1229
1366
  padPlain,
1230
1367
  THREAD_ITEMS_MAX,
@@ -1233,7 +1370,9 @@ export {
1233
1370
  TOOL_TEXT_MAX,
1234
1371
  PENDING_BODY_MAX,
1235
1372
  SNAPSHOT_BODY_MAX,
1373
+ OperationIdSchema,
1236
1374
  UNTYPABLE,
1375
+ CHAT_HISTORY_BUDGET,
1237
1376
  PhoneClientMessageSchema,
1238
1377
  hostAllowed,
1239
1378
  requestAuthorized,
@@ -1,23 +1,23 @@
1
1
  import {
2
2
  DaemonRefusedError,
3
3
  connect
4
- } from "./chunk-PFDZ6VYG.js";
4
+ } from "./chunk-PXO4FFJR.js";
5
5
  import {
6
6
  exitFile,
7
7
  spawnSession
8
- } from "./chunk-ULWVO3TH.js";
8
+ } from "./chunk-KGEF6OIU.js";
9
9
  import {
10
10
  PROTOCOL_PORT
11
- } from "./chunk-5LVPB3ET.js";
11
+ } from "./chunk-2LQITRTR.js";
12
12
  import {
13
13
  realTmux
14
- } from "./chunk-FPW7MF5X.js";
14
+ } from "./chunk-2H5FJH4Q.js";
15
15
  import {
16
16
  log
17
- } from "./chunk-5DDBCJX2.js";
17
+ } from "./chunk-BNKPXSUY.js";
18
18
  import {
19
19
  ensureLedgeDir
20
- } from "./chunk-QBJEUZYW.js";
20
+ } from "./chunk-R5462SQM.js";
21
21
 
22
22
  // src/cli/launch.ts
23
23
  import { spawn } from "child_process";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  paths
3
- } from "./chunk-QBJEUZYW.js";
3
+ } from "./chunk-R5462SQM.js";
4
4
 
5
5
  // src/log.ts
6
6
  import { appendFileSync } from "fs";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  connect,
3
3
  sendMessageAndAwaitAck
4
- } from "./chunk-PFDZ6VYG.js";
4
+ } from "./chunk-PXO4FFJR.js";
5
5
  import {
6
6
  CODEX_HOOK_VAR
7
- } from "./chunk-ULWVO3TH.js";
7
+ } from "./chunk-KGEF6OIU.js";
8
8
 
9
9
  // src/cli/send.ts
10
10
  import { randomUUID } from "crypto";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  pickName
3
- } from "./chunk-5LVPB3ET.js";
3
+ } from "./chunk-2LQITRTR.js";
4
4
  import {
5
5
  paths
6
- } from "./chunk-QBJEUZYW.js";
6
+ } from "./chunk-R5462SQM.js";
7
7
 
8
8
  // src/daemon/transcript.ts
9
9
  import { closeSync, fstatSync, openSync, readSync } from "fs";
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var LEDGE_VERSION = true ? "0.15.1" : "0.0.0-dev";
3
+
4
+ export {
5
+ LEDGE_VERSION
6
+ };
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-PFDZ6VYG.js";
3
+ } from "./chunk-PXO4FFJR.js";
4
4
  import {
5
5
  isArchived
6
- } from "./chunk-5LVPB3ET.js";
6
+ } from "./chunk-2LQITRTR.js";
7
7
 
8
8
  // src/cli/who.ts
9
9
  import { basename } from "path";
@@ -4,7 +4,7 @@ import {
4
4
  SUMMARY_MAX,
5
5
  parseServerMessage,
6
6
  readDaemonToken
7
- } from "./chunk-5LVPB3ET.js";
7
+ } from "./chunk-2LQITRTR.js";
8
8
 
9
9
  // src/client.ts
10
10
  import WebSocket from "ws";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  cleanTopic,
3
3
  sessionTitle
4
- } from "./chunk-5LVPB3ET.js";
4
+ } from "./chunk-2LQITRTR.js";
5
5
 
6
6
  // src/daemon/crossSession.ts
7
7
  var FIRST_LINE = /^Another Claude session sent a message:\s*\n/;
@@ -356,9 +356,10 @@ function reduceEvent(s, ev) {
356
356
  case "PanePrompt": {
357
357
  const incoming = pendingFor(ev);
358
358
  const moved = s.pending?.source === "hook" && s.pending.kind === "question" && incoming.kind === "question" && !sameQuestion(incoming.text, s.pending.text);
359
- if (s.pending?.source === "hook" && !moved) return s;
359
+ const bare = s.pending?.source === "hook" && s.pending.kind === "permission" && !s.pending.tool;
360
+ if (s.pending?.source === "hook" && !moved && !bare) return s;
360
361
  if (!moved && hooksReportPrompts(s) && lastHookAt && Date.parse(at) - Date.parse(lastHookAt) < HOOK_PRIORITY_MS) return s;
361
- let pending = moved ? { ...incoming, ...s.pending.helperId ? { helperId: s.pending.helperId } : {} } : mergePending(s.pending, incoming);
362
+ let pending = moved ? { ...incoming, ...s.pending.helperId ? { helperId: s.pending.helperId } : {} } : bare ? incoming : mergePending(s.pending, incoming);
362
363
  if (pending.kind === "plan" && !pending.body && s.tool === "codex" && s.lastReply) pending = { ...pending, body: s.lastReply.text };
363
364
  if (s.pending && samePending(s.pending, pending)) return s;
364
365
  const state = pending.kind === "question" ? "waiting_input" : "waiting_permission";
@@ -17,7 +17,7 @@ function ledgeDir() {
17
17
  }
18
18
  function paths() {
19
19
  const dir = ledgeDir();
20
- return { dir, tmuxConf: join(dir, "tmux.conf"), journal: join(dir, "sessions.json"), log: join(dir, "daemon.log"), pid: join(dir, "daemon.pid"), history: join(dir, "history"), machines: join(dir, "machines.json"), projects: join(dir, "projects.json"), island: join(dir, "island.json"), token: join(dir, "token"), mobile: join(dir, "mobile.json"), mobileSession: join(dir, "mobile-session.json"), mobileKey: join(dir, "mobile-key"), devices: join(dir, "devices.json") };
20
+ return { dir, tmuxConf: join(dir, "tmux.conf"), journal: join(dir, "sessions.json"), log: join(dir, "daemon.log"), pid: join(dir, "daemon.pid"), history: join(dir, "history"), machines: join(dir, "machines.json"), projects: join(dir, "projects.json"), island: join(dir, "island.json"), token: join(dir, "token"), mobile: join(dir, "mobile.json"), mobileSession: join(dir, "mobile-session.json"), mobileKey: join(dir, "mobile-key"), devices: join(dir, "devices.json"), revocations: join(dir, "revocations.json") };
21
21
  }
22
22
  async function ensureLedgeDir() {
23
23
  const p = paths();
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  run
3
- } from "./chunk-FPW7MF5X.js";
3
+ } from "./chunk-2H5FJH4Q.js";
4
4
  import {
5
5
  ensureLedgeDir,
6
6
  paths
7
- } from "./chunk-QBJEUZYW.js";
7
+ } from "./chunk-R5462SQM.js";
8
8
 
9
9
  // src/cli/setup.ts
10
- import { readFile as readFile2, writeFile, mkdir, rename, realpath, rm, stat, lstat, readlink, symlink, unlink } from "fs/promises";
10
+ import { readFile as readFile2, writeFile, mkdir, rename, realpath, rm, stat, lstat, readlink } from "fs/promises";
11
11
  import { execFile } from "child_process";
12
12
  import { homedir, userInfo } from "os";
13
13
  import { join, dirname } from "path";
@@ -237,14 +237,23 @@ async function isDir(path) {
237
237
  return false;
238
238
  }
239
239
  }
240
- async function relinkBin(opts) {
240
+ var BIN_MARK = "#!/bin/sh\n# Written by `ledge setup`";
241
+ var shQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
242
+ async function writeBin(opts) {
241
243
  const st = await lstat(opts.link).catch(() => null);
242
- if (!st?.isSymbolicLink()) return null;
243
- const current = await readlink(opts.link);
244
- if (current === opts.target || !current.endsWith("/dist/cli/index.js")) return null;
245
- await unlink(opts.link);
246
- await symlink(opts.target, opts.link);
247
- return `${opts.link} now points to ${opts.target} (was ${current})`;
244
+ if (!st) return null;
245
+ const was = st.isSymbolicLink() ? await readlink(opts.link) : null;
246
+ if (was !== null && !was.endsWith("/dist/cli/index.js")) return null;
247
+ const current = was === null && st.isFile() ? await readFile2(opts.link, "utf8").catch(() => "") : "";
248
+ if (was === null && !current.startsWith(BIN_MARK)) return null;
249
+ const script = `${BIN_MARK}: the hooks run \`ledge\` wherever node is not on the PATH.
250
+ exec ${shQuote(opts.node)} ${shQuote(opts.target)} "$@"
251
+ `;
252
+ if (current === script) return null;
253
+ const tmp = `${opts.link}.tmp`;
254
+ await writeFile(tmp, script, { mode: 493 });
255
+ await rename(tmp, opts.link);
256
+ return `${opts.link} runs ${opts.target} with ${opts.node}${was !== null ? ` (was a link to ${was})` : ""}`;
248
257
  }
249
258
  async function runSetup(opts = {}) {
250
259
  const out = [];
@@ -263,7 +272,7 @@ async function runSetup(opts = {}) {
263
272
  else if (await mergeJsonFile(join(cursorDir, "hooks.json"), mergeCursorHooks, "hooks.json")) out.push(`Cursor Agent hooks added in ${join(cursorDir, "hooks.json")}`);
264
273
  else out.push("Cursor Agent hooks already present");
265
274
  if ((opts.service ?? true) && !process.argv[1]?.endsWith(".ts")) {
266
- const line = await relinkBin({ link: join(homedir(), ".local", "bin", "ledge"), target: await realpath(process.argv[1]) });
275
+ const line = await writeBin({ link: join(homedir(), ".local", "bin", "ledge"), node: process.execPath, target: await realpath(process.argv[1]) });
267
276
  if (line) out.push(line);
268
277
  }
269
278
  if (opts.service ?? true) {
@@ -286,7 +295,8 @@ export {
286
295
  stopDetachedDaemon,
287
296
  installService,
288
297
  isDir,
289
- relinkBin,
298
+ BIN_MARK,
299
+ writeBin,
290
300
  runSetup,
291
301
  inDaemonService,
292
302
  tmuxServerAtRisk