@paradow/ledge 0.33.0 → 0.35.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.
Files changed (41) hide show
  1. package/dist/cli/index.js +39003 -46
  2. package/dist/cli/reduceWorker.js +2037 -0
  3. package/dist/ledge-guide.md +26 -49
  4. package/package.json +5 -1
  5. package/dist/cli/action-QW2ZQQBN.js +0 -27
  6. package/dist/cli/attach-ULTYWCJ7.js +0 -74
  7. package/dist/cli/bridge-CSLF7EW6.js +0 -70
  8. package/dist/cli/chunk-3DB3VOMM.js +0 -723
  9. package/dist/cli/chunk-4HXJQEUN.js +0 -118
  10. package/dist/cli/chunk-4VNKZOC2.js +0 -160
  11. package/dist/cli/chunk-6IYPRUQG.js +0 -17
  12. package/dist/cli/chunk-ASYAWVFW.js +0 -95
  13. package/dist/cli/chunk-CCSUP5ZI.js +0 -45
  14. package/dist/cli/chunk-CPERVAPQ.js +0 -232
  15. package/dist/cli/chunk-CSLVBADC.js +0 -60
  16. package/dist/cli/chunk-E3LXONBI.js +0 -6
  17. package/dist/cli/chunk-GXL3X7M7.js +0 -144
  18. package/dist/cli/chunk-HOOHS3MX.js +0 -39
  19. package/dist/cli/chunk-JEGPTNWV.js +0 -270
  20. package/dist/cli/chunk-K7UEKSB2.js +0 -305
  21. package/dist/cli/chunk-NLL2EQJ2.js +0 -94
  22. package/dist/cli/chunk-NWIICISV.js +0 -290
  23. package/dist/cli/chunk-OLLQBK6N.js +0 -41
  24. package/dist/cli/chunk-PZ3F4QH3.js +0 -9758
  25. package/dist/cli/chunk-U67V476Y.js +0 -35
  26. package/dist/cli/chunk-WXE2NOE5.js +0 -2674
  27. package/dist/cli/chunk-XOZD4BP5.js +0 -85
  28. package/dist/cli/daemon-5GT545BB.js +0 -2322
  29. package/dist/cli/decide-PFHHOCFZ.js +0 -59
  30. package/dist/cli/handover-EMO6JT7O.js +0 -17
  31. package/dist/cli/hook-W2BU74H5.js +0 -523
  32. package/dist/cli/launch-LP7YJR5N.js +0 -36
  33. package/dist/cli/send-NEI4X33R.js +0 -19
  34. package/dist/cli/setup-K42IVMHS.js +0 -38
  35. package/dist/cli/status-U2HGGKHF.js +0 -28
  36. package/dist/cli/task-PDDCE23D.js +0 -19
  37. package/dist/cli/team-S3ASP22H.js +0 -205
  38. package/dist/cli/tui-UPMRMCPC.js +0 -19216
  39. package/dist/cli/uninstall-4IMMR55T.js +0 -162
  40. package/dist/cli/version-LUMK3Z3T.js +0 -7
  41. package/dist/cli/who-NM3GPAPJ.js +0 -12
