@paradow/ledge 0.8.0 → 0.9.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.
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  attachEnv
3
- } from "./chunk-7AKUPFMS.js";
4
- import "./chunk-ZPANTZKC.js";
3
+ } from "./chunk-N3MPPCON.js";
4
+ import "./chunk-564TJJZ6.js";
5
5
  import {
6
6
  realTmux
7
7
  } from "./chunk-3TNPEAMR.js";
8
8
  import "./chunk-L6IGH4LF.js";
9
9
  import {
10
10
  connect
11
- } from "./chunk-6ZKNEJND.js";
11
+ } from "./chunk-XPWGZIYL.js";
12
12
  import {
13
13
  PROTOCOL_PORT
14
- } from "./chunk-KJWMK65Y.js";
14
+ } from "./chunk-WM4MAV46.js";
15
15
 
16
16
  // src/cli/attach.ts
17
17
  import { spawn } from "child_process";
@@ -6,7 +6,7 @@ import {
6
6
  PROTOCOL_PORT,
7
7
  parseClientMessage,
8
8
  parseServerMessage
9
- } from "./chunk-KJWMK65Y.js";
9
+ } from "./chunk-WM4MAV46.js";
10
10
 
11
11
  // src/cli/bridge.ts
12
12
  import WebSocket from "ws";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-L6IGH4LF.js";
4
4
  import {
5
5
  pickName
6
- } from "./chunk-KJWMK65Y.js";
6
+ } from "./chunk-WM4MAV46.js";
7
7
 
8
8
  // src/daemon/transcript.ts
9
9
  import { closeSync, fstatSync, openSync, readSync } from "fs";
@@ -105,6 +105,10 @@ var CODEX_HOOK_VAR = "LEDGE_CODEX_HOOK_SESSION";
105
105
  function agentIdArgs(tool, id) {
106
106
  return tool === "codex" ? ["-c", `shell_environment_policy.set.${CODEX_HOOK_VAR}="${id}"`] : [];
107
107
  }
108
+ var CLAUDE_OWN_NAME = /^(-n|--name)(=|$)/;
109
+ function agentNameArgs(tool, name, args) {
110
+ return tool === "claude" && !args.some((a) => CLAUDE_OWN_NAME.test(a)) ? ["-n", name] : [];
111
+ }
108
112
  var CLAUDE_OWN_SESSION = /^(--resume|-r|--continue|-c|--session-id|--fork-session)(=|$)/;
