@paradow/ledge 0.33.0 → 0.34.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 (42) hide show
  1. package/dist/cli/{action-QW2ZQQBN.js → action-K42H5BDA.js} +5 -5
  2. package/dist/cli/{attach-ULTYWCJ7.js → attach-KEE4A5YP.js} +28 -15
  3. package/dist/cli/{bridge-CSLF7EW6.js → bridge-X327AZY4.js} +2 -2
  4. package/dist/cli/{chunk-3DB3VOMM.js → chunk-25RVT5MR.js} +3 -103
  5. package/dist/cli/{chunk-GXL3X7M7.js → chunk-3OJCWJAA.js} +2 -2
  6. package/dist/cli/chunk-4OYVM4MT.js +49 -0
  7. package/dist/cli/{chunk-JEGPTNWV.js → chunk-4TCSADFR.js} +5 -23
  8. package/dist/cli/{chunk-HOOHS3MX.js → chunk-53ISXX2R.js} +1 -1
  9. package/dist/cli/{chunk-6IYPRUQG.js → chunk-54KF5RBY.js} +1 -1
  10. package/dist/cli/{chunk-ASYAWVFW.js → chunk-7NHQGP6J.js} +1 -1
  11. package/dist/cli/{chunk-4HXJQEUN.js → chunk-G7XPEVCG.js} +5 -5
  12. package/dist/cli/{chunk-WXE2NOE5.js → chunk-IWY7DSSO.js} +835 -1033
  13. package/dist/cli/{chunk-CSLVBADC.js → chunk-KPDTYX7F.js} +3 -3
  14. package/dist/cli/chunk-RPH773TY.js +6 -0
  15. package/dist/cli/{chunk-CCSUP5ZI.js → chunk-RX5CJ7YH.js} +3 -5
  16. package/dist/cli/{chunk-NLL2EQJ2.js → chunk-T547FLQN.js} +38 -4
  17. package/dist/cli/{chunk-K7UEKSB2.js → chunk-UJ2WBGLW.js} +2 -2
  18. package/dist/cli/{chunk-XOZD4BP5.js → chunk-ULOMKEB6.js} +4 -4
  19. package/dist/cli/{chunk-CPERVAPQ.js → chunk-V4XKKL6K.js} +13 -40
  20. package/dist/cli/{chunk-PZ3F4QH3.js → daemon-RO43S3S5.js} +4655 -2964
  21. package/dist/cli/{decide-PFHHOCFZ.js → decide-KZFYL3HY.js} +5 -5
  22. package/dist/cli/{handover-EMO6JT7O.js → handover-BWNMI2RA.js} +5 -5
  23. package/dist/cli/{hook-W2BU74H5.js → hook-ZLWXI5EQ.js} +21 -71
  24. package/dist/cli/index.js +18 -25
  25. package/dist/cli/launch-GCX7TXOH.js +28 -0
  26. package/dist/cli/{send-NEI4X33R.js → send-SXZAPVRH.js} +5 -5
  27. package/dist/cli/{setup-K42IVMHS.js → setup-TJ32FMVJ.js} +4 -4
  28. package/dist/cli/{status-U2HGGKHF.js → status-YDVRES4E.js} +3 -3
  29. package/dist/cli/{task-PDDCE23D.js → task-TUSOVJK7.js} +5 -5
  30. package/dist/cli/{tui-UPMRMCPC.js → tui-TCVIRZCQ.js} +1401 -1269
  31. package/dist/cli/{uninstall-4IMMR55T.js → uninstall-QO7YMSLA.js} +4 -4
  32. package/dist/cli/{version-LUMK3Z3T.js → version-BAYSYWFP.js} +1 -1
  33. package/dist/cli/who-6AZYSP7T.js +12 -0
  34. package/dist/ledge-guide.md +26 -49
  35. package/package.json +2 -1
  36. package/dist/cli/chunk-4VNKZOC2.js +0 -160
  37. package/dist/cli/chunk-E3LXONBI.js +0 -6
  38. package/dist/cli/chunk-NWIICISV.js +0 -290
  39. package/dist/cli/daemon-5GT545BB.js +0 -2322
  40. package/dist/cli/launch-LP7YJR5N.js +0 -36
  41. package/dist/cli/team-S3ASP22H.js +0 -205
  42. package/dist/cli/who-NM3GPAPJ.js +0 -12
