@paradow/ledge 0.9.0 → 0.10.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,18 @@
1
1
  import {
2
2
  attachEnv
3
- } from "./chunk-N3MPPCON.js";
4
- import "./chunk-564TJJZ6.js";
3
+ } from "./chunk-IC5RHOX6.js";
4
+ import "./chunk-GMKS55KF.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-XPWGZIYL.js";
11
+ } from "./chunk-TC3XJUJA.js";
12
12
  import {
13
13
  PROTOCOL_PORT
14
- } from "./chunk-WM4MAV46.js";
14
+ } from "./chunk-JOCNCNRP.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-WM4MAV46.js";
8
+ parseServerMessage,
9
+ readDaemonToken
10
+ } from "./chunk-JOCNCNRP.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
  };
@@ -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,9 +1,9 @@
1
- import {
2
- paths
3
- } from "./chunk-L6IGH4LF.js";
4
1
  import {
5
2
  pickName
6
- } from "./chunk-WM4MAV46.js";
3
+ } from "./chunk-JOCNCNRP.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";
@@ -145,6 +172,8 @@ export {
145
172
  agentMessages,
146
173
  codexPlan,
147
174
  claudePlan,
175
+ claudeOpenCall,
176
+ claudeToolAnswered,
148
177
  CODEX_ENV_VAR,
149
178
  CODEX_HOOK_VAR,
150
179
  exitFile,
@@ -1,20 +1,23 @@
1
1
  import {
2
2
  exitFile,
3
3
  spawnSession
4
- } from "./chunk-564TJJZ6.js";
4
+ } from "./chunk-GMKS55KF.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-XPWGZIYL.js";
14
+ } from "./chunk-TC3XJUJA.js";
15
15
  import {
16
16
  PROTOCOL_PORT
17
- } from "./chunk-WM4MAV46.js";
17
+ } from "./chunk-JOCNCNRP.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
  };
@@ -1,3 +1,126 @@
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
+ }
123
+
1
124
  // ../protocol/src/session.ts
2
125
  import { z } from "zod";
3
126
  var ToolSchema = z.enum(["claude", "codex", "cursor"]);
