@paradow/ledge 0.8.1 → 0.9.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.
@@ -1,17 +1,18 @@
1
1
  import {
2
2
  attachEnv
3
- } from "./chunk-7AKUPFMS.js";
4
- import "./chunk-ZPANTZKC.js";
3
+ } from "./chunk-65XGYBHL.js";
4
+ import "./chunk-LYTRXJAQ.js";
5
5
  import {
6
6
  realTmux
7
- } from "./chunk-3TNPEAMR.js";
8
- import "./chunk-L6IGH4LF.js";
7
+ } from "./chunk-TO4XO2ML.js";
8
+ import "./chunk-YUSY3E47.js";
9
9
  import {
10
10
  connect
11
- } from "./chunk-6ZKNEJND.js";
11
+ } from "./chunk-IWJAJAPK.js";
12
12
  import {
13
13
  PROTOCOL_PORT
14
- } from "./chunk-KJWMK65Y.js";
14
+ } from "./chunk-KDYAHPYN.js";
15
+ import "./chunk-FGOFRWIB.js";
15
16
 
16
17
  // src/cli/attach.ts
17
18
  import { spawn } from "child_process";
@@ -5,14 +5,20 @@ import {
5
5
  import {
6
6
  PROTOCOL_PORT,
7
7
  parseClientMessage,
8
- parseServerMessage
9
- } from "./chunk-KJWMK65Y.js";
8
+ parseServerMessage,
9
+ readDaemonToken
10
+ } from "./chunk-KDYAHPYN.js";
11
+ import "./chunk-FGOFRWIB.js";
10
12
 
11
13
  // src/cli/bridge.ts
12
14
  import WebSocket from "ws";
15
+ function bearerHeaders() {
16
+ const token = readDaemonToken();
17
+ return token ? { authorization: `Bearer ${token}` } : {};
18
+ }
13
19
  function bridge(opts = {}) {
14
20
  const input = opts.input ?? process.stdin, output = opts.output ?? process.stdout;
15
- const ws = new WebSocket(`ws://127.0.0.1:${opts.port ?? PROTOCOL_PORT}`, { maxPayload: MAX_FRAME, handshakeTimeout: 1e4 });
21
+ const ws = new WebSocket(`ws://127.0.0.1:${opts.port ?? PROTOCOL_PORT}`, { maxPayload: MAX_FRAME, handshakeTimeout: 1e4, headers: bearerHeaders() });
16
22
  return new Promise((resolve, reject) => {
17
23
  let settled = false, off = () => {
18
24
  };
@@ -1,20 +1,23 @@
1
1
  import {
2
2
  exitFile,
3
3
  spawnSession
4
- } from "./chunk-ZPANTZKC.js";
4
+ } from "./chunk-LYTRXJAQ.js";
5
5
  import {
6
6
  realTmux
7
- } from "./chunk-3TNPEAMR.js";
7
+ } from "./chunk-TO4XO2ML.js";
8
8
  import {
9
- ensureLedgeDir,
10
9
  log
11
- } from "./chunk-L6IGH4LF.js";
10
+ } from "./chunk-YUSY3E47.js";
12
11
  import {
12
+ DaemonRefusedError,
13
13
  connect
14
- } from "./chunk-6ZKNEJND.js";
14
+ } from "./chunk-IWJAJAPK.js";
15
15
  import {
16
16
  PROTOCOL_PORT
17
- } from "./chunk-KJWMK65Y.js";
17
+ } from "./chunk-KDYAHPYN.js";
18
+ import {
19
+ ensureLedgeDir
20
+ } from "./chunk-FGOFRWIB.js";
18
21
 
19
22
  // src/cli/launch.ts
20
23
  import { spawn } from "child_process";
