@paradow/ledge 0.5.1 → 0.5.3

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/README.md CHANGED
@@ -12,7 +12,7 @@ npm i -g @paradow/ledge && ledge setup
12
12
 
13
13
  ## Update
14
14
 
15
- From the Ledge island: the card in the list updates the daemon, then the Windows app. By hand:
15
+ From the Ledge island: the card in the list updates the connected servers' daemons, then the WSL daemon, then the Windows app. By hand, on each machine:
16
16
 
17
17
  ```sh
18
18
  npm i -g @paradow/ledge@latest && systemctl --user restart ledge-daemon
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  attachEnv
3
- } from "./chunk-QHDPDZOJ.js";
4
- import "./chunk-U4A4XKDM.js";
3
+ } from "./chunk-ISPFJBJM.js";
4
+ import "./chunk-U4RU6V35.js";
5
5
  import {
6
6
  realTmux
7
7
  } from "./chunk-XZMLFV6H.js";
8
8
  import "./chunk-P2SHPHAN.js";
9
9
  import {
10
10
  connect
11
- } from "./chunk-5HVWJTTJ.js";
11
+ } from "./chunk-IDGN7JGC.js";
12
12
  import {
13
13
  PROTOCOL_PORT
14
- } from "./chunk-Y4ODVTTN.js";
14
+ } from "./chunk-E6ZQOGY4.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-Y4ODVTTN.js";
9
+ } from "./chunk-E6ZQOGY4.js";
10
10
 
11
11
  // src/cli/bridge.ts
12
12
  import WebSocket from "ws";
@@ -8,6 +8,8 @@ var SessionStateSchema = z.enum(["starting", "working", "waiting_input", "waitin
8
8
  var PendingSchema = z.object({
9
9
  kind: z.enum(["question", "permission", "plan", "diff"]),
10
10
  text: z.string(),
11
+ /** What to review in full before deciding, when `text` is only its summary: a plan's markdown. */
12
+ body: z.string().optional(),
11
13
  tool: z.string().optional(),
12
14
  options: z.array(z.string()).optional(),
13
15
  /** Each option's description, by index ('' when it has none); left out when no option has one. */
@@ -20,7 +22,9 @@ var PendingSchema = z.object({
20
22
  });
21
23
  var ReplySchema = z.object({
22
24
  text: z.string(),
23
- at: z.string().datetime()
25
+ at: z.string().datetime(),
26
+ /** The reply is a plan the agent proposes (Codex Plan mode): nothing in it is done yet. */
27
+ plan: z.boolean().optional()
24
28
  });
25
29
  var HelperSchema = z.object({
26
30
  id: z.string(),
@@ -56,6 +60,10 @@ var SessionSchema = z.object({
56
60
  startedAt: z.string().datetime(),
57
61
  lastActivityAt: z.string().datetime(),
58
62
  lastHookAt: z.string().datetime().optional(),
63
+ /** The agent's own session id when Ledge chose it at launch (Claude Code `--session-id`). */
64
+ agentSessionId: z.string().optional(),
65
+ /** The agent's transcript: the latest one a hook named (it follows /clear and --resume), else the one the launch implies. */
66
+ transcriptPath: z.string().optional(),
59
67
  exitCode: z.number().int().nullable().optional(),
60
68
  exitedAt: z.string().datetime().optional(),
61
69
  lastError: z.string().optional(),
@@ -195,6 +203,8 @@ var SessionEventSchema = z7.object({
195
203
  /** Each option's description, by index ('' when it has none); left out when no option has one. */
196
204
  optionDetails: z7.array(z7.string()).optional(),
197
205
  kind: z7.enum(["question", "permission", "plan", "diff"]).optional(),
206
+ /** PanePrompt: what to review in full before deciding (a plan read in the terminal), when `text` is its summary. */
207
+ body: z7.string().optional(),
198
208
  exitCode: z7.number().int().nullable().optional(),
199
209
  helperId: z7.string().optional(),
200
210
  /** The helper's description, once known (Claude Code: its `.meta.json`, written after SubagentStart). */
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  PROTOCOL_PORT,
3
3
  parseServerMessage
4
- } from "./chunk-Y4ODVTTN.js";
4
+ } from "./chunk-E6ZQOGY4.js";
5
5
 
6
6
  // src/client.ts
7
7
  import WebSocket from "ws";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  exitFile,
3
3
  spawnSession
4
- } from "./chunk-U4A4XKDM.js";
4
+ } from "./chunk-U4RU6V35.js";
5
5
  import {
6
6
  realTmux
7
7
  } from "./chunk-XZMLFV6H.js";
@@ -11,10 +11,10 @@ import {
11
11
  } from "./chunk-P2SHPHAN.js";
12
12
  import {
13
13
  connect
14
- } from "./chunk-5HVWJTTJ.js";
14
+ } from "./chunk-IDGN7JGC.js";
15
15
  import {
16
16
  PROTOCOL_PORT
17
- } from "./chunk-Y4ODVTTN.js";
17
+ } from "./chunk-E6ZQOGY4.js";
18
18
 
19
19
  // src/cli/launch.ts
20
20
  import { spawn } from "child_process";
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var LEDGE_VERSION = true ? "0.5.3" : "0.0.0-dev";
3
+
4
+ export {
5
+ LEDGE_VERSION
6
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  cleanTopic,
3
3
  sessionTitle
4
- } from "./chunk-Y4ODVTTN.js";
4
+ } from "./chunk-E6ZQOGY4.js";
5
5
 
6
6
  // src/daemon/state.ts
7
7
  var HOOK_PRIORITY_MS = 2e3;
@@ -18,13 +18,14 @@ function pendingFor(ev) {
18
18
  return { kind: "question", text: ev.text ?? "", options: ev.options, ...details(ev), source: "hook", ...ev.tool !== "AskUserQuestion" ? { tool: ev.tool } : {} };
19
19
  }
20
20
  if (ev.name === "PreToolUse" && ev.tool === "ExitPlanMode") {
21
- return { kind: "plan", text: ev.text ?? "", source: "hook" };
21
+ const plan = ev.toolInput?.plan;
22
+ return { kind: "plan", text: ev.text ?? "", ...typeof plan === "string" && plan.trim() ? { body: plan } : {}, source: "hook" };
22
23
  }
23
24
  if (ev.name === "Notification" && ev.kind === "permission") {
24
25
  return { kind: "permission", text: ev.text ?? "", source: "hook" };
25
26
  }
26
27
  if (ev.name === "PanePrompt") {
27
- return { kind: ev.kind ?? "permission", text: ev.text ?? "", options: ev.options, ...details(ev), source: "pane" };
28
+ return { kind: ev.kind ?? "permission", text: ev.text ?? "", ...ev.body ? { body: ev.body } : {}, options: ev.options, ...details(ev), source: "pane" };
28
29
  }
29
30
  return void 0;
30
31
  }
@@ -139,6 +140,7 @@ function reduce(s, ev) {
139
140
  const lastHookAt = s.lastHookAt;
140
141
  if (ev.name === "SubagentStart" || ev.name === "SubagentStop") return ev.helperId ? reduceHelper(s, { ...ev, helperId: ev.helperId }) : s;
141
142
  if (ev.helperId && ev.source === "hook" && ev.name !== "SessionEnd") return reduceHelper(s, { ...ev, helperId: ev.helperId });
143
+ if (ev.source === "hook" && ev.transcriptPath && ev.transcriptPath !== s.transcriptPath) s = { ...s, transcriptPath: ev.transcriptPath };
142
144
  switch (ev.name) {
143
145
  case "SessionEnd":
144
146
  return { ...s, state: "exited", pending: void 0, exitCode: ev.exitCode ?? null, exitedAt: s.exitedAt ?? at, lastActivityAt: at };
@@ -167,7 +169,7 @@ function reduce(s, ev) {
167
169
  return { ...s, state: "working", pending: void 0, lastError: void 0, lastHookAt: at, lastActivityAt: at };
168
170
  }
169
171
  case "Stop": {
170
- const lastReply = ev.text ? { text: ev.text, at } : s.lastReply;
172
+ const lastReply = ev.text ? { text: ev.text, at, ...ev.kind === "plan" ? { plan: true } : {} } : s.lastReply;
171
173
  if (s.pending?.helperId) return underHelper(s, "idle", at, { lastReply });
172
174
  return { ...s, state: "idle", pending: void 0, lastReply, lastHookAt: at, lastActivityAt: at };
173
175
  }
@@ -187,7 +189,8 @@ function reduce(s, ev) {
187
189
  case "PanePrompt": {
188
190
  if (s.pending?.source === "hook") return s;
189
191
  if (hooksReportPrompts(s) && lastHookAt && Date.parse(at) - Date.parse(lastHookAt) < HOOK_PRIORITY_MS) return s;
190
- const pending = mergePending(s.pending, pendingFor(ev));
192
+ let pending = mergePending(s.pending, pendingFor(ev));
193
+ if (pending.kind === "plan" && !pending.body && s.tool === "codex" && s.lastReply) pending = { ...pending, body: s.lastReply.text };
191
194
  if (s.pending && samePending(s.pending, pending)) return s;
192
195
  const state = pending.kind === "question" ? "waiting_input" : "waiting_permission";
193
196
  return { ...s, state, pending, lastActivityAt: at };
@@ -0,0 +1,149 @@
1
+ import {
2
+ paths
3
+ } from "./chunk-P2SHPHAN.js";
4
+ import {
5
+ pickName
6
+ } from "./chunk-E6ZQOGY4.js";
7
+
8
+ // src/daemon/transcript.ts
9
+ import { closeSync, fstatSync, openSync, readSync } from "fs";
10
+ import { homedir } from "os";
11
+ import { join } from "path";
12
+ var TAIL_BYTES = 512 * 1024;
13
+ function tailLines(path, bytes = TAIL_BYTES) {
14
+ let fd;
15
+ try {
16
+ fd = openSync(path, "r");
17
+ } catch {
18
+ return [];
19
+ }
20
+ try {
21
+ const size = fstatSync(fd).size;
22
+ const start = Math.max(0, size - bytes);
23
+ const buf = Buffer.alloc(size - start);
24
+ readSync(fd, buf, 0, buf.length, start);
25
+ const text = buf.toString("utf8");
26
+ const lines = text.split("\n");
27
+ if (start > 0) lines.shift();
28
+ if (!text.endsWith("\n")) lines.pop();
29
+ return lines.filter((l) => l.trim());
30
+ } finally {
31
+ closeSync(fd);
32
+ }
33
+ }
34
+ var texts = (blocks, kind) => (Array.isArray(blocks) ? blocks : []).filter((b) => b?.type === kind && typeof b.text === "string").map((b) => b.text.trim()).filter(Boolean).join("\n\n");
35
+ function agentMessages(path, opts = {}) {
36
+ const out = [];
37
+ for (const line of tailLines(path, opts.bytes ?? TAIL_BYTES)) {
38
+ let e;
39
+ try {
40
+ e = JSON.parse(line);
41
+ } catch {
42
+ continue;
43
+ }
44
+ const at = typeof e.timestamp === "string" ? e.timestamp : void 0;
45
+ if (!at) continue;
46
+ if (e.type === "assistant" && (opts.sidechain || e.isSidechain !== true)) {
47
+ const text = texts(e.message?.content, "text");
48
+ if (text && typeof e.uuid === "string") out.push({ id: e.uuid, at, text });
49
+ continue;
50
+ }
51
+ if (e.type === "event_msg") {
52
+ const p = e.payload;
53
+ const item = p?.type === "item_completed" ? p.item : void 0;
54
+ if (item?.type !== "AgentMessage" || typeof item.id !== "string") continue;
55
+ const text = texts(item.content, "Text");
56
+ if (text) out.push({ id: item.id, at, text });
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+ function codexPlan(path, turnId) {
62
+ let plan;
63
+ for (const line of tailLines(path)) {
64
+ let e;
65
+ try {
66
+ e = JSON.parse(line);
67
+ } catch {
68
+ continue;
69
+ }
70
+ const p = e.payload;
71
+ if (e.type === "event_msg" && p?.type === "item_completed" && p.turn_id === turnId && p.item?.type === "Plan" && typeof p.item.text === "string") plan = p.item.text.trim() || plan;
72
+ }
73
+ return plan;
74
+ }
75
+ var CLAUDE_DIR_MAX = 200;
76
+ function claudeTranscriptPath(cwd, id, env = process.env) {
77
+ const dir = cwd.replace(/[^a-zA-Z0-9]/g, "-");
78
+ if (dir.length > CLAUDE_DIR_MAX) return void 0;
79
+ return join(env.CLAUDE_CONFIG_DIR || join(env.HOME || homedir(), ".claude"), "projects", dir, `${id}.jsonl`);
80
+ }
81
+ function claudePlan(path) {
82
+ let plan;
83
+ for (const line of tailLines(path)) {
84
+ let e;
85
+ try {
86
+ e = JSON.parse(line);
87
+ } catch {
88
+ continue;
89
+ }
90
+ if (e.isSidechain === true || !Array.isArray(e.message?.content)) continue;
91
+ for (const b of e.message.content) {
92
+ if (e.type === "assistant" && b?.type === "tool_use" && b.name === "ExitPlanMode" && typeof b.id === "string" && typeof b.input?.plan === "string" && b.input.plan.trim()) plan = { id: b.id, text: b.input.plan.trim() };
93
+ else if (e.type === "user" && b?.type === "tool_result" && plan && b.tool_use_id === plan.id) plan = void 0;
94
+ }
95
+ }
96
+ return plan?.text;
97
+ }
98
+
99
+ // src/spawn.ts
100
+ import { basename, join as join2 } from "path";
101
+ import { randomBytes, randomUUID } from "crypto";
102
+ var AGENT_BIN = { claude: "claude", codex: "codex", cursor: "cursor-agent" };
103
+ var CODEX_ENV_VAR = "LEDGE_CODEX_SESSION";
104
+ var CODEX_HOOK_VAR = "LEDGE_CODEX_HOOK_SESSION";
105
+ function agentIdArgs(tool, id) {
106
+ return tool === "codex" ? ["-c", `shell_environment_policy.set.${CODEX_HOOK_VAR}="${id}"`] : [];
107
+ }
108
+ var CLAUDE_OWN_SESSION = /^(--resume|-r|--continue|-c|--session-id|--fork-session)(=|$)/;
109
+ function claudeSessionId(tool, args) {
110
+ return tool === "claude" && !args.some((a) => CLAUDE_OWN_SESSION.test(a)) ? randomUUID() : void 0;
111
+ }
112
+ function exitFile(id) {
113
+ return join2(paths().dir, `exit-${id}`);
114
+ }
115
+ function agentEnv(base, extra) {
116
+ const env = {};
117
+ for (const [k, v] of Object.entries(base)) if (v !== void 0 && k !== "TMUX" && !k.startsWith("TMUX_")) env[k] = v;
118
+ return { ...env, ...extra };
119
+ }
120
+ async function spawnSession(opts) {
121
+ const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
122
+ const takenIds = new Set(opts.taken.ids);
123
+ let id;
124
+ do
125
+ id = randomBytes(2).toString("hex");
126
+ while (takenIds.has(id));
127
+ const name = pickName(opts.taken.names, opts.name);
128
+ const project = basename(opts.cwd) || opts.cwd;
129
+ const tmuxSession = `ledge-${id}`;
130
+ 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];
132
+ const env = opts.env ?? process.env;
133
+ const transcriptPath = agentSessionId ? claudeTranscriptPath(opts.cwd, agentSessionId, env) : void 0;
134
+ const command = opts.recordExit ? ["sh", "-c", '"$@"; echo $? > "$0"', exitFile(id), ...agent] : agent;
135
+ await opts.tmux.newSession({ name: tmuxSession, cwd: opts.cwd, env: agentEnv(env, { LEDGE_SESSION: id, LEDGE_NAME: name, ...opts.tool === "codex" ? { [CODEX_ENV_VAR]: id } : {} }), command });
136
+ const at = now();
137
+ return { id, name, tool: opts.tool, cwd: opts.cwd, project, title: `${name} \xB7 ${project}`, tmuxSession, state: "starting", helpers: [], startedAt: at, lastActivityAt: at, source: "launcher", ...agentSessionId ? { agentSessionId } : {}, ...transcriptPath ? { transcriptPath } : {} };
138
+ }
139
+
140
+ export {
141
+ agentMessages,
142
+ codexPlan,
143
+ claudePlan,
144
+ CODEX_ENV_VAR,
145
+ CODEX_HOOK_VAR,
146
+ exitFile,
147
+ agentEnv,
148
+ spawnSession
149
+ };
@@ -1,13 +1,15 @@
1
1
  import {
2
2
  QUESTION_TOOLS,
3
3
  reduce
4
- } from "./chunk-SMA4Z3N2.js";
4
+ } from "./chunk-NKBJ3LD4.js";
5
5
  import {
6
+ agentMessages,
7
+ claudePlan,
6
8
  spawnSession
7
- } from "./chunk-U4A4XKDM.js";
9
+ } from "./chunk-U4RU6V35.js";
8
10
  import {
9
11
  LEDGE_VERSION
10
- } from "./chunk-HUNVLIN7.js";
12
+ } from "./chunk-JG7QTD6F.js";
11
13
  import {
12
14
  SERVICE,
13
15
  inDaemonService,
@@ -39,7 +41,7 @@ import {
39
41
  parseRelayBlock,
40
42
  parseServerMessage,
41
43
  pickName
42
- } from "./chunk-Y4ODVTTN.js";
44
+ } from "./chunk-E6ZQOGY4.js";
43
45
 
44
46
  // src/daemon/index.ts
45
47
  import { writeFile as writeFile5, rm as rm5 } from "fs/promises";
@@ -236,58 +238,6 @@ function filesOf(tool, input) {
236
238
  }
237
239
  }
238
240
 
239
- // src/daemon/transcript.ts
240
- import { closeSync, fstatSync, openSync, readSync } from "fs";
241
- var TAIL_BYTES = 512 * 1024;
242
- function tailLines(path, bytes = TAIL_BYTES) {
243
- let fd;
244
- try {
245
- fd = openSync(path, "r");
246
- } catch {
247
- return [];
248
- }
249
- try {
250
- const size = fstatSync(fd).size;
251
- const start = Math.max(0, size - bytes);
252
- const buf = Buffer.alloc(size - start);
253
- readSync(fd, buf, 0, buf.length, start);
254
- const text = buf.toString("utf8");
255
- const lines = text.split("\n");
256
- if (start > 0) lines.shift();
257
- if (!text.endsWith("\n")) lines.pop();
258
- return lines.filter((l) => l.trim());
259
- } finally {
260
- closeSync(fd);
261
- }
262
- }
263
- var texts = (blocks, kind) => (Array.isArray(blocks) ? blocks : []).filter((b) => b?.type === kind && typeof b.text === "string").map((b) => b.text.trim()).filter(Boolean).join("\n\n");
264
- function agentMessages(path, opts = {}) {
265
- const out = [];
266
- for (const line of tailLines(path, opts.bytes ?? TAIL_BYTES)) {
267
- let e;
268
- try {
269
- e = JSON.parse(line);
270
- } catch {
271
- continue;
272
- }
273
- const at = typeof e.timestamp === "string" ? e.timestamp : void 0;
274
- if (!at) continue;
275
- if (e.type === "assistant" && (opts.sidechain || e.isSidechain !== true)) {
276
- const text = texts(e.message?.content, "text");
277
- if (text && typeof e.uuid === "string") out.push({ id: e.uuid, at, text });
278
- continue;
279
- }
280
- if (e.type === "event_msg") {
281
- const p = e.payload;
282
- const item = p?.type === "item_completed" ? p.item : void 0;
283
- if (item?.type !== "AgentMessage" || typeof item.id !== "string") continue;
284
- const text = texts(item.content, "Text");
285
- if (text) out.push({ id: item.id, at, text });
286
- }
287
- }
288
- return out;
289
- }
290
-
291
241
  // src/daemon/history.ts
292
242
  var MAX_ITEMS = 400;
293
243
  var SYNC_MS = 1e3;
@@ -304,7 +254,7 @@ function handBack(text) {
304
254
  const m = HANDBACK.exec(text.trimStart());
305
255
  if (!m) return void 0;
306
256
  const lines = m[2].split("\n");
307
- while (lines.length && (!lines[0].trim() || /^\s*\[[^\]]*\]/.test(lines[0]))) lines.shift();
257
+ while (lines.length && (!lines[0].trim() || /^\[[^\]]*\]/.test(lines[0]))) lines.shift();
308
258
  const indent = Math.min(...lines.filter((l) => l.trim()).map((l) => l.length - l.trimStart().length));
309
259
  return { helperId: m[1], report: lines.map((l) => l.slice(Number.isFinite(indent) ? indent : 0)).join("\n").trim() };
310
260
  }
@@ -953,6 +903,7 @@ function parseQuestion(lines) {
953
903
  var PLAN_ASK = /^Ready to build\?$/;
954
904
  var PLAN_OPTION = /^(?:→\s*)?\d+\.\s+(.+?)\s+\([^)]*\)$/;
955
905
  var REVISE = /Describe how to revise the plan\b/;
906
+ var tidy = (lines) => lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
956
907
  function parsePlan(lines) {
957
908
  const box = squareBox(lines);
958
909
  if (!box) return null;
@@ -962,7 +913,9 @@ function parsePlan(lines) {
962
913
  if (!options.length) return null;
963
914
  const head = box.findIndex((l) => l === "Suggested Plan");
964
915
  const title = head < 0 ? "" : box.slice(head + 1).find((l) => l !== "") ?? "";
965
- return { title: title || "Ready to build?", options };
916
+ const end = head < 0 ? -1 : box.findIndex((l, i) => i > head && (/^Saved to /.test(l) || /^─{5,}/.test(l)));
917
+ const body = head < 0 ? "" : tidy(box.slice(head + 1, end < 0 ? ask : end));
918
+ return { title: title || "Ready to build?", options, ...body ? { body } : {} };
966
919
  }
967
920
  var TELL_INSTEAD = /Tell the agent what to do instead\b/;
968
921
  var INPUT = /^\s{1,3}→ /;
@@ -983,7 +936,7 @@ function detectPrompt3(pane) {
983
936
  const q = parseQuestion(lines);
984
937
  if (q) return { kind: "question", text: q.question, options: q.options };
985
938
  const plan = parsePlan(lines);
986
- if (plan) return { kind: "plan", text: plan.title, options: plan.options };
939
+ if (plan) return { kind: "plan", text: plan.title, options: plan.options, ...plan.body ? { body: plan.body } : {} };
987
940
  const d = parseDialog3(lines);
988
941
  return d ? { kind: "permission", text: dialogText(d) } : null;
989
942
  }
@@ -1224,7 +1177,7 @@ function startPoller(opts) {
1224
1177
  const prev = hashes.get(s.id);
1225
1178
  if (prev === h) {
1226
1179
  const m2 = matches.get(s.id);
1227
- if (m2 && !store.get(s.id)?.pending) store.apply(s.id, { name: "PanePrompt", at: now(), source: "pane", kind: m2.kind, text: m2.text, options: m2.options, ...m2.optionDetails ? { optionDetails: m2.optionDetails } : {} });
1180
+ if (m2 && !store.get(s.id)?.pending) store.apply(s.id, { name: "PanePrompt", at: now(), source: "pane", kind: m2.kind, text: m2.text, options: m2.options, ...m2.optionDetails ? { optionDetails: m2.optionDetails } : {}, ...withBody(s, m2) });
1228
1181
  rested(s.id, t);
1229
1182
  return;
1230
1183
  }
@@ -1258,7 +1211,11 @@ function startPoller(opts) {
1258
1211
  return;
1259
1212
  }
1260
1213
  matches.set(s.id, m);
1261
- store.apply(s.id, { name: "PanePrompt", at: now(), source: "pane", kind: m.kind, text: m.text, options: m.options, ...m.optionDetails ? { optionDetails: m.optionDetails } : {} });
1214
+ store.apply(s.id, { name: "PanePrompt", at: now(), source: "pane", kind: m.kind, text: m.text, options: m.options, ...m.optionDetails ? { optionDetails: m.optionDetails } : {}, ...withBody(s, m) });
1215
+ }
1216
+ function withBody(s, m) {
1217
+ const body = m.body ?? (m.kind === "plan" && s.tool === "claude" && s.transcriptPath ? claudePlan(s.transcriptPath) : void 0);
1218
+ return body ? { body } : {};
1262
1219
  }
1263
1220
  function rested(id, t) {
1264
1221
  const since = restSince.get(id);
@@ -1,17 +1,18 @@
1
1
  import {
2
2
  QUESTION_TOOLS
3
- } from "./chunk-SMA4Z3N2.js";
3
+ } from "./chunk-NKBJ3LD4.js";
4
4
  import {
5
5
  CODEX_ENV_VAR,
6
- CODEX_HOOK_VAR
7
- } from "./chunk-U4A4XKDM.js";
6
+ CODEX_HOOK_VAR,
7
+ codexPlan
8
+ } from "./chunk-U4RU6V35.js";
8
9
  import {
9
10
  log
10
11
  } from "./chunk-P2SHPHAN.js";
11
12
  import {
12
13
  connect
13
- } from "./chunk-5HVWJTTJ.js";
14
- import "./chunk-Y4ODVTTN.js";
14
+ } from "./chunk-IDGN7JGC.js";
15
+ import "./chunk-E6ZQOGY4.js";
15
16
 
16
17
  // src/cli/hook.ts
17
18
  import { readFileSync, openSync, fstatSync, readSync, closeSync } from "fs";
@@ -36,16 +37,18 @@ function describeTool(tool, input) {
36
37
  }
37
38
  var REPLY_MAX = 24e3;
38
39
  function lastReply(p) {
39
- const text = str(p.last_assistant_message)?.trim();
40
+ return cap(str(p.last_assistant_message)?.trim());
41
+ }
42
+ function cap(text) {
40
43
  if (!text) return void 0;
41
44
  return text.length > REPLY_MAX ? `${text.slice(0, REPLY_MAX)}
