@paradow/ledge 0.16.1 → 0.18.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 (40) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/{attach-5H727FCW.js → attach-I6TTCGNV.js} +12 -7
  3. package/dist/cli/{bridge-LXMOQN6Q.js → bridge-APNFBPNE.js} +2 -2
  4. package/dist/cli/chunk-3EIRKAX7.js +165 -0
  5. package/dist/cli/chunk-3HCID6NW.js +145 -0
  6. package/dist/cli/{chunk-BNKPXSUY.js → chunk-43D6NMYH.js} +1 -1
  7. package/dist/cli/{daemon-DKH63BGV.js → chunk-7IQ6FV2F.js} +6117 -6985
  8. package/dist/cli/{chunk-R5462SQM.js → chunk-7ZAA75HG.js} +2 -1
  9. package/dist/cli/chunk-BVBX222F.js +117 -0
  10. package/dist/cli/{chunk-S4ANPQGA.js → chunk-FQZ3IX6E.js} +2 -2
  11. package/dist/cli/chunk-G6K2NSQS.js +6 -0
  12. package/dist/cli/{chunk-DEXDXDO4.js → chunk-GXFU4XAX.js} +1 -1
  13. package/dist/cli/chunk-HP3G2TYJ.js +1772 -0
  14. package/dist/cli/{chunk-DBSCO3HI.js → chunk-KKNE7BFZ.js} +108 -7
  15. package/dist/cli/{chunk-MQICEVKR.js → chunk-QQZALYNI.js} +38 -12
  16. package/dist/cli/{chunk-H36JD4E3.js → chunk-TRTS24PF.js} +23 -5
  17. package/dist/cli/{chunk-TB47EHML.js → chunk-WCS24ICX.js} +5 -3
  18. package/dist/cli/{chunk-2H5FJH4Q.js → chunk-WJ6YMZTT.js} +2 -2
  19. package/dist/cli/chunk-Y5DJ5INP.js +60 -0
  20. package/dist/cli/{chunk-QJVODYJ5.js → chunk-ZUYOA55W.js} +43 -4
  21. package/dist/cli/daemon-SKC7VKYQ.js +2240 -0
  22. package/dist/cli/handover-UXISKYPS.js +16 -0
  23. package/dist/cli/{hook-LY2YTK5U.js → hook-5UAARBWU.js} +93 -29
  24. package/dist/cli/index.js +45 -12
  25. package/dist/cli/launch-T5FJ42XS.js +34 -0
  26. package/dist/cli/send-NLVR6PL2.js +18 -0
  27. package/dist/cli/{setup-3N2EOJXS.js → setup-EEIXRAMU.js} +4 -4
  28. package/dist/cli/{status-ZQSYXILE.js → status-2OCYHOWZ.js} +3 -3
  29. package/dist/cli/task-AOOWHPMN.js +18 -0
  30. package/dist/cli/team-TVNSVYWW.js +101 -0
  31. package/dist/cli/tui-MQLMUUJB.js +4314 -0
  32. package/dist/cli/{uninstall-W2OMKU3B.js → uninstall-UUN7LIIT.js} +4 -4
  33. package/dist/cli/{version-6BMBIXHN.js → version-AEN2CRT2.js} +1 -1
  34. package/dist/cli/who-52MENZTT.js +11 -0
  35. package/package.json +7 -1
  36. package/dist/cli/chunk-J4ICZEGP.js +0 -6
  37. package/dist/cli/chunk-Q5WWH7FG.js +0 -1423
  38. package/dist/cli/launch-UQEFFPBN.js +0 -27
  39. package/dist/cli/send-EY63XQXI.js +0 -14
  40. package/dist/cli/who-VGIMGQNS.js +0 -11