@@ -59,6 +62,7 @@ async function probe(port, timeoutMs) {
59
62
  try {
60
63
  client = await connect({ port, timeoutMs });
61
64
  } catch (e) {
65
+ if (e instanceof DaemonRefusedError) return { client: null, state: "unauthorized" };
62
66
  if (e.message === "connect timeout") return { client: null, state: "timeout" };
63
67
  return { client: null, state: await portOpen(port) ? "foreign" : "refused" };
64
68
  }
@@ -80,6 +84,10 @@ async function ensureDaemon(opts = {}) {
80
84
  log(`port ${port} is open but does not speak the Ledge protocol`);
81
85
  return { client: null, reason: "foreign" };
82
86
  }
87
+ if (first.state === "unauthorized") {
88
+ log(`the daemon on port ${port} refused this client's token`);
89
+ return { client: null, reason: "unauthorized" };
90
+ }
83
91
  if (first.state === "timeout") {
84
92
  log(`port ${port} accepted a connection but did not answer within ${timeoutMs} ms`);
85
93
  return { client: null, reason: "unresponsive" };
@@ -142,6 +150,7 @@ async function readExitCode(id) {
142
150
  }
143
151
  var UNTRACKED = {
144
152
  foreign: (port) => `ledge: port ${port} is used by another program, session not tracked`,
153
+ unauthorized: () => "ledge: the daemon refused this client's token (missing or changed), session not tracked; restart the daemon: systemctl --user restart ledge-daemon",
145
154
  unresponsive: () => "ledge: daemon not responding, session not tracked",
146
155
  unreachable: () => "ledge: daemon not running, session not tracked"
147
156
  };
@@ -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") };
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") };
21
21
  }
22
22
  async function ensureLedgeDir() {
23
23
  const p = paths();
@@ -32,18 +32,7 @@ async function ensureLedgeDir() {
32
32
  ${TITLE_LINES}`);
33
33
  }
34
34
 
35
- // src/log.ts
36
- import { appendFileSync } from "fs";
37
- function log(line) {
38
- try {
39
- appendFileSync(paths().log, `${(/* @__PURE__ */ new Date()).toISOString()} | ${line}
40
- `);
41
- } catch {
42
- }
43
- }
44
-
45
35
  export {
46
36
  paths,
47
- ensureLedgeDir,
48
- log
37
+ ensureLedgeDir
49
38
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  cleanTopic,
3
3
  sessionTitle
4
- } from "./chunk-KJWMK65Y.js";
4
+ } from "./chunk-KDYAHPYN.js";
5
5
 
6
6
  // src/daemon/crossSession.ts
7
7
  var FIRST_LINE = /^Another Claude session sent a message:\s*\n/;
@@ -19,6 +19,44 @@ function crossSession(text) {
19
19
  }
20
20
  return out.length ? out : void 0;
21
21
  }
22
+ var NOTICE_HEAD = /^\[Cross-session ([\w -]+?) notice\]/;
23
+ var NOTICE_SPLIT = /\n\s*(?=\[Cross-session [\w -]+? notice\])/;
24
+ var DELIVERY = [
25
+ [/\bheld by that session\b/, "held"],
26
+ [/\bdenied by the recipient\b/, "denied"],
27
+ [/\bnot approved before expiry\b/, "expired"],
28
+ [/\bapproved and released\b/, "delivered"],
29
+ [/\bdropped at that session's inbox\b/, "dropped"],
30
+ [/\b(?:was|were) refused\b/, "refused"]
31
+ ];
32
+ function crossSessionNotices(text) {
33
+ const body = text.trim();
34
+ if (!NOTICE_HEAD.test(body)) return void 0;
35
+ return body.split(NOTICE_SPLIT).map((part) => notice(part.trim()));
36
+ }
37
+ function notice(text) {
38
+ const kind = NOTICE_HEAD.exec(text)[1];
39
+ if (kind === "idle") {
40
+ const peer = /"([^"]*)"/.exec(text)?.[1] ?? "";
41
+ if (/^\[[^\]]*\]\s*No idle signal arrived from "/.test(text)) return { kind: "expired", peer, text };
42
+ const exited = /\bhas exited(?: \(at ([^)]+)\))?/.exec(text);
43
+ if (exited) return { kind: "exited", peer, ...exited[1] ? { time: exited[1] } : {}, text };
44
+ const unavailable = /is not holding your idle subscription(?: \(([^)]*)\))?/.exec(text);
45
+ if (unavailable) return { kind: "unavailable", peer, ...unavailable[1] ? { reason: unavailable[1] } : {}, text };
46
+ const time = /\bfinished a turn at (.+?)\.(?=\s|$)/.exec(text)?.[1];
47
+ const summary = /Its harness reports:\s*«([\s\S]*?)»/.exec(text)?.[1]?.trim();
48
+ return { kind: "idle", peer, ...time ? { time } : {}, ...summary ? { summary } : {}, text };
49
+ }
50
+ if (kind === "delivery") {
51
+ const status = DELIVERY.find(([re]) => re.test(text))?.[1] ?? "unknown";
52
+ const many = /\b(\d+) of your messages\b/.exec(text);
53
+ const to = /\brecipients?:\s*([^;)]*)/.exec(text)?.[1];
54
+ const recipients = to ? to.split(",").map((r) => r.trim()).filter((r) => r && r !== "\u2026") : [];
55
+ const reason = status === "dropped" ? /NOT delivered \(([^)]*)\)/.exec(text)?.[1] : void 0;
56
+ return { kind: "delivery", status, count: many ? Number(many[1]) : 1, recipients, ...reason ? { reason } : {}, text };
57
+ }
58
+ return { kind: "other", text };
59
+ }
22
60
 
23
61
  // src/daemon/state.ts
24
62
  var HOOK_PRIORITY_MS = 2e3;
@@ -63,6 +101,11 @@ function pendingTool(p) {
63
101
  if (p.kind === "plan") return "ExitPlanMode";
64
102
  return p.tool;
65
103
  }
104
+ function answeredBy(p, ev) {
105
+ if (p.source !== "hook" || ev.name !== "PostToolUse" && ev.name !== "PostToolUseFailure") return false;
106
+ const tool = pendingTool(p);
107
+ return tool !== void 0 && ev.tool === tool && ev.helperId === p.helperId;
108
+ }
66
109
  function clearedByToolRun(p, tool) {
67
110
  const t = pendingTool(p);
68
111
  return p.source === "pane" || t === void 0 || t === tool;
@@ -235,16 +278,17 @@ function reduceEvent(s, ev) {
235
278
  case "PreToolUse":
236
279
  case "PermissionRequest":
237
280
  case "Notification": {
238
- if (ev.name === "UserPromptSubmit" && ev.text && !s.topic && !crossSession(ev.text)) s = withTopic(s, ev.text, "prompt");
281
+ const notice2 = ev.name === "UserPromptSubmit" && !!ev.text && !!crossSessionNotices(ev.text);
282
+ if (ev.name === "UserPromptSubmit" && ev.text && !s.topic && !notice2 && !crossSession(ev.text)) s = withTopic(s, ev.text, "prompt");
239
283
  const incoming = pendingFor(ev);
240
284
  if (incoming) {
241
- if (ev.name === "Notification" && s.pending?.source === "hook") return { ...s, lastHookAt: at, lastActivityAt: at };
285
+ if (ev.name === "Notification" && s.pending) return { ...s, lastHookAt: at, lastActivityAt: at };
242
286
  const pending = mergePending(s.pending, incoming);
243
287
  const state = pending.kind === "question" ? "waiting_input" : "waiting_permission";
244
288
  return { ...s, state, pending, lastHookAt: at, lastActivityAt: at };
245
289
  }
246
290
  if (ev.name === "Notification" || ev.name === "SessionStart") return { ...s, lastHookAt: at, lastActivityAt: at };
247
- const prompted = ev.name === "UserPromptSubmit" && s.lastReply ? withoutReply(s) : s;
291
+ const prompted = ev.name === "UserPromptSubmit" && s.lastReply && !notice2 ? withoutReply(s) : s;
248
292
  if (prompted.pending?.helperId) return underHelper(prompted, "working", at);
249
293
  if (prompted === s && s.state === "working" && s.lastActivityAt === at && !s.pending) return s;
250
294
  return { ...prompted, state: "working", pending: void 0, lastHookAt: at, lastActivityAt: at };
@@ -252,8 +296,56 @@ function reduceEvent(s, ev) {
252
296
  }
253
297
  }
254
298
 
299
+ // src/daemon/git.ts
300
+ import { execFile } from "child_process";
301
+ import { basename, dirname, resolve } from "path";
302
+ function repositoryId(remote) {
303
+ const value = remote.trim();
304
+ try {
305
+ const url = new URL(value);
306
+ if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return void 0;
307
+ return `${url.hostname.toLowerCase()}${url.pathname.replace(/\.git\/?$/, "").replace(/\/$/, "")}`;
308
+ } catch {
309
+ const scp = value.match(/^(?:[^@\s]+@)?([a-zA-Z0-9.-]+):([^\s]+)$/);
310
+ return scp ? `${scp[1].toLowerCase()}/${scp[2].replace(/\.git$/, "").replace(/\/$/, "")}` : void 0;
311
+ }
312
+ }
313
+ var execGit = (file, args, cwd) => new Promise((res) => {
314
+ execFile(file, args, { cwd, timeout: 3e3, maxBuffer: 1024 * 1024 }, (err, stdout) => res({ code: err ? 1 : 0, stdout: String(stdout) }));
315
+ });
316
+ async function gitInfo(cwd, exec = execGit) {
317
+ const r = await exec("git", ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-common-dir", "--abbrev-ref", "HEAD"], cwd);
318
+ if (r.code !== 0) return null;
319
+ const [toplevel = "", common = "", abbrev = ""] = r.stdout.split("\n").map((l) => l.trim());
320
+ if (!toplevel || !common || !abbrev) return null;
321
+ const worktree = resolve(common) !== resolve(toplevel, ".git");
322
+ const repoPath = worktree ? dirname(resolve(common)) : resolve(toplevel);
323
+ let branch = abbrev;
324
+ if (abbrev === "HEAD") {
325
+ const sha = await exec("git", ["rev-parse", "--short", "HEAD"], cwd);
326
+ if (sha.code !== 0 || !sha.stdout.trim()) return null;
327
+ branch = sha.stdout.trim();
328
+ }
329
+ const remote = await exec("git", ["config", "--get", "remote.origin.url"], cwd);
330
+ const repoId = remote.code === 0 ? repositoryId(remote.stdout) : void 0;
331
+ return { repo: basename(repoPath), repoPath, branch, worktree, ...repoId ? { repoId } : {} };
332
+ }
333
+ async function gitSummary(cwd, exec = execGit) {
334
+ const status = await exec("git", ["status", "--short", "--branch"], cwd);
335
+ if (status.code !== 0) return null;
336
+ const log = await exec("git", ["log", "--oneline", "-10"], cwd);
337
+ const diff = await exec("git", ["diff", "--stat"], cwd);
338
+ return [`# git status --short --branch`, status.stdout.trimEnd(), "", "# git log --oneline -10", log.stdout.trimEnd(), "", "# git diff --stat", diff.stdout.trimEnd() || "(clean)"].join("\n");
339
+ }
340
+
255
341
  export {
256
342
  crossSession,
343
+ crossSessionNotices,
257
344
  QUESTION_TOOLS,
258
- reduce
345
+ pendingTool,
346
+ answeredBy,
347
+ reduce,
348
+ repositoryId,
349
+ gitInfo,
350
+ gitSummary
259
351
  };
@@ -1,14 +1,23 @@
1
1
  import {
2
2
  PROTOCOL_PORT,
3
- parseServerMessage
4
- } from "./chunk-KJWMK65Y.js";
3
+ parseServerMessage,
4
+ readDaemonToken
5
+ } from "./chunk-KDYAHPYN.js";
5
6
 
6
7
  // src/client.ts
7
8
  import WebSocket from "ws";
9
+ var DaemonRefusedError = class extends Error {
10
+ };
8
11
  function connect(opts = {}) {
9
12
  const port = opts.port ?? PROTOCOL_PORT;
10
13
  const host = opts.host ?? "127.0.0.1";
11
- const ws = new WebSocket(`ws://${host}:${port}`);
14
+ let token;
15
+ try {
16
+ token = opts.token === void 0 ? readDaemonToken() : opts.token;
17
+ } catch {
18
+ token = null;
19
+ }
20
+ const ws = new WebSocket(`ws://${host}:${port}`, token ? { headers: { authorization: `Bearer ${token}` } } : {});
12
21
  const queue = [];
13
22
  const MAX_QUEUED = 100;
14
23
  const waiters = [];
@@ -66,12 +75,13 @@ function connect(opts = {}) {
66
75
  if (!settled) {
67
76
  settled = true;
68
77
  clearTimeout(t);
69
- reject(e);
78
+ reject(/Unexpected server response: 401/.test(e.message) ? new DaemonRefusedError("daemon refused the token") : e);
70
79
  }
71
80
  });
72
81
  });
