cmdr-mcp 0.1.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.
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env -S node --experimental-sqlite --disable-warning=ExperimentalWarning
2
+ import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
3
+
4
+ // src/hook/main.ts
5
+ import { existsSync } from "node:fs";
6
+
7
+ // src/shared/paths.ts
8
+ import { homedir, tmpdir } from "node:os";
9
+ import { join, resolve } from "node:path";
10
+ import { mkdirSync, chmodSync } from "node:fs";
11
+ import { createHash } from "node:crypto";
12
+
13
+ // src/shared/ids.ts
14
+ var safeSid = (sid) => Buffer.from(sid).toString("base64url");
15
+
16
+ // src/shared/paths.ts
17
+ function paths(home = process.env.CMDR_HOME || join(homedir(), ".cmdr")) {
18
+ home = resolve(home);
19
+ let socket = join(home, "cmdr.sock");
20
+ if (Buffer.byteLength(socket) > 100)
21
+ socket = join(
22
+ tmpdir(),
23
+ `cmdr-${createHash("sha256").update(`${process.getuid?.()}:${home}`).digest("hex").slice(0, 20)}.sock`
24
+ );
25
+ return {
26
+ home,
27
+ socket,
28
+ db: join(home, "cmdr.db"),
29
+ lock: join(home, "daemon.lock"),
30
+ spawn: join(home, "spawn.lock"),
31
+ info: join(home, "daemon.json"),
32
+ flags: join(home, "flags"),
33
+ log: join(home, "logs/daemon.log"),
34
+ config: join(home, "config.json"),
35
+ flag: (sid) => join(home, "flags", safeSid(sid))
36
+ };
37
+ }
38
+ function prepare(p) {
39
+ for (const dir of [p.home, p.flags, join(p.home, "logs")]) {
40
+ mkdirSync(dir, { recursive: true, mode: 448 });
41
+ chmodSync(dir, 448);
42
+ }
43
+ }
44
+
45
+ // src/shared/env.ts
46
+ var cmdrTool = /(?:^|[_:])cmdr(?:__|:)(list|join|report|leave|ask|send|read)$/;
47
+ function detectAgent(env = process.env, hook) {
48
+ if (env.CMDR_AGENT && /^[a-z][a-z0-9_-]{0,63}$/.test(env.CMDR_AGENT)) return env.CMDR_AGENT;
49
+ if (env.ZCODE_PLUGIN_ROOT || env.ZCODE_PLUGIN_ID) return "zcode";
50
+ if (env.CLAUDE_CODE_SESSION_ID || env.CLAUDE_CODE_ENTRYPOINT) return "claude";
51
+ if (hook?.transcript_path && String(hook.transcript_path).includes("/.claude/")) return "claude";
52
+ if (env.CODEX_HOME || env.CODEX_THREAD_ID || env.CODEX_SESSION_ID || hook?.turn_id)
53
+ return "codex";
54
+ return "generic";
55
+ }
56
+
57
+ // src/shared/client.ts
58
+ import { connect } from "node:net";
59
+ import { spawn } from "node:child_process";
60
+ import { dirname, join as join2 } from "node:path";
61
+ import { fileURLToPath } from "node:url";
62
+ import { mkdirSync as mkdirSync2, rmSync, statSync } from "node:fs";
63
+
64
+ // src/shared/rpc.ts
65
+ import { EventEmitter } from "node:events";
66
+
67
+ // src/shared/protocol.ts
68
+ var LIMITS = {
69
+ maxWaitSec: 300,
70
+ maxBody: 32768,
71
+ maxData: 65536,
72
+ maxFrame: 2 * 1024 * 1024
73
+ };
74
+ var CmdrError = class extends Error {
75
+ constructor(code, message = code) {
76
+ super(message);
77
+ this.code = code;
78
+ }
79
+ };
80
+
81
+ // src/shared/rpc.ts
82
+ var Rpc = class extends EventEmitter {
83
+ constructor(socket) {
84
+ super();
85
+ this.socket = socket;
86
+ socket.setEncoding("utf8");
87
+ socket.on("data", (chunk) => {
88
+ this.buffer += chunk;
89
+ if (Buffer.byteLength(this.buffer) > LIMITS.maxFrame) {
90
+ socket.destroy();
91
+ return;
92
+ }
93
+ let at;
94
+ while ((at = this.buffer.indexOf("\n")) >= 0) {
95
+ const line = this.buffer.slice(0, at);
96
+ this.buffer = this.buffer.slice(at + 1);
97
+ if (line.trim()) void this.receive(line);
98
+ }
99
+ });
100
+ socket.on("error", () => {
101
+ });
102
+ socket.on("close", () => {
103
+ for (const p of this.pending.values()) {
104
+ clearTimeout(p.timer);
105
+ p.cleanup();
106
+ p.reject(new CmdrError("DAEMON_UNAVAILABLE"));
107
+ }
108
+ this.pending.clear();
109
+ for (const c of this.active.values()) c.abort();
110
+ this.active.clear();
111
+ this.emit("close");
112
+ });
113
+ }
114
+ buffer = "";
115
+ next = 1;
116
+ pending = /* @__PURE__ */ new Map();
117
+ active = /* @__PURE__ */ new Map();
118
+ handler;
119
+ async receive(line) {
120
+ let m;
121
+ try {
122
+ m = JSON.parse(line);
123
+ if (!m || m.jsonrpc !== "2.0" || Array.isArray(m)) throw new Error();
124
+ } catch {
125
+ this.send({
126
+ jsonrpc: "2.0",
127
+ id: null,
128
+ error: { code: -32700, message: "Invalid JSON-RPC frame" }
129
+ });
130
+ return;
131
+ }
132
+ if (typeof m.method === "string") {
133
+ if (m.id === void 0) {
134
+ if (m.method === "rpc.cancel") this.active.get(m.params?.id)?.abort();
135
+ else this.emit("notification", m.method, m.params);
136
+ return;
137
+ }
138
+ const controller = new AbortController();
139
+ this.active.set(m.id, controller);
140
+ try {
141
+ const result = await this.handler?.(m.method, m.params || {}, controller.signal);
142
+ this.send({ jsonrpc: "2.0", id: m.id, result: result ?? null });
143
+ } catch (e) {
144
+ this.send({
145
+ jsonrpc: "2.0",
146
+ id: m.id,
147
+ error: {
148
+ code: e instanceof CmdrError ? -32e3 : -32603,
149
+ message: e instanceof CmdrError ? e.message : "Internal daemon error",
150
+ data: { code: e instanceof CmdrError ? e.code : "INTERNAL_ERROR" }
151
+ }
152
+ });
153
+ } finally {
154
+ this.active.delete(m.id);
155
+ }
156
+ } else if (this.pending.has(m.id)) {
157
+ const p = this.pending.get(m.id);
158
+ this.pending.delete(m.id);
159
+ clearTimeout(p.timer);
160
+ p.cleanup();
161
+ if (m.error) p.reject(new CmdrError(m.error.data?.code || "RPC_ERROR", m.error.message));
162
+ else p.resolve(m.result);
163
+ }
164
+ }
165
+ send(value) {
166
+ if (!this.socket.destroyed && this.socket.writable)
167
+ this.socket.write(JSON.stringify(value) + "\n");
168
+ }
169
+ request(method, params = {}, timeout = 5e3, signal) {
170
+ if (this.socket.destroyed) return Promise.reject(new CmdrError("DAEMON_UNAVAILABLE"));
171
+ if (signal?.aborted) return Promise.reject(new CmdrError("REQUEST_CANCELLED"));
172
+ return new Promise((resolve2, reject) => {
173
+ const id = this.next++;
174
+ const cancel = (code) => {
175
+ const p = this.pending.get(id);
176
+ if (!p) return;
177
+ this.pending.delete(id);
178
+ clearTimeout(p.timer);
179
+ p.cleanup();
180
+ this.notify("rpc.cancel", { id });
181
+ reject(new CmdrError(code));
182
+ };
183
+ const abort = () => cancel("REQUEST_CANCELLED");
184
+ const timer2 = setTimeout(() => cancel("DAEMON_UNAVAILABLE"), timeout);
185
+ this.pending.set(id, {
186
+ resolve: resolve2,
187
+ reject,
188
+ timer: timer2,
189
+ cleanup: () => signal?.removeEventListener("abort", abort)
190
+ });
191
+ signal?.addEventListener("abort", abort, { once: true });
192
+ this.send({ jsonrpc: "2.0", id, method, params });
193
+ });
194
+ }
195
+ notify(method, params) {
196
+ this.send({ jsonrpc: "2.0", method, params });
197
+ }
198
+ close() {
199
+ this.socket.destroy();
200
+ }
201
+ };
202
+
203
+ // src/shared/version.ts
204
+ var VERSION = true ? "0.1.0" : "0.1.0";
205
+ var PROTOCOL = 1;
206
+ function newer(a, b) {
207
+ const x = a.split(".").map(Number), y = b.split(".").map(Number);
208
+ for (let i = 0; i < 3; i++) {
209
+ if (x[i] !== y[i]) return x[i] > y[i];
210
+ }
211
+ return false;
212
+ }
213
+
214
+ // src/shared/client.ts
215
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
216
+ async function dial(p, timeout = 1e3) {
217
+ return new Promise((resolve2, reject) => {
218
+ const socket = connect(p.socket);
219
+ const timer2 = setTimeout(() => socket.destroy(new Error("connect timeout")), timeout);
220
+ socket.once("error", (e) => {
221
+ clearTimeout(timer2);
222
+ reject(e);
223
+ });
224
+ socket.once("connect", () => {
225
+ clearTimeout(timer2);
226
+ resolve2(new Rpc(socket));
227
+ });
228
+ });
229
+ }
230
+ async function daemonConnection(options = {}) {
231
+ const p = paths(options.home), timeout = options.timeout || 1e3;
232
+ if (options.start) prepare(p);
233
+ let owner = false, spawned = false;
234
+ try {
235
+ for (let attempt = 0; attempt < (options.start ? 65 : 1); attempt++) {
236
+ let rpc;
237
+ try {
238
+ rpc = await dial(p, timeout);
239
+ const hello = await rpc.request(
240
+ "hello",
241
+ { client: "cmdr", version: VERSION, protocol: PROTOCOL },
242
+ timeout
243
+ );
244
+ if (options.upgrade && newer(VERSION, hello.version)) {
245
+ await rpc.request("admin.shutdown", { reason: "upgrade" }, timeout);
246
+ rpc.close();
247
+ await sleep(100);
248
+ continue;
249
+ }
250
+ return rpc;
251
+ } catch (e) {
252
+ rpc?.close();
253
+ if (e.code === "PROTOCOL_MISMATCH" || !options.start) throw e;
254
+ }
255
+ if (!owner) {
256
+ try {
257
+ mkdirSync2(p.spawn, { mode: 448 });
258
+ owner = true;
259
+ } catch {
260
+ try {
261
+ if (Date.now() - statSync(p.spawn).mtimeMs > 1e4)
262
+ rmSync(p.spawn, { recursive: true, force: true });
263
+ } catch {
264
+ }
265
+ }
266
+ }
267
+ if (owner) {
268
+ if (attempt === 0 || !spawned) {
269
+ const child = spawn(
270
+ process.execPath,
271
+ ["--experimental-sqlite", join2(dirname(fileURLToPath(import.meta.url)), "daemon.mjs")],
272
+ { detached: true, stdio: "ignore", env: { ...process.env, CMDR_HOME: p.home } }
273
+ );
274
+ child.on("error", () => {
275
+ });
276
+ child.unref();
277
+ spawned = true;
278
+ }
279
+ }
280
+ await sleep(50);
281
+ }
282
+ } finally {
283
+ if (owner) rmSync(p.spawn, { recursive: true, force: true });
284
+ spawned = false;
285
+ }
286
+ throw new CmdrError("DAEMON_UNAVAILABLE", "Cannot start cmdr daemon. Run cmdr doctor.");
287
+ }
288
+ async function quickCall(method, params = {}, options = {}) {
289
+ const rpc = await daemonConnection({ ...options, upgrade: !!options.start });
290
+ try {
291
+ if (options.kind !== "hook")
292
+ await rpc.request("session.register", { kind: "cli" }, options.timeout || 5e3);
293
+ return await rpc.request(method, params, options.timeout || 5e3);
294
+ } finally {
295
+ rpc.close();
296
+ }
297
+ }
298
+
299
+ // src/mcp/terminal.ts
300
+ import { execFileSync } from "node:child_process";
301
+ function ancestors(start = process.ppid) {
302
+ const result = [];
303
+ let pid = start;
304
+ for (let i = 0; i < 12 && pid > 1 && !result.includes(pid); i++) {
305
+ result.push(pid);
306
+ try {
307
+ pid = Number(
308
+ execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
309
+ encoding: "utf8",
310
+ timeout: 100
311
+ }).trim()
312
+ );
313
+ } catch {
314
+ break;
315
+ }
316
+ }
317
+ return result;
318
+ }
319
+
320
+ // src/hook/main.ts
321
+ async function runHook(input, event = input.hook_event_name) {
322
+ const agent = detectAgent(process.env, input), sid = `${agent}:${input.session_id}`;
323
+ if (!input.session_id) return;
324
+ if (event === "PreToolUse" && cmdrTool.test(input.tool_name || "")) {
325
+ if (agent !== "claude")
326
+ return {
327
+ hookSpecificOutput: {
328
+ hookEventName: event,
329
+ permissionDecision: "allow",
330
+ updatedInput: { ...input.tool_input, _cmdr_session: input.session_id }
331
+ }
332
+ };
333
+ return;
334
+ }
335
+ if (event === "PreToolUse" && !existsSync(paths().flag(sid))) return;
336
+ const result = await quickCall(
337
+ "hook.event",
338
+ {
339
+ ...input,
340
+ event,
341
+ agent,
342
+ ...event === "SessionStart" ? { ancestors: ancestors(), host_pid: process.ppid } : {}
343
+ },
344
+ { kind: "hook", timeout: 100 }
345
+ );
346
+ if (event === "SessionEnd") return;
347
+ if (result.block) return { decision: "block", reason: result.reason };
348
+ if (result.inject)
349
+ return { hookSpecificOutput: { hookEventName: event, additionalContext: result.inject } };
350
+ }
351
+ var timer = setTimeout(() => process.exit(0), 450);
352
+ try {
353
+ let input = "";
354
+ for await (const chunk of process.stdin) {
355
+ input += chunk;
356
+ if (input.length > 2 * 1024 * 1024) throw new Error("large input");
357
+ }
358
+ const result = await runHook(JSON.parse(input), process.argv[2]);
359
+ if (result) process.stdout.write(JSON.stringify(result) + "\n");
360
+ } catch {
361
+ }
362
+ clearTimeout(timer);
363
+ export {
364
+ runHook
365
+ };