42
45
 
43
46
  [\u2026]` : text;
44
47
  }
45
48
  var INPUT_MAX = 8e3;
46
- function trimInput(input) {
49
+ function trimInput(input, max = INPUT_MAX) {
47
50
  if (!input || typeof input !== "object" || Array.isArray(input)) return void 0;
48
- const trim = (v) => typeof v === "string" ? v.slice(0, INPUT_MAX) : Array.isArray(v) ? v.map(trim) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, trim(x)])) : v;
51
+ const trim = (v) => typeof v === "string" ? v.slice(0, max) : Array.isArray(v) ? v.map(trim) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, trim(x)])) : v;
49
52
  return trim(input);
50
53
  }
51
54
  function toolOutput(response) {
@@ -115,7 +118,7 @@ function mapEvent(eventName, p, at, launched) {
115
118
  const agentId = str(p.agent_id) || void 0;
116
119
  const base = { at, source: "hook", ...agentId ? { helperId: agentId } : {} };
117
120
  const toolUseId = str(p.tool_use_id) || void 0;
118
- const toolInput = trimInput(p.tool_input);
121
+ const toolInput = trimInput(p.tool_input, tool === "ExitPlanMode" ? REPLY_MAX : INPUT_MAX);
119
122
  const call = { ...toolUseId ? { toolUseId } : {}, ...toolInput ? { toolInput } : {} };
120
123
  const transcript = str(p.transcript_path) ? { transcriptPath: str(p.transcript_path) } : {};
121
124
  const duration = typeof p.duration_ms === "number" && p.duration_ms >= 0 ? { durationMs: p.duration_ms } : {};
@@ -151,8 +154,12 @@ function mapEvent(eventName, p, at, launched) {
151
154
  case "UserPromptSubmit":
152
155
  return { name: "UserPromptSubmit", ...base, ...promptText(p), ...transcript };
153
156
  case "Stop": {
154
- const text = lastReply(p);
155
- return { name: "Stop", ...base, ...text ? { text } : {}, ...transcript };
157
+ const turnId = str(p.turn_id);
158
+ const path = str(p.transcript_path);
159
+ const said = lastReply(p);
160
+ const plan = said ? void 0 : turnId && path ? cap(codexPlan(path, turnId)) : void 0;
161
+ const text = said ?? plan;
162
+ return { name: "Stop", ...base, ...plan ? { kind: "plan" } : {}, ...text ? { text } : {}, ...transcript };
156
163
  }
157
164
  case "SessionEnd":
158
165
  if (launched) return null;
package/dist/cli/index.js CHANGED
@@ -16,7 +16,7 @@ async function hookMain(args) {
16
16
  try {
17
17
  const chunks = [];
18
18
  for await (const c of process.stdin) chunks.push(c);
19
- const { runHook } = await import("./hook-KFSK75QH.js");
19
+ const { runHook } = await import("./hook-ARQEXMYY.js");
20
20
  await runHook(event, Buffer.concat(chunks).toString("utf8"), { tool });
21
21
  } catch {
22
22
  }
@@ -26,7 +26,7 @@ async function main(argv) {
26
26
  const [cmd, ...rest] = argv;
27
27
  switch (cmd) {
28
28
  case "daemon": {
29
- const { startDaemon } = await import("./daemon-RULSS2LK.js");
29
+ const { startDaemon } = await import("./daemon-V3YOATI4.js");
30
30
  await startDaemon();
31
31
  await new Promise(() => {
32
32
  });
@@ -34,7 +34,7 @@ async function main(argv) {
34
34
  }
35
35
  case "--version":
36
36
  case "version": {
37
- const { LEDGE_VERSION } = await import("./version-X3IWEFL7.js");
37
+ const { LEDGE_VERSION } = await import("./version-Y2F3F6FY.js");
38
38
  console.log(LEDGE_VERSION);
39
39
  return 0;
40
40
  }
@@ -50,23 +50,23 @@ async function main(argv) {
50
50
  return r.ok ? 0 : 1;
51
51
  }
52
52
  case "status": {
53
- const { printStatus } = await import("./status-I7FHW6ZM.js");
53
+ const { printStatus } = await import("./status-6OOVUG3V.js");
54
54
  console.log(await printStatus());
55
55
  return 0;
56
56
  }
57
57
  case "bridge": {
58
- const { bridge } = await import("./bridge-R6DKTSYF.js");
58
+ const { bridge } = await import("./bridge-7RXMJSOP.js");
59
59
  await bridge();
60
60
  return 0;
61
61
  }
62
62
  case "claude":
63
63
  case "codex":
64
64
  case "cursor": {
65
- const { launch } = await import("./launch-YKYPT6BU.js");
65
+ const { launch } = await import("./launch-GRWEWGSC.js");
66
66
  return launch(cmd, rest);
67
67
  }
68
68
  case "attach": {
69
- const { attach } = await import("./attach-CUKFTN63.js");
69
+ const { attach } = await import("./attach-QE22G2GY.js");
70
70
  return attach(rest[0] ?? "");
71
71
  }
72
72
  default:
@@ -5,15 +5,15 @@ import {
5
5
  ensureDaemon,
6
6
  launch,
7
7
  parseLaunchArgs
8
- } from "./chunk-QHDPDZOJ.js";
8
+ } from "./chunk-ISPFJBJM.js";
9
9
  import {
10
10
  agentEnv,
11
11
  exitFile
12
- } from "./chunk-U4A4XKDM.js";
12
+ } from "./chunk-U4RU6V35.js";
13
13
  import "./chunk-XZMLFV6H.js";
14
14
  import "./chunk-P2SHPHAN.js";
15
- import "./chunk-5HVWJTTJ.js";
16
- import "./chunk-Y4ODVTTN.js";
15
+ import "./chunk-IDGN7JGC.js";
16
+ import "./chunk-E6ZQOGY4.js";
17
17
  export {
18
18
  DaemonError,
19
19
  agentEnv,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-5HVWJTTJ.js";
4
- import "./chunk-Y4ODVTTN.js";
3
+ } from "./chunk-IDGN7JGC.js";
4
+ import "./chunk-E6ZQOGY4.js";
5
5
 
6
6
  // src/cli/status.ts
7
7
  async function printStatus(opts = {}) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  LEDGE_VERSION
3
- } from "./chunk-HUNVLIN7.js";
3
+ } from "./chunk-JG7QTD6F.js";
4
4
  export {
5
5
  LEDGE_VERSION
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paradow/ledge",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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" },
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var LEDGE_VERSION = true ? "0.5.1" : "0.0.0-dev";
3
-
4
- export {
5
- LEDGE_VERSION
6
- };
@@ -1,48 +0,0 @@
1
- import {
2
- paths
3
- } from "./chunk-P2SHPHAN.js";
4
- import {
5
- pickName
6
- } from "./chunk-Y4ODVTTN.js";
7
-
8
- // src/spawn.ts
9
- import { basename, join } from "path";
10
- import { randomBytes } from "crypto";
11
- var AGENT_BIN = { claude: "claude", codex: "codex", cursor: "cursor-agent" };
12
- var CODEX_ENV_VAR = "LEDGE_CODEX_SESSION";
13
- var CODEX_HOOK_VAR = "LEDGE_CODEX_HOOK_SESSION";
14
- function agentIdArgs(tool, id) {
15
- return tool === "codex" ? ["-c", `shell_environment_policy.set.${CODEX_HOOK_VAR}="${id}"`] : [];
16
- }
17
- function exitFile(id) {
18
- return join(paths().dir, `exit-${id}`);
19
- }
20
- function agentEnv(base, extra) {
21
- const env = {};
22
- for (const [k, v] of Object.entries(base)) if (v !== void 0 && k !== "TMUX" && !k.startsWith("TMUX_")) env[k] = v;
23
- return { ...env, ...extra };
24
- }
25
- async function spawnSession(opts) {
26
- const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
27
- const takenIds = new Set(opts.taken.ids);
28
- let id;
29
- do
30
- id = randomBytes(2).toString("hex");
31
- while (takenIds.has(id));
32
- const name = pickName(opts.taken.names, opts.name);
33
- const project = basename(opts.cwd) || opts.cwd;
34
- const tmuxSession = `ledge-${id}`;
35
- const agent = opts.command ?? [AGENT_BIN[opts.tool], ...agentIdArgs(opts.tool, id), ...opts.args];
36
- const command = opts.recordExit ? ["sh", "-c", '"$@"; echo $? > "$0"', exitFile(id), ...agent] : agent;
37
- await opts.tmux.newSession({ name: tmuxSession, cwd: opts.cwd, env: agentEnv(opts.env ?? process.env, { LEDGE_SESSION: id, LEDGE_NAME: name, ...opts.tool === "codex" ? { [CODEX_ENV_VAR]: id } : {} }), command });
38
- const at = now();
39
- return { id, name, tool: opts.tool, cwd: opts.cwd, project, title: `${name} \xB7 ${project}`, tmuxSession, state: "starting", helpers: [], startedAt: at, lastActivityAt: at, source: "launcher" };
40
- }
41
-
42
- export {
43
- CODEX_ENV_VAR,
44
- CODEX_HOOK_VAR,
45
- exitFile,
46
- agentEnv,
47
- spawnSession
48
- };