cmdr-mcp 0.1.0 → 0.2.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/.claude-plugin/marketplace.json +1 -1
- package/README.md +31 -15
- package/docs/README.zh-CN.md +31 -10
- package/docs/agent-integration.md +15 -5
- package/docs/implementation.md +27 -9
- package/docs/iteration-plan-zcode.md +152 -0
- package/docs/long-running-collaboration.md +99 -0
- package/docs/publishing.md +26 -9
- package/docs/troubleshooting.md +88 -0
- package/marketplace.json +1 -1
- package/package.json +8 -7
- package/plugins/cmdr/.claude-plugin/plugin.json +1 -1
- package/plugins/cmdr/.codex-plugin/plugin.json +1 -1
- package/plugins/cmdr/.zcode-plugin/plugin.json +1 -1
- package/plugins/cmdr/README.md +6 -4
- package/plugins/cmdr/bin/cmdr +9 -1
- package/plugins/cmdr/bin/cmdr-check.mjs +97 -0
- package/plugins/cmdr/bin/cmdr-daemon +9 -1
- package/plugins/cmdr/bin/cmdr-hook +9 -1
- package/plugins/cmdr/bin/cmdr-mcp +9 -1
- package/plugins/cmdr/bin/cmdr-node +26 -3
- package/plugins/cmdr/commands/cmdr.md +4 -4
- package/plugins/cmdr/dist/cli.mjs +4943 -229
- package/plugins/cmdr/dist/daemon.mjs +1170 -131
- package/plugins/cmdr/dist/hook.mjs +86 -16
- package/plugins/cmdr/dist/integrity.json +25 -0
- package/plugins/cmdr/dist/mcp.mjs +142 -52
- package/plugins/cmdr/skills/cmdr-commander/SKILL.md +10 -8
- package/plugins/cmdr/skills/cmdr-executor/SKILL.md +9 -7
- package/plugins/cmdr/skills/using-cmdr/SKILL.md +7 -7
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --experimental-sqlite --disable-warning=ExperimentalWarning
|
|
2
2
|
import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
|
|
3
3
|
|
|
4
|
-
// src/
|
|
5
|
-
import {
|
|
4
|
+
// src/shared/diagnostics.ts
|
|
5
|
+
import { mkdirSync as mkdirSync2, readFileSync, writeFileSync, renameSync, rmSync } from "node:fs";
|
|
6
|
+
import { join as join2 } from "node:path";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
6
8
|
|
|
7
9
|
// src/shared/paths.ts
|
|
8
10
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -42,6 +44,46 @@ function prepare(p) {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
// src/shared/diagnostics.ts
|
|
48
|
+
var events = ["SessionStart", "UserPromptSubmit", "PreToolUse", "Stop", "SessionEnd"];
|
|
49
|
+
function diagnostic(code, detail = {}, home) {
|
|
50
|
+
const dir = join2(paths(home).home, "logs/diagnostics");
|
|
51
|
+
const file = join2(dir, `${code}.json`);
|
|
52
|
+
let temp;
|
|
53
|
+
try {
|
|
54
|
+
const previous = JSON.parse(readFileSync(file, "utf8"));
|
|
55
|
+
if (Date.now() - previous.at < 1e4) return;
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
60
|
+
temp = `${file}.${randomUUID()}.tmp`;
|
|
61
|
+
const allowed = Object.fromEntries(
|
|
62
|
+
Object.entries(detail).filter(([k]) => ["agent", "event", "from", "to", "protocol"].includes(k)).map(([k, v]) => [
|
|
63
|
+
k,
|
|
64
|
+
String(v).replace(/[^a-zA-Z0-9_.:-]/g, "").slice(0, 80)
|
|
65
|
+
])
|
|
66
|
+
);
|
|
67
|
+
writeFileSync(temp, JSON.stringify({ code, at: Date.now(), ...allowed }), { mode: 384 });
|
|
68
|
+
renameSync(temp, file);
|
|
69
|
+
} catch {
|
|
70
|
+
} finally {
|
|
71
|
+
if (temp) {
|
|
72
|
+
try {
|
|
73
|
+
rmSync(temp, { force: true });
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function observedHook(event, agent) {
|
|
80
|
+
if (events.includes(event))
|
|
81
|
+
diagnostic(`hook-${event}`, { event, agent });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/hook/main.ts
|
|
85
|
+
import { existsSync } from "node:fs";
|
|
86
|
+
|
|
45
87
|
// src/shared/env.ts
|
|
46
88
|
var cmdrTool = /(?:^|[_:])cmdr(?:__|:)(list|join|report|leave|ask|send|read)$/;
|
|
47
89
|
function detectAgent(env = process.env, hook) {
|
|
@@ -57,9 +99,20 @@ function detectAgent(env = process.env, hook) {
|
|
|
57
99
|
// src/shared/client.ts
|
|
58
100
|
import { connect } from "node:net";
|
|
59
101
|
import { spawn } from "node:child_process";
|
|
60
|
-
import { dirname, join as
|
|
102
|
+
import { dirname, join as join3 } from "node:path";
|
|
61
103
|
import { fileURLToPath } from "node:url";
|
|
62
|
-
import { mkdirSync as
|
|
104
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, rmSync as rmSync2, statSync } from "node:fs";
|
|
105
|
+
|
|
106
|
+
// src/daemon/lock.ts
|
|
107
|
+
function alive(pid) {
|
|
108
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
109
|
+
try {
|
|
110
|
+
process.kill(pid, 0);
|
|
111
|
+
return true;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return e.code === "EPERM";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
63
116
|
|
|
64
117
|
// src/shared/rpc.ts
|
|
65
118
|
import { EventEmitter } from "node:events";
|
|
@@ -201,7 +254,7 @@ var Rpc = class extends EventEmitter {
|
|
|
201
254
|
};
|
|
202
255
|
|
|
203
256
|
// src/shared/version.ts
|
|
204
|
-
var VERSION = true ? "0.
|
|
257
|
+
var VERSION = true ? "0.2.0" : MIN_CLIENT_VERSION;
|
|
205
258
|
var PROTOCOL = 1;
|
|
206
259
|
function newer(a, b) {
|
|
207
260
|
const x = a.split(".").map(Number), y = b.split(".").map(Number);
|
|
@@ -242,33 +295,44 @@ async function daemonConnection(options = {}) {
|
|
|
242
295
|
timeout
|
|
243
296
|
);
|
|
244
297
|
if (options.upgrade && newer(VERSION, hello.version)) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
298
|
+
diagnostic(
|
|
299
|
+
"upgrade",
|
|
300
|
+
{ from: hello.version, to: VERSION, protocol: hello.protocol },
|
|
301
|
+
options.home
|
|
302
|
+
);
|
|
303
|
+
throw new CmdrError(
|
|
304
|
+
"UPGRADE_REQUIRED",
|
|
305
|
+
`Daemon ${hello.version} is older than client ${VERSION}. Run cmdr daemon restart from this installation; automatic replacement is disabled to protect live sessions.`
|
|
306
|
+
);
|
|
249
307
|
}
|
|
250
308
|
return rpc;
|
|
251
309
|
} catch (e) {
|
|
252
310
|
rpc?.close();
|
|
253
|
-
if (e.code === "PROTOCOL_MISMATCH" || !options.start)
|
|
311
|
+
if (e.code === "PROTOCOL_MISMATCH" || e.code === "UPGRADE_REQUIRED" || !options.start)
|
|
312
|
+
throw e;
|
|
254
313
|
}
|
|
255
314
|
if (!owner) {
|
|
256
315
|
try {
|
|
257
|
-
|
|
316
|
+
mkdirSync3(p.spawn, { mode: 448 });
|
|
258
317
|
owner = true;
|
|
259
318
|
} catch {
|
|
260
319
|
try {
|
|
261
320
|
if (Date.now() - statSync(p.spawn).mtimeMs > 1e4)
|
|
262
|
-
|
|
321
|
+
rmSync2(p.spawn, { recursive: true, force: true });
|
|
263
322
|
} catch {
|
|
264
323
|
}
|
|
265
324
|
}
|
|
266
325
|
}
|
|
267
|
-
|
|
326
|
+
let daemonOwnsLock = false;
|
|
327
|
+
try {
|
|
328
|
+
daemonOwnsLock = alive(Number(readFileSync2(p.lock, "utf8")));
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
if (owner && !daemonOwnsLock) {
|
|
268
332
|
if (attempt === 0 || !spawned) {
|
|
269
333
|
const child = spawn(
|
|
270
334
|
process.execPath,
|
|
271
|
-
["--experimental-sqlite",
|
|
335
|
+
["--experimental-sqlite", join3(dirname(fileURLToPath(import.meta.url)), "daemon.mjs")],
|
|
272
336
|
{ detached: true, stdio: "ignore", env: { ...process.env, CMDR_HOME: p.home } }
|
|
273
337
|
);
|
|
274
338
|
child.on("error", () => {
|
|
@@ -280,7 +344,7 @@ async function daemonConnection(options = {}) {
|
|
|
280
344
|
await sleep(50);
|
|
281
345
|
}
|
|
282
346
|
} finally {
|
|
283
|
-
if (owner)
|
|
347
|
+
if (owner) rmSync2(p.spawn, { recursive: true, force: true });
|
|
284
348
|
spawned = false;
|
|
285
349
|
}
|
|
286
350
|
throw new CmdrError("DAEMON_UNAVAILABLE", "Cannot start cmdr daemon. Run cmdr doctor.");
|
|
@@ -320,7 +384,12 @@ function ancestors(start = process.ppid) {
|
|
|
320
384
|
// src/hook/main.ts
|
|
321
385
|
async function runHook(input, event = input.hook_event_name) {
|
|
322
386
|
const agent = detectAgent(process.env, input), sid = `${agent}:${input.session_id}`;
|
|
387
|
+
observedHook(event, agent);
|
|
323
388
|
if (!input.session_id) return;
|
|
389
|
+
if (process.env.CMDR_SESSION_ID && process.env.CMDR_SESSION_ID !== input.session_id) {
|
|
390
|
+
diagnostic("identity-conflict", { agent });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
324
393
|
if (event === "PreToolUse" && cmdrTool.test(input.tool_name || "")) {
|
|
325
394
|
if (agent !== "claude")
|
|
326
395
|
return {
|
|
@@ -357,7 +426,8 @@ try {
|
|
|
357
426
|
}
|
|
358
427
|
const result = await runHook(JSON.parse(input), process.argv[2]);
|
|
359
428
|
if (result) process.stdout.write(JSON.stringify(result) + "\n");
|
|
360
|
-
} catch {
|
|
429
|
+
} catch (e) {
|
|
430
|
+
diagnostic(e.code === "DAEMON_UNAVAILABLE" ? "hook-unavailable" : "hook-error");
|
|
361
431
|
}
|
|
362
432
|
clearTimeout(timer);
|
|
363
433
|
export {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2.0",
|
|
3
|
+
"files": {
|
|
4
|
+
".claude-plugin/plugin.json": "f0996b4d63fd2419ed0b5b5f69a20423716967540367eee4f59597e0578ebca7",
|
|
5
|
+
".codex-plugin/plugin.json": "0a95305705501d504dfd9416dcecc986bc1df63f7a864c555f5cc65bde77d431",
|
|
6
|
+
".mcp.json": "34e310a2874c8fda625745672a54df19614f32dabd2be4013f8ce871260e88c4",
|
|
7
|
+
".zcode-plugin/plugin.json": "1f1b077a4313e721f16f0afc6f89ee89f80e77371cc17758ba86ce57cf40d508",
|
|
8
|
+
"THIRD_PARTY_NOTICES.txt": "e68f09a58095667d51b337c7e0b57b0600e9c69c0cfa494a678446536df1fbaf",
|
|
9
|
+
"bin/cmdr": "be6ea6b84ce121738ca729e9c6dbfb87bbf349a84d251acc711e3ea11f8dd8fd",
|
|
10
|
+
"bin/cmdr-check.mjs": "6192d158606b253637635416e706f8ba782b8e8d63c1613c76f5011c09a58a85",
|
|
11
|
+
"bin/cmdr-daemon": "7f8ed3a755f975751ddda077242adf661834dddbb8be7d3a88caee0c70cce5db",
|
|
12
|
+
"bin/cmdr-hook": "e1fa5950325775e467e4e01aeff3c5cb8c1dba35c4535d7cb569f658f95e7f15",
|
|
13
|
+
"bin/cmdr-mcp": "356bea13c2b82075c09fd663106791edab9e06fdb04fc679a98de33514b10a83",
|
|
14
|
+
"bin/cmdr-node": "ce5952648b9a7deccdd855808331d51ff8569675100278379ef1fc84cd3d528e",
|
|
15
|
+
"commands/cmdr.md": "d68c74a57448a1479b570839b321e9ee63c34d3df3b2606ff90e9a69b425b680",
|
|
16
|
+
"dist/cli.mjs": "13cbbe9f2be50e13a04e69b887142b9593f860dac8c37f1722dc621599d13294",
|
|
17
|
+
"dist/daemon.mjs": "844662506fab68838aeac319022dc58e76922a29274e8a5a9f61084f6e60ceb6",
|
|
18
|
+
"dist/hook.mjs": "7cc89aeee448c5851901358d24e40c66fdab6a8ca7c63948d301533c4c6b9e73",
|
|
19
|
+
"dist/mcp.mjs": "3cd895266c9c99c2bd8fb29f09ef6330c7a749b1ee595f103e7a822b3071cf7f",
|
|
20
|
+
"hooks/hooks.json": "6ff937910fc04eb81db4a84dfeb91a92ab971364d684a45a85f3a9953eb21259",
|
|
21
|
+
"skills/cmdr-commander/SKILL.md": "1089e452a3cb8bc010725d1b97ffec63817e0a1dea37696cb70fd09b09f78462",
|
|
22
|
+
"skills/cmdr-executor/SKILL.md": "ad2104ad2c7acec3e712ab0639aac881638053c69e4d6861dce38359b17f9934",
|
|
23
|
+
"skills/using-cmdr/SKILL.md": "c37c27a78de8b502439380d283678b828eb2f748e93af9446697ad7c53ec5a43"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -7195,6 +7195,31 @@ var require_dist = __commonJS({
|
|
|
7195
7195
|
}
|
|
7196
7196
|
});
|
|
7197
7197
|
|
|
7198
|
+
// src/shared/protocol.ts
|
|
7199
|
+
var LIMITS = {
|
|
7200
|
+
maxWaitSec: 300,
|
|
7201
|
+
maxBody: 32768,
|
|
7202
|
+
maxData: 65536,
|
|
7203
|
+
maxFrame: 2 * 1024 * 1024
|
|
7204
|
+
};
|
|
7205
|
+
var CmdrError = class extends Error {
|
|
7206
|
+
constructor(code, message = code) {
|
|
7207
|
+
super(message);
|
|
7208
|
+
this.code = code;
|
|
7209
|
+
}
|
|
7210
|
+
};
|
|
7211
|
+
|
|
7212
|
+
// src/shared/methods.ts
|
|
7213
|
+
var methods = {
|
|
7214
|
+
join: "session.join",
|
|
7215
|
+
list: "session.list",
|
|
7216
|
+
send: "msg.send",
|
|
7217
|
+
report: "msg.report",
|
|
7218
|
+
ask: "msg.ask",
|
|
7219
|
+
read: "msg.read",
|
|
7220
|
+
leave: "session.leave"
|
|
7221
|
+
};
|
|
7222
|
+
|
|
7198
7223
|
// node_modules/zod/v3/external.js
|
|
7199
7224
|
var external_exports = {};
|
|
7200
7225
|
__export(external_exports, {
|
|
@@ -21432,20 +21457,6 @@ var StdioServerTransport = class {
|
|
|
21432
21457
|
}
|
|
21433
21458
|
};
|
|
21434
21459
|
|
|
21435
|
-
// src/shared/protocol.ts
|
|
21436
|
-
var LIMITS = {
|
|
21437
|
-
maxWaitSec: 300,
|
|
21438
|
-
maxBody: 32768,
|
|
21439
|
-
maxData: 65536,
|
|
21440
|
-
maxFrame: 2 * 1024 * 1024
|
|
21441
|
-
};
|
|
21442
|
-
var CmdrError = class extends Error {
|
|
21443
|
-
constructor(code, message = code) {
|
|
21444
|
-
super(message);
|
|
21445
|
-
this.code = code;
|
|
21446
|
-
}
|
|
21447
|
-
};
|
|
21448
|
-
|
|
21449
21460
|
// src/shared/schemas.ts
|
|
21450
21461
|
var text = external_exports.string().min(1).refine((v) => Buffer.byteLength(v) <= LIMITS.maxBody, "MESSAGE_TOO_LARGE");
|
|
21451
21462
|
var data = external_exports.record(external_exports.unknown()).refine((v) => Buffer.byteLength(JSON.stringify(v)) <= LIMITS.maxData, "MESSAGE_TOO_LARGE").optional();
|
|
@@ -21458,16 +21469,20 @@ var schemas = {
|
|
|
21458
21469
|
squad: external_exports.string().optional(),
|
|
21459
21470
|
name: external_exports.string().trim().min(1).max(64).optional(),
|
|
21460
21471
|
note: text.optional(),
|
|
21461
|
-
squad_name: external_exports.string().trim().min(1).max(64).optional()
|
|
21472
|
+
squad_name: external_exports.string().trim().min(1).max(64).optional(),
|
|
21473
|
+
takeover: external_exports.boolean().default(false),
|
|
21474
|
+
standby: external_exports.enum(["auto", "manual"]).optional(),
|
|
21475
|
+
rebind: external_exports.string().optional()
|
|
21462
21476
|
}).strict(),
|
|
21463
21477
|
list: external_exports.object({
|
|
21464
21478
|
...identity,
|
|
21479
|
+
full: external_exports.boolean().default(false),
|
|
21465
21480
|
scope: external_exports.enum(["squad", "all"]).optional(),
|
|
21466
21481
|
squad: external_exports.string().optional()
|
|
21467
21482
|
}).strict(),
|
|
21468
21483
|
report: external_exports.object({
|
|
21469
21484
|
...identity,
|
|
21470
|
-
status: external_exports.enum(["ready", "working", "blocked", "done", "failed"]),
|
|
21485
|
+
status: external_exports.enum(["ready", "working", "blocked", "done", "failed", "cancelled"]),
|
|
21471
21486
|
message: text,
|
|
21472
21487
|
reply_to: external_exports.string().optional(),
|
|
21473
21488
|
data
|
|
@@ -21477,7 +21492,10 @@ var schemas = {
|
|
|
21477
21492
|
...identity,
|
|
21478
21493
|
to: external_exports.union([external_exports.string().min(1), external_exports.array(external_exports.string().min(1)).min(1).max(1e3)]),
|
|
21479
21494
|
message: text,
|
|
21480
|
-
type: external_exports.enum(["command", "answer", "info"]).default("command"),
|
|
21495
|
+
type: external_exports.enum(["command", "cancel", "answer", "info"]).default("command"),
|
|
21496
|
+
task_key: external_exports.string().min(1).max(128).optional(),
|
|
21497
|
+
reassign: external_exports.string().optional(),
|
|
21498
|
+
attention: external_exports.boolean().optional(),
|
|
21481
21499
|
priority: external_exports.enum(["high", "normal", "low"]).optional(),
|
|
21482
21500
|
reply_to: external_exports.string().optional(),
|
|
21483
21501
|
data
|
|
@@ -21488,13 +21506,16 @@ var schemas = {
|
|
|
21488
21506
|
limit: external_exports.number().int().min(1).max(100).default(20),
|
|
21489
21507
|
peek: external_exports.boolean().default(false),
|
|
21490
21508
|
history: external_exports.boolean().default(false),
|
|
21491
|
-
since: external_exports.string().optional()
|
|
21509
|
+
since: external_exports.string().optional(),
|
|
21510
|
+
id: external_exports.string().optional(),
|
|
21511
|
+
recover: external_exports.boolean().default(false),
|
|
21512
|
+
full: external_exports.boolean().default(false)
|
|
21492
21513
|
}).strict(),
|
|
21493
21514
|
leave: external_exports.object({ ...identity, dissolve: external_exports.boolean().default(false), message: text.optional() }).strict()
|
|
21494
21515
|
};
|
|
21495
21516
|
|
|
21496
21517
|
// src/shared/version.ts
|
|
21497
|
-
var VERSION = true ? "0.
|
|
21518
|
+
var VERSION = true ? "0.2.0" : MIN_CLIENT_VERSION;
|
|
21498
21519
|
var PROTOCOL = 1;
|
|
21499
21520
|
function newer(a, b) {
|
|
21500
21521
|
const x = a.split(".").map(Number), y = b.split(".").map(Number);
|
|
@@ -21514,18 +21535,25 @@ function detectAgent(env = process.env, hook) {
|
|
|
21514
21535
|
return "codex";
|
|
21515
21536
|
return "generic";
|
|
21516
21537
|
}
|
|
21517
|
-
function
|
|
21518
|
-
if (agent2 === "claude") return 300;
|
|
21538
|
+
function waitRecommendation(agent2, env = process.env) {
|
|
21519
21539
|
const timeout = Number(env.CMDR_TOOL_TIMEOUT_SEC);
|
|
21520
|
-
|
|
21540
|
+
if (Number.isFinite(timeout) && timeout > 0) {
|
|
21541
|
+
return {
|
|
21542
|
+
seconds: Math.min(300, Math.max(0, timeout - Math.min(15, timeout / 4))),
|
|
21543
|
+
source: "CMDR_TOOL_TIMEOUT_SEC",
|
|
21544
|
+
timeout
|
|
21545
|
+
};
|
|
21546
|
+
}
|
|
21547
|
+
return { seconds: agent2 === "claude" ? 300 : 45, source: "host default", timeout: null };
|
|
21548
|
+
}
|
|
21549
|
+
function recommendedWait(agent2, env = process.env) {
|
|
21550
|
+
return waitRecommendation(agent2, env).seconds;
|
|
21521
21551
|
}
|
|
21522
21552
|
|
|
21523
|
-
// src/shared/
|
|
21524
|
-
import {
|
|
21525
|
-
import {
|
|
21526
|
-
import {
|
|
21527
|
-
import { fileURLToPath } from "node:url";
|
|
21528
|
-
import { mkdirSync as mkdirSync2, rmSync, statSync } from "node:fs";
|
|
21553
|
+
// src/shared/diagnostics.ts
|
|
21554
|
+
import { mkdirSync as mkdirSync2, readFileSync, writeFileSync, renameSync, rmSync } from "node:fs";
|
|
21555
|
+
import { join as join2 } from "node:path";
|
|
21556
|
+
import { randomUUID } from "node:crypto";
|
|
21529
21557
|
|
|
21530
21558
|
// src/shared/paths.ts
|
|
21531
21559
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -21565,6 +21593,56 @@ function prepare(p) {
|
|
|
21565
21593
|
}
|
|
21566
21594
|
}
|
|
21567
21595
|
|
|
21596
|
+
// src/shared/diagnostics.ts
|
|
21597
|
+
function diagnostic(code, detail = {}, home) {
|
|
21598
|
+
const dir = join2(paths(home).home, "logs/diagnostics");
|
|
21599
|
+
const file = join2(dir, `${code}.json`);
|
|
21600
|
+
let temp;
|
|
21601
|
+
try {
|
|
21602
|
+
const previous = JSON.parse(readFileSync(file, "utf8"));
|
|
21603
|
+
if (Date.now() - previous.at < 1e4) return;
|
|
21604
|
+
} catch {
|
|
21605
|
+
}
|
|
21606
|
+
try {
|
|
21607
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
21608
|
+
temp = `${file}.${randomUUID()}.tmp`;
|
|
21609
|
+
const allowed = Object.fromEntries(
|
|
21610
|
+
Object.entries(detail).filter(([k]) => ["agent", "event", "from", "to", "protocol"].includes(k)).map(([k, v]) => [
|
|
21611
|
+
k,
|
|
21612
|
+
String(v).replace(/[^a-zA-Z0-9_.:-]/g, "").slice(0, 80)
|
|
21613
|
+
])
|
|
21614
|
+
);
|
|
21615
|
+
writeFileSync(temp, JSON.stringify({ code, at: Date.now(), ...allowed }), { mode: 384 });
|
|
21616
|
+
renameSync(temp, file);
|
|
21617
|
+
} catch {
|
|
21618
|
+
} finally {
|
|
21619
|
+
if (temp) {
|
|
21620
|
+
try {
|
|
21621
|
+
rmSync(temp, { force: true });
|
|
21622
|
+
} catch {
|
|
21623
|
+
}
|
|
21624
|
+
}
|
|
21625
|
+
}
|
|
21626
|
+
}
|
|
21627
|
+
|
|
21628
|
+
// src/shared/client.ts
|
|
21629
|
+
import { connect } from "node:net";
|
|
21630
|
+
import { spawn } from "node:child_process";
|
|
21631
|
+
import { dirname, join as join3 } from "node:path";
|
|
21632
|
+
import { fileURLToPath } from "node:url";
|
|
21633
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, rmSync as rmSync2, statSync } from "node:fs";
|
|
21634
|
+
|
|
21635
|
+
// src/daemon/lock.ts
|
|
21636
|
+
function alive(pid) {
|
|
21637
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
21638
|
+
try {
|
|
21639
|
+
process.kill(pid, 0);
|
|
21640
|
+
return true;
|
|
21641
|
+
} catch (e) {
|
|
21642
|
+
return e.code === "EPERM";
|
|
21643
|
+
}
|
|
21644
|
+
}
|
|
21645
|
+
|
|
21568
21646
|
// src/shared/rpc.ts
|
|
21569
21647
|
import { EventEmitter } from "node:events";
|
|
21570
21648
|
var Rpc = class extends EventEmitter {
|
|
@@ -21719,33 +21797,44 @@ async function daemonConnection(options = {}) {
|
|
|
21719
21797
|
timeout
|
|
21720
21798
|
);
|
|
21721
21799
|
if (options.upgrade && newer(VERSION, hello.version)) {
|
|
21722
|
-
|
|
21723
|
-
|
|
21724
|
-
|
|
21725
|
-
|
|
21800
|
+
diagnostic(
|
|
21801
|
+
"upgrade",
|
|
21802
|
+
{ from: hello.version, to: VERSION, protocol: hello.protocol },
|
|
21803
|
+
options.home
|
|
21804
|
+
);
|
|
21805
|
+
throw new CmdrError(
|
|
21806
|
+
"UPGRADE_REQUIRED",
|
|
21807
|
+
`Daemon ${hello.version} is older than client ${VERSION}. Run cmdr daemon restart from this installation; automatic replacement is disabled to protect live sessions.`
|
|
21808
|
+
);
|
|
21726
21809
|
}
|
|
21727
21810
|
return rpc;
|
|
21728
21811
|
} catch (e) {
|
|
21729
21812
|
rpc?.close();
|
|
21730
|
-
if (e.code === "PROTOCOL_MISMATCH" || !options.start)
|
|
21813
|
+
if (e.code === "PROTOCOL_MISMATCH" || e.code === "UPGRADE_REQUIRED" || !options.start)
|
|
21814
|
+
throw e;
|
|
21731
21815
|
}
|
|
21732
21816
|
if (!owner) {
|
|
21733
21817
|
try {
|
|
21734
|
-
|
|
21818
|
+
mkdirSync3(p.spawn, { mode: 448 });
|
|
21735
21819
|
owner = true;
|
|
21736
21820
|
} catch {
|
|
21737
21821
|
try {
|
|
21738
21822
|
if (Date.now() - statSync(p.spawn).mtimeMs > 1e4)
|
|
21739
|
-
|
|
21823
|
+
rmSync2(p.spawn, { recursive: true, force: true });
|
|
21740
21824
|
} catch {
|
|
21741
21825
|
}
|
|
21742
21826
|
}
|
|
21743
21827
|
}
|
|
21744
|
-
|
|
21828
|
+
let daemonOwnsLock = false;
|
|
21829
|
+
try {
|
|
21830
|
+
daemonOwnsLock = alive(Number(readFileSync2(p.lock, "utf8")));
|
|
21831
|
+
} catch {
|
|
21832
|
+
}
|
|
21833
|
+
if (owner && !daemonOwnsLock) {
|
|
21745
21834
|
if (attempt === 0 || !spawned) {
|
|
21746
21835
|
const child = spawn(
|
|
21747
21836
|
process.execPath,
|
|
21748
|
-
["--experimental-sqlite",
|
|
21837
|
+
["--experimental-sqlite", join3(dirname(fileURLToPath(import.meta.url)), "daemon.mjs")],
|
|
21749
21838
|
{ detached: true, stdio: "ignore", env: { ...process.env, CMDR_HOME: p.home } }
|
|
21750
21839
|
);
|
|
21751
21840
|
child.on("error", () => {
|
|
@@ -21757,7 +21846,7 @@ async function daemonConnection(options = {}) {
|
|
|
21757
21846
|
await sleep(50);
|
|
21758
21847
|
}
|
|
21759
21848
|
} finally {
|
|
21760
|
-
if (owner)
|
|
21849
|
+
if (owner) rmSync2(p.spawn, { recursive: true, force: true });
|
|
21761
21850
|
spawned = false;
|
|
21762
21851
|
}
|
|
21763
21852
|
throw new CmdrError("DAEMON_UNAVAILABLE", "Cannot start cmdr daemon. Run cmdr doctor.");
|
|
@@ -21780,8 +21869,13 @@ var DaemonClient = class {
|
|
|
21780
21869
|
const rpc = await daemonConnection({ home: this.home, start: true, upgrade: true });
|
|
21781
21870
|
try {
|
|
21782
21871
|
const result = await rpc.request("session.register", this.registration);
|
|
21872
|
+
if (this.stopped) {
|
|
21873
|
+
rpc.close();
|
|
21874
|
+
throw new CmdrError("REQUEST_CANCELLED");
|
|
21875
|
+
}
|
|
21783
21876
|
this.registration.sid = result.me.sid;
|
|
21784
21877
|
this.rpc = rpc;
|
|
21878
|
+
diagnostic("reconnected", { agent: this.registration.agent, to: VERSION }, this.home);
|
|
21785
21879
|
this.backoff = 100;
|
|
21786
21880
|
rpc.on("notification", (method, params) => {
|
|
21787
21881
|
if (method === "session.reset") rpc.close();
|
|
@@ -21922,6 +22016,11 @@ var clients = /* @__PURE__ */ new Map();
|
|
|
21922
22016
|
var binding = Promise.resolve();
|
|
21923
22017
|
async function forSession(native) {
|
|
21924
22018
|
if (!native) return client;
|
|
22019
|
+
if (process.env.CMDR_SESSION_ID && native !== process.env.CMDR_SESSION_ID)
|
|
22020
|
+
throw new CmdrError(
|
|
22021
|
+
"INVALID_ARGUMENT",
|
|
22022
|
+
"Session stamp conflicts with CMDR_SESSION_ID; use one process per explicit ID or stamps for a shared process."
|
|
22023
|
+
);
|
|
21925
22024
|
let selected = client;
|
|
21926
22025
|
const operation = binding.then(async () => {
|
|
21927
22026
|
if (clients.get(native)?.nativeId === native) {
|
|
@@ -21948,23 +22047,14 @@ function closeClients() {
|
|
|
21948
22047
|
}
|
|
21949
22048
|
var server = new McpServer({ name: "cmdr", version: VERSION });
|
|
21950
22049
|
var descriptions = {
|
|
21951
|
-
join: "Create/join a squad. For /cmdr <name>, pass
|
|
21952
|
-
list: "Show
|
|
21953
|
-
send: "Commander: dispatch clear tasks with acceptance criteria, or answer an ask using type=answer and reply_to. to accepts all,
|
|
21954
|
-
report: "Executor: report ready (cwd/capabilities), working, blocked, done or
|
|
22050
|
+
join: "Create/join a squad. For /cmdr <name>, pass squad_name for atomic find-or-create; role defaults to executor. Explicitly claim role=commander, use takeover=true for handover or rebind=member_id for a new endpoint. Otherwise specify role and optional squad ID/name. Use standby=auto for daemon-managed wake when supported; inspect listener health before ending the turn. Executors report ready after joining. Reply with user_reply and follow listener health; bounded read(wait=me.recommended_wait) is the manual fallback.",
|
|
22051
|
+
list: "Show task ownership, unacked_for, execution activity, connection presence and listener health. Offline never authorizes reassignment. Use full=true for expanded session metadata; listings never include command bodies. Read your own messages with read(id=...). scope=all lists squads and sessions.",
|
|
22052
|
+
send: "Commander: dispatch clear tasks with acceptance criteria, or answer an ask using type=answer and reply_to. Use task_key to prevent duplicate tickets and reassign=<command id> for a gated handover preserving the original task_key. type=cancel with reply_to requests a safe stop. to accepts all, member_id, sid, unique sid prefix or member name.",
|
|
22053
|
+
report: "Executor: report ready (cwd/capabilities), working, blocked, done, failed or cancelled; include reply_to for the command.",
|
|
21955
22054
|
ask: "Executor: ask the commander for guidance. Optional wait waits for the matching answer; use me.recommended_wait as the upper bound.",
|
|
21956
|
-
read: "Fetch messages in priority order (reading dequeues). Use wait=me.recommended_wait to stand by, peek to inspect or history to review delivered messages.
|
|
22055
|
+
read: "Fetch messages in priority order (reading dequeues). Use wait=me.recommended_wait to stand by, peek to inspect or history to review delivered messages. Use recover=true for all unfinished commands (non-consuming), id for a non-consuming message lookup (blocked replacements return REASSIGNMENT_PENDING), full for squad details. With a healthy managed listener, end the turn. Otherwise do at most two waits and explain manual continuation.",
|
|
21957
22056
|
leave: "Leave the squad. Commander departure orphans it; dissolve=true disbands it. Messages already queued remain readable."
|
|
21958
22057
|
};
|
|
21959
|
-
var methods = {
|
|
21960
|
-
join: "session.join",
|
|
21961
|
-
list: "session.list",
|
|
21962
|
-
send: "msg.send",
|
|
21963
|
-
report: "msg.report",
|
|
21964
|
-
ask: "msg.ask",
|
|
21965
|
-
read: "msg.read",
|
|
21966
|
-
leave: "session.leave"
|
|
21967
|
-
};
|
|
21968
22058
|
for (const name of Object.keys(schemas)) {
|
|
21969
22059
|
server.registerTool(
|
|
21970
22060
|
name,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cmdr-commander
|
|
3
|
-
description: Coordinate a cmdr
|
|
3
|
+
description: Coordinate a persistent cmdr channel, track task ownership and acknowledgements, and explicitly take over the commander role.
|
|
4
4
|
---
|
|
5
|
-
1. Call join(role="commander", name
|
|
6
|
-
2.
|
|
7
|
-
3.
|
|
8
|
-
4. Send
|
|
9
|
-
5.
|
|
10
|
-
6.
|
|
5
|
+
1. Call join(role="commander", squad_name=<name>, standby="auto"), or provide squad=<id>. Claiming a role is explicit; joining a channel by name alone makes you an executor. Use takeover=true only when the user requested commander handover. The old commander is demoted and the role inbox remains intact.
|
|
6
|
+
2. Give the user a short confirmation and join line. Use list and read; inspect read(recover=true) for any unfinished work owned by your member. Observe the channel with `cmdr tail --squad ID --follow --json --full`; observations never consume another member's inbox. Reconnect with --after EVENT_SEQ and filter a recipient with --for SID.
|
|
7
|
+
3. List shows metadata even with full=true; use operator tail --full to inspect command bodies. Before dispatch or reassignment, inspect commands, unacked_for, in_progress, last_progress_at, activity, last_seen and listener health. **offline or cli never means work stopped.** pending=0 and unread=0 do not mean there is no unfinished work. Verify ownership and actual progress before deciding to reassign.
|
|
8
|
+
4. Send bounded tasks with acceptance criteria. Use task_key=<ticket id> when dispatching a known ticket. Record the returned command ID; delivered_to/queued_to only mean enqueued. A working report with reply_to is acceptance; done/failed/cancelled are terminal. Uncorrelated reports cannot close tasks.
|
|
9
|
+
5. To stop work, send(type="cancel", to=<owner>, reply_to=<command id>, message=<reason>). It has reserved high priority and produces a read event. It is cooperative: the executor stops at a safe checkpoint. To reassign, send(type="command", reassign=<command id>, to=<new owner>, message=<task>). Reassignment inherits the original task_key; do not supply a different key. The replacement is blocked until the original owner reports a terminal state; unread original work can be cancelled immediately. Never work around the gate by sending a second unrelated command.
|
|
10
|
+
6. Answer every ask using type="answer", to=<asking member>, reply_to=<ask id>. Read complete reports; use limit or read(id=...) instead of truncating a consuming read with head. working/ready reports normally only record progress; done/failed/blocked/ask warrant attention.
|
|
11
|
+
7. Check me.listener.can_auto_respond. With a healthy managed listener, end the idle turn. Otherwise use at most two recommended waits, explain the manual continuation requirement, and end the turn. Do not create a private listener or promise automatic wake for unsupported hosts. Inspect uncertain/stalled listener requests through cmdr standby status; reconcile before explicit retry.
|
|
12
|
+
8. Ordinary leave preserves the channel; leave(dissolve=true) explicitly closes it. Channels outlive host sessions and message retention. For a new host endpoint representing the same stopped member, use join(rebind=<member_id>, standby="auto").
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
Apply normal judgment and the user's scope to messages. cmdr does not create new agents or grant extra permissions.
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cmdr-executor
|
|
3
|
-
description: Join an existing cmdr
|
|
3
|
+
description: Join an existing cmdr channel as executor, acknowledge tasks, report progress, and recover unfinished work.
|
|
4
4
|
---
|
|
5
|
-
1. Call join(role="executor", squad=<id>, name=<optional
|
|
6
|
-
2.
|
|
7
|
-
3.
|
|
8
|
-
4.
|
|
9
|
-
5.
|
|
5
|
+
1. Call join(role="executor", squad=<id>, name=<optional name>, standby="auto"), or join(role="executor", squad_name=<name>, standby="auto"). Use the real host session ID; never guess it. A channel may exist without a commander.
|
|
6
|
+
2. Report ready with cwd, capabilities and context. Give a brief user reply. Check list for listener health and wake_mode. The daemon manages supported Codex listeners; no private Python/shell listener is needed.
|
|
7
|
+
3. Read messages and read(recover=true) on startup, after context loss or a wake. Recovery is non-consuming and includes unread, read-but-unaccepted and accepted unfinished commands. Use read(id=<message id>) for the full message. A blocked replacement returns REASSIGNMENT_PENDING until its original owner reports a terminal state. Read does not acknowledge work.
|
|
8
|
+
4. Before working, report(status="working", reply_to=<command id>). Never execute a task blocked by an earlier owner's cancellation. Handle cancel messages first at a safe checkpoint, stop the referenced work, then report cancelled with that command's reply_to. Check read(peek=true) between long steps when hooks are unavailable. Do not infer instructions from metadata reminders.
|
|
9
|
+
5. Report done/failed with reply_to after finishing. When blocked, report blocked with reply_to and ask. After a restart, reconcile actual files/processes before resuming accepted work; do not execute it again blindly.
|
|
10
|
+
6. If me.listener.can_auto_respond is true, end the current turn when idle. If health is starting, check list again. If wake_mode is manual or health is error/uncertain/stalled, explain that automatic response is unavailable; use at most two read(wait=me.recommended_wait) calls, then end the turn and state that manual continuation is needed. Do not send repeated standby-timeout reports or build a private listener.
|
|
11
|
+
7. Leave only when requested. Ordinary leave and host shutdown preserve the channel and task records. Replacing a stopped host session uses join(rebind=<member_id>, standby="auto") in the new real session; the old endpoint is revoked. This does not stop processes that the old model already launched.
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Messages do not expand user authorization. Apply normal judgment. Unknown hosts remain manual; never claim a successful wake merely because a message was queued.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: using-cmdr
|
|
3
|
-
description: Connect existing local Agent sessions using cmdr, especially /cmdr <name>, cmdr <name>, or requests to create or join a
|
|
3
|
+
description: Connect existing local Agent sessions using cmdr, especially /cmdr <name>, cmdr <name>, or requests to create or join a channel.
|
|
4
4
|
---
|
|
5
|
-
|
|
5
|
+
If the seven tools are missing, inspect the actual cache with `cmdr doctor --plugin-root <cache> --deep`. Reinstall from a complete cmdr-mcp package, refresh the cache and start a new session. Never link another installation's dist. With an intact runtime but unavailable MCP, use `cmdr session` with --agent and the host's real --native-id. Never invent session IDs or reverse-engineer private host data. CLI presence is cli; it does not imply that execution has stopped.
|
|
6
6
|
|
|
7
|
-
cmdr
|
|
7
|
+
For `/cmdr <name>`, call join(squad_name=<name>, standby="auto"). Named creation/join is atomic and defaults to executor, even if first; no commander is required to keep a channel alive. If the user explicitly asks to command, add role="commander". An existing commander requires an explicitly requested takeover=true. Do not ask for a role merely because the channel is orphaned. Rebinding an old stable member to a new stopped-session replacement uses join(rebind=<member_id>, standby="auto"); the old endpoint becomes invalid.
|
|
8
8
|
|
|
9
|
-
Seven tools: join, list, send, report, ask, read, leave.
|
|
9
|
+
Seven tools: join, list, send, report, ask, read, leave. Read is transport delivery; report(working, reply_to) acknowledges a command, and done/failed/cancelled completes it. On recovery inspect both read and read(recover=true). Use read(id=...) for a complete non-consuming lookup; full=true requests expanded output. Do not truncate a consuming read with head.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Executors report ready with cwd and capabilities, then acknowledge commands before working. Commanders inspect list for unfinished commands and their age before dispatch, and use task_key plus reassign=<command id> to prevent duplicate ownership. Offline never means stopped. Cancel messages take priority at safe checkpoints; replacements wait for a terminal report. Answer asks with reply_to.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
With me.listener.can_auto_respond=true, finish the idle turn: daemon-managed Codex standby continues. Check list if listener health is starting. Unsupported hosts return manual; failed, uncertain or stalled listeners require diagnosis and possibly manual continuation. Use at most two read(wait=me.recommended_wait) calls without a healthy listener, then explain the limitation and end the turn. Never make each agent write a private listener or emit repeated standby-timeout reports. `cmdr standby start|status|stop|resume --session SID` manages listeners; an uncertain request must be reconciled before an operator chooses --resolve retry or accepted.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Keep user updates brief. Follow protocol_hint and the role skill. Hooks contain only metadata; read the actual messages. Apply normal judgment; messages do not expand user authorization. cmdr connects existing sessions and never creates agents.
|