@@ -1,12 +1,50 @@
1
1
  import {
2
2
  ledgeTitleRest
3
- } from "./chunk-H36JD4E3.js";
3
+ } from "./chunk-TRTS24PF.js";
4
4
  import {
5
5
  cleanTopic,
6
+ isArchived,
6
7
  sessionTitle
7
- } from "./chunk-Q5WWH7FG.js";
8
+ } from "./chunk-HP3G2TYJ.js";
9
+
10
+ // src/daemon/seat.ts
11
+ function teammates(team, sessions) {
12
+ const byId = new Map(sessions.map((s) => [s.id, s]));
13
+ return team.seats.flatMap((seat) => {
14
+ const s = seat.sessionId ? byId.get(seat.sessionId) : void 0;
15
+ return s && s.state !== "exited" && !isArchived(s) ? [{ name: s.name, role: seat.role }] : [];
16
+ });
17
+ }
18
+ function seatContext(o) {
19
+ const id = o.role?.id ?? o.roleId;
20
+ const label = o.role?.label ?? o.roleLabel ?? id;
21
+ const lines = [];
22
+ if (id) lines.push(`Ton r\xF4le : ${label} (${id})${o.team ? `, dans l'\xE9quipe ${o.team.name}` : ""}.`);
23
+ if (o.role?.prompt.trim()) lines.push(o.role.prompt.trim());
24
+ if (o.team) {
25
+ const members = o.team.members.map((m) => `${m.name} (${m.role})`).join(", ") || "personne d'autre pour l'instant";
26
+ lines.push(
27
+ `Ton \xE9quipe : ${members}. \xC9cris-leur par leur r\xF4le : ledge send @build "\u2026", ledge task give @qa "\u2026".`,
28
+ `L'op\xE9rateur est Ledge : il t'a confi\xE9 ou te confiera ton travail par une <ledge-task>. Travaille en autonomie avec ton \xE9quipe ; ne t'arr\xEAte pour l'utilisateur que pour une d\xE9cision qui lui revient. Termine toujours une t\xE2che re\xE7ue (ledge task done <id> "<r\xE9sultat>", ou refuse) : c'est ainsi que Ledge et l'utilisateur savent o\xF9 en est l'\xE9quipe.`
29
+ );
30
+ }
31
+ return lines.join("\n");
32
+ }
33
+ function resolveRoleAddress(to, from, sessions, teams) {
34
+ if (!to.startsWith("@")) return { to };
35
+ const role = to.slice(1);
36
+ const team = teams.find((t) => t.state !== "dissolved" && t.seats.some((s) => s.sessionId === from));
37
+ if (!team) return { reason: `${to} n'existe que dans une \xE9quipe` };
38
+ const byId = new Map(sessions.map((s) => [s.id, s]));
39
+ const held = team.seats.filter((s) => s.role === role && s.sessionId).map((s) => byId.get(s.sessionId)).filter((s) => !!s && s.state !== "exited" && !isArchived(s));
40
+ if (held.length === 1) return { to: held[0].name };
41
+ if (held.length > 1) return { reason: `plusieurs sessions tiennent ${to} dans ${team.name} : ${held.map((s) => s.name).join(", ")}` };
42
+ const roles = teammates(team, sessions).map((m) => `@${m.role}`);
43
+ return { reason: `personne ne tient ${to} dans l'\xE9quipe ${team.name}${roles.length ? ` (r\xF4les tenus : ${roles.join(", ")})` : ""}` };
44
+ }
8
45
 
9
46
  // src/daemon/crossSession.ts
47
+ import { createHash } from "crypto";
10
48
  var FIRST_LINE = /^Another Claude session sent a message:\s*\n/;
11
49
  var BLOCK = /<cross-session-message\b([^>]*)>([\s\S]*?)(?:<\/cross-session-message>|(?=\n[ \t]*<cross-session-message\b)|$)/g;
12
50
  var ATTR = /([\w-]+)="([^"]*)"/g;
@@ -24,7 +62,7 @@ function crossSession(text) {
24
62
  }
25
63
  var LEDGE_CLOSE = "</ledge-message>";
26
64
  var LEDGE_BLOCK = /^<ledge-message\b([^>]*)>([\s\S]*?)(?:<\/ledge-message>|$)/;