73
82
  }
74
83
 
75
84
  export {
85
+ DaemonRefusedError,
76
86
  connect
77
87
  };
@@ -1,5 +1,125 @@
1
- // ../protocol/src/messages.ts
2
- import { z as z13 } from "zod";
1
+ import {
2
+ paths
3
+ } from "./chunk-FGOFRWIB.js";
4
+
5
+ // src/auth.ts
6
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
7
+ import { chmod, link, mkdir, readFile, unlink, writeFile } from "fs/promises";
8
+ import { readFileSync } from "fs";
9
+ import { dirname } from "path";
10
+ var TOKEN_RE = /^[0-9a-f]{64}$/;
11
+ var DAEMON_PROTOCOL_PREFIX = "ledge.";
12
+ function isDaemonToken(value) {
13
+ return TOKEN_RE.test(value);
14
+ }
15
+ function tokenEquals(presented, expected) {
16
+ const a = createHash("sha256").update(presented.slice(0, 256), "utf8").digest();
17
+ const b = createHash("sha256").update(expected, "utf8").digest();
18
+ return timingSafeEqual(a, b);
19
+ }
20
+ function hostAllowed(hostHeader) {
21
+ const raw = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
22
+ if (!raw || raw.length > 255) return false;
23
+ const host = raw.trim().toLowerCase();
24
+ if (!host || host.includes(" ") || host.endsWith(".")) return false;
25
+ let name;
26
+ if (host.startsWith("[")) {
27
+ const end = host.indexOf("]");
28
+ if (end < 0) return false;
29
+ name = host.slice(1, end);
30
+ const rest = host.slice(end + 1);
31
+ if (rest !== "" && !/^:\d{1,5}$/.test(rest)) return false;
32
+ } else {
33
+ const colon = host.lastIndexOf(":");
34
+ if (colon >= 0) {
35
+ if (!/^\d{1,5}$/.test(host.slice(colon + 1))) return false;
36
+ name = host.slice(0, colon);
37
+ } else name = host;
38
+ }
39
+ return name === "localhost" || name === "127.0.0.1" || name === "::1";
40
+ }
41
+ function headerValue(value, max) {
42
+ const raw = Array.isArray(value) ? value.join(",") : value;
43
+ return !raw || raw.length > max ? "" : raw;
44
+ }
45
+ function bearerToken(authorization) {
46
+ if (!authorization || authorization.length > 512) return "";
47
+ return /^Bearer[ \t]+(\S+)$/i.exec(authorization)?.[1] ?? "";
48
+ }
49
+ function protocolToken(header) {
50
+ const raw = headerValue(header, 512);
51
+ if (!raw) return "";
52
+ for (const part of raw.split(",").slice(0, 8)) {
53
+ const protocol = part.trim();
54
+ if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX)) return protocol.slice(DAEMON_PROTOCOL_PREFIX.length);
55
+ }
56
+ return "";
57
+ }
58
+ function requestAuthorized(headers, expected) {
59
+ const bearerOk = tokenEquals(bearerToken(typeof headers.authorization === "string" ? headers.authorization : void 0), expected);
60
+ const presented = protocolToken(headers["sec-websocket-protocol"]);
61
+ return bearerOk || presented !== "" && tokenEquals(presented, expected);
62
+ }
63
+ function selectDaemonProtocol(protocols, expected) {
64
+ for (const protocol of protocols) {
65
+ if (protocol.startsWith(DAEMON_PROTOCOL_PREFIX) && tokenEquals(protocol.slice(DAEMON_PROTOCOL_PREFIX.length), expected)) return protocol;
66
+ }
67
+ return false;
68
+ }
69
+ async function readTokenFile(file) {
70
+ try {
71
+ const raw = (await readFile(file, "utf8")).trim();
72
+ return isDaemonToken(raw) ? raw : void 0;
73
+ } catch (e) {
74
+ if (e.code === "ENOENT") return void 0;
75
+ throw e;
76
+ }
77
+ }
78
+ async function lockTokenFile(file) {
79
+ await chmod(file, 384);
80
+ }
81
+ async function ensureDaemonToken() {
82
+ const file = paths().token;
83
+ await mkdir(dirname(file), { recursive: true });
84
+ const current = await readTokenFile(file);
85
+ if (current) {
86
+ await lockTokenFile(file);
87
+ return current;
88
+ }
89
+ const token = randomBytes(32).toString("hex");
90
+ const tmp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
91
+ await writeFile(tmp, `${token}
92
+ `, { mode: 384, flag: "wx" });
93
+ await chmod(tmp, 384);
94
+ try {
95
+ await link(tmp, file);
96
+ } catch (e) {
97
+ await unlink(tmp).catch(() => {
98
+ });
99
+ if (e.code !== "EEXIST") throw e;
100
+ const raced = await readTokenFile(file);
101
+ if (raced) {
102
+ await lockTokenFile(file);
103
+ return raced;
104
+ }
105
+ await writeFile(file, `${token}
106
+ `, { mode: 384 });
107
+ }
108
+ await unlink(tmp).catch(() => {
109
+ });
110
+ await lockTokenFile(file);
111
+ const stored = await readTokenFile(file);
112
+ if (!stored) throw new Error("daemon token was not stored");
113
+ return stored;
114
+ }
115
+ function readDaemonToken() {
116
+ try {
117
+ const raw = readFileSync(paths().token, "utf8").trim();
118
+ return isDaemonToken(raw) ? raw : void 0;
119
+ } catch {
120
+ return void 0;
121
+ }
122
+ }
3
123
 
