@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.
- package/dist/cli/index.js +39003 -46
- package/dist/cli/reduceWorker.js +2037 -0
- package/dist/ledge-guide.md +26 -49
- package/package.json +5 -1
- package/dist/cli/action-QW2ZQQBN.js +0 -27
- package/dist/cli/attach-ULTYWCJ7.js +0 -74
- package/dist/cli/bridge-CSLF7EW6.js +0 -70
- package/dist/cli/chunk-3DB3VOMM.js +0 -723
- package/dist/cli/chunk-4HXJQEUN.js +0 -118
- package/dist/cli/chunk-4VNKZOC2.js +0 -160
- package/dist/cli/chunk-6IYPRUQG.js +0 -17
- package/dist/cli/chunk-ASYAWVFW.js +0 -95
- package/dist/cli/chunk-CCSUP5ZI.js +0 -45
- package/dist/cli/chunk-CPERVAPQ.js +0 -232
- package/dist/cli/chunk-CSLVBADC.js +0 -60
- package/dist/cli/chunk-E3LXONBI.js +0 -6
- package/dist/cli/chunk-GXL3X7M7.js +0 -144
- package/dist/cli/chunk-HOOHS3MX.js +0 -39
- package/dist/cli/chunk-JEGPTNWV.js +0 -270
- package/dist/cli/chunk-K7UEKSB2.js +0 -305
- package/dist/cli/chunk-NLL2EQJ2.js +0 -94
- package/dist/cli/chunk-NWIICISV.js +0 -290
- package/dist/cli/chunk-OLLQBK6N.js +0 -41
- package/dist/cli/chunk-PZ3F4QH3.js +0 -9758
- package/dist/cli/chunk-U67V476Y.js +0 -35
- package/dist/cli/chunk-WXE2NOE5.js +0 -2674
- package/dist/cli/chunk-XOZD4BP5.js +0 -85
- package/dist/cli/daemon-5GT545BB.js +0 -2322
- package/dist/cli/decide-PFHHOCFZ.js +0 -59
- package/dist/cli/handover-EMO6JT7O.js +0 -17
- package/dist/cli/hook-W2BU74H5.js +0 -523
- package/dist/cli/launch-LP7YJR5N.js +0 -36
- package/dist/cli/send-NEI4X33R.js +0 -19
- package/dist/cli/setup-K42IVMHS.js +0 -38
- package/dist/cli/status-U2HGGKHF.js +0 -28
- package/dist/cli/task-PDDCE23D.js +0 -19
- package/dist/cli/team-S3ASP22H.js +0 -205
- package/dist/cli/tui-UPMRMCPC.js +0 -19216
- package/dist/cli/uninstall-4IMMR55T.js +0 -162
- package/dist/cli/version-LUMK3Z3T.js +0 -7
- package/dist/cli/who-NM3GPAPJ.js +0 -12
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CODEX_HOOK_VAR
|
|
3
|
-
} from "./chunk-JEGPTNWV.js";
|
|
4
|
-
import {
|
|
5
|
-
connect
|
|
6
|
-
} from "./chunk-ASYAWVFW.js";
|
|
7
|
-
import {
|
|
8
|
-
LEDGE_GIVER,
|
|
9
|
-
TASK_TEXT_MAX,
|
|
10
|
-
TASK_TITLE_MAX
|
|
11
|
-
} from "./chunk-WXE2NOE5.js";
|
|
12
|
-
|
|
13
|
-
// src/cli/task.ts
|
|
14
|
-
import { randomUUID } from "crypto";
|
|
15
|
-
var TASK_USAGE = `ledge task give <nom|@r\xF4le> "<titre>" ["<d\xE9tail>"] confier un travail \xE0 une session (@r\xF4le : dans ton \xE9quipe)
|
|
16
|
-
ledge task give ledge "<question>" "<options, recommandation>" une d\xE9cision pour l'utilisateur ; sa r\xE9ponse te r\xE9veille
|
|
17
|
-
ledge task list [--all] tes t\xE2ches (tenues ou confi\xE9es), ouvertes ; --all : closes aussi
|
|
18
|
-
ledge task show <id> une t\xE2che, son d\xE9tail et son historique
|
|
19
|
-
ledge task take <id> la prendre
|
|
20
|
-
ledge task done <id> "<r\xE9sultat>" la terminer, avec ce qui en est sorti (la session qui l'a confi\xE9e est pr\xE9venue)
|
|
21
|
-
ledge task refuse <id> "<raison>" la refuser, ou l'annuler si tu l'as confi\xE9e
|
|
22
|
-
ledge task pass <id> <nom|@r\xF4le> ["<note>"] la passer \xE0 une autre session`;
|
|
23
|
-
var TASK_MS = 4e3;
|
|
24
|
-
var ID = /^t[0-9a-f]{6}$/;
|
|
25
|
-
function taskArgs(args) {
|
|
26
|
-
const [op, ...rest] = args;
|
|
27
|
-
const id = rest[0] && ID.test(rest[0]) ? rest[0] : void 0;
|
|
28
|
-
const fits = (min, max) => rest.length >= min && rest.length <= max && rest.slice(0, min).every((a) => a.trim() !== "");
|
|
29
|
-
switch (op) {
|
|
30
|
-
case "give":
|
|
31
|
-
return fits(2, 3) && rest[1].length <= TASK_TITLE_MAX && (rest[2]?.length ?? 0) <= TASK_TEXT_MAX ? { op, to: rest[0], title: rest[1], ...rest[2] ? { text: rest[2] } : {} } : void 0;
|
|
32
|
-
case "list":
|
|
33
|
-
return rest.length === 0 ? { op } : rest.length === 1 && rest[0] === "--all" ? { op, all: true } : void 0;
|
|
34
|
-
case "show":
|
|
35
|
-
case "take":
|
|
36
|
-
return fits(1, 1) && id ? { op, task: id } : void 0;
|
|
37
|
-
case "done":
|
|
38
|
-
return fits(2, 2) && id && rest[1].length <= TASK_TEXT_MAX ? { op, task: id, text: rest[1] } : void 0;
|
|
39
|
-
case "refuse":
|
|
40
|
-
return fits(2, 2) && id && rest[1].length <= TASK_TEXT_MAX ? { op, task: id, text: rest[1] } : void 0;
|
|
41
|
-
case "pass":
|
|
42
|
-
return fits(2, 3) && id && (rest[2]?.length ?? 0) <= TASK_TEXT_MAX ? { op, task: id, to: rest[1], ...rest[2] ? { text: rest[2] } : {} } : void 0;
|
|
43
|
-
default:
|
|
44
|
-
return void 0;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
var STATE = { open: "\xE0 prendre", taken: "prise", done: "termin\xE9e", refused: "refus\xE9e", dropped: "abandonn\xE9e" };
|
|
48
|
-
var time = (iso) => iso.slice(0, 16).replace("T", " ");
|
|
49
|
-
function formatTask(t) {
|
|
50
|
-
return [
|
|
51
|
-
`${t.id} \xB7 ${STATE[t.state]} \xB7 de ${t.giverName} \xE0 ${t.ownerName} \xB7 ${time(t.at)}`,
|
|
52
|
-
t.title,
|
|
53
|
-
...t.text ? ["", t.text] : [],
|
|
54
|
-
...t.result ? ["", `${t.state === "done" ? "R\xE9sultat" : "Raison"} : ${t.result}`] : [],
|
|
55
|
-
"",
|
|
56
|
-
"Historique :",
|
|
57
|
-
...t.log.map((l) => ` ${time(l.at)} ${l.by} : ${l.what}`)
|
|
58
|
-
].join("\n");
|
|
59
|
-
}
|
|
60
|
-
async function taskOnClient(client, args, by, ms) {
|
|
61
|
-
const op = taskArgs(args);
|
|
62
|
-
if (!op) return { line: TASK_USAGE, code: 1 };
|
|
63
|
-
if (by === LEDGE_GIVER) return { line: "refus\xE9 : seul Ledge confie une t\xE2che en son nom", code: 1 };
|
|
64
|
-
try {
|
|
65
|
-
client.send({ type: "task.cmd", rid: randomUUID(), by, ...op });
|
|
66
|
-
const r = await client.next("task.result", ms);
|
|
67
|
-
if (r.reason !== void 0) return { line: `refus\xE9 : ${r.reason}`, code: 1 };
|
|
68
|
-
if (op.op === "list") {
|
|
69
|
-
const list = r.tasks ?? [];
|
|
70
|
-
if (!list.length) return { line: op.all ? "aucune t\xE2che" : "aucune t\xE2che ouverte", code: 0 };
|
|
71
|
-
return { line: list.map((t2) => `${t2.id} ${STATE[t2.state].padEnd(10)} ${t2.giverName} \u2192 ${t2.ownerName} ${t2.title}`).join("\n"), code: 0 };
|
|
72
|
-
}
|
|
73
|
-
const t = r.task;
|
|
74
|
-
switch (op.op) {
|
|
75
|
-
case "give":
|
|
76
|
-
return { line: t.owner === LEDGE_GIVER ? `d\xE9cision ${t.id} pos\xE9e \xE0 l'utilisateur (ledge decide) : sa r\xE9ponse t'arrivera comme une t\xE2che termin\xE9e` : `t\xE2che ${t.id} confi\xE9e \xE0 ${t.ownerName} : elle lui arrive \xE0 son prochain moment libre`, code: 0 };
|
|
77
|
-
case "take":
|
|
78
|
-
return { line: `t\xE2che ${t.id} prise`, code: 0 };
|
|
79
|
-
case "done":
|
|
80
|
-
return { line: `t\xE2che ${t.id} termin\xE9e ; Ledge pr\xE9vient ${t.giverName}`, code: 0 };
|
|
81
|
-
case "refuse":
|
|
82
|
-
return { line: t.giver === by ? `t\xE2che ${t.id} annul\xE9e ; Ledge pr\xE9vient ${t.ownerName}` : `t\xE2che ${t.id} refus\xE9e ; Ledge pr\xE9vient ${t.giverName}`, code: 0 };
|
|
83
|
-
case "pass":
|
|
84
|
-
return { line: `t\xE2che ${t.id} pass\xE9e \xE0 ${t.ownerName}`, code: 0 };
|
|
85
|
-
default:
|
|
86
|
-
return { line: formatTask(t), code: 0 };
|
|
87
|
-
}
|
|
88
|
-
} catch (e) {
|
|
89
|
-
return { line: `pas de r\xE9ponse de Ledge : ${e.message}`, code: 1 };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
async function runTask(args, opts = {}) {
|
|
93
|
-
if (!taskArgs(args)) return { line: TASK_USAGE, code: args.length ? 1 : 0 };
|
|
94
|
-
const env = opts.env ?? process.env;
|
|
95
|
-
const by = env[CODEX_HOOK_VAR] || env.LEDGE_SESSION;
|
|
96
|
-
if (!by) return { line: "ledge task ne marche que dans une session lanc\xE9e par Ledge", code: 1 };
|
|
97
|
-
const deadline = Date.now() + (opts.timeoutMs ?? TASK_MS);
|
|
98
|
-
const left = () => Math.max(0, deadline - Date.now());
|
|
99
|
-
let client;
|
|
100
|
-
try {
|
|
101
|
-
client = await connect({ port: opts.port, timeoutMs: left() });
|
|
102
|
-
} catch {
|
|
103
|
-
return { line: "daemon not running (start a session with `ledge claude`, or run `ledge daemon`)", code: 1 };
|
|
104
|
-
}
|
|
105
|
-
try {
|
|
106
|
-
return await taskOnClient(client, args, by, left());
|
|
107
|
-
} finally {
|
|
108
|
-
await client.close();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export {
|
|
113
|
-
TASK_USAGE,
|
|
114
|
-
taskArgs,
|
|
115
|
-
formatTask,
|
|
116
|
-
taskOnClient,
|
|
117
|
-
runTask
|
|
118
|
-
};
|
|
@@ -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,17 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
paths
|
|
3
|
-
} from "./chunk-HOOHS3MX.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
|
-
};
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MESSAGE_MAX,
|
|
3
|
-
PROTOCOL_PORT,
|
|
4
|
-
SUMMARY_MAX,
|
|
5
|
-
parseServerMessage,
|
|
6
|
-
readDaemonToken
|
|
7
|
-
} from "./chunk-WXE2NOE5.js";
|
|
8
|
-
|
|
9
|
-
// src/client.ts
|
|
10
|
-
import WebSocket from "ws";
|
|
11
|
-
async function sendMessageAndAwaitAck(client, msg, timeoutMs) {
|
|
12
|
-
if (msg.text.length > MESSAGE_MAX) return { type: "message.ack", rid: msg.rid, outcome: "refused", reason: `message trop long (${msg.text.length} caract\xE8res, ${MESSAGE_MAX} au plus)` };
|
|
13
|
-
client.send(msg.summary && msg.summary.length > SUMMARY_MAX ? { ...msg, summary: msg.summary.slice(0, SUMMARY_MAX) } : msg);
|
|
14
|
-
return client.next("message.ack", timeoutMs);
|
|
15
|
-
}
|
|
16
|
-
var DaemonRefusedError = class extends Error {
|
|
17
|
-
};
|
|
18
|
-
function connect(opts = {}) {
|
|
19
|
-
const port = opts.port ?? PROTOCOL_PORT;
|
|
20
|
-
const host = opts.host ?? "127.0.0.1";
|
|
21
|
-
let token;
|
|
22
|
-
try {
|
|
23
|
-
token = opts.token === void 0 ? readDaemonToken() : opts.token;
|
|
24
|
-
} catch {
|
|
25
|
-
token = null;
|
|
26
|
-
}
|
|
27
|
-
const ws = new WebSocket(`ws://${host}:${port}`, token ? { headers: { authorization: `Bearer ${token}` } } : {});
|
|
28
|
-
const queue = [];
|
|
29
|
-
const MAX_QUEUED = 100;
|
|
30
|
-
const waiters = [];
|
|
31
|
-
ws.on("message", (data) => {
|
|
32
|
-
let msg;
|
|
33
|
-
try {
|
|
34
|
-
msg = parseServerMessage(JSON.parse(String(data)));
|
|
35
|
-
} catch {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const i = waiters.findIndex((w) => w.type === msg.type);
|
|
39
|
-
if (i >= 0) waiters.splice(i, 1)[0].resolve(msg);
|
|
40
|
-
else {
|
|
41
|
-
queue.push(msg);
|
|
42
|
-
if (queue.length > MAX_QUEUED) queue.shift();
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
const client = {
|
|
46
|
-
send: (msg) => ws.send(JSON.stringify(msg)),
|
|
47
|
-
next(type, timeoutMs = 1500) {
|
|
48
|
-
const i = queue.findIndex((m) => m.type === type);
|
|
49
|
-
if (i >= 0) return Promise.resolve(queue.splice(i, 1)[0]);
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
const w = { type, resolve: (m) => {
|
|
52
|
-
clearTimeout(t);
|
|
53
|
-
resolve(m);
|
|
54
|
-
} };
|
|
55
|
-
const t = setTimeout(() => {
|
|
56
|
-
waiters.splice(waiters.indexOf(w), 1);
|
|
57
|
-
reject(new Error(`timeout waiting for ${type}`));
|
|
58
|
-
}, timeoutMs);
|
|
59
|
-
waiters.push(w);
|
|
60
|
-
});
|
|
61
|
-
},
|
|
62
|
-
close: () => new Promise((res) => {
|
|
63
|
-
if (ws.readyState === WebSocket.CLOSED) return res();
|
|
64
|
-
ws.once("close", () => res());
|
|
65
|
-
ws.close();
|
|
66
|
-
})
|
|
67
|
-
};
|
|
68
|
-
return new Promise((resolve, reject) => {
|
|
69
|
-
const t = setTimeout(() => {
|
|
70
|
-
ws.terminate();
|
|
71
|
-
reject(new Error("connect timeout"));
|
|
72
|
-
}, opts.timeoutMs ?? 1e3);
|
|
73
|
-
ws.once("open", () => {
|
|
74
|
-
clearTimeout(t);
|
|
75
|
-
resolve(client);
|
|
76
|
-
});
|
|
77
|
-
let settled = false;
|
|
78
|
-
ws.once("open", () => {
|
|
79
|
-
settled = true;
|
|
80
|
-
});
|
|
81
|
-
ws.on("error", (e) => {
|
|
82
|
-
if (!settled) {
|
|
83
|
-
settled = true;
|
|
84
|
-
clearTimeout(t);
|
|
85
|
-
reject(/Unexpected server response: 401/.test(e.message) ? new DaemonRefusedError("daemon refused the token") : e);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export {
|
|
92
|
-
sendMessageAndAwaitAck,
|
|
93
|
-
DaemonRefusedError,
|
|
94
|
-
connect
|
|
95
|
-
};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
connect
|
|
3
|
-
} from "./chunk-ASYAWVFW.js";
|
|
4
|
-
import {
|
|
5
|
-
isArchived
|
|
6
|
-
} from "./chunk-WXE2NOE5.js";
|
|
7
|
-
|
|
8
|
-
// src/cli/who.ts
|
|
9
|
-
import { basename } from "path";
|
|
10
|
-
function formatWho(sessions, self) {
|
|
11
|
-
const live = sessions.filter((s) => s.state !== "exited" && !isArchived(s));
|
|
12
|
-
if (live.length === 0) return "aucune autre session";
|
|
13
|
-
const roles = live.some((s) => s.role);
|
|
14
|
-
const rows = live.map((s) => [
|
|
15
|
-
s.id === self ? "*" : " ",
|
|
16
|
-
s.name,
|
|
17
|
-
...roles ? [s.role ? `@${s.role}` : "-"] : [],
|
|
18
|
-
s.tool,
|
|
19
|
-
s.state,
|
|
20
|
-
`${s.repo ?? s.project}${s.worktree ? ` (${basename(s.cwd)})` : ""}`,
|
|
21
|
-
s.branch ?? "-",
|
|
22
|
-
`${s.topic ?? "-"}${s.source === "hook_only" ? " \xB7 lecture seule" : ""}`
|
|
23
|
-
]);
|
|
24
|
-
const head = [" ", "NOM", ...roles ? ["R\xD4LE"] : [], "OUTIL", "\xC9TAT", "PROJET", "BRANCHE", "SUJET"];
|
|
25
|
-
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
26
|
-
return [head, ...rows].map((r) => r.map((c, i) => i === r.length - 1 ? c : c.padEnd(widths[i])).join(" ")).join("\n");
|
|
27
|
-
}
|
|
28
|
-
async function printWho(opts = {}) {
|
|
29
|
-
let client;
|
|
30
|
-
try {
|
|
31
|
-
client = await connect({ port: opts.port, timeoutMs: 800 });
|
|
32
|
-
} catch {
|
|
33
|
-
return "daemon not running (start a session with `ledge claude`, or run `ledge daemon`)";
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
return formatWho((await client.next("snapshot")).sessions, opts.self ?? process.env.LEDGE_SESSION);
|
|
37
|
-
} finally {
|
|
38
|
-
await client.close();
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export {
|
|
43
|
-
formatWho,
|
|
44
|
-
printWho
|
|
45
|
-
};
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
EFFORT_ARGS,
|
|
3
|
-
MODEL_ARGS
|
|
4
|
-
} from "./chunk-PZ3F4QH3.js";
|
|
5
|
-
import {
|
|
6
|
-
findRole
|
|
7
|
-
} from "./chunk-4VNKZOC2.js";
|
|
8
|
-
import {
|
|
9
|
-
exitFile,
|
|
10
|
-
spawnSession
|
|
11
|
-
} from "./chunk-JEGPTNWV.js";
|
|
12
|
-
import {
|
|
13
|
-
DaemonRefusedError,
|
|
14
|
-
connect
|
|
15
|
-
} from "./chunk-ASYAWVFW.js";
|
|
16
|
-
import {
|
|
17
|
-
PROTOCOL_PORT,
|
|
18
|
-
autonomyArgs
|
|
19
|
-
} from "./chunk-WXE2NOE5.js";
|
|
20
|
-
import {
|
|
21
|
-
realTmux
|
|
22
|
-
} from "./chunk-NLL2EQJ2.js";
|
|
23
|
-
import {
|
|
24
|
-
log
|
|
25
|
-
} from "./chunk-6IYPRUQG.js";
|
|
26
|
-
import {
|
|
27
|
-
ensureLedgeDir
|
|
28
|
-
} from "./chunk-HOOHS3MX.js";
|
|
29
|
-
|
|
30
|
-
// src/cli/launch.ts
|
|
31
|
-
import { spawn } from "child_process";
|
|
32
|
-
import { readFile, rm } from "fs/promises";
|
|
33
|
-
import { createConnection } from "net";
|
|
34
|
-
var TMUX_SOCKET = "ledge";
|
|
35
|
-
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
36
|
-
function parseLaunchArgs(argv) {
|
|
37
|
-
const args = [];
|
|
38
|
-
let name;
|
|
39
|
-
let role;
|
|
40
|
-
for (let i = 0; i < argv.length; i++) {
|
|
41
|
-
const a = argv[i];
|
|
42
|
-
if (a === "--name") {
|
|
43
|
-
name = argv[++i];
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (a === "--role") {
|
|
47
|
-
role = argv[++i];
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (a === "--") {
|
|
51
|
-
args.push(...argv.slice(i + 1));
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
args.push(a);
|
|
55
|
-
}
|
|
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)];
|
|
63
|
-
}
|
|
64
|
-
function portOpen(port) {
|
|
65
|
-
return new Promise((res) => {
|
|
66
|
-
const s = createConnection({ host: "127.0.0.1", port });
|
|
67
|
-
s.once("connect", () => {
|
|
68
|
-
s.end();
|
|
69
|
-
res(true);
|
|
70
|
-
});
|
|
71
|
-
s.once("error", () => res(false));
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
function spawnDetachedDaemon() {
|
|
75
|
-
const child = spawn(process.execPath, [process.argv[1], "daemon"], { detached: true, stdio: "ignore" });
|
|
76
|
-
child.unref();
|
|
77
|
-
}
|
|
78
|
-
var closeQuietly = (c) => Promise.race([c.close(), sleep(500)]);
|
|
79
|
-
async function probe(port, timeoutMs) {
|
|
80
|
-
let client;
|
|
81
|
-
try {
|
|
82
|
-
client = await connect({ port, timeoutMs });
|
|
83
|
-
} catch (e) {
|
|
84
|
-
if (e instanceof DaemonRefusedError) return { client: null, state: "unauthorized" };
|
|
85
|
-
if (e.message === "connect timeout") return { client: null, state: "timeout" };
|
|
86
|
-
return { client: null, state: await portOpen(port) ? "foreign" : "refused" };
|
|
87
|
-
}
|
|
88
|
-
try {
|
|
89
|
-
await client.next("snapshot", timeoutMs);
|
|
90
|
-
return { client };
|
|
91
|
-
} catch {
|
|
92
|
-
void closeQuietly(client);
|
|
93
|
-
return { client: null, state: "foreign" };
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
async function ensureDaemon(opts = {}) {
|
|
97
|
-
await ensureLedgeDir();
|
|
98
|
-
const port = opts.port ?? PROTOCOL_PORT;
|
|
99
|
-
const timeoutMs = opts.timeoutMs ?? 2e3;
|
|
100
|
-
const first = await probe(port, timeoutMs);
|
|
101
|
-
if (first.client) return { client: first.client };
|
|
102
|
-
if (first.state === "foreign") {
|
|
103
|
-
log(`port ${port} is open but does not speak the Ledge protocol`);
|
|
104
|
-
return { client: null, reason: "foreign" };
|
|
105
|
-
}
|
|
106
|
-
if (first.state === "unauthorized") {
|
|
107
|
-
log(`the daemon on port ${port} refused this client's token`);
|
|
108
|
-
return { client: null, reason: "unauthorized" };
|
|
109
|
-
}
|
|
110
|
-
if (first.state === "timeout") {
|
|
111
|
-
log(`port ${port} accepted a connection but did not answer within ${timeoutMs} ms`);
|
|
112
|
-
return { client: null, reason: "unresponsive" };
|
|
113
|
-
}
|
|
114
|
-
(opts.spawnDaemon ?? spawnDetachedDaemon)();
|
|
115
|
-
for (let i = 0; i < 3; i++) {
|
|
116
|
-
await sleep(opts.retryDelayMs ?? 500);
|
|
117
|
-
const r = await probe(port, timeoutMs);
|
|
118
|
-
if (r.client) return { client: r.client };
|
|
119
|
-
}
|
|
120
|
-
return { client: null, reason: "unreachable" };
|
|
121
|
-
}
|
|
122
|
-
var DaemonError = class extends Error {
|
|
123
|
-
};
|
|
124
|
-
async function createSession(opts) {
|
|
125
|
-
const names = [];
|
|
126
|
-
const ids = [];
|
|
127
|
-
if (opts.client) {
|
|
128
|
-
let snap;
|
|
129
|
-
try {
|
|
130
|
-
opts.client.send({ type: "snapshot.request" });
|
|
131
|
-
snap = await opts.client.next("snapshot");
|
|
132
|
-
} catch (e) {
|
|
133
|
-
throw new DaemonError(`daemon did not answer: ${e.message}`);
|
|
134
|
-
}
|
|
135
|
-
for (const s of snap.sessions) {
|
|
136
|
-
names.push(s.name);
|
|
137
|
-
ids.push(s.id);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
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 } });
|
|
142
|
-
try {
|
|
143
|
-
opts.client?.send({ type: "session.register", session });
|
|
144
|
-
} catch (e) {
|
|
145
|
-
log(`register failed for ${session.id}: ${e.message}`);
|
|
146
|
-
}
|
|
147
|
-
return session;
|
|
148
|
-
}
|
|
149
|
-
function attachEnv(env) {
|
|
150
|
-
const { TMUX: _ignored, ...rest } = env;
|
|
151
|
-
return rest;
|
|
152
|
-
}
|
|
153
|
-
function attachInherit(args, env) {
|
|
154
|
-
return new Promise((resolve) => {
|
|
155
|
-
const child = spawn("tmux", args, { stdio: "inherit", env });
|
|
156
|
-
child.on("error", () => resolve(1));
|
|
157
|
-
child.on("exit", (c) => resolve(c ?? 1));
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
async function readExitCode(id) {
|
|
161
|
-
const file = exitFile(id);
|
|
162
|
-
let raw = null;
|
|
163
|
-
try {
|
|
164
|
-
raw = await readFile(file, "utf8");
|
|
165
|
-
} catch {
|
|
166
|
-
}
|
|
167
|
-
await rm(file, { force: true });
|
|
168
|
-
const t = raw?.trim();
|
|
169
|
-
return t && /^\d+$/.test(t) ? Number(t) : null;
|
|
170
|
-
}
|
|
171
|
-
var UNTRACKED = {
|
|
172
|
-
foreign: (port) => `ledge: port ${port} is used by another program, session not tracked`,
|
|
173
|
-
unauthorized: () => "ledge: the daemon refused this client's token (missing or changed), session not tracked; restart the daemon: systemctl --user restart ledge-daemon",
|
|
174
|
-
unresponsive: () => "ledge: daemon not responding, session not tracked",
|
|
175
|
-
unreachable: () => "ledge: daemon not running, session not tracked"
|
|
176
|
-
};
|
|
177
|
-
async function launch(tool, argv, opts = {}) {
|
|
178
|
-
await ensureLedgeDir();
|
|
179
|
-
const { name, role: roleId, args } = parseLaunchArgs(argv);
|
|
180
|
-
const port = opts.port ?? PROTOCOL_PORT;
|
|
181
|
-
const socket = opts.tmuxSocket ?? TMUX_SOCKET;
|
|
182
|
-
const tmux = realTmux(socket);
|
|
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
|
-
}
|
|
189
|
-
const daemon = await ensureDaemon({ port, spawnDaemon: opts.spawnDaemon, retryDelayMs: opts.retryDelayMs });
|
|
190
|
-
let client = daemon.client;
|
|
191
|
-
if (!daemon.client) console.error(UNTRACKED[daemon.reason](port));
|
|
192
|
-
const create = (c) => createSession({ tool, args, command: opts.command, recordExit: true, name, ...role ? { role } : {}, cwd, tmux, client: c });
|
|
193
|
-
let session;
|
|
194
|
-
try {
|
|
195
|
-
session = await create(client);
|
|
196
|
-
} catch (e) {
|
|
197
|
-
if (!(e instanceof DaemonError)) throw e;
|
|
198
|
-
log(e.message);
|
|
199
|
-
if (client) void closeQuietly(client);
|
|
200
|
-
client = null;
|
|
201
|
-
console.error(UNTRACKED.unresponsive(port));
|
|
202
|
-
session = await create(null);
|
|
203
|
-
}
|
|
204
|
-
if (client) await closeQuietly(client);
|
|
205
|
-
const tmuxName = session.tmuxSession;
|
|
206
|
-
if (process.stdout.isTTY) process.stdout.write(`\x1B]0;${session.title}\x07`);
|
|
207
|
-
const attachCode = await (opts.attach ?? attachInherit)(tmux.attachArgs(tmuxName), attachEnv(process.env));
|
|
208
|
-
if (await tmux.hasSession(tmuxName)) {
|
|
209
|
-
if (attachCode !== 0) console.error(`ledge: could not attach; ${session.name} keeps running in tmux session ${tmuxName} (tmux -L ${socket} attach -t ${tmuxName})`);
|
|
210
|
-
return attachCode;
|
|
211
|
-
}
|
|
212
|
-
const exitCode = await readExitCode(session.id);
|
|
213
|
-
if (client) {
|
|
214
|
-
try {
|
|
215
|
-
const c = await connect({ port, timeoutMs: 800 });
|
|
216
|
-
c.send({ type: "session.unregister", id: session.id, exitCode });
|
|
217
|
-
await closeQuietly(c);
|
|
218
|
-
} catch {
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return exitCode ?? 1;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export {
|
|
225
|
-
parseLaunchArgs,
|
|
226
|
-
roleArgs,
|
|
227
|
-
ensureDaemon,
|
|
228
|
-
DaemonError,
|
|
229
|
-
createSession,
|
|
230
|
-
attachEnv,
|
|
231
|
-
launch
|
|
232
|
-
};
|