@@ -53,6 +176,8 @@ var SessionSchema = z.object({
53
176
  repoPath: z.string().optional(),
54
177
  branch: z.string().optional(),
55
178
  worktree: z.boolean().optional(),
179
+ /** The folder the agent works in now, as its last hook reported it: `cwd` stays where it started, this follows a worktree it entered. */
180
+ workdir: z.string().optional(),
56
181
  tmuxSession: z.string().nullable(),
57
182
  state: SessionStateSchema,
58
183
  pending: PendingSchema.optional(),
@@ -524,7 +649,14 @@ var ClientMessageSchema = z13.discriminatedUnion("type", [
524
649
  /** Copied into the `folder.listing` that answers; the fleet relays only that listing, from the machine asked. */
525
650
  requestId: z13.string().min(1).max(100).optional()
526
651
  }),
527
- z13.object({ type: z13.literal("folder.create"), parent: z13.string().min(1), name: z13.string().min(1) }),
652
+ z13.object({
653
+ type: z13.literal("folder.create"),
654
+ parent: z13.string().min(1),
655
+ name: z13.string().min(1),
656
+ /** As for `folder.list`: routing by the fleet, and the request its `folder.listing` answers. */
657
+ machineId: z13.string().optional(),
658
+ requestId: z13.string().min(1).max(100).optional()
659
+ }),
528
660
  z13.object({ type: z13.literal("session.reply"), id: z13.string(), text: z13.string().min(1) }),
529
661
  /** The chat card's instruction (spec 2026-10-12 relay §4): sent now if the session is free, else queued until it is. */
530
662
  z13.object({ type: z13.literal("session.relay"), qid: z13.string().min(1) }),
@@ -720,6 +852,11 @@ function sessionTitle(s) {
720
852
  }
721
853
 
722
854
  export {
855
+ hostAllowed,
856
+ requestAuthorized,
857
+ selectDaemonProtocol,
858
+ ensureDaemonToken,
859
+ readDaemonToken,
723
860
  SessionSchema,
724
861
  isArchived,
725
862
  MAX_PROPOSALS,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  cleanTopic,
3
3
  sessionTitle
4
- } from "./chunk-WM4MAV46.js";
4
+ } from "./chunk-JOCNCNRP.js";
5
5
 
6
6
  // src/daemon/crossSession.ts
7
7
  var FIRST_LINE = /^Another Claude session sent a message:\s*\n/;
@@ -101,6 +101,11 @@ function pendingTool(p) {
101
101
  if (p.kind === "plan") return "ExitPlanMode";
102
102
  return p.tool;
103
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
+ }
104
109
  function clearedByToolRun(p, tool) {
105
110
  const t = pendingTool(p);
106
111
  return p.source === "pane" || t === void 0 || t === tool;
@@ -277,7 +282,7 @@ function reduceEvent(s, ev) {
277
282
  if (ev.name === "UserPromptSubmit" && ev.text && !s.topic && !notice2 && !crossSession(ev.text)) s = withTopic(s, ev.text, "prompt");
278
283
  const incoming = pendingFor(ev);
279
284
  if (incoming) {
280
- 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 };
281
286
  const pending = mergePending(s.pending, incoming);
282
287
  const state = pending.kind === "question" ? "waiting_input" : "waiting_permission";
283
288
  return { ...s, state, pending, lastHookAt: at, lastActivityAt: at };
@@ -337,6 +342,8 @@ export {
337
342
  crossSession,
338
343
  crossSessionNotices,
339
344
  QUESTION_TOOLS,
345
+ pendingTool,
346
+ answeredBy,
340
347
  reduce,
341
348
  repositoryId,
342
349
  gitInfo,
@@ -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,14 +1,23 @@
1
1
  import {
2
2
  PROTOCOL_PORT,
3
- parseServerMessage
4
- } from "./chunk-WM4MAV46.js";
3
+ parseServerMessage,
4
+ readDaemonToken
5
+ } from "./chunk-JOCNCNRP.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,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";
@@ -0,0 +1,6 @@
1
+ // src/version.ts
2
+ var LEDGE_VERSION = true ? "0.10.0" : "0.0.0-dev";
3
+
4
+ export {
5
+ LEDGE_VERSION
6
+ };
@@ -0,0 +1,17 @@
1
+ import {
2
+ paths
3
+ } from "./chunk-FGOFRWIB.js";
4
+
5
+ // src/log.ts
6
+ import { appendFileSync } from "fs";
7
+ function log(line) {
8
+ try {
9
+ appendFileSync(paths().log, `${(/* @__PURE__ */ new Date()).toISOString()} | ${line}
10
+ `);
11
+ } catch {
12
+ }
13
+ }
14
+
15
+ export {
16
+ log
17
+ };
@@ -4,35 +4,37 @@ import {
4
4
  } from "./chunk-OLLQBK6N.js";
5
5
  import {
6
6
  QUESTION_TOOLS,
7
+ answeredBy,
7
8
  crossSession,
8
9
  crossSessionNotices,
9
10
  gitInfo,
10
11
  gitSummary,
12
+ pendingTool,
11
13
  reduce,
12
14
  repositoryId
13
- } from "./chunk-JVELEEIO.js";
15
+ } from "./chunk-OHKQKXYM.js";
14
16
  import {
15
17
  agentMessages,
18
+ claudeOpenCall,
16
19
  claudePlan,
20
+ claudeToolAnswered,
17
21
  spawnSession
18
- } from "./chunk-564TJJZ6.js";
22
+ } from "./chunk-GMKS55KF.js";
19
23
  import {
20
24
  LEDGE_VERSION
21
- } from "./chunk-LWPKX7SB.js";
25
+ } from "./chunk-XOPH7YDV.js";
22
26
  import {
23
27
  SERVICE,
24
28
  inDaemonService,
25
29
  tmuxServerAtRisk
26
- } from "./chunk-Q5CLZPYJ.js";
30
+ } from "./chunk-Q7RCYJ4X.js";
27
31
  import {
28
32
  realTmux,
29
33
  runFile
30
- } from "./chunk-3TNPEAMR.js";
34
+ } from "./chunk-TO4XO2ML.js";
31
35
  import {
32
- ensureLedgeDir,
33
- log,
34
- paths
35
- } from "./chunk-L6IGH4LF.js";
36
+ log
37
+ } from "./chunk-YUSY3E47.js";
36
38
  import {
37
39
  DEFAULT_PROJECTS,
38
40
  DOCUMENT_MAX_BYTES,
@@ -46,7 +48,9 @@ import {
46
48
  RouteResultSchema,
47
49
  SessionSchema,
48
50
  SshAliasSchema,
51
+ ensureDaemonToken,
49
52
  extractProposal,
53
+ hostAllowed,
50
54
  isArchived,
51
55
  offers,
52
56
  parseClientMessage,
@@ -57,8 +61,14 @@ import {
57
61
  pendingKey,
58
62
  pickName,
59
63
  projectOf,
60
- proposalRows
61
- } from "./chunk-WM4MAV46.js";
64
+ proposalRows,
65
+ requestAuthorized,
66
+ selectDaemonProtocol
67
+ } from "./chunk-JOCNCNRP.js";
68
+ import {
69
+ ensureLedgeDir,
70
+ paths
71
+ } from "./chunk-FGOFRWIB.js";
62
72
 
63
73
  // src/daemon/index.ts
64
74
  import { writeFile as writeFile6, rm as rm5 } from "fs/promises";
@@ -583,6 +593,7 @@ var History = class {
583
593
  };
584
594
 
585
595
  // src/daemon/server.ts
596
+ import { createServer } from "http";
586
597
  import { homedir as homedir5 } from "os";
587
598
  import { basename as basename3 } from "path";
588
599
  import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
@@ -637,6 +648,9 @@ function dialogMismatch(shown, pending, width) {
637
648
  if (pending.tool !== void 0 && COMMAND_TOOLS.has(pending.tool)) return shown.command ? commandMismatch(shown.command, pending.text, width) : UNCHECKED;
638
649
  return null;
639
650
  }
651
+ function bottomLines(pane, n = 5) {
652
+ return pane.split("\n").filter((l) => l.trim() !== "").slice(-n).join("\n");
653
+ }
640
654
  var flatten = (text) => text.replace(/\s*[\r\n]+\s*/g, " ").trim();
641
655
 
642
656
  // src/adapters/claude.ts
@@ -766,7 +780,7 @@ function promptStillVisible(lastLines2) {
766
780
  }
767
781
  var squash = (t) => t.replace(/\s+/g, " ").trim();
768
782
  function promptGone(pane, pending) {
769
- if (pending.kind !== "question" || !squash(pending.text)) return !promptStillVisible(pane.replace(/\s+$/, "").split("\n").slice(-5).join("\n"));
783
+ if (pending.kind !== "question" || !squash(pending.text)) return !promptStillVisible(bottomLines(pane));
770
784
  const m = detectPrompt(pane);
771
785
  if (!m || m.kind !== "question") return true;
772
786
  const shown = squash(m.text);
@@ -2569,10 +2583,18 @@ async function readDocument(request, context) {
2569
2583
  if (/^(?:file:|[a-zA-Z]:[\\/]|\\\\)/.test(source) || source.includes("\0")) return failure(request, "unsupported_path");
2570
2584
  if (request.basePath && (!isAbsolute3(request.basePath) || !markdown(request.basePath))) return failure(request, "unsupported_path");
2571
2585
  const expanded = source === "~" ? context.home : source.startsWith("~/") ? resolve6(context.home, source.slice(2)) : source;
2572
- const base = request.basePath ? dirname5(request.basePath) : context.cwd;
2573
- if (!isAbsolute3(expanded) && !base) return failure(request, "ambiguous_path");
2574
- const path = resolve6(base ?? context.home, expanded);
2575
- if (!markdown(path)) return failure(request, "unsupported_type");
2586
+ const bases = request.basePath ? [dirname5(request.basePath)] : [...new Set([context.workdir, context.cwd].filter((d) => !!d))];
2587
+ if (!isAbsolute3(expanded) && !bases.length) return failure(request, "ambiguous_path");
2588
+ const paths2 = isAbsolute3(expanded) ? [resolve6(expanded)] : bases.map((base) => resolve6(base, expanded));
2589
+ if (!markdown(paths2[0])) return failure(request, "unsupported_type");
2590
+ let read = failure(request, "not_found");
2591
+ for (const path of paths2) {
2592
+ read = await readAt(request, path);
2593
+ if (read.type === "document.content" || read.code !== "not_found") return read;
2594
+ }
2595
+ return read;
2596
+ }
2597
+ async function readAt(request, path) {
2576
2598
  let file;
2577
2599
  try {
2578
2600
  file = await open2(path, constants2.O_RDONLY | constants2.O_NONBLOCK);
@@ -2859,6 +2881,25 @@ var ALLOWED_ORIGINS = [
2859
2881
  function originAllowed(origin) {
2860
2882
  return origin === void 0 || ALLOWED_ORIGINS.includes(origin);
2861
2883
  }
2884
+ function requestPath(url) {
2885
+ const raw = url ?? "/";
2886
+ const q = raw.indexOf("?");
2887
+ return q === -1 ? raw : raw.slice(0, q);
2888
+ }
2889
+ function refusal(req, token) {
2890
+ const path = requestPath(req.url);
2891
+ if (!(path === "/" || path.startsWith("/ssh/") && SshAliasSchema.safeParse(path.slice(5)).success)) return "path";
2892
+ if (!originAllowed(req.headers.origin)) return "origin";
2893
+ if (!hostAllowed(req.headers.host)) return "host";
2894
+ if (!requestAuthorized(req.headers, token)) return "auth";
2895
+ return null;
2896
+ }
2897
+ function unauthorized(req, res) {
2898
+ req.resume();
2899
+ const body = "Unauthorized\n";
2900
+ res.writeHead(401, { "Content-Type": "text/plain; charset=utf-8", "Content-Length": Buffer.byteLength(body), "Cache-Control": "no-store", Connection: "close" });
2901
+ res.end(body);
2902
+ }
2862
2903
  function logDroppedRequest(id, ev, before, after) {
2863
2904
  if (ev.name !== "PermissionRequest" || !after || after.pending !== before?.pending) return;
2864
2905
  const held = before?.pending;
@@ -2874,22 +2915,36 @@ async function startServer(opts) {
2874
2915
  const agentEnv = opts.agentEnv ?? (async () => ({ ...process.env, ...await loginShellEnv() }));
2875
2916
  const chat = opts.chat ?? createChat({ store, tmux, home: paths().dir, userHome: opts.home, env: agentEnv, bin: opts.claudeBin, now, projects: () => projects, forge: createForge({ env: agentEnv }) });
2876
2917
  const router = createRouter({ env: agentEnv });
2918
+ const token = await ensureDaemonToken();
2919
+ const httpServer = createServer(unauthorized);
2920
+ const sockets = /* @__PURE__ */ new Set();
2921
+ httpServer.on("connection", (socket) => {
2922
+ sockets.add(socket);
2923
+ socket.on("close", () => sockets.delete(socket));
2924
+ });
2877
2925
  const wss = new WebSocketServer({
2878
- host: opts.host ?? "127.0.0.1",
2879
- port: opts.port ?? PROTOCOL_PORT,
2926
+ server: httpServer,
2880
2927
  maxPayload: MAX_FRAME,
2881
2928
  verifyClient: ({ req }) => {
2882
- const path = req.url ?? "/";
2883
- const ok = originAllowed(req.headers.origin) && (path === "/" || path.startsWith("/ssh/") && SshAliasSchema.safeParse(path.slice(5)).success);
2884
- if (!ok) log(`refused a connection from origin ${String(req.headers.origin)}`);
2885
- return ok;
2886
- }
2929
+ const why = refusal(req, token);
2930
+ if (why) {
2931
+ const origin = String(req.headers.origin ?? "none").replace(/[\r\n]/g, "").slice(0, 120);
2932
+ log(`refused a connection (${why}) from origin ${origin}`);
2933
+ }
2934
+ return why === null;
2935
+ },
2936
+ handleProtocols: (protocols) => selectDaemonProtocol(protocols, token)
2887
2937
  });
2888
2938
  await new Promise((res, rej) => {
2889
- wss.once("listening", res);
2890
- wss.once("error", rej);
2939
+ const onError = (e) => rej(e);
2940
+ wss.once("error", onError);
2941
+ httpServer.listen(opts.port ?? PROTOCOL_PORT, opts.host ?? "127.0.0.1", () => {
2942
+ wss.off("error", onError);
2943
+ res();
2944
+ });
2891
2945
  });
2892
- const port = wss.address().port;
2946
+ wss.on("error", (e) => log(`daemon server error: ${e.message}`));
2947
+ const port = httpServer.address().port;
2893
2948
  const machinesFile = opts.machinesFile ?? paths().machines;
2894
2949
  let machines = readMachines(machinesFile).map((m) => ({ ...m }));
2895
2950
  const machinesMsg = () => ({ type: "machines", machines });
@@ -2971,6 +3026,16 @@ async function startServer(opts) {
2971
3026
  }
2972
3027
  }
2973
3028
  const inFlight = /* @__PURE__ */ new Set();
3029
+ const CONFIRM_MS = opts.confirmMs ?? 3e3;
3030
+ const awaited = /* @__PURE__ */ new Map();
3031
+ function settleByHook(id, ev) {
3032
+ const a = awaited.get(id);
3033
+ const cur = store.get(id)?.pending;
3034
+ if (!a || a.settled || !answeredBy(a.pending, ev) || !cur || pendingKey(cur) !== pendingKey(a.pending)) return;
3035
+ a.settled = true;
3036
+ a.byHook = true;
3037
+ store.apply(id, { name: "Decided", at: now(), source: "daemon", ...a.decision ? { decision: a.decision } : {} });
3038
+ }
2974
3039
  async function injectAndVerify(id, planFor, requirePrompt, decision) {
2975
3040
  const s = store.get(id);
2976
3041
  if (!s) throw new Error(`unknown session ${id}`);
@@ -2986,7 +3051,7 @@ async function startServer(opts) {
2986
3051
  let before = null;
2987
3052
  if (requirePrompt) {
2988
3053
  before = s.tmuxSession ? await tmux.capture(s.tmuxSession, DETECT_LINES) : null;
2989
- if (before === null || !adapter.promptStillVisible(before.split("\n").slice(-5).join("\n"))) throw new Error(NO_PROMPT);
3054
+ if (before === null || !adapter.promptStillVisible(bottomLines(before))) throw new Error(NO_PROMPT);
2990
3055
  if (!stillTheSame()) throw new Error(STALE);
2991
3056
  if (decided) {
2992
3057
  const shown = adapter.detectPrompt(before) ?? adapter.detectPrompt(await tmux.capture(s.tmuxSession, CHECK_LINES) ?? "");
@@ -2995,21 +3060,41 @@ async function startServer(opts) {
2995
3060
  }
2996
3061
  }
2997
3062
  const plan = typeof planFor === "function" ? planFor(before ?? "") : planFor;
2998
- const promptGone3 = () => decided && adapter.promptGone ? verifyGone(() => tmux.capture(s.tmuxSession, DETECT_LINES), (p) => !adapter.promptGone(p, decided), opts.verify) : verifyGone(() => tmux.capture(s.tmuxSession, 5), (p) => adapter.promptStillVisible(p), opts.verify);
3063
+ const stillShown = (p) => decided && adapter.promptGone ? !adapter.promptGone(p, decided) : adapter.promptStillVisible(bottomLines(p));
3064
+ const tool = s.tool === "claude" && decided?.source === "hook" ? pendingTool(decided) : void 0;
3065
+ const answer = tool ? { pending: decided, ...decision ? { decision } : {}, settled: false, byHook: false } : void 0;
3066
+ const transcript = tool && !decided.helperId && s.transcriptPath ? s.transcriptPath : void 0;
3067
+ const call = transcript ? claudeOpenCall(transcript, tool) : void 0;
3068
+ const promptGone3 = async () => {
3069
+ if (!answer) return verifyGone(() => tmux.capture(s.tmuxSession, DETECT_LINES), stillShown, opts.verify);
3070
+ const interval = opts.verify?.intervalMs ?? 200;
3071
+ for (const end = Date.now() + CONFIRM_MS; ; ) {
3072
+ if (answer.byHook || call && claudeToolAnswered(transcript, call)) return true;
3073
+ const pane = await tmux.capture(s.tmuxSession, DETECT_LINES);
3074
+ if (answer.byHook || pane !== null && !stillShown(pane)) return true;
3075
+ if (Date.now() >= end) return false;
3076
+ await new Promise((r) => setTimeout(r, interval));
3077
+ }
3078
+ };
3079
+ if (answer) awaited.set(id, answer);
2999
3080
  try {
3000
3081
  await runPlan(s, plan);
3001
3082
  } catch (e) {
3002
3083
  if (e instanceof NoteNotTyped) {
3003
3084
  if (await promptGone3()) {
3004
- if (stillTheSame()) store.apply(id, { name: "Decided", at: now(), source: "daemon", ...decision ? { decision } : {} });
3085
+ if (answer) answer.settled = true;
3086
+ if (!answer?.byHook && stillTheSame()) store.apply(id, { name: "Decided", at: now(), source: "daemon", ...decision ? { decision } : {} });
3005
3087
  throw e;
3006
3088
  }
3007
3089
  e = new Error(NOT_TAKEN);
3008
3090
  }
3091
+ if (answer?.byHook) return true;
3009
3092
  store.apply(id, { name: "InjectFailed", at: now(), source: "daemon", text: e.message });
3010
3093
  throw e;
3011
3094
  }
3012
3095
  const gone = await promptGone3();
3096
+ if (answer) answer.settled = true;
3097
+ if (answer?.byHook) return true;
3013
3098
  if (!stillTheSame()) return false;
3014
3099
  if (gone) {
3015
3100
  store.apply(id, { name: "Decided", at: now(), source: "daemon", ...decision ? { decision } : {} });
@@ -3019,6 +3104,7 @@ async function startServer(opts) {
3019
3104
  throw new Error(NOT_TAKEN);
3020
3105
  } finally {
3021
3106
  inFlight.delete(id);
3107
+ awaited.delete(id);
3022
3108
  }
3023
3109
  }
3024
3110
  const READ_ONLY = "session started without ledge, read only";
@@ -3085,7 +3171,7 @@ async function startServer(opts) {
3085
3171
  case "document.read": {
3086
3172
  const session = msg.sessionId ? store.get(msg.sessionId) : void 0;
3087
3173
  if (msg.sessionId && !session) return send(ws, { type: "document.error", requestId: msg.requestId, path: msg.path, code: "session_missing" });
3088
- return send(ws, await readDocument(msg, { cwd: session?.cwd, home: opts.home ?? process.env.HOME ?? "" }));
3174
+ return send(ws, await readDocument(msg, { cwd: session?.cwd, workdir: session?.workdir, home: opts.home ?? process.env.HOME ?? "" }));
3089
3175
  }
3090
3176
  case "repos.inspect":
3091
3177
  return send(ws, { type: "repos.info", requestId: msg.requestId, repos: await inspectRepos(msg.paths, { home: opts.home }) });
@@ -3176,7 +3262,7 @@ async function startServer(opts) {
3176
3262
  case "folder.list":
3177
3263
  return send(ws, await listFolder(msg.path, opts.home, msg.requestId));
3178
3264
  case "folder.create": {
3179
- const listing = await createFolder(msg.parent, msg.name, opts.home);
3265
+ const listing = await createFolder(msg.parent, msg.name, opts.home, msg.requestId);
3180
3266
  if (!listing.error) log(`created folder ${listing.path}`);
3181
3267
  return send(ws, listing);
3182
3268
  }
@@ -3203,7 +3289,11 @@ async function startServer(opts) {
3203
3289
  const base = { id: msg.id, name: pickName(taken), tool: msg.agent ?? "claude", cwd, project: basename3(cwd) || "unknown", title: "untracked session", tmuxSession: null, state: "working", helpers: [], startedAt: msg.event.at, lastActivityAt: msg.event.at, source: "hook_only" };
3204
3290
  store.upsert(base);
3205
3291
  store.apply(msg.id, msg.event);
3206
- } else store.apply(msg.id, msg.event);
3292
+ } else {
3293
+ if (msg.cwd) store.patch(msg.id, { workdir: msg.cwd });
3294
+ settleByHook(msg.id, ev);
3295
+ store.apply(msg.id, msg.event);
3296
+ }
3207
3297
  logDroppedRequest(msg.id, msg.event, before, store.get(msg.id));
3208
3298
  return;
3209
3299
  }
@@ -3336,14 +3426,15 @@ async function startServer(opts) {
3336
3426
  }
3337
3427
  wss.on("connection", (ws, req) => {
3338
3428
  ws.on("error", (e) => log(`socket error: ${e.message}`));
3339
- if (req.url?.startsWith("/ssh/")) {
3429
+ const path = requestPath(req.url);
3430
+ if (path.startsWith("/ssh/")) {
3340
3431
  if (remoteClients.size >= 20) {
3341
3432
  ws.close(1013, "Too many remote connections");
3342
3433
  return;
3343
3434
  }
3344
3435
  remoteClients.add(ws);
3345
3436
  ws.once("close", () => remoteClients.delete(ws));
3346
- const parsed = SshAliasSchema.safeParse(req.url.slice(5));
3437
+ const parsed = SshAliasSchema.safeParse(path.slice(5));
3347
3438
  if (!parsed.success) {
3348
3439
  ws.close(1008, "Invalid SSH alias");
3349
3440
  remoteClients.delete(ws);
@@ -3388,7 +3479,8 @@ async function startServer(opts) {
3388
3479
  for (const u of unsubscribeHistory) u();
3389
3480
  relay.close();
3390
3481
  for (const c of wss.clients) c.terminate();
3391
- wss.close(() => res());
3482
+ for (const socket of sockets) socket.destroy();
3483
+ wss.close(() => httpServer.close(() => res()));
3392
3484
  })
3393
3485
  };
3394
3486
  }
@@ -1,19 +1,20 @@
1
1
  import {
2
2
  QUESTION_TOOLS,
3
3
  gitInfo
4
- } from "./chunk-JVELEEIO.js";
4
+ } from "./chunk-OHKQKXYM.js";
5
5
  import {
6
6
  CODEX_ENV_VAR,
7
7
  CODEX_HOOK_VAR,
8
8
  codexPlan
9
- } from "./chunk-564TJJZ6.js";
9
+ } from "./chunk-GMKS55KF.js";
10
10
  import {
11
11
  log
12
- } from "./chunk-L6IGH4LF.js";
12
+ } from "./chunk-YUSY3E47.js";
13
13
  import {
14
14
  connect
15
- } from "./chunk-XPWGZIYL.js";
16
- import "./chunk-WM4MAV46.js";
15
+ } from "./chunk-TC3XJUJA.js";
16
+ import "./chunk-JOCNCNRP.js";
17
+ import "./chunk-FGOFRWIB.js";
17
18
 
18
19
  // src/cli/hook.ts
19
20
  import { readFileSync, openSync, fstatSync, readSync, closeSync } from "fs";
package/dist/cli/index.js CHANGED
@@ -17,7 +17,7 @@ async function hookMain(args) {
17
17
  try {
18
18
  const chunks = [];
19
19
  for await (const c of process.stdin) chunks.push(c);
20
- const { runHook } = await import("./hook-WQYL4YFH.js");
20
+ const { runHook } = await import("./hook-W46IWB7I.js");
21
21
  const out = await runHook(event, Buffer.concat(chunks).toString("utf8"), { tool });
22
22
  if (out) process.stdout.write(`${out}
23
23
  `);
@@ -29,7 +29,7 @@ async function main(argv) {
29
29
  const [cmd, ...rest] = argv;
30
30
  switch (cmd) {
31
31
  case "daemon": {
32
- const { startDaemon } = await import("./daemon-SGTQRTHV.js");
32
+ const { startDaemon } = await import("./daemon-RI47DJDX.js");
33
33
  await startDaemon();
34
34
  await new Promise(() => {
35
35
  });
@@ -37,44 +37,44 @@ async function main(argv) {
37
37
  }
38
38
  case "--version":
39
39
  case "version": {
40
- const { LEDGE_VERSION } = await import("./version-XFBINYKQ.js");
40
+ const { LEDGE_VERSION } = await import("./version-SO5OMPGD.js");
41
41
  console.log(LEDGE_VERSION);
42
42
  return 0;
43
43
  }
44
44
  case "setup": {
45
- const { runSetup } = await import("./setup-RMPIUCPH.js");
45
+ const { runSetup } = await import("./setup-7AEH5DC3.js");
46
46
  for (const l of await runSetup()) console.log(l);
47
47
  return 0;
48
48
  }
49
49
  case "uninstall": {
50
- const { runUninstall } = await import("./uninstall-GI4WT3UQ.js");
50
+ const { runUninstall } = await import("./uninstall-7U7ENKOS.js");
51
51
  const r = await runUninstall({ purge: rest.includes("--purge") });
52
52
  for (const l of r.lines) console.log(l);
53
53
  return r.ok ? 0 : 1;
54
54
  }
55
55
  case "status": {
56
- const { printStatus } = await import("./status-GGXF4DSQ.js");
56
+ const { printStatus } = await import("./status-QOXWNLLF.js");
57
57
  console.log(await printStatus());
58
58
  return 0;
59
59
  }
60
60
  case "who": {
61
- const { printWho } = await import("./who-EJWN7HM6.js");
61
+ const { printWho } = await import("./who-WVYFPCII.js");
62
62
  console.log(await printWho());
63
63
  return 0;
64
64
  }
65
65
  case "bridge": {
66
- const { bridge } = await import("./bridge-2CVD7BMI.js");
66
+ const { bridge } = await import("./bridge-ZOHP53DV.js");
67
67
  await bridge();
68
68
  return 0;
69
69
  }
70
70
  case "claude":
71
71
  case "codex":
72
72
  case "cursor": {
73
- const { launch } = await import("./launch-I4T5MFWF.js");
73
+ const { launch } = await import("./launch-S6XFRSZA.js");
74
74
  return launch(cmd, rest);
75
75
  }
76
76
  case "attach": {
77
- const { attach } = await import("./attach-HPGWEUKX.js");
77
+ const { attach } = await import("./attach-2DEXQEC2.js");
78
78
  return attach(rest[0] ?? "");
79
79
  }
80
80
  default:
@@ -5,15 +5,16 @@ import {
5
5
  ensureDaemon,
6
6
  launch,
7
7
  parseLaunchArgs
8
- } from "./chunk-N3MPPCON.js";
8
+ } from "./chunk-IC5RHOX6.js";
9
9
  import {
10
10
  agentEnv,
11
11
  exitFile
12
- } from "./chunk-564TJJZ6.js";
13
- import "./chunk-3TNPEAMR.js";
14
- import "./chunk-L6IGH4LF.js";
15
- import "./chunk-XPWGZIYL.js";
16
- import "./chunk-WM4MAV46.js";
12
+ } from "./chunk-GMKS55KF.js";
13
+ import "./chunk-TO4XO2ML.js";
14
+ import "./chunk-YUSY3E47.js";
15
+ import "./chunk-TC3XJUJA.js";
16
+ import "./chunk-JOCNCNRP.js";
17
+ import "./chunk-FGOFRWIB.js";
17
18
  export {
18
19
  DaemonError,
19
20
  agentEnv,
@@ -13,9 +13,10 @@ import {
13
13
  runSetup,
14
14
  stopDetachedDaemon,
15
15
  systemdUnit
16
- } from "./chunk-Q5CLZPYJ.js";
17
- import "./chunk-3TNPEAMR.js";
18
- import "./chunk-L6IGH4LF.js";
16
+ } from "./chunk-Q7RCYJ4X.js";
17
+ import "./chunk-TO4XO2ML.js";
18
+ import "./chunk-YUSY3E47.js";
19
+ import "./chunk-FGOFRWIB.js";
19
20
  export {
20
21
  CLAUDE_EVENTS,
21
22
  CODEX_EVENTS,
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-XPWGZIYL.js";
4
- import "./chunk-WM4MAV46.js";
3
+ } from "./chunk-TC3XJUJA.js";
4
+ import "./chunk-JOCNCNRP.js";
5
+ import "./chunk-FGOFRWIB.js";
5
6
 
6
7
  // src/cli/status.ts
7
8
  async function printStatus(opts = {}) {
@@ -2,11 +2,12 @@ import {
2
2
  SERVICE,
3
3
  execRunner,
4
4
  stopDetachedDaemon
5
- } from "./chunk-Q5CLZPYJ.js";
6
- import "./chunk-3TNPEAMR.js";
5
+ } from "./chunk-Q7RCYJ4X.js";
6
+ import "./chunk-TO4XO2ML.js";
7
+ import "./chunk-YUSY3E47.js";
7
8
  import {
8
9
  paths
9
- } from "./chunk-L6IGH4LF.js";
10
+ } from "./chunk-FGOFRWIB.js";
10
11
 
11
12
  // src/cli/uninstall.ts
12
13
  import { readFile, writeFile, rename, rm, lstat, realpath } from "fs/promises";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  LEDGE_VERSION
3
- } from "./chunk-LWPKX7SB.js";
3
+ } from "./chunk-XOPH7YDV.js";
4
4
  export {
5
5
  LEDGE_VERSION
6
6
  };
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-XPWGZIYL.js";
3
+ } from "./chunk-TC3XJUJA.js";
4
4
  import {
5
5
  isArchived
6
- } from "./chunk-WM4MAV46.js";
6
+ } from "./chunk-JOCNCNRP.js";
7
+ import "./chunk-FGOFRWIB.js";
7
8
 
8
9
  // src/cli/who.ts
9
10
  import { basename } from "path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paradow/ledge",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Follow and answer your WSL agent sessions (Claude Code, Codex, Cursor Agent) from the Ledge island on Windows.",
5
5
  "type": "module",
6
6
  "bin": { "ledge": "dist/cli/index.js" },
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var LEDGE_VERSION = true ? "0.9.0" : "0.0.0-dev";
3
-
4
- export {
5
- LEDGE_VERSION
6
- };