27
- var messageNote = (from, to) => `(message de la session ${from}, pas de l'utilisateur ; pour r\xE9pondre : ${to === "codex" ? `ledge send ${from} "\u2026"` : `SendMessage vers ${from}`})`;
65
+ var messageNote = (from, to) => `(message de la session ${from}, pas de l'utilisateur ; pour r\xE9pondre : ${to === "claude" ? `SendMessage vers ${from}` : `ledge send ${from} "\u2026"`})`;
28
66
  var NOTE_SENDER = /^\(message de la session (.+?), pas de l'utilisateur ; /;
29
67
  function stripNote(text) {
30
68
  const body = text.trimStart();
@@ -36,9 +74,20 @@ var ESCAPES = /\x1b(?:\[[0-?]*[ -/]*[@-~]|[\]PX^_][^\x07\x1b]*(?:\x07|\x1b\\)|[
36
74
  var CONTROLS = /[\x00-\x1f\x7f-\x9f]/g;
37
75
  var typable = (text, newline) => text.replace(ESCAPES, "").replace(/\r\n|\r|\n/g, newline).replace(CONTROLS, " ");
38
76
  var ledgeBody = (text) => typable(text.replaceAll("<", "\u2039"), " \u23CE ");
77
+ var INLINE_MAX = 800;
78
+ var INLINE_LINES = 8;
79
+ var POINTER_HEAD = 200;
80
+ var lineCount = (text) => text.split(/\r\n|\r|\n/).length;
81
+ var mailId = (rid, length = 6) => `m${createHash("sha1").update(rid).digest("hex").slice(0, length)}`;
82
+ var inline = (text) => text.length <= INLINE_MAX && lineCount(text) <= INLINE_LINES;
83
+ var attr = (s) => typable(s, " ").replace(/["<>]/g, "").trim();
39
84
  function formatLedgeMessage(m) {
40
- const from = typable(m.from, " ").replace(/["<>]/g, "").trim();
41
- return `<ledge-message from="${from}" tool="${m.tool}">${messageNote(from, m.to)} ${ledgeBody(m.text)}${LEDGE_CLOSE}`;
85
+ const from = attr(m.from);
86
+ const open = `<ledge-message from="${from}" tool="${m.tool}"${m.id ? ` id="${attr(m.id)}"` : ""}>${messageNote(from, m.to)} `;
87
+ if (!m.id || inline(m.text)) return `${open}${ledgeBody(m.text)}${LEDGE_CLOSE}`;
88
+ const first = m.summary?.trim() || m.text.trim().split(/\r\n|\r|\n/)[0].trim();
89
+ const head = ledgeBody(first.length > POINTER_HEAD ? `${first.slice(0, POINTER_HEAD)}\u2026` : first);
90
+ return `${open}${head} \u2026 [message de ${m.text.length} caract\xE8res, ${lineCount(m.text)} lignes : lis-le en entier avec ledge read ${attr(m.id)}]${LEDGE_CLOSE}`;
42
91
  }
43
92
  function ledgeMessages(text) {
44
93
  const m = LEDGE_BLOCK.exec(text.trimStart());
@@ -46,8 +95,49 @@ function ledgeMessages(text) {
46
95
  const attrs = new Map([...m[1].matchAll(ATTR)].map((a) => [a[1], a[2]]));
47
96
  const from = attrs.get("from");
48
97
  const tool = attrs.get("tool");
98
+ const id = attrs.get("id");
49
99
  if (!from) return void 0;
50
- return [{ from, ...tool ? { tool } : {}, text: stripNote(m[2]).text.trim() }];
100
+ return [{ from, ...tool ? { tool } : {}, ...id ? { id } : {}, text: stripNote(m[2]).text.trim() }];
101
+ }
102
+ var NOTICE_BLOCK = /^<ledge-notice\b([^>]*)>([\s\S]*?)(?:<\/ledge-notice>|$)/;
103
+ var NOTICE_QUOTE = 80;
104
+ var NOTICE_REASON = " \xBB) : ";
105
+ function formatLedgeNotice(n) {
106
+ const to = attr(n.to);
107
+ const flat = typable(n.text, " ").trim();
108
+ const quote = ledgeBody(flat.length > NOTICE_QUOTE ? `${flat.slice(0, NOTICE_QUOTE)}\u2026` : flat);
109
+ return `<ledge-notice to="${to}">Ledge n'a pas pu remettre ton message \xE0 ${to} (\xAB ${quote}${NOTICE_REASON}${ledgeBody(n.reason)}</ledge-notice>`;
110
+ }
111
+ function formatTeamNotice(n) {
112
+ return `<ledge-notice team="${attr(n.team)}">${ledgeBody(n.text)}</ledge-notice>`;
113
+ }
114
+ var TEAM_NOTICE = /^<ledge-notice\b([^>]*\bteam="[^"]*"[^>]*)>([\s\S]*?)(?:<\/ledge-notice>|$)/;
115
+ function ledgeTeamNotice(text) {
116
+ const m = TEAM_NOTICE.exec(text.trimStart());
117
+ if (!m) return void 0;
118
+ const team = new Map([...m[1].matchAll(ATTR)].map((a) => [a[1], a[2]])).get("team");
119
+ return team ? { team, text: m[2].trim() } : void 0;
120
+ }
121
+ var TASK_BLOCK = /^<ledge-task\b([^>]*)>([\s\S]*?)(?:<\/ledge-task>|$)/;
122
+ function formatLedgeTask(t) {
123
+ return `<ledge-task id="${attr(t.id)}" from="${attr(t.from)}">${ledgeBody(t.text)}</ledge-task>`;
124
+ }
125
+ function ledgeTask(text) {
126
+ const m = TASK_BLOCK.exec(text.trimStart());
127
+ if (!m) return void 0;
128
+ const attrs = new Map([...m[1].matchAll(ATTR)].map((a) => [a[1], a[2]]));
129
+ const id = attrs.get("id");
130
+ const from = attrs.get("from");
131
+ return id ? { id, ...from ? { from } : {}, text: m[2].trim() } : void 0;
132
+ }
133
+ function ledgeNotice(text) {
134
+ const m = NOTICE_BLOCK.exec(text.trimStart());
135
+ if (!m) return void 0;
136
+ const to = new Map([...m[1].matchAll(ATTR)].map((a) => [a[1], a[2]])).get("to");
137
+ if (!to) return void 0;
138
+ const body = m[2].trim();
139
+ const at = body.lastIndexOf(NOTICE_REASON);
140
+ return { to, ...at >= 0 ? { reason: body.slice(at + NOTICE_REASON.length) } : {}, text: body };
51
141
  }
52
142
  var NOTICE_HEAD = /^\[Cross-session ([\w -]+?) notice\]/;
53
143
  var NOTICE_SPLIT = /\n\s*(?=\[Cross-session [\w -]+? notice\])/;
@@ -393,7 +483,7 @@ function reduceEvent(s, ev) {
393
483
  case "PreToolUse":
394
484
  case "PermissionRequest":
395
485
  case "Notification": {
396
- const notice2 = ev.name === "UserPromptSubmit" && !!ev.text && !!crossSessionNotices(ev.text);
486
+ const notice2 = ev.name === "UserPromptSubmit" && !!ev.text && (!!crossSessionNotices(ev.text) || !!ledgeNotice(ev.text) || !!ledgeTask(ev.text) || !!ledgeTeamNotice(ev.text));
397
487
  if (ev.name === "UserPromptSubmit" && ev.text && !s.topic && !notice2 && !crossSession(ev.text)) s = withTopic(s, ev.text, "prompt");
398
488
  if (s.pending?.source === "rpc" && ev.name === "PreToolUse") return { ...s, lastHookAt: at, lastActivityAt: at };
399
489
  const incoming = pendingFor(ev);
@@ -520,12 +610,23 @@ function resolveTarget(to, sessions, peers) {
520
610
  }
521
611
 
522
612
  export {
613
+ teammates,
614
+ seatContext,
615
+ resolveRoleAddress,
523
616
  crossSession,
524
617
  messageNote,
525
618
  stripNote,
526
619
  ledgeBody,
620
+ mailId,
621
+ inline,
527
622
  formatLedgeMessage,
528
623
  ledgeMessages,
624
+ formatLedgeNotice,
625
+ formatTeamNotice,
626
+ ledgeTeamNotice,
627
+ formatLedgeTask,
628
+ ledgeTask,
629
+ ledgeNotice,
529
630
  crossSessionNotices,
530
631
  Refusal,
531
632
  NO_ALWAYS,
@@ -1,23 +1,31 @@
1
1
  import {
2
- DaemonRefusedError,
3
- connect
4
- } from "./chunk-DEXDXDO4.js";
2
+ EFFORT_ARGS,
3
+ MODEL_ARGS
4
+ } from "./chunk-7IQ6FV2F.js";
5
+ import {
6
+ autonomyArgs,
7
+ findRole
8
+ } from "./chunk-3HCID6NW.js";
5
9
  import {
6
10
  exitFile,
7
11
  spawnSession
8
- } from "./chunk-H36JD4E3.js";
12
+ } from "./chunk-TRTS24PF.js";
13
+ import {
14
+ DaemonRefusedError,
15
+ connect
16
+ } from "./chunk-GXFU4XAX.js";
9
17
  import {
10
18
  PROTOCOL_PORT
11
- } from "./chunk-Q5WWH7FG.js";
19
+ } from "./chunk-HP3G2TYJ.js";
12
20
  import {
13
21
  realTmux
14
- } from "./chunk-2H5FJH4Q.js";
22
+ } from "./chunk-WJ6YMZTT.js";
15
23
  import {
16
24
  log
17
- } from "./chunk-BNKPXSUY.js";
25
+ } from "./chunk-43D6NMYH.js";
18
26
  import {
19
27
  ensureLedgeDir
20
- } from "./chunk-R5462SQM.js";
28
+ } from "./chunk-7ZAA75HG.js";
21
29
 
22
30
  // src/cli/launch.ts
23
31
  import { spawn } from "child_process";
@@ -28,19 +36,30 @@ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
28
36
  function parseLaunchArgs(argv) {
29
37
  const args = [];
30
38
  let name;
39
+ let role;
31
40
  for (let i = 0; i < argv.length; i++) {
32
41
  const a = argv[i];
33
42
  if (a === "--name") {
34
43
  name = argv[++i];
35
44
  continue;
36
45
  }
46
+ if (a === "--role") {
47
+ role = argv[++i];
48
+ continue;
49
+ }
37
50
  if (a === "--") {
38
51
  args.push(...argv.slice(i + 1));
39
52
  break;
40
53
  }
41
54
  args.push(a);
42
55
  }
43
- return { name, args };
56
+ return { name, ...role ? { role } : {}, args };
57
+ }
58
+ function roleArgs(tool, role, args) {
59
+ const own = role.tool === void 0 || role.tool === tool;
60
+ const hasModel = args.some((a) => a === "--model" || a === "-m" || a.startsWith("--model="));
61
+ const hasEffort = args.some((a) => a === "--effort" || a.startsWith("--effort=") || a.includes("model_reasoning_effort"));
62
+ return [...own && role.model && !hasModel ? MODEL_ARGS[tool](role.model) : [], ...own && role.effort && !hasEffort ? EFFORT_ARGS[tool]?.(role.effort) ?? [] : [], ...autonomyArgs(tool, role.autonomy)];
44
63
  }
45
64
  function portOpen(port) {
46
65
  return new Promise((res) => {
@@ -118,7 +137,8 @@ async function createSession(opts) {
118
137
  ids.push(s.id);
119
138
  }
120
139
  }
121
- const session = await spawnSession({ ...opts, taken: { names, ids } });
140
+ const { role, ...rest } = opts;
141
+ const session = await spawnSession({ ...rest, ...role ? { args: [...roleArgs(opts.tool, role, opts.args), ...opts.args], role: { id: role.id, label: role.label }, ...role.autonomy ? { autonomy: role.autonomy } : {} } : {}, taken: { names, ids } });
122
142
  try {
123
143
  opts.client?.send({ type: "session.register", session });
124
144
  } catch (e) {
@@ -156,15 +176,20 @@ var UNTRACKED = {
156
176
  };
157
177
  async function launch(tool, argv, opts = {}) {
158
178
  await ensureLedgeDir();
159
- const { name, args } = parseLaunchArgs(argv);
179
+ const { name, role: roleId, args } = parseLaunchArgs(argv);
160
180
  const port = opts.port ?? PROTOCOL_PORT;
161
181
  const socket = opts.tmuxSocket ?? TMUX_SOCKET;
162
182
  const tmux = realTmux(socket);
163
183
  const cwd = opts.cwd ?? process.cwd();
184
+ const role = roleId ? findRole(roleId, cwd) : void 0;
185
+ if (roleId && !role) {
186
+ console.error(`ledge: r\xF4le inconnu : ${roleId} (aucun fichier .ledge/roles/${roleId}.md ni ~/.ledge/roles/${roleId}.md)`);
187
+ return 1;
188
+ }
164
189
  const daemon = await ensureDaemon({ port, spawnDaemon: opts.spawnDaemon, retryDelayMs: opts.retryDelayMs });
165
190
  let client = daemon.client;
166
191
  if (!daemon.client) console.error(UNTRACKED[daemon.reason](port));
167
- const create = (c) => createSession({ tool, args, command: opts.command, recordExit: true, name, cwd, tmux, client: c });
192
+ const create = (c) => createSession({ tool, args, command: opts.command, recordExit: true, name, ...role ? { role } : {}, cwd, tmux, client: c });
168
193
  let session;
169
194
  try {
170
195
  session = await create(client);
@@ -198,6 +223,7 @@ async function launch(tool, argv, opts = {}) {
198
223
 
199
224
  export {
200
225
  parseLaunchArgs,
226
+ roleArgs,
201
227
  ensureDaemon,
202
228
  DaemonError,
203
229
  createSession,
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  NAMES,
3
3
  pickName
4
- } from "./chunk-Q5WWH7FG.js";
4
+ } from "./chunk-HP3G2TYJ.js";
5
5
  import {
6
6
  paths
7
- } from "./chunk-R5462SQM.js";
7
+ } from "./chunk-7ZAA75HG.js";
8
8
 
9
9
  // src/daemon/transcript.ts
10
10
  import { appendFileSync, closeSync, fstatSync, openSync, readFileSync, readSync, statSync } from "fs";
@@ -206,7 +206,7 @@ var PARENT_AGENT_VARS = [
206
206
  var PARENT_AGENT = new Set(PARENT_AGENT_VARS);
207
207
  function agentEnv(base, extra) {
208
208
  const env = {};
209
- for (const [k, v] of Object.entries(base)) if (v !== void 0 && k !== "TMUX" && !k.startsWith("TMUX_") && !PARENT_AGENT.has(k)) env[k] = v;
209
+ for (const [k, v] of Object.entries(base)) if (v !== void 0 && k !== "TMUX" && !k.startsWith("TMUX_") && !PARENT_AGENT.has(k) && k !== "LEDGE_ROLE" && k !== "LEDGE_TEAM") env[k] = v;
210
210
  return { ...env, ...extra };
211
211
  }
212
212
  async function spawnSession(opts) {
@@ -224,9 +224,27 @@ async function spawnSession(opts) {
224
224
  const env = opts.env ?? process.env;
225
225
  const transcriptPath = agentSessionId ? claudeTranscriptPath(opts.cwd, agentSessionId, env) : void 0;
226
226
  const command = opts.recordExit ? ["sh", "-c", '"$@"; echo $? > "$0"', exitFile(id), ...agent] : agent;
227
- 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 } : {} }), unset: PARENT_AGENT_VARS, command });
227
+ await opts.tmux.newSession({ name: tmuxSession, cwd: opts.cwd, env: agentEnv(env, { LEDGE_SESSION: id, LEDGE_NAME: name, ...opts.role ? { LEDGE_ROLE: opts.role.id } : {}, ...opts.team ? { LEDGE_TEAM: opts.team } : {}, ...opts.tool === "codex" ? { [CODEX_ENV_VAR]: id } : {} }), unset: PARENT_AGENT_VARS, command });
228
228
  const at = now();
229
- 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 } : {} };
229
+ return {
230
+ id,
231
+ name,
232
+ tool: opts.tool,
233
+ cwd: opts.cwd,
234
+ project,
235
+ title: `${name} \xB7 ${project}`,
236
+ tmuxSession,
237
+ state: "starting",
238
+ helpers: [],
239
+ startedAt: at,
240
+ lastActivityAt: at,
241
+ source: "launcher",
242
+ ...agentSessionId ? { agentSessionId } : {},
243
+ ...transcriptPath ? { transcriptPath } : {},
244
+ ...opts.role ? { role: opts.role.id, roleLabel: opts.role.label } : {},
245
+ ...opts.team ? { team: opts.team } : {},
246
+ ...opts.autonomy && opts.autonomy !== "ask" ? { autonomy: opts.autonomy } : {}
247
+ };
230
248
  }
231
249
 
232
250
  export {
@@ -1,25 +1,27 @@
1
1
  import {
2
2
  connect
3
- } from "./chunk-DEXDXDO4.js";
3
+ } from "./chunk-GXFU4XAX.js";
4
4
  import {
5
5
  isArchived
6
- } from "./chunk-Q5WWH7FG.js";
6
+ } from "./chunk-HP3G2TYJ.js";
7
7
 
8
8
  // src/cli/who.ts
9
9
  import { basename } from "path";
10
10
  function formatWho(sessions, self) {
11
11
  const live = sessions.filter((s) => s.state !== "exited" && !isArchived(s));
12
12
  if (live.length === 0) return "aucune autre session";
13
+ const roles = live.some((s) => s.role);
13
14
  const rows = live.map((s) => [
14
15
  s.id === self ? "*" : " ",
15
16
  s.name,
17
+ ...roles ? [s.role ? `@${s.role}` : "-"] : [],
16
18
  s.tool,
17
19
  s.state,
18
20
  `${s.repo ?? s.project}${s.worktree ? ` (${basename(s.cwd)})` : ""}`,
19
21
  s.branch ?? "-",
20
22
  `${s.topic ?? "-"}${s.source === "hook_only" ? " \xB7 lecture seule" : ""}`
21
23
  ]);
22
- const head = [" ", "NOM", "OUTIL", "\xC9TAT", "PROJET", "BRANCHE", "SUJET"];
24
+ const head = [" ", "NOM", ...roles ? ["R\xD4LE"] : [], "OUTIL", "\xC9TAT", "PROJET", "BRANCHE", "SUJET"];
23
25
  const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
24
26
  return [head, ...rows].map((r) => r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" ")).join("\n");
25
27
  }
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  log
3
- } from "./chunk-BNKPXSUY.js";
3
+ } from "./chunk-43D6NMYH.js";
4
4
  import {
5
5
  paths
6
- } from "./chunk-R5462SQM.js";
6
+ } from "./chunk-7ZAA75HG.js";
7
7
 