109
113
  function claudeSessionId(tool, args) {
110
114
  return tool === "claude" && !args.some((a) => CLAUDE_OWN_SESSION.test(a)) ? randomUUID() : void 0;
@@ -128,7 +132,7 @@ async function spawnSession(opts) {
128
132
  const project = basename(opts.cwd) || opts.cwd;
129
133
  const tmuxSession = `ledge-${id}`;
130
134
  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];
135
+ 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
136
  const env = opts.env ?? process.env;
133
137
  const transcriptPath = agentSessionId ? claudeTranscriptPath(opts.cwd, agentSessionId, env) : void 0;
134
138
  const command = opts.recordExit ? ["sh", "-c", '"$@"; echo $? > "$0"', exitFile(id), ...agent] : agent;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  cleanTopic,
3
3
  sessionTitle
4
- } from "./chunk-KJWMK65Y.js";
4
+ } from "./chunk-WM4MAV46.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;
@@ -235,7 +273,8 @@ function reduceEvent(s, ev) {
235
273
  case "PreToolUse":
236
274
  case "PermissionRequest":
237
275
  case "Notification": {
238
- if (ev.name === "UserPromptSubmit" && ev.text && !s.topic && !crossSession(ev.text)) s = withTopic(s, ev.text, "prompt");
276
+ const notice2 = ev.name === "UserPromptSubmit" && !!ev.text && !!crossSessionNotices(ev.text);
277
+ if (ev.name === "UserPromptSubmit" && ev.text && !s.topic && !notice2 && !crossSession(ev.text)) s = withTopic(s, ev.text, "prompt");
239
278
  const incoming = pendingFor(ev);
240
279
  if (incoming) {
241
280
  if (ev.name === "Notification" && s.pending?.source === "hook") return { ...s, lastHookAt: at, lastActivityAt: at };
@@ -244,7 +283,7 @@ function reduceEvent(s, ev) {
244
283
  return { ...s, state, pending, lastHookAt: at, lastActivityAt: at };
245
284
  }
246
285
  if (ev.name === "Notification" || ev.name === "SessionStart") return { ...s, lastHookAt: at, lastActivityAt: at };
247
- const prompted = ev.name === "UserPromptSubmit" && s.lastReply ? withoutReply(s) : s;
286
+ const prompted = ev.name === "UserPromptSubmit" && s.lastReply && !notice2 ? withoutReply(s) : s;
248
287
  if (prompted.pending?.helperId) return underHelper(prompted, "working", at);
249
288
  if (prompted === s && s.state === "working" && s.lastActivityAt === at && !s.pending) return s;
250
289
  return { ...prompted, state: "working", pending: void 0, lastHookAt: at, lastActivityAt: at };
@@ -252,8 +291,54 @@ function reduceEvent(s, ev) {
252
291
  }
253
292
  }
254
293
 
294
+ // src/daemon/git.ts
295
+ import { execFile } from "child_process";
296
+ import { basename, dirname, resolve } from "path";
297
+ function repositoryId(remote) {
298
+ const value = remote.trim();
299
+ try {
300
+ const url = new URL(value);
301
+ if (!["https:", "http:", "ssh:", "git:"].includes(url.protocol)) return void 0;
302
+ return `${url.hostname.toLowerCase()}${url.pathname.replace(/\.git\/?$/, "").replace(/\/$/, "")}`;
303
+ } catch {
304
+ const scp = value.match(/^(?:[^@\s]+@)?([a-zA-Z0-9.-]+):([^\s]+)$/);
305
+ return scp ? `${scp[1].toLowerCase()}/${scp[2].replace(/\.git$/, "").replace(/\/$/, "")}` : void 0;
306
+ }
307
+ }
308
+ var execGit = (file, args, cwd) => new Promise((res) => {
309
+ execFile(file, args, { cwd, timeout: 3e3, maxBuffer: 1024 * 1024 }, (err, stdout) => res({ code: err ? 1 : 0, stdout: String(stdout) }));
310
+ });
311
+ async function gitInfo(cwd, exec = execGit) {
312
+ const r = await exec("git", ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-common-dir", "--abbrev-ref", "HEAD"], cwd);
313
+ if (r.code !== 0) return null;
314
+ const [toplevel = "", common = "", abbrev = ""] = r.stdout.split("\n").map((l) => l.trim());
315
+ if (!toplevel || !common || !abbrev) return null;
316
+ const worktree = resolve(common) !== resolve(toplevel, ".git");
317
+ const repoPath = worktree ? dirname(resolve(common)) : resolve(toplevel);
318
+ let branch = abbrev;
319
+ if (abbrev === "HEAD") {
320
+ const sha = await exec("git", ["rev-parse", "--short", "HEAD"], cwd);
321
+ if (sha.code !== 0 || !sha.stdout.trim()) return null;
322
+ branch = sha.stdout.trim();
323
+ }
324
+ const remote = await exec("git", ["config", "--get", "remote.origin.url"], cwd);
325
+ const repoId = remote.code === 0 ? repositoryId(remote.stdout) : void 0;
326
+ return { repo: basename(repoPath), repoPath, branch, worktree, ...repoId ? { repoId } : {} };
327
+ }
328
+ async function gitSummary(cwd, exec = execGit) {
329
+ const status = await exec("git", ["status", "--short", "--branch"], cwd);
330
+ if (status.code !== 0) return null;
331
+ const log = await exec("git", ["log", "--oneline", "-10"], cwd);
332
+ const diff = await exec("git", ["diff", "--stat"], cwd);
333
+ return [`# git status --short --branch`, status.stdout.trimEnd(), "", "# git log --oneline -10", log.stdout.trimEnd(), "", "# git diff --stat", diff.stdout.trimEnd() || "(clean)"].join("\n");
334
+ }
335
+
255
336
  export {
256
337
  crossSession,
338
+ crossSessionNotices,
257
339
  QUESTION_TOOLS,
258
- reduce
340
+ reduce,
341
+ repositoryId,
342
+ gitInfo,
343
+ gitSummary
259
344
  };
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var LEDGE_VERSION = true ? "0.9.0" : "0.0.0-dev";
3
+
4
+ export {
5
+ LEDGE_VERSION
6
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  exitFile,
3
3
  spawnSession
4
- } from "./chunk-ZPANTZKC.js";
4
+ } from "./chunk-564TJJZ6.js";
5
5
  import {
6
6
  realTmux
7
7
  } from "./chunk-3TNPEAMR.js";
@@ -11,10 +11,10 @@ import {
11
11
  } from "./chunk-L6IGH4LF.js";
12
12
  import {
13
13
  connect
14
- } from "./chunk-6ZKNEJND.js";
14
+ } from "./chunk-XPWGZIYL.js";
15
15
  import {
16
16
  PROTOCOL_PORT
17
- } from "./chunk-KJWMK65Y.js";
17
+ } from "./chunk-WM4MAV46.js";
18
18
 
19
19
  // src/cli/launch.ts
20
20
  import { spawn } from "child_process";
@@ -1,6 +1,3 @@
1
- // ../protocol/src/messages.ts
2
- import { z as z13 } from "zod";
3
-
4
1
  // ../protocol/src/session.ts
5
2
  import { z } from "zod";
6
3
  var ToolSchema = z.enum(["claude", "codex", "cursor"]);
@@ -83,6 +80,9 @@ function isArchived(s) {
83
80
  return s.archivedAt !== void 0;
84
81
  }
85
82
 
83
+ // ../protocol/src/messages.ts
84
+ import { z as z13 } from "zod";
85
+
86
86
  // ../protocol/src/chat.ts
87
87
  import { z as z2 } from "zod";