4
124
  // ../protocol/src/session.ts
5
125
  import { z } from "zod";
@@ -83,6 +203,9 @@ function isArchived(s) {
83
203
  return s.archivedAt !== void 0;
84
204
  }
85
205
 
206
+ // ../protocol/src/messages.ts
207
+ import { z as z13 } from "zod";
208
+
86
209
  // ../protocol/src/chat.ts
87
210
  import { z as z2 } from "zod";
88
211
  var ChatStepSchema = z2.object({
@@ -99,6 +222,8 @@ var ChatProposalSchema = z2.object({
99
222
  launchedId: z2.string().optional(),
100
223
  error: z2.string().optional()
101
224
  });
225
+ var MAX_PROPOSALS = 8;
226
+ var ChatProposalsSchema = z2.array(ChatProposalSchema).min(1).max(MAX_PROPOSALS);
102
227
  var ChatRelaySchema = z2.object({
103
228
  id: z2.string().min(1),
104
229
  name: z2.string(),
@@ -126,9 +251,11 @@ var ChatTurnSchema = z2.object({
126
251
  answeredAt: z2.string().datetime().optional(),
127
252
  error: z2.string().optional(),
128
253
  proposal: ChatProposalSchema.optional(),
254
+ proposals: ChatProposalsSchema.optional(),
129
255
  relay: ChatRelaySchema.optional(),
130
256
  projectProposal: ChatProjectProposalSchema.optional()
131
257
  });
258
+ var proposalRows = (t) => t?.proposals ?? (t?.proposal ? [t.proposal] : []);
132
259
 
133
260
  // ../protocol/src/repos.ts
134
261
  import { z as z3 } from "zod";
@@ -144,7 +271,7 @@ var ReposInfoSchema = z3.object({ type: z3.literal("repos.info"), requestId: z3.
144
271
  // ../protocol/src/scan.ts
145
272
  import { z as z4 } from "zod";
146
273
  var ReposScanSchema = z4.object({ type: z4.literal("repos.scan"), requestId: z4.string().min(1).max(100), machineId: z4.string().optional() });
147
- var FoundRepoSchema = z4.object({ path: z4.string(), branch: z4.string().nullable(), remote: z4.string().optional() });
274
+ var FoundRepoSchema = z4.object({ path: z4.string(), branch: z4.string().nullable(), remote: z4.string().optional(), manifest: z4.string().max(200).optional() });
148
275
  var ReposFoundSchema = z4.object({ type: z4.literal("repos.found"), requestId: z4.string(), repos: z4.array(FoundRepoSchema).max(500), truncated: z4.boolean(), error: z4.string().optional() });
149
276
 
150
277
  // ../protocol/src/documents.ts
@@ -339,7 +466,24 @@ var ThreadItemSchema = z11.discriminatedUnion("kind", [
339
466
  /** 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. */
340
467
  z11.object({ kind: z11.literal("sent"), ...base, to: z11.string(), peer: z11.string().optional(), summary: z11.string().optional(), text: z11.string(), status: z11.enum(["sending", "delivered", "read", "failed"]), received: z11.string().optional() }),
341
468
  /** A message another agent sent to this session; `from` is its from-name (else its address), `sent` the paired item's id in the peer's thread. */
342
- z11.object({ kind: z11.literal("received"), ...base, from: z11.string(), peer: z11.string().optional(), text: z11.string(), sent: z11.string().optional() })
469
+ z11.object({ kind: z11.literal("received"), ...base, from: z11.string(), peer: z11.string().optional(), text: z11.string(), sent: z11.string().optional() }),
470
+ /** An automated notice of the agent's harness about messages between sessions (Claude Code's `[Cross-session idle
471
+ * notice]` / `[Cross-session delivery notice]`): a system event, never the user's words. `name` is the session it is
472
+ * about as quoted, `peer` its Ledge session id; a delivery notice says the outcome of `count` messages to `recipients`. */
473
+ z11.object({
474
+ kind: z11.literal("notice"),
475
+ ...base,
476
+ notice: z11.enum(["idle", "exited", "unavailable", "expired", "delivery", "other"]),
477
+ name: z11.string().optional(),
478
+ peer: z11.string().optional(),
479
+ time: z11.string().optional(),
480
+ summary: z11.string().optional(),
481
+ status: z11.enum(["held", "denied", "expired", "delivered", "refused", "dropped", "unknown"]).optional(),
482
+ count: z11.number().int().positive().optional(),
483
+ recipients: z11.array(z11.string()).optional(),
484
+ reason: z11.string().optional(),
485
+ text: z11.string()
486
+ })
343
487
  ]);
344
488
 
345
489
  // ../protocol/src/decisions.ts
@@ -447,7 +591,7 @@ var ServerMessageSchema = z13.discriminatedUnion("type", [
447
591
  /** 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. */
448
592
  z13.object({ type: z13.literal("chat.step"), qid: z13.string(), step: ChatStepSchema }),
449
593
  z13.object({ type: z13.literal("chat.delta"), qid: z13.string(), text: z13.string() }),
450
- z13.object({ type: z13.literal("chat.done"), qid: z13.string(), text: z13.string(), at: z13.string().datetime(), proposal: ChatProposalSchema.optional(), relay: ChatRelaySchema.optional(), projectProposal: ChatProjectProposalSchema.optional() }),
594
+ z13.object({ type: z13.literal("chat.done"), qid: z13.string(), text: z13.string(), at: z13.string().datetime(), proposal: ChatProposalSchema.optional(), proposals: ChatProposalsSchema.optional(), relay: ChatRelaySchema.optional(), projectProposal: ChatProjectProposalSchema.optional() }),
451
595
  z13.object({ type: z13.literal("chat.error"), qid: z13.string(), message: z13.string() }),
452
596
  z13.object({ type: z13.literal("chat.history"), turns: z13.array(ChatTurnSchema) }),
453
597
  /** The daemon updating itself: npm is running, then the service restarts (the socket closes right after). */
@@ -486,6 +630,8 @@ var ClientMessageSchema = z13.discriminatedUnion("type", [
486
630
  prompt: z13.string().optional(),
487
631
  continue: z13.boolean().optional(),
488
632
  qid: z13.string().min(1).optional(),
633
+ /** With `qid`: the card's rows to launch (#24), indexes into the turn's `proposals`, each once; the first row without it. */
634
+ rows: z13.array(z13.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(),
489
635
  /** Folders in addition to `cwd` (Claude: `--add-dir`); resolved like `cwd`. */
490
636
  dirs: z13.array(z13.string().min(1)).max(20).optional(),
491
637
  /** Routing by the fleet, removed before the message reaches the daemon. */
@@ -501,7 +647,14 @@ var ClientMessageSchema = z13.discriminatedUnion("type", [
501
647
  /** Copied into the `folder.listing` that answers; the fleet relays only that listing, from the machine asked. */
502
648
  requestId: z13.string().min(1).max(100).optional()
503
649
  }),
504
- z13.object({ type: z13.literal("folder.create"), parent: z13.string().min(1), name: z13.string().min(1) }),
650
+ z13.object({
651
+ type: z13.literal("folder.create"),
652
+ parent: z13.string().min(1),
653
+ name: z13.string().min(1),
654
+ /** As for `folder.list`: routing by the fleet, and the request its `folder.listing` answers. */
655
+ machineId: z13.string().optional(),
656
+ requestId: z13.string().min(1).max(100).optional()
657
+ }),
505
658
  z13.object({ type: z13.literal("session.reply"), id: z13.string(), text: z13.string().min(1) }),
506
659
  /** The chat card's instruction (spec 2026-10-12 relay §4): sent now if the session is free, else queued until it is. */
507
660
  z13.object({ type: z13.literal("session.relay"), qid: z13.string().min(1) }),
@@ -658,7 +811,7 @@ function extractProposal(answer) {
658
811
  return "";
659
812
  });
660
813
  const last = found.at(-1);
661
- return last ? { text: text.replace(/\n{3,}/g, "\n\n").trim(), kind: last.kind, raw: last.raw } : { text: answer, kind: null, raw: null };
814
+ return last ? { text: text.replace(/\n{3,}/g, "\n\n").trim(), kind: last.kind, raw: last.raw, blocks: found } : { text: answer, kind: null, raw: null, blocks: [] };
662
815
  }
663
816
  function parseProposalBlock(raw) {
664
817
  try {
@@ -697,8 +850,15 @@ function sessionTitle(s) {
697
850
  }
698
851
 
699
852
  export {
853
+ hostAllowed,
854
+ requestAuthorized,
855
+ selectDaemonProtocol,
856
+ ensureDaemonToken,
857
+ readDaemonToken,
700
858
  SessionSchema,
701
859
  isArchived,
860
+ MAX_PROPOSALS,
861
+ proposalRows,
702
862
  NOTE_NOT_TYPED,
703
863
  offers,
704
864
  pendingKey,
@@ -1,9 +1,9 @@
1
- import {
2
- paths
3
- } from "./chunk-L6IGH4LF.js";
4
1
  import {
5
2
  pickName
6
- } from "./chunk-KJWMK65Y.js";
3
+ } from "./chunk-KDYAHPYN.js";
4
+ import {
5
+ paths
6
+ } from "./chunk-FGOFRWIB.js";
7
7
 
8
8
  // src/daemon/transcript.ts
9
9
  import { closeSync, fstatSync, openSync, readSync } from "fs";
@@ -95,6 +95,33 @@ function claudePlan(path) {
95
95
  }
96
96
  return plan?.text;
97
97
  }
98
+ function claudeCalls(path) {
99
+ const calls = [];
100
+ for (const line of tailLines(path)) {
101
+ let e;
102
+ try {
103
+ e = JSON.parse(line);
104
+ } catch {
105
+ continue;
106
+ }
107
+ if (e.isSidechain === true || !Array.isArray(e.message?.content)) continue;
108
+ for (const b of e.message.content) {
109
+ if (e.type === "assistant" && b?.type === "tool_use" && typeof b.id === "string" && typeof b.name === "string") calls.push({ id: b.id, name: b.name, answered: false });
110
+ else if (e.type === "user" && b?.type === "tool_result") {
111
+ const c = calls.find((c2) => c2.id === b.tool_use_id);
112
+ if (c) c.answered = true;
113
+ }
114
+ }
115
+ }
116
+ return calls;
117
+ }
118
+ function claudeOpenCall(path, tool) {
119
+ const last = claudeCalls(path).filter((c) => c.name === tool).at(-1);
120
+ return last && !last.answered ? last.id : void 0;
121
+ }
122
+ function claudeToolAnswered(path, id) {
123
+ return claudeCalls(path).some((c) => c.id === id && c.answered);
124
+ }
98
125
 
99
126
  // src/spawn.ts
100
127
  import { basename, join as join2 } from "path";
@@ -105,6 +132,10 @@ var CODEX_HOOK_VAR = "LEDGE_CODEX_HOOK_SESSION";
105
132
  function agentIdArgs(tool, id) {
106
133
  return tool === "codex" ? ["-c", `shell_environment_policy.set.${CODEX_HOOK_VAR}="${id}"`] : [];
107
134
  }
135
+ var CLAUDE_OWN_NAME = /^(-n|--name)(=|$)/;
136
+ function agentNameArgs(tool, name, args) {
137
+ return tool === "claude" && !args.some((a) => CLAUDE_OWN_NAME.test(a)) ? ["-n", name] : [];
138
+ }
108
139
  var CLAUDE_OWN_SESSION = /^(--resume|-r|--continue|-c|--session-id|--fork-session)(=|$)/;
109
140
  function claudeSessionId(tool, args) {
110
141
  return tool === "claude" && !args.some((a) => CLAUDE_OWN_SESSION.test(a)) ? randomUUID() : void 0;
@@ -128,7 +159,7 @@ async function spawnSession(opts) {
128
159
  const project = basename(opts.cwd) || opts.cwd;
129
160
  const tmuxSession = `ledge-${id}`;
130
161
  const agentSessionId = opts.command ? void 0 : claudeSessionId(opts.tool, opts.args);
131
- const agent = opts.command ?? [AGENT_BIN[opts.tool], ...agentIdArgs(opts.tool, id), ...agentSessionId ? ["--session-id", agentSessionId] : [], ...opts.args];
162
+ const agent = opts.command ?? [AGENT_BIN[opts.tool], ...agentIdArgs(opts.tool, id), ...agentSessionId ? ["--session-id", agentSessionId] : [], ...agentNameArgs(opts.tool, name, opts.args), ...opts.args];
132
163
  const env = opts.env ?? process.env;
133
164
  const transcriptPath = agentSessionId ? claudeTranscriptPath(opts.cwd, agentSessionId, env) : void 0;
134
165
  const command = opts.recordExit ? ["sh", "-c", '"$@"; echo $? > "$0"', exitFile(id), ...agent] : agent;
@@ -141,6 +172,8 @@ export {
141
172
  agentMessages,
142
173
  codexPlan,
143
174
  claudePlan,
175
+ claudeOpenCall,
176
+ claudeToolAnswered,
144
177
  CODEX_ENV_VAR,
145
178
  CODEX_HOOK_VAR,
146
179
  exitFile,
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  run
3
- } from "./chunk-3TNPEAMR.js";
3
+ } from "./chunk-TO4XO2ML.js";
4
4
  import {
5
5
  ensureLedgeDir,
6
6
  paths
7
- } from "./chunk-L6IGH4LF.js";
7
+ } from "./chunk-FGOFRWIB.js";
8
8
 
9
9
  // src/cli/setup.ts
10
10
  import { readFile as readFile2, writeFile, mkdir, rename, realpath, rm, stat, lstat, readlink, symlink, unlink } from "fs/promises";
@@ -1,7 +1,9 @@
1
1
  import {
2
- log,
2
+ log
3
+ } from "./chunk-YUSY3E47.js";
4
+ import {
3
5
  paths
4
- } from "./chunk-L6IGH4LF.js";
6
+ } from "./chunk-FGOFRWIB.js";
5
7
 
6
8
  // src/tmux.ts
7
9
  import { execFile } from "child_process";