8
8
  // src/tmux.ts
9
9
  import { execFile } from "child_process";
@@ -0,0 +1,60 @@
1
+ import {
2
+ CODEX_HOOK_VAR
3
+ } from "./chunk-TRTS24PF.js";
4
+ import {
5
+ connect
6
+ } from "./chunk-GXFU4XAX.js";
7
+ import {
8
+ BRIEF_MAX
9
+ } from "./chunk-HP3G2TYJ.js";
10
+
11
+ // src/cli/handover.ts
12
+ import { randomUUID } from "crypto";
13
+ var HANDOVER_USAGE = `ledge handover "<bilan>" ton bilan de rel\xE8ve, quand Ledge te le demande (fait, en cours, suite, pi\xE8ges, fichiers cl\xE9s)
14
+ ledge handover show relire le bilan de la session dont tu as pris la rel\xE8ve`;
15
+ var HANDOVER_MS = 4e3;
16
+ function handoverArgs(args) {
17
+ if (args.length === 1 && args[0] === "show") return { op: "show" };
18
+ const text = args.join(" ").trim();
19
+ if (!args.length || !text || text.length > BRIEF_MAX) return void 0;
20
+ return { op: "brief", text };
21
+ }
22
+ async function handoverOnClient(client, args, by, ms) {
23
+ const op = handoverArgs(args);
24
+ if (!op) return { line: HANDOVER_USAGE, code: 1 };
25
+ try {
26
+ const rid = randomUUID();
27
+ client.send(op.op === "show" ? { type: "handover.show", rid, by } : { type: "handover.brief", rid, by, text: op.text });
28
+ const r = await client.next("handover.result", ms);
29
+ if (r.reason !== void 0) return { line: `refus\xE9 : ${r.reason}`, code: 1 };
30
+ return op.op === "show" ? { line: r.text ?? "", code: 0 } : { line: "bilan re\xE7u : Ledge lance la session qui prend ta rel\xE8ve et lui passe tes t\xE2ches. Arr\xEAte-toi l\xE0.", code: 0 };
31
+ } catch (e) {
32
+ return { line: `pas de r\xE9ponse de Ledge : ${e.message}`, code: 1 };
33
+ }
34
+ }
35
+ async function runHandover(args, opts = {}) {
36
+ if (!handoverArgs(args)) return { line: HANDOVER_USAGE, code: args.length ? 1 : 0 };
37
+ const env = opts.env ?? process.env;
38
+ const by = env[CODEX_HOOK_VAR] || env.LEDGE_SESSION;
39
+ if (!by) return { line: "ledge handover ne marche que dans une session lanc\xE9e par Ledge", code: 1 };
40
+ const deadline = Date.now() + (opts.timeoutMs ?? HANDOVER_MS);
41
+ const left = () => Math.max(0, deadline - Date.now());
42
+ let client;
43
+ try {
44
+ client = await connect({ port: opts.port, timeoutMs: left() });
45
+ } catch {
46
+ return { line: "daemon not running (start a session with `ledge claude`, or run `ledge daemon`)", code: 1 };
47
+ }
48
+ try {
49
+ return await handoverOnClient(client, args, by, left());
50
+ } finally {
51
+ await client.close();
52
+ }
53
+ }
54
+
55
+ export {
56
+ HANDOVER_USAGE,
57
+ handoverArgs,
58
+ handoverOnClient,
59
+ runHandover
60
+ };
@@ -1,10 +1,10 @@
1
+ import {
2
+ CODEX_HOOK_VAR
3
+ } from "./chunk-TRTS24PF.js";
1
4
  import {
2
5
  connect,
3
6
  sendMessageAndAwaitAck
4
- } from "./chunk-DEXDXDO4.js";
5
- import {
6
- CODEX_HOOK_VAR
7
- } from "./chunk-H36JD4E3.js";
7
+ } from "./chunk-GXFU4XAX.js";
8
8
 
9
9
  // src/cli/send.ts
10
10
  import { randomUUID } from "crypto";
@@ -58,6 +58,40 @@ async function sendMessage(args, opts = {}) {
58
58
  await client.close();
59
59
  }
60
60
  }
61
+ var READ_USAGE = "ledge read <id> print a long message whole (its id is in the pointer you received)";
62
+ var MAIL_ID = /^m[0-9a-f]{6,40}$/;
63
+ async function readOnClient(client, args, by, ms) {
64
+ const [id] = args;
65
+ if (args.length !== 1 || !id || !MAIL_ID.test(id)) return { line: READ_USAGE, code: 1 };
66
+ try {
67
+ client.send({ type: "message.read", id, by });
68
+ const body = await client.next("message.body", ms);
69
+ if (body.text === void 0) return { line: `introuvable : ${body.reason ?? "raison inconnue"}`, code: 1 };
70
+ return { line: `De ${body.from} \xE0 ${body.to}, ${body.at} :
71
+ ${body.text}`, code: 0 };
72
+ } catch (e) {
73
+ return { line: `pas de r\xE9ponse de Ledge : ${e.message}`, code: 1 };
74
+ }
75
+ }
76
+ async function readMessage(args, opts = {}) {
77
+ if (args.length !== 1 || !MAIL_ID.test(args[0])) return { line: READ_USAGE, code: 1 };
78
+ const env = opts.env ?? process.env;
79
+ const by = env[CODEX_HOOK_VAR] || env.LEDGE_SESSION;
80
+ if (!by) return { line: "ledge read ne marche que dans une session lanc\xE9e par Ledge", code: 1 };
81
+ const deadline = Date.now() + (opts.timeoutMs ?? SEND_MS);
82
+ const left = () => Math.max(0, deadline - Date.now());
83
+ let client;
84
+ try {
85
+ client = await connect({ port: opts.port, timeoutMs: left() });
86
+ } catch {
87
+ return { line: "daemon not running (start a session with `ledge claude`, or run `ledge daemon`)", code: 1 };
88
+ }
89
+ try {
90
+ return await readOnClient(client, args, by, left());
91
+ } finally {
92
+ await client.close();
93
+ }
94
+ }
61
95
  function parseLedgeCommand(command) {
62
96
  command = command.replace(/[\r\n\s]+$/, "");
63
97
  const words = [];
@@ -95,11 +129,16 @@ function parseLedgeCommand(command) {
95
129
  if (words[0] !== "ledge") return void 0;
96
130
  if (words[1] === "who" && words.length === 2) return { cmd: "who" };
97
131
  if (words[1] === "send") return { cmd: "send", args: words.slice(2) };
132
+ if (words[1] === "read") return { cmd: "read", args: words.slice(2) };
133
+ if (words[1] === "task") return { cmd: "task", args: words.slice(2) };
134
+ if (words[1] === "handover") return { cmd: "handover", args: words.slice(2) };
98
135
  return void 0;
99
136
  }
100
137
 
101
138
  export {
102
139
  sendOnClient,
103
140
  sendMessage,
141
+ readOnClient,
142
+ readMessage,
104
143
  parseLedgeCommand
105
144
  };