88
88
  var ChatStepSchema = z2.object({
@@ -99,6 +99,8 @@ var ChatProposalSchema = z2.object({
99
99
  launchedId: z2.string().optional(),
100
100
  error: z2.string().optional()
101
101
  });
102
+ var MAX_PROPOSALS = 8;
103
+ var ChatProposalsSchema = z2.array(ChatProposalSchema).min(1).max(MAX_PROPOSALS);
102
104
  var ChatRelaySchema = z2.object({
103
105
  id: z2.string().min(1),
104
106
  name: z2.string(),
@@ -126,9 +128,11 @@ var ChatTurnSchema = z2.object({
126
128
  answeredAt: z2.string().datetime().optional(),
127
129
  error: z2.string().optional(),
128
130
  proposal: ChatProposalSchema.optional(),
131
+ proposals: ChatProposalsSchema.optional(),
129
132
  relay: ChatRelaySchema.optional(),
130
133
  projectProposal: ChatProjectProposalSchema.optional()
131
134
  });
135
+ var proposalRows = (t) => t?.proposals ?? (t?.proposal ? [t.proposal] : []);
132
136
 
133
137
  // ../protocol/src/repos.ts
134
138
  import { z as z3 } from "zod";
@@ -144,7 +148,7 @@ var ReposInfoSchema = z3.object({ type: z3.literal("repos.info"), requestId: z3.
144
148
  // ../protocol/src/scan.ts
145
149
  import { z as z4 } from "zod";
146
150
  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() });
151
+ var FoundRepoSchema = z4.object({ path: z4.string(), branch: z4.string().nullable(), remote: z4.string().optional(), manifest: z4.string().max(200).optional() });
148
152
  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
153
 
150
154
  // ../protocol/src/documents.ts
@@ -339,7 +343,24 @@ var ThreadItemSchema = z11.discriminatedUnion("kind", [
339
343
  /** 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
344
  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
345
  /** 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() })
346
+ z11.object({ kind: z11.literal("received"), ...base, from: z11.string(), peer: z11.string().optional(), text: z11.string(), sent: z11.string().optional() }),
347
+ /** An automated notice of the agent's harness about messages between sessions (Claude Code's `[Cross-session idle
348
+ * notice]` / `[Cross-session delivery notice]`): a system event, never the user's words. `name` is the session it is
349
+ * about as quoted, `peer` its Ledge session id; a delivery notice says the outcome of `count` messages to `recipients`. */
350
+ z11.object({
351
+ kind: z11.literal("notice"),
352
+ ...base,
353
+ notice: z11.enum(["idle", "exited", "unavailable", "expired", "delivery", "other"]),
354
+ name: z11.string().optional(),
355
+ peer: z11.string().optional(),
356
+ time: z11.string().optional(),
357
+ summary: z11.string().optional(),
358
+ status: z11.enum(["held", "denied", "expired", "delivered", "refused", "dropped", "unknown"]).optional(),
359
+ count: z11.number().int().positive().optional(),
360
+ recipients: z11.array(z11.string()).optional(),
361
+ reason: z11.string().optional(),
362
+ text: z11.string()
363
+ })
343
364
  ]);
344
365
 
345
366
  // ../protocol/src/decisions.ts
@@ -447,7 +468,7 @@ var ServerMessageSchema = z13.discriminatedUnion("type", [
447
468
  /** 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
469
  z13.object({ type: z13.literal("chat.step"), qid: z13.string(), step: ChatStepSchema }),
449
470
  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() }),
471
+ 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
472
  z13.object({ type: z13.literal("chat.error"), qid: z13.string(), message: z13.string() }),
452
473
  z13.object({ type: z13.literal("chat.history"), turns: z13.array(ChatTurnSchema) }),
453
474
  /** The daemon updating itself: npm is running, then the service restarts (the socket closes right after). */
@@ -486,6 +507,8 @@ var ClientMessageSchema = z13.discriminatedUnion("type", [
486
507
  prompt: z13.string().optional(),
487
508
  continue: z13.boolean().optional(),
488
509
  qid: z13.string().min(1).optional(),
510
+ /** With `qid`: the card's rows to launch (#24), indexes into the turn's `proposals`, each once; the first row without it. */
511
+ 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
512
  /** Folders in addition to `cwd` (Claude: `--add-dir`); resolved like `cwd`. */
490
513
  dirs: z13.array(z13.string().min(1)).max(20).optional(),
491
514
  /** Routing by the fleet, removed before the message reaches the daemon. */
@@ -658,7 +681,7 @@ function extractProposal(answer) {
658
681
  return "";
659
682
  });
660
683
  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 };
684
+ 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
685
  }
663
686
  function parseProposalBlock(raw) {
664
687
  try {
@@ -699,6 +722,8 @@ function sessionTitle(s) {
699
722
  export {
700
723
  SessionSchema,
701
724
  isArchived,
725
+ MAX_PROPOSALS,
726
+ proposalRows,
702
727
  NOTE_NOT_TYPED,
703
728
  offers,
704
729
  pendingKey,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PROTOCOL_PORT,
3
3
  parseServerMessage
4
- } from "./chunk-KJWMK65Y.js";
4
+ } from "./chunk-WM4MAV46.js";
5
5
 
6
6
  // src/client.ts
7
7
  import WebSocket from "ws";