@@ -1,305 +0,0 @@
1
- import {
2
- run
3
- } from "./chunk-NLL2EQJ2.js";
4
- import {
5
- ensureLedgeDir,
6
- paths
7
- } from "./chunk-HOOHS3MX.js";
8
-
9
- // src/cli/setup.ts
10
- import { readFile as readFile2, writeFile, mkdir, rename, realpath, rm, stat, lstat, readlink } from "fs/promises";
11
- import { execFile } from "child_process";
12
- import { homedir, userInfo } from "os";
13
- import { join, dirname } from "path";
14
-
15
- // src/update/guard.ts
16
- import { readFile } from "fs/promises";
17
- var inDaemonService = (cgroup) => cgroup.split("\n").some((l) => l.split("/").includes(SERVICE));
18
- async function tmuxServerAtRisk(opts = {}) {
19
- const runTmux = opts.runTmux ?? run;
20
- const readCgroup = opts.readCgroup ?? ((pid2) => readFile(`/proc/${pid2}/cgroup`, "utf8"));
21
- const r = await runTmux(["-L", opts.socket ?? "ledge", "list-sessions", "-F", "#{pid}"]);
22
- if (r.code !== 0) return false;
23
- const pid = r.stdout.split("\n").map((s) => s.trim()).find(Boolean);
24
- if (!pid) return false;
25
- try {
26
- return inDaemonService(await readCgroup(pid));
27
- } catch {
28
- return true;
29
- }
30
- }
31
-
32
- // src/cli/setup.ts
33
- var CLAUDE_EVENTS = [
34
- "SessionStart",
35
- "UserPromptSubmit",
36
- "PreToolUse",
37
- "PermissionRequest",
38
- "Notification",
39
- "Stop",
40
- "SessionEnd",
41
- "PostToolUse",
42
- "PostToolUseFailure",
43
- "StopFailure",
44
- "SubagentStart",
45
- "SubagentStop"
46
- ];
47
- var CODEX_EVENTS = [
48
- "SessionStart",
49
- "UserPromptSubmit",
50
- "PreToolUse",
51
- "PermissionRequest",
52
- "PostToolUse",
53
- "Stop",
54
- "SubagentStart",
55
- "SubagentStop",
56
- "Interrupt",
57
- "SessionEnd"
58
- ];
59
- function mergeHooks(settings, events, command, file) {
60
- if (settings === void 0) settings = {};
61
- if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new Error(`${file} root is not an object, not modified`);
62
- const base = settings;
63
- if (base.hooks !== void 0 && (!base.hooks || typeof base.hooks !== "object" || Array.isArray(base.hooks))) throw new Error(`${file} "hooks" is not an object, not modified`);
64
- const hooks = { ...base.hooks ?? {} };
65
- let changed = false;
66
- for (const ev of events) {
67
- if (hooks[ev] !== void 0 && !Array.isArray(hooks[ev])) throw new Error(`${file} hooks.${ev} is not an array, not modified`);
68
- const list = [...hooks[ev] ?? []];
69
- const mine = `${command} ${ev}`;
70
- if (!list.some((e) => e.hooks?.some((h) => h.command === mine))) {
71
- list.push({ hooks: [{ type: "command", command: mine, timeout: 5 }] });
72
- changed = true;
73
- }
74
- hooks[ev] = list;
75
- }
76
- return { settings: { ...base, hooks }, changed };
77
- }
78
- function mergeClaudeHooks(settings, command = "ledge hook") {
79
- return mergeHooks(settings, CLAUDE_EVENTS, command, "settings.json");
80
- }
81
- function mergeCodexHooks(hooks, command = "ledge hook codex") {
82
- return mergeHooks(hooks, CODEX_EVENTS, command, "hooks.json");
83
- }
84
- var CURSOR_EVENTS = [
85
- "sessionStart",
86
- "beforeSubmitPrompt",
87
- "preToolUse",
88
- "postToolUse",
89
- "postToolUseFailure",
90
- "stop",
91
- "afterAgentResponse",
92
- "sessionEnd"
93
- ];
94
- function mergeCursorHooks(config, command = "ledge hook cursor") {
95
- if (config === void 0) config = {};
96
- if (!config || typeof config !== "object" || Array.isArray(config)) throw new Error("hooks.json root is not an object, not modified");
97
- const base = config;
98
- if (base.hooks !== void 0 && (!base.hooks || typeof base.hooks !== "object" || Array.isArray(base.hooks))) throw new Error('hooks.json "hooks" is not an object, not modified');
99
- const hooks = { ...base.hooks ?? {} };
100
- let changed = base.version === void 0;
101
- for (const ev of CURSOR_EVENTS) {
102
- if (hooks[ev] !== void 0 && !Array.isArray(hooks[ev])) throw new Error(`hooks.json hooks.${ev} is not an array, not modified`);
103
- const list = [...hooks[ev] ?? []];
104
- const mine = `${command} ${ev}`;
105
- if (!list.some((e) => e?.command === mine)) {
106
- list.push({ command: mine, timeout: 5 });
107
- changed = true;
108
- }
109
- hooks[ev] = list;
110
- }
111
- return { settings: { version: 1, ...base, hooks }, changed };
112
- }
113
- async function mergeJsonFile(path, merge, name) {
114
- let raw = null;
115
- try {
116
- raw = await readFile2(path, "utf8");
117
- } catch (e) {
118
- if (e.code !== "ENOENT") throw e;
119
- }
120
- let current;
121
- if (raw !== null) {
122
- try {
123
- current = JSON.parse(raw);
124
- } catch {
125
- throw new Error(`${name} is not valid JSON, not modified`);
126
- }
127
- }
128
- const { settings, changed } = merge(current);
129
- if (!changed) return false;
130
- await mkdir(dirname(path), { recursive: true });
131
- const tmp = `${path}.ledge-${process.pid}.tmp`;
132
- await writeFile(tmp, JSON.stringify(settings, null, 2) + "\n");
133
- await rename(tmp, path);
134
- return true;
135
- }
136
- var SERVICE = "ledge-daemon.service";
137
- var execRunner = (cmd, args) => new Promise((resolve) => {
138
- execFile(cmd, args, (err, stdout, stderr) => {
139
- const code = err && typeof err.code === "number" ? err.code : err ? 1 : 0;
140
- resolve({ code, stdout: String(stdout), stderr: String(stderr) });
141
- });
142
- });
143
- function systemdUnit(opts) {
144
- return `[Unit]
145
- Description=Ledge daemon (agent sessions for the Windows overlay)
146
-
147
- [Service]
148
- ExecStart=${opts.node} ${opts.script} daemon
149
- Restart=on-failure
150
- RestartSec=2
151
-
152
- [Install]
153
- WantedBy=default.target
154
- `;
155
- }
156
- async function writeIfChanged(path, content) {
157
- let current = null;
158
- try {
159
- current = await readFile2(path, "utf8");
160
- } catch (e) {
161
- if (e.code !== "ENOENT") throw e;
162
- }
163
- if (current === content) return false;
164
- await mkdir(dirname(path), { recursive: true });
165
- const tmp = `${path}.ledge-${process.pid}.tmp`;
166
- await writeFile(tmp, content);
167
- await rename(tmp, path);
168
- return true;
169
- }
170
- async function stopDetachedDaemon(pidFile) {
171
- let pid;
172
- try {
173
- pid = Number((await readFile2(pidFile, "utf8")).trim());
174
- } catch {
175
- return null;
176
- }
177
- if (!Number.isInteger(pid) || pid <= 0) return null;
178
- try {
179
- process.kill(pid, 0);
180
- } catch {
181
- await rm(pidFile, { force: true });
182
- return null;
183
- }
184
- try {
185
- process.kill(pid, "SIGTERM");
186
- } catch {
187
- return null;
188
- }
189
- const deadline = Date.now() + 3e3;
190
- while (Date.now() < deadline) {
191
- await new Promise((r) => setTimeout(r, 50));
192
- try {
193
- process.kill(pid, 0);
194
- } catch {
195
- return pid;
196
- }
197
- }
198
- return pid;
199
- }
200
- async function installService(opts = {}) {
201
- const out = [];
202
- const run2 = opts.run ?? execRunner;
203
- const unitDir = opts.unitDir ?? join(homedir(), ".config", "systemd", "user");
204
- const node = opts.node ?? await realpath(process.execPath);
205
- const script = opts.script ?? await realpath(process.argv[1]);
206
- const changed = await writeIfChanged(join(unitDir, SERVICE), systemdUnit({ node, script }));
207
- const reload = await run2("systemctl", ["--user", "daemon-reload"]);
208
- if (reload.code !== 0) throw new Error(`systemd user session unavailable (systemctl --user daemon-reload failed: ${reload.stderr.trim() || reload.code}); the daemon will still be started on demand by ledge claude`);
209
- const active = (await run2("systemctl", ["--user", "is-active", SERVICE])).code === 0;
210
- if (active && changed && await (opts.atRisk ?? tmuxServerAtRisk)()) {
211
- out.push(`${SERVICE} unit updated but not restarted: the tmux server of your agents runs inside the service and would stop with it; close the sessions, then run ledge setup again (or systemctl --user restart ${SERVICE})`);
212
- } else if (active && changed) {
213
- const r = await run2("systemctl", ["--user", "restart", SERVICE]);
214
- if (r.code !== 0) throw new Error(`systemctl --user restart ${SERVICE} failed: ${r.stderr.trim()}`);
215
- out.push(`${SERVICE} updated and restarted`);
216
- } else if (active) {
217
- out.push(`${SERVICE} already running`);
218
- } else {
219
- const pid = await stopDetachedDaemon(paths().pid);
220
- if (pid !== null) out.push(`detached daemon (pid ${pid}) stopped, the service takes over`);
221
- const r = await run2("systemctl", ["--user", "enable", "--now", SERVICE]);
222
- if (r.code !== 0) throw new Error(`systemctl --user enable --now ${SERVICE} failed: ${r.stderr.trim()}`);
223
- out.push(`${SERVICE} installed and started (journalctl --user -u ${SERVICE} for its output)`);
224
- }
225
- const user = process.env.USER ?? userInfo().username;
226
- const linger = await run2("loginctl", ["show-user", user, "-p", "Linger"]);
227
- if (linger.code === 0 && /^Linger=yes/m.test(linger.stdout)) return out;
228
- const enable = await run2("loginctl", ["enable-linger"]);
229
- if (enable.code === 0) out.push("linger enabled: the daemon starts with WSL, before any shell is open");
230
- else out.push(`linger not enabled (${enable.stderr.trim() || `exit ${enable.code}`}): the daemon starts with your first WSL shell; run \`sudo loginctl enable-linger ${user}\` to start it at WSL boot`);
231
- return out;
232
- }
233
- async function isDir(path) {
234
- try {
235
- return (await stat(path)).isDirectory();
236
- } catch {
237
- return false;
238
- }
239
- }
240
- var BIN_MARK = "#!/bin/sh\n# Written by `ledge setup`";
241
- var shQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
242
- async function writeBin(opts) {
243
- const st = await lstat(opts.link).catch(() => null);
244
- const was = st?.isSymbolicLink() ? await readlink(opts.link) : null;
245
- if (was !== null && !was.endsWith("/dist/cli/index.js")) return null;
246
- const current = st && was === null && st.isFile() ? await readFile2(opts.link, "utf8").catch(() => "") : "";
247
- if (st && was === null && !current.startsWith(BIN_MARK)) return null;
248
- const script = `${BIN_MARK}: the hooks run \`ledge\` wherever node is not on the PATH.
249
- exec ${shQuote(opts.node)} ${shQuote(opts.target)} "$@"
250
- `;
251
- if (current === script) return null;
252
- await mkdir(dirname(opts.link), { recursive: true });
253
- const tmp = `${opts.link}.tmp`;
254
- await writeFile(tmp, script, { mode: 493 });
255
- await rename(tmp, opts.link);
256
- return `${opts.link} runs ${opts.target} with ${opts.node}${was !== null ? ` (was a link to ${was})` : ""}`;
257
- }
258
- async function runSetup(opts = {}) {
259
- const out = [];
260
- await ensureLedgeDir();
261
- out.push("~/.ledge ready (tmux.conf written if missing)");
262
- const settingsPath = opts.settingsPath ?? join(homedir(), ".claude", "settings.json");
263
- if (await mergeJsonFile(settingsPath, mergeClaudeHooks, "settings.json")) out.push(`Claude Code hooks added in ${settingsPath}`);
264
- else out.push("Claude Code hooks already present");
265
- const codexDir = opts.codexDir ?? process.env.CODEX_HOME ?? join(homedir(), ".codex");
266
- if (!await isDir(codexDir)) out.push(`Codex not found (${codexDir} missing), Codex hooks skipped`);
267
- else if (await mergeJsonFile(join(codexDir, "hooks.json"), mergeCodexHooks, "hooks.json")) {
268
- out.push(`Codex hooks added in ${join(codexDir, "hooks.json")}; Codex asks to review them on its next start: trust them there or from Ledge`);
269
- } else out.push("Codex hooks already present");
270
- const cursorDir = opts.cursorDir ?? join(homedir(), ".cursor");
271
- if (!await isDir(cursorDir)) out.push(`Cursor Agent not found (${cursorDir} missing), Cursor Agent hooks skipped`);
272
- else if (await mergeJsonFile(join(cursorDir, "hooks.json"), mergeCursorHooks, "hooks.json")) out.push(`Cursor Agent hooks added in ${join(cursorDir, "hooks.json")}`);
273
- else out.push("Cursor Agent hooks already present");
274
- if (opts.service ?? true) {
275
- const cli = opts.cli ?? process.argv[1] ?? "";
276
- if (cli.endsWith(".ts")) out.push("systemd service skipped: run setup from the built CLI (pnpm --filter @paradow/ledge build, then ledge setup)");
277
- else {
278
- const script = await realpath(cli);
279
- const line = await writeBin({ link: opts.link ?? join(homedir(), ".local", "bin", "ledge"), node: process.execPath, target: script });
280
- if (line) out.push(line);
281
- out.push(...await installService({ script, unitDir: opts.unitDir, run: opts.run }));
282
- }
283
- }
284
- return out;
285
- }
286
-
287
- export {
288
- CLAUDE_EVENTS,
289
- CODEX_EVENTS,
290
- mergeClaudeHooks,
291
- mergeCodexHooks,
292
- CURSOR_EVENTS,
293
- mergeCursorHooks,
294
- SERVICE,
295
- execRunner,
296
- systemdUnit,
297
- stopDetachedDaemon,
298
- installService,
299
- isDir,
300
- BIN_MARK,
301
- writeBin,
302
- runSetup,
303
- inDaemonService,
304
- tmuxServerAtRisk
305
- };
@@ -1,94 +0,0 @@
1
- import {
2
- log
3
- } from "./chunk-6IYPRUQG.js";
4
- import {
5
- paths
6
- } from "./chunk-HOOHS3MX.js";
7
-
8
- // src/tmux.ts
9
- import { execFile } from "child_process";
10
- import { existsSync } from "fs";
11
- import { join } from "path";
12
- function runFile(file, args, env, opts) {
13
- return new Promise((resolve) => {
14
- execFile(file, args, { maxBuffer: 4 * 1024 * 1024, env, ...opts?.timeoutMs ? { timeout: opts.timeoutMs, killSignal: "SIGTERM" } : {} }, (err, stdout, stderr) => {
15
- const code = err && opts?.timeoutMs && err.killed ? 124 : err && typeof err.code === "number" ? err.code : err ? 1 : 0;
16
- resolve({ code, stdout: String(stdout), stderr: String(stderr) });
17
- });
18
- });
19
- }
20
- var run = (args, env) => runFile("tmux", args, env);
21
- function systemdAvailable(env = process.env, exists = existsSync) {
22
- return !!env.XDG_RUNTIME_DIR && exists(join(env.XDG_RUNTIME_DIR, "systemd", "private"));
23
- }
24
- function newSessionCommand(tmuxArgs, scoped, unit) {
25
- if (!scoped) return { file: "tmux", args: tmuxArgs };
26
- return { file: "systemd-run", args: ["--user", "--scope", "--quiet", "--collect", `--unit=${unit}`, "--", "tmux", ...tmuxArgs] };
27
- }
28
- function realTmux(socket = "ledge", opts = {}) {
29
- const scope = opts.scope ?? (async () => systemdAvailable());
30
- const base = () => ["-L", socket, "-f", paths().tmuxConf];
31
- const t = (args, env) => run([...base(), ...args], env);
32
- return {
33
- async newSession({ name, cwd, env, unset = [], command }) {
34
- const envArgs = Object.entries(env).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
35
- if (unset.length) command = ["env", ...unset.flatMap((v) => ["-u", v]), ...command];
36
- const clientEnv = env.PATH === void 0 ? void 0 : { ...process.env, PATH: env.PATH };
37
- const args = [...base(), "new-session", "-d", "-s", name, "-c", cwd, "-x", "200", "-y", "50", ...envArgs, "--", ...command];
38
- const serverUp = (await t(["list-sessions"])).code === 0;
39
- const scoped = !serverUp && await scope();
40
- const { file, args: argv } = newSessionCommand(args, scoped, `ledge-tmux-${Date.now()}`);
41
- let r = await runFile(file, argv, clientEnv);
42
- if (r.code !== 0 && scoped) {
43
- log(`tmux: scoped start failed (${r.stderr.trim()}), starting unscoped`);
44
- r = await runFile("tmux", args, clientEnv);
45
- }
46
- if (r.code !== 0) throw new Error(`tmux new-session failed: ${r.stderr.trim()}`);
47
- },
48
- async sendText(name, text) {
49
- if (/[\r\n]/.test(text)) throw new Error("sendText: text must not contain newlines; flatten it first");
50
- const arg = text.endsWith(";") ? `${text.slice(0, -1)}\\;` : text;
51
- const r = await t(["send-keys", "-t", `=${name}:`, "-l", "--", arg]);
52
- if (r.code !== 0) throw new Error(`tmux send-keys failed: ${r.stderr.trim()}`);
53
- },
54
- async sendKeys(name, keys) {
55
- const r = await t(["send-keys", "-t", `=${name}:`, ...keys]);
56
- if (r.code !== 0) throw new Error(`tmux send-keys failed: ${r.stderr.trim()}`);
57
- },
58
- async capture(name, lines) {
59
- const r = await t(["capture-pane", "-p", "-J", "-t", `=${name}:`, "-S", String(-lines)]);
60
- if (r.code !== 0) return null;
61
- return r.stdout.replace(/\s+$/g, "").split("\n").slice(-lines).join("\n");
62
- },
63
- async width(name) {
64
- const r = await t(["display-message", "-p", "-t", `=${name}:`, "#{pane_width}"]);
65
- const n = Number(r.stdout.trim());
66
- return r.code === 0 && Number.isInteger(n) && n > 0 ? n : null;
67
- },
68
- async hasSession(name) {
69
- return (await t(["has-session", "-t", `=${name}`])).code === 0;
70
- },
71
- async listSessions() {
72
- const r = await t(["list-sessions", "-F", "#{session_name}"]);
73
- if (r.code === 0) return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
74
- if (/no server running|error connecting to/i.test(r.stderr)) return [];
75
- throw new Error(`tmux list-sessions failed: ${r.stderr.trim()}`);
76
- },
77
- async setOption(name, option, value) {
78
- const r = await t(["set-option", "-t", `=${name}:`, option, value]);
79
- if (r.code !== 0) throw new Error(`tmux set-option failed: ${r.stderr.trim()}`);
80
- },
81
- async killSession(name) {
82
- await t(["kill-session", "-t", `=${name}`]);
83
- },
84
- attachArgs(name, opts2) {
85
- return [...base(), "attach-session", ...opts2?.readOnly ? ["-r"] : [], "-t", `=${name}`];
86
- }
87
- };
88
- }
89
-
90
- export {
91
- runFile,
92
- run,
93
- realTmux
94
- };
@@ -1,290 +0,0 @@
1
- import {
2
- definitionDirs,
3
- definitionFiles,
4
- definitionHash,
5
- listRoles,
6
- loadRole,
7
- quoteScalar,
8
- readSmall,
9
- repoRoot,
10
- scalarValue
11
- } from "./chunk-4VNKZOC2.js";
12
- import {
13
- SeatsSchema,
14
- TeamNameSchema,
15
- TemplateIdSchema
16
- } from "./chunk-WXE2NOE5.js";
17
- import {
18
- log
19
- } from "./chunk-6IYPRUQG.js";
20
- import {
21
- ledgeDir
22
- } from "./chunk-HOOHS3MX.js";
23
-
24
- // src/daemon/teams.ts
25
- import { readFileSync as readFileSync2 } from "fs";
26
- import { join as join2 } from "path";
27
-
28
- // src/daemon/definitions.ts
29
- import { linkSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "fs";
30
- import { basename, dirname, join } from "path";
31
- function roleText(r) {
32
- const lines = [];
33
- for (const [key, value] of [["label", r.label], ["tool", r.tool], ["model", r.model], ["effort", r.effort], ["autonomy", r.autonomy]]) {
34
- if (value === void 0) continue;
35
- const q = quoteScalar(value);
36
- if (q === null) return null;
37
- lines.push(`${key}: ${q}`);
38
- }
39
- return `---
40
- ${lines.join("\n")}
41
- ---
42
-
43
- ${r.prompt}
44
- `;
45
- }
46
- function current(path) {
47
- try {
48
- return readFileSync(path, "utf8");
49
- } catch (e) {
50
- if (e.code === "ENOENT") return void 0;
51
- throw e;
52
- }
53
- }
54
- function writeDefinition(path, text, base) {
55
- const now = current(path);
56
- if (base === null) {
57
- if (now !== void 0) return { code: "exists" };
58
- } else if (now === void 0 || definitionHash(now) !== base) return { code: "changed" };
59
- mkdirSync(dirname(path), { recursive: true });
60
- const tmp = `${path}.${process.pid}.tmp`;
61
- try {
62
- writeFileSync(tmp, text);
63
- if (base === null) linkSync(tmp, path);
64
- else renameSync(tmp, path);
65
- } catch (e) {
66
- rmSync(tmp, { force: true });
67
- if (base === null && e.code === "EEXIST") return { code: "exists" };
68
- throw e;
69
- }
70
- rmSync(tmp, { force: true });
71
- return { hash: definitionHash(readFileSync(path, "utf8")) };
72
- }
73
- function deleteDefinition(path, base) {
74
- const now = current(path);
75
- if (now === void 0) return { code: "missing" };
76
- if (definitionHash(now) !== base) return { code: "changed" };
77
- unlinkSync(path);
78
- return { ok: true };
79
- }
80
- function scopeDir(kind, scope, repoFolder) {
81
- if (scope === "home") return join(ledgeDir(), kind);
82
- if (!repoFolder) throw new Error("the repository scope needs a folder");
83
- return join(repoRoot(repoFolder), ".ledge", kind);
84
- }
85
- function scopeDefinitions(dir, scope) {
86
- const problems = [];
87
- const roles = [];
88
- const templates = /* @__PURE__ */ new Map();
89
- for (const path of definitionFiles(dir.roles, [".md"])) {
90
- const r = loadRole(basename(path, ".md"), path, scope, problems);
91
- if (r) roles.push(r);
92
- }
93
- for (const path of definitionFiles(dir.teams, [".yaml", ".yml"])) {
94
- const id = basename(path).replace(/\.ya?ml$/, "");
95
- if (templates.has(id)) continue;
96
- const t = loadTemplate(id, path, scope, problems);
97
- if (t) templates.set(id, t);
98
- }
99
- return { roles: roles.sort((a, b) => a.id.localeCompare(b.id)), templates: [...templates.values()].sort((a, b) => a.id.localeCompare(b.id)), problems };
100
- }
101
- function promptRecipients(roleId, path, sessions, resolve) {
102
- return sessions.filter((s) => s.role === roleId && s.source === "launcher" && s.state !== "exited" && s.archivedAt === void 0 && resolve(roleId, s.cwd)?.path === path);
103
- }
104
-
105
- // src/daemon/teams.ts
106
- var scalar = scalarValue;
107
- function flowMap(raw) {
108
- const inner = raw.trim().replace(/^\{/, "").replace(/\}$/, "");
109
- const out = {};
110
- for (const part of inner.split(",")) {
111
- if (!part.trim()) continue;
112
- const i = part.indexOf(":");
113
- if (i <= 0) throw new Error(`not a key: value pair: ${part.trim().slice(0, 40)}`);
114
- out[part.slice(0, i).trim()] = scalar(part.slice(i + 1));
115
- }
116
- return out;
117
- }
118
- function parseTemplateText(text) {
119
- const src = text.replace(/^/, "").replace(/\r\n/g, "\n");
120
- if (src.trimStart().startsWith("{")) return JSON.parse(src);
121
- const out = {};
122
- let list = null;
123
- let item = null;
124
- let itemIndent = -1;
125
- for (const raw of src.split("\n")) {
126
- if (!raw.trim() || raw.trimStart().startsWith("#")) continue;
127
- if (raw.includes(" ")) throw new Error("tabs are not allowed in YAML");
128
- const indent = raw.length - raw.trimStart().length;
129
- const line = raw.trim();
130
- if (indent === 0 && !line.startsWith("- ")) {
131
- item = null;
132
- list = null;
133
- const i2 = line.indexOf(":");
134
- if (i2 <= 0) throw new Error(`line without a key: ${line.slice(0, 60)}`);
135
- const key = line.slice(0, i2).trim();
136
- const rest = line.slice(i2 + 1).trim();
137
- if (rest === "") {
138
- list = [];
139
- out[key] = list;
140
- } else if (rest.startsWith("[")) out[key] = rest.replace(/^\[|\]$/g, "").split(",").map(scalar).filter(Boolean);
141
- else out[key] = scalar(rest);
142
- continue;
143
- }
144
- if (!list) throw new Error(`indented line outside a list: ${line.slice(0, 60)}`);
145
- if (line.startsWith("- ") || line === "-") {
146
- const rest = line.slice(1).trim();
147
- if (rest.startsWith("{")) {
148
- list.push(flowMap(rest));
149
- item = null;
150
- continue;
151
- }
152
- item = {};
153
- itemIndent = indent;
154
- list.push(item);
155
- if (rest) {
156
- const i2 = rest.indexOf(":");
157
- if (i2 <= 0) {
158
- list[list.length - 1] = scalar(rest);
159
- item = null;
160
- continue;
161
- }
162
- item[rest.slice(0, i2).trim()] = scalar(rest.slice(i2 + 1));
163
- }
164
- continue;
165
- }
166
- if (!item || indent <= itemIndent) throw new Error(`line outside a seat: ${line.slice(0, 60)}`);
167
- const i = line.indexOf(":");
168
- if (i <= 0) throw new Error(`line without a key: ${line.slice(0, 60)}`);
169
- item[line.slice(0, i).trim()] = scalar(line.slice(i + 1));
170
- }
171
- return out;
172
- }
173
- function parseTemplate(id, text, source, path) {
174
- if (!TemplateIdSchema.safeParse(id).success) return "nom de fichier invalide : lettres, chiffres, - et _ seulement (64 au plus, sans point), puis .yaml";
175
- let raw;
176
- try {
177
- raw = parseTemplateText(text);
178
- } catch (e) {
179
- return e.message;
180
- }
181
- const name = TeamNameSchema.safeParse(typeof raw.name === "string" ? raw.name : id);
182
- if (!name.success) return "name: a name of 1 to 60 characters";
183
- const seats = SeatsSchema.safeParse(raw.seats);
184
- if (!seats.success) {
185
- const issue = seats.error.issues[0];
186
- return `seats${issue.path.length ? `.${issue.path.join(".")}` : ""}: ${issue.message}`;
187
- }
188
- return { id, name: name.data, seats: seats.data, source, path };
189
- }
190
- function loadTemplate(id, path, source, problems) {
191
- let t;
192
- try {
193
- const text = readSmall(path);
194
- t = parseTemplate(id, text, source, path);
195
- if (typeof t !== "string") t = { ...t, hash: definitionHash(text) };
196
- } catch (e) {
197
- t = e.message;
198
- }
199
- if (typeof t === "string") {
200
- log(`team ${path} skipped: ${t}`);
201
- problems.push({ path, reason: t });
202
- return void 0;
203
- }
204
- return t;
205
- }
206
- function listTemplates(cwd) {
207
- const templates = /* @__PURE__ */ new Map();
208
- const problems = [];
209
- for (const { dir, source } of definitionDirs("teams", cwd)) {
210
- for (const path of definitionFiles(dir, [".yaml", ".yml"])) {
211
- const id = path.slice(dir.length + 1).replace(/\.ya?ml$/, "");
212
- if (templates.has(id)) continue;
213
- const t = loadTemplate(id, path, source, problems);
214
- if (t) templates.set(id, t);
215
- }
216
- }
217
- return { templates: [...templates.values()].sort((a, b) => a.id.localeCompare(b.id)), problems };
218
- }
219
- function resolveSeats(seats, roles, defaults = { tool: "claude" }) {
220
- const byId = new Map(roles.map((r) => [r.id, r]));
221
- const unknown = seats.filter((s) => !byId.has(s.role)).map((s) => s.role);
222
- if (unknown.length) return { unknown };
223
- return seats.map((s) => {
224
- const role = byId.get(s.role);
225
- const tool = s.tool ?? role.tool ?? defaults.tool;
226
- const own = s.tool === void 0 || s.tool === role.tool;
227
- const model = s.model ?? (own ? role.model : void 0) ?? (tool === defaults.tool ? defaults.model : void 0);
228
- const effort = s.effort ?? (own ? role.effort : void 0) ?? (tool === defaults.tool ? defaults.effort : void 0);
229
- return { role, tool, ...model ? { model } : {}, ...effort ? { effort } : {}, ...s.name ? { name: s.name } : {} };
230
- });
231
- }
232
- function listDefinitions(cwd) {
233
- const r = listRoles(cwd);
234
- const t = listTemplates(cwd);
235
- return { roles: r.roles, templates: t.templates, problems: [...r.problems, ...t.problems] };
236
- }
237
- function templateId(name) {
238
- const id = name.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
239
- return id || "team";
240
- }
241
- var SEAT_KEYS = ["role", "tool", "model", "effort", "name"];
242
- function templateText(name, seats) {
243
- const q = (v) => v.includes(" ") ? null : quoteScalar(v);
244
- const head = q(name);
245
- if (head === null) return null;
246
- const lines = [`name: ${head}`, "seats:"];
247
- for (const s of seats) {
248
- let first = true;
249
- for (const k of SEAT_KEYS) {
250
- if (s[k] === void 0) continue;
251
- const v = q(String(s[k]));
252
- if (v === null) return null;
253
- lines.push(`${first ? " - " : " "}${k}: ${v}`);
254
- first = false;
255
- }
256
- }
257
- return `${lines.join("\n")}
258
- `;
259
- }
260
- var templatePath = (name, dir) => join2(dir, `${templateId(name)}.yaml`);
261
- function templateHash(name, dir = join2(ledgeDir(), "teams")) {
262
- try {
263
- return definitionHash(readFileSync2(templatePath(name, dir), "utf8"));
264
- } catch (e) {
265
- if (e.code === "ENOENT") return void 0;
266
- throw e;
267
- }
268
- }
269
- function saveTemplate(name, seats, base, dir = join2(ledgeDir(), "teams")) {
270
- const path = templatePath(name, dir);
271
- const text = templateText(name, seats);
272
- if (text === null) throw new Error("a team name or seat value cannot be written as YAML");
273
- const r = writeDefinition(path, text, base ?? null);
274
- return "code" in r ? { path, code: r.code } : { path };
275
- }
276
-
277
- export {
278
- roleText,
279
- writeDefinition,
280
- deleteDefinition,
281
- scopeDir,
282
- scopeDefinitions,
283
- promptRecipients,
284
- resolveSeats,
285
- listDefinitions,
286
- templateId,
287
- templateText,
288
- templateHash,
289
- saveTemplate
290
- };