@@ -3,12 +3,12 @@ import {
3
3
  SERVICE,
4
4
  execRunner,
5
5
  stopDetachedDaemon
6
- } from "./chunk-K7UEKSB2.js";
7
- import "./chunk-NLL2EQJ2.js";
8
- import "./chunk-6IYPRUQG.js";
6
+ } from "./chunk-UJ2WBGLW.js";
7
+ import "./chunk-T547FLQN.js";
8
+ import "./chunk-54KF5RBY.js";
9
9
  import {
10
10
  paths
11
- } from "./chunk-HOOHS3MX.js";
11
+ } from "./chunk-53ISXX2R.js";
12
12
  import "./chunk-U67V476Y.js";
13
13
 
14
14
  // src/cli/uninstall.ts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  LEDGE_VERSION
3
- } from "./chunk-E3LXONBI.js";
3
+ } from "./chunk-RPH773TY.js";
4
4
  import "./chunk-U67V476Y.js";
5
5
  export {
6
6
  LEDGE_VERSION
@@ -0,0 +1,12 @@
1
+ import {
2
+ formatWho,
3
+ printWho
4
+ } from "./chunk-RX5CJ7YH.js";
5
+ import "./chunk-7NHQGP6J.js";
6
+ import "./chunk-IWY7DSSO.js";
7
+ import "./chunk-53ISXX2R.js";
8
+ import "./chunk-U67V476Y.js";
9
+ export {
10
+ formatWho,
11
+ printWho
12
+ };
@@ -4,14 +4,14 @@ Ce guide est écrit à la main et versionné avec le code. Ledge le recopie dans
4
4
 
5
5
  ## Ce qu'est Ledge
6
6
 
7
- Ledge veille sur les sessions d'agents de l'utilisateur : Claude Code, Codex, Cursor Agent et OMP, lancés dans WSL ou sur un serveur SSH. Il montre ce que fait chaque agent et ce qu'il attend. Il permet de répondre à une demande sans ouvrir son terminal, de lancer des sessions et de monter des équipes.
7
+ Ledge veille sur les sessions d'agents de l'utilisateur : Claude Code, Codex, Cursor Agent et OMP, lancés dans WSL ou sur un serveur SSH. Il montre ce que fait chaque agent et ce qu'il attend. Il permet de répondre à une demande sans ouvrir son terminal et de lancer des sessions.
8
8
 
9
- - **Le daemon** (`ledge daemon`, paquet npm `@paradow/ledge`) tourne dans WSL ou sur un serveur. Il suit les sessions par leurs hooks et par tmux, garde les projets, les équipes, les tâches et les messages entre sessions, et fait tourner le chat de Ledge. La CLI `ledge` lui parle :
9
+ - **Le daemon** (`ledge daemon`, paquet npm `@paradow/ledge`) tourne dans WSL ou sur un serveur. Il suit les sessions par leurs hooks et par tmux, garde les projets, les tâches et les messages entre sessions, et fait tourner le chat de Ledge. La CLI `ledge` lui parle :
10
10
  - `ledge claude|codex|cursor` lance un agent suivi ;
11
11
  - `ledge attach <id>` met le terminal sur une session ;
12
12
  - `ledge who` et `ledge status` listent les sessions ;
13
13
  - `ledge send` et `ledge read` servent aux messages entre sessions ;
14
- - `ledge task`, `ledge decide`, `ledge team`, `ledge handover` et `ledge close` gèrent les tâches, les décisions, les équipes, la passation de relais et la fermeture ;
14
+ - `ledge task`, `ledge decide`, `ledge handover` et `ledge close` gèrent les tâches, les décisions, la passation de relais et la fermeture ;
15
15
  - `ledge tui` est Ledge dans un terminal ;
16
16
  - `ledge setup` et `ledge uninstall` installent et retirent les hooks et `~/.ledge`.
17
17
  - **Ledge Desktop** est l'application Windows : la fenêtre principale (accueil, atelier, sessions, réglages) et l'**encoche**.
@@ -51,24 +51,16 @@ L'application s'ouvre ici. On y vient aussi par `/` ou par l'entrée « Accueil
51
51
  #### Conversation (`conversation`)
52
52
 
53
53
  C'est la conversation avec Ledge, en pleine largeur.
54
- - On y pose des questions et on mentionne une session ou une équipe.
54
+ - On y pose des questions et on mentionne une session.
55
55
  - On y valide les cartes que Ledge propose.
56
- - Le panneau « Le point » résume trois sections : « À toi », « Équipes » et « Confiées par Ledge ». Le bouton « Résumer la journée » est là aussi.
56
+ - Le panneau « Le point » résume deux sections : « À toi » et « Confiées par Ledge ». Le bouton « Résumer la journée » est là aussi.
57
57
 
58
58
  Y renvoyer l'utilisateur pour une longue discussion avec Ledge, ou pour faire le point sur la journée.
59
59
 
60
- #### Équipes (`teams`)
61
-
62
- Le catalogue a trois parties : « En cours » (les équipes actives), « Modèles » et « Rôles ».
63
- - Les boutons « Lancer une session », « Monter une équipe… » et « Lancer cette équipe… » sont ici.
64
- - Les rôles et les modèles se lisent ici mais se modifient dans leurs fichiers : `~/.ledge/roles/<id>.md` et `~/.ledge/teams/<id>.yaml`, ou les mêmes sous `<dépôt>/.ledge/`.
65
-
66
- Y renvoyer l'utilisateur pour voir ou monter une équipe, ou pour consulter un rôle.
67
-
68
60
  #### Tâches (`tasks`)
69
61
 
70
62
  Toutes les tâches données entre sessions et par Ledge.
71
- - Filtres : « Toutes / Ouvertes / Closes », donneur, équipe, « Décisions ».
63
+ - Filtres : « Toutes / Ouvertes / Closes », donneur, « Décisions ».
72
64
  - Une tâche ouverte montre son résultat, sa raison et son historique.
73
65
  - Une décision se tranche ici : « Répondre », puis « Envoyer la réponse », puis « Confirmer l'envoi ».
74
66
 
@@ -80,10 +72,8 @@ Les conversations du jour avec Ledge, chacune avec « Voir dans la conversation
80
72
 
81
73
  ### Atelier (`overview`)
82
74
 
83
- On y vient par Échap depuis l'accueil ou une session, par « Retour à l'atelier », ou par la vue « Atelier » de la barre latérale. Chaque projet est une table, avec ses agents assis autour et leurs sous-agents en petits bots. Une équipe a sa pièce.
75
+ On y vient par Échap depuis l'accueil ou une session, par « Retour à l'atelier », ou par la vue « Atelier » de la barre latérale. Chaque projet est une table, avec ses agents assis autour et leurs sous-agents en petits bots.
84
76
  - Le menu d'une table propose « Nouvelle session », « Modifier le projet » et « Isoler cette table ».
85
- - Le menu d'une pièce propose « Nouvelle équipe ici… ».
86
- - Le menu d'une équipe propose « Voir les tâches » et « Dissoudre l'équipe ».
87
77
  - On range une session en la glissant sur une table.
88
78
  - Le zoom se règle au clavier (+, −, 0) ou avec `Ctrl` + molette.
89
79
  - Le bureau « Parler à Ledge » ouvre le chat ancré.
@@ -91,6 +81,19 @@ On y vient par Échap depuis l'accueil ou une session, par « Retour à l'atelie
91
81
 
92
82
  Y renvoyer l'utilisateur pour avoir une vue d'ensemble de qui travaille où, ou pour ranger une session dans un projet.
93
83
 
84
+ ### OpenRig (`openrig`)
85
+
86
+ On y vient par « OpenRig » dans la barre latérale, sous « Atelier ». La page pilote OpenRig sur une machine ; les puces du haut changent de machine.
87
+ - **Daemon :** « Démarrer », « Arrêter », « Redémarrer ». La case « Au démarrage, relancer les rigs qui tournaient » lance `rig start --last` ; « Sans le kernel » ajoute `--no-kernel`.
88
+ - **Kernel :** l'opérateur d'OpenRig (le rig nommé `kernel`), avec « Démarrer », « Arrêter », « Redémarrer ».
89
+ - **Rigs :** chaque rig montre son état, ses sièges et son dernier snapshot ; un clic sur son nom déplie ses sièges. « Démarrer » lance `rig up <rig> --existing`, « Arrêter » `rig down <rig>`, « Redémarrer » les deux avec un snapshot.
90
+ - **Permissions au lancement** : « Celle de la spec », « Auto » (mode auto de Claude), « Standard », « Ouvert », « YOLO » (aucune demande) ou « Verrouillé », pour tout le rig ; la colonne « Permissions » de la table change un rôle. Avec YOLO, « Accepter les avertissements de premier lancement » (`--non-interruptive`) évite qu'un siège s'arrête sur l'avertissement de Claude.
91
+ - **± Sièges** (rig en marche) : un compteur par rôle ajoute des copies du premier siège du rôle (`build-2`, `build-3`) ou retire les dernières, sans arrêter le rig. Les sièges ajoutés sont reliés par paires ou chacun à tous (OpenRig 0.7 et plus), puis un snapshot garde la nouvelle taille pour un redémarrage.
92
+ - Tout arrêt ou redémarrage demande confirmation, nomme les sièges qui travaillent et montre la commande `rig` lancée.
93
+ - **Lancer un rig…** : choisir une spec (« mes specs » ou celles d'OpenRig), le dossier de travail et le nom, le nombre de sièges par rôle avec leur harnais et leur modèle, puis comment les sièges ajoutés délèguent (par paires ou chacun à tous). « Vérifier » montre ce que `rig up --plan` conclut et la spec qui sera lancée ; « Lancer » la lance. Une spec modifiée est écrite dans `~/.ledge/rigs/<nom>/`, et « Enregistrer dans mes specs » la garde dans la bibliothèque OpenRig.
94
+
95
+ Y renvoyer l'utilisateur pour démarrer, arrêter ou redémarrer OpenRig, son kernel ou un rig, ou pour lancer un rig (par exemple trois développeurs et trois relecteurs), sur n'importe quelle machine.
96
+
94
97
  ### Session (`session`)
95
98
 
96
99
  On l'ouvre en cliquant sur un siège, sur une ligne de la barre latérale ou sur un élément « À toi ». On y accède aussi par `Alt+1…9`, par `Alt+←/→`, ou par `Ctrl+K`. Un sous-agent a sa propre page.
@@ -170,10 +173,9 @@ S'ouvre par `?`. Liste complète des raccourcis.
170
173
  #### Nouvelle session (`launch`)
171
174
 
172
175
  S'ouvre par `N` dans l'atelier, `Ctrl+Alt+N` partout, « Nouvelle session » dans la barre latérale ou un menu de table.
173
- - **Champs :** consigne, agent (Claude Code, Codex, Cursor Agent, OMP), modèle et effort, projet, machine, dossiers et dépôts, rôle éventuel.
174
- - **Mode équipe :** « Nouvelle équipe ». On choisit un modèle, ou des sièges un par un, et le nom de l'équipe.
176
+ - **Champs :** consigne, agent (Claude Code, Codex, Cursor Agent, OMP), modèle et effort, projet, machine, dossiers et dépôts.
175
177
 
176
- Y renvoyer l'utilisateur pour lancer lui-même une session ou une équipe avec des réglages précis.
178
+ Y renvoyer l'utilisateur pour lancer lui-même une session avec des réglages précis.
177
179
 
178
180
  #### Projet (`project`)
179
181
 
@@ -185,7 +187,7 @@ Crée (« Nouveau projet »), modifie ou supprime un projet. On y donne son nom,
185
187
 
186
188
  ## Ledge mobile
187
189
 
188
- Une application web pour le téléphone, avec quatre onglets en bas : « À traiter », « Agents », « Ledge », « Équipes ». Pour la lier :
190
+ Une application web pour le téléphone, avec trois onglets en bas : « À traiter », « Agents », « Ledge ». Pour la lier :
189
191
  1. Sur le PC, dans Réglages › Accès mobile, activer l'accès, choisir le relais et se connecter avec GitHub.
190
192
  2. Sur le téléphone, ouvrir l'adresse du relais et se connecter avec le même compte.
191
193
  3. Comparer le code affiché sur les deux appareils et approuver sur le PC.
@@ -196,11 +198,11 @@ Les demandes en attente, les plus anciennes d'abord, filtrées par type (autoris
196
198
 
197
199
  ### Agents (`agents`)
198
200
 
199
- Les sessions par projet et par table, avec les tables d'équipe. Une session ouvre son fil, l'en-tête d'une équipe ouvre l'équipe en cours.
201
+ Les sessions par projet. Une session ouvre son fil.
200
202
 
201
203
  ### Ledge (`chat`)
202
204
 
203
- Le chat de Ledge sur le téléphone, avec « Le point ». Les cartes proposent une session, une équipe ou une tâche. Cette conversation est propre au téléphone.
205
+ Le chat de Ledge sur le téléphone, avec « Le point ». Les cartes proposent une session ou une tâche. Cette conversation est propre au téléphone.
204
206
 
205
207
  ### Fil d'une session (`thread`)
206
208
 
@@ -218,30 +220,6 @@ Une demande seule : autorisation, question, plan ou modifications. On l'approuve
218
220
 
219
221
  S'ouvre depuis l'état de la liaison en en-tête. On y voit le PC, le compte, le serveur et les empreintes, et on y règle l'apparence. « Le PC » met à jour ou redémarre le daemon du PC. On peut aussi s'y déconnecter.
220
222
 
221
- ### Équipes (`teams`)
222
-
223
- Les équipes et les rôles. « Nouvelle équipe », « Nouveau rôle ». « Lever » lance une équipe dans un projet, après confirmation par passkey.
224
-
225
- ### Éditeur d'équipe (`team`)
226
-
227
- Les sièges d'une équipe, où la ranger (maison ou dépôt), Enregistrer ou Supprimer.
228
-
229
- ### Copier une équipe (`team-copy`)
230
-
231
- Enregistre la composition d'une équipe en cours comme nouvelle équipe.
232
-
233
- ### Éditeur de rôle (`role`)
234
-
235
- Identifiant, libellé, consigne, et les valeurs par défaut : outil, modèle, effort, autonomie.
236
-
237
- ### Équipe en cours (`live`)
238
-
239
- La fiche d'une équipe active : ses sièges, sa consigne, « Modifier la composition », « Enregistrer comme équipe », « Dissoudre l'équipe ».
240
-
241
- ### Composition (`compose`)
242
-
243
- Modifier les sièges d'une équipe en cours, puis « Appliquer ».
244
-
245
223
  ### Écrans qui s'affichent d'eux-mêmes
246
224
 
247
225
  Ces écrans ne sont pas des adresses : l'application les montre d'elle-même, selon l'état de la liaison avec le PC.
@@ -255,11 +233,10 @@ Ces écrans ne sont pas des adresses : l'application les montre d'elle-même, se
255
233
  |---|---|
256
234
  | répondre à un agent, voir ses modifications | Session (desktop), Demande ou Fil (mobile) |
257
235
  | voir ce qui l'attend | « À toi » (desktop), À traiter (mobile) |
258
- | lancer une session ou une équipe lui-même | Nouvelle session (`N`, `Ctrl+Alt+N`) |
236
+ | lancer une session lui-même | Nouvelle session (`N`, `Ctrl+Alt+N`) |
259
237
  | créer, modifier ou supprimer un projet | Réglages › Projets, ou le menu d'une table |
260
238
  | renommer, archiver, supprimer une session, passer le relais | Session › « Gérer la session » |
261
239
  | trancher une décision, suivre une tâche | Accueil › Tâches |
262
- | écrire un rôle ou un modèle d'équipe | Ses fichiers `.ledge/roles` et `.ledge/teams`, ou Équipes (mobile) |
263
240
  | ajouter un serveur | Réglages › Machines |
264
241
  | lier un téléphone | Réglages › Accès mobile |
265
242
  | changer l'agent du chat | Réglages › Général › Agent par défaut |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paradow/ledge",
3
- "version": "0.33.0",
3
+ "version": "0.34.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" },
@@ -22,6 +22,7 @@
22
22
  "react": "^19.3.0",
23
23
  "string-width": "^8.3.0",
24
24
  "ws": "^8.18.0",
25
+ "yaml": "2.8.1",
25
26
  "zod": "^3.23.8"
26
27
  },
27
28
  "devDependencies": {
@@ -1,160 +0,0 @@
1
- import {
2
- ROLE_LABEL_MAX,
3
- ROLE_PROMPT_MAX,
4
- RoleSchema
5
- } from "./chunk-WXE2NOE5.js";
6
- import {
7
- log
8
- } from "./chunk-6IYPRUQG.js";
9
- import {
10
- ledgeDir
11
- } from "./chunk-HOOHS3MX.js";
12
-
13
- // src/daemon/roles.ts
14
- import { createHash } from "crypto";
15
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
16
- import { dirname, join, resolve } from "path";
17
- var FILE_MAX = 64 * 1024;
18
- function repoRoot(cwd) {
19
- const start = resolve(cwd);
20
- for (let dir = start; ; dir = dirname(dir)) {
21
- if (existsSync(join(dir, ".git"))) return dir;
22
- if (dirname(dir) === dir) return start;
23
- }
24
- }
25
- function definitionDirs(kind, cwd) {
26
- const home = join(ledgeDir(), kind);
27
- const repo = cwd ? join(repoRoot(cwd), ".ledge", kind) : void 0;
28
- return [...repo && resolve(repo) !== resolve(home) ? [{ dir: repo, source: "repo" }] : [], { dir: home, source: "home" }];
29
- }
30
- function definitionFiles(dir, ext) {
31
- let names;
32
- try {
33
- names = readdirSync(dir);
34
- } catch {
35
- return [];
36
- }
37
- return names.filter((n) => ext.some((e) => n.endsWith(e)) && !n.startsWith(".")).sort().map((n) => join(dir, n)).filter((p) => {
38
- try {
39
- return statSync(p).isFile();
40
- } catch {
41
- return false;
42
- }
43
- });
44
- }
45
- function readSmall(path) {
46
- if (statSync(path).size > FILE_MAX) throw new Error(`file over ${FILE_MAX / 1024} KB`);
47
- return readFileSync(path, "utf8");
48
- }
49
- function frontMatter(text) {
50
- const src = text.replace(/^/, "").replace(/\r\n/g, "\n");
51
- const m = /^---\n([\s\S]*?)\n---[ \t]*(?:\n|$)/.exec(src);
52
- if (!m) return { fields: {}, body: src.trim() };
53
- const fields = {};
54
- for (const raw of m[1].split("\n")) {
55
- const line = raw.trim();
56
- if (!line || line.startsWith("#")) continue;
57
- const i = line.indexOf(":");
58
- if (i <= 0) throw new Error(`front matter line without a key: ${line.replace(/\s+#.*$/, "").slice(0, 60)}`);
59
- fields[line.slice(0, i).trim()] = scalarValue(line.slice(i + 1));
60
- }
61
- return { fields, body: src.slice(m[0].length).trim() };
62
- }
63
- function scalarValue(raw) {
64
- const v = raw.trim();
65
- const q = v[0];
66
- if (q === '"' || q === "'") {
67
- const end = v.indexOf(q, 1);
68
- if (end > 0 && /^(\s+#.*)?$/.test(v.slice(end + 1))) return v.slice(1, end);
69
- }
70
- return unquote(v.replace(/(^|\s)#.*$/, "").trim());
71
- }
72
- function quoteScalar(v) {
73
- if (/[\r\n]/.test(v)) return null;
74
- if (/^[\w./@:+-]+$/.test(v) && !/^(true|false|null|yes|no|~)$/i.test(v)) return v;
75
- if (!v.includes('"')) return `"${v}"`;
76
- if (!v.includes("'")) return `'${v}'`;
77
- return null;
78
- }
79
- var definitionHash = (text) => createHash("sha256").update(text).digest("hex").slice(0, 16);
80
- var unquote = (v) => v.length >= 2 && (v[0] === '"' || v[0] === "'") && v.at(-1) === v[0] ? v.slice(1, -1) : v;
81
- var ROLE_KEYS = /* @__PURE__ */ new Set(["label", "tool", "model", "effort", "autonomy"]);
82
- function parseRole(id, text, source, path) {
83
- let parsed;
84
- try {
85
- parsed = frontMatter(text);
86
- } catch (e) {
87
- return e.message;
88
- }
89
- const { fields, body } = parsed;
90
- const unknown = Object.keys(fields).find((k) => !ROLE_KEYS.has(k));
91
- if (unknown) return `unknown key ${unknown}`;
92
- if (body.length > ROLE_PROMPT_MAX) return `prompt over ${ROLE_PROMPT_MAX} characters`;
93
- const label = (fields.label ?? id).slice(0, ROLE_LABEL_MAX);
94
- const r = RoleSchema.safeParse({
95
- id,
96
- label,
97
- prompt: body,
98
- source,
99
- path,
100
- ...fields.tool ? { tool: fields.tool } : {},
101
- ...fields.model ? { model: fields.model } : {},
102
- ...fields.effort ? { effort: fields.effort } : {},
103
- ...fields.autonomy ? { autonomy: fields.autonomy } : {}
104
- });
105
- if (!r.success) {
106
- const issue = r.error.issues[0];
107
- return `${issue.path.join(".") || "role"}: ${issue.message}`;
108
- }
109
- return r.data;
110
- }
111
- function loadRole(id, path, source, problems) {
112
- let r;
113
- try {
114
- const text = readSmall(path);
115
- r = parseRole(id, text, source, path);
116
- if (typeof r !== "string") r = { ...r, hash: definitionHash(text) };
117
- } catch (e) {
118
- r = e.message;
119
- }
120
- if (typeof r === "string") {
121
- log(`role ${path} skipped: ${r}`);
122
- problems.push({ path, reason: r });
123
- return void 0;
124
- }
125
- return r;
126
- }
127
- function listRoles(cwd) {
128
- const roles = /* @__PURE__ */ new Map();
129
- const problems = [];
130
- for (const { dir, source } of definitionDirs("roles", cwd)) {
131
- for (const path of definitionFiles(dir, [".md"])) {
132
- const id = path.slice(dir.length + 1, -".md".length);
133
- const won = roles.get(id);
134
- if (won) {
135
- if (source === "home") roles.set(id, { ...won, shadows: path });
136
- continue;
137
- }
138
- const r = loadRole(id, path, source, problems);
139
- if (r) roles.set(id, r);
140
- }
141
- }
142
- return { roles: [...roles.values()].sort((a, b) => a.id.localeCompare(b.id)), problems };
143
- }
144
- function findRole(id, cwd) {
145
- return listRoles(cwd).roles.find((r) => r.id === id);
146
- }
147
-
148
- export {
149
- repoRoot,
150
- definitionDirs,
151
- definitionFiles,
152
- readSmall,
153
- frontMatter,
154
- scalarValue,
155
- quoteScalar,
156
- definitionHash,
157
- loadRole,
158
- listRoles,
159
- findRole
160
- };
@@ -1,6 +0,0 @@
1
- // src/version.ts
2
- var LEDGE_VERSION = true ? "0.33.0" : "0.0.0-dev";
3
-
4
- export {
5
- LEDGE_VERSION
6
- };
@@ -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
- };