mastermind-md 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.
Files changed (52) hide show
  1. package/.claude/skills/master/SKILL.md +10 -0
  2. package/.claude/skills/mastermind/SKILL.md +61 -0
  3. package/.claude/skills/mastermind/reference/demo.md +35 -0
  4. package/LICENSE +21 -0
  5. package/README.md +135 -0
  6. package/assets/agent/global.md +12 -0
  7. package/bin/mastermind.js +17 -0
  8. package/dist/cli/index.js +1284 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/server/index.js +1752 -0
  11. package/dist/server/index.js.map +1 -0
  12. package/dist/ui/assets/FindBar-CSKdPrxm.js +1 -0
  13. package/dist/ui/assets/RenameDialog-C6yP_6D8.js +1 -0
  14. package/dist/ui/assets/SettingsPanel-C6-wwvJr.js +1 -0
  15. package/dist/ui/assets/SourceEditor-DN44HaIZ.js +37 -0
  16. package/dist/ui/assets/TranslatedView-G29rKesM.js +1 -0
  17. package/dist/ui/assets/index-Bhf9eUP2.css +1 -0
  18. package/dist/ui/assets/index-cMAuoyhV.js +99 -0
  19. package/dist/ui/assets/jsx-runtime-BrnrUjgG.js +1 -0
  20. package/dist/ui/assets/react-DoK4WR2u.js +1 -0
  21. package/dist/ui/index.html +16 -0
  22. package/package.json +91 -0
  23. package/themes/carbon/theme.json +11 -0
  24. package/themes/carbon/tokens.css +67 -0
  25. package/themes/cobalt/theme.json +11 -0
  26. package/themes/cobalt/tokens.css +67 -0
  27. package/themes/fonts/BricolageGrotesque-Variable.woff2 +0 -0
  28. package/themes/fonts/CrimsonPro-Variable.woff2 +0 -0
  29. package/themes/fonts/Fraunces-Variable.woff2 +0 -0
  30. package/themes/fonts/GeistSans-Variable.woff2 +0 -0
  31. package/themes/fonts/HankenGrotesk-Variable.woff2 +0 -0
  32. package/themes/fonts/Inter-Variable.woff2 +0 -0
  33. package/themes/fonts/JetBrainsMono-Variable.woff2 +0 -0
  34. package/themes/fonts/Lora-Variable.woff2 +0 -0
  35. package/themes/fonts/Manrope-Variable.woff2 +0 -0
  36. package/themes/fonts/Newsreader-Variable.woff2 +0 -0
  37. package/themes/fonts/Outfit-Variable.woff2 +0 -0
  38. package/themes/fonts/SpaceGrotesk-Variable.woff2 +0 -0
  39. package/themes/fonts/SplineSansMono-Variable.woff2 +0 -0
  40. package/themes/fonts/UbuntuSansMono-Variable.woff2 +0 -0
  41. package/themes/grid/fonts/GeistMono-Variable.woff2 +0 -0
  42. package/themes/grid/fonts/SchibstedGrotesk-Variable.woff2 +0 -0
  43. package/themes/grid/theme.json +11 -0
  44. package/themes/grid/tokens.css +67 -0
  45. package/themes/nacht/theme.json +11 -0
  46. package/themes/nacht/tokens.css +67 -0
  47. package/themes/rose/theme.json +11 -0
  48. package/themes/rose/tokens.css +67 -0
  49. package/themes/sepia/theme.json +11 -0
  50. package/themes/sepia/tokens.css +67 -0
  51. package/themes/slate/theme.json +11 -0
  52. package/themes/slate/tokens.css +67 -0
@@ -0,0 +1,1284 @@
1
+ // src/cli/index.ts
2
+ import { Command } from "commander";
3
+ import { spawn as spawn2 } from "child_process";
4
+ import fs6 from "fs";
5
+ import path5 from "path";
6
+ import os2 from "os";
7
+
8
+ // package.json
9
+ var package_default = {
10
+ name: "mastermind-md",
11
+ version: "0.1.0",
12
+ type: "module",
13
+ description: "Review Markdown with your coding agent \u2014 local-first, CriticMarkup review loop, bilingual, the file is the protocol",
14
+ license: "MIT",
15
+ author: "Kevin Ding <kevincentding@gmail.com>",
16
+ homepage: "https://github.com/Jingquank/Mastermind#readme",
17
+ repository: {
18
+ type: "git",
19
+ url: "git+https://github.com/Jingquank/Mastermind.git"
20
+ },
21
+ bugs: {
22
+ url: "https://github.com/Jingquank/Mastermind/issues"
23
+ },
24
+ keywords: [
25
+ "markdown",
26
+ "review",
27
+ "criticmarkup",
28
+ "ai-agents",
29
+ "claude",
30
+ "cli",
31
+ "bilingual",
32
+ "local-first"
33
+ ],
34
+ engines: {
35
+ node: ">=20"
36
+ },
37
+ bin: {
38
+ mastermind: "bin/mastermind.js",
39
+ "mastermind-md": "bin/mastermind.js"
40
+ },
41
+ files: [
42
+ "dist/",
43
+ "bin/",
44
+ "themes/",
45
+ ".claude/skills/",
46
+ "assets/agent/",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ scripts: {
51
+ build: "rm -rf dist && vite build && tsup",
52
+ dev: 'concurrently -k "MASTERMIND_PORT=5199 tsx watch src/server/index.ts" "vite"',
53
+ test: "vitest run",
54
+ "test:watch": "vitest",
55
+ typecheck: "tsc -p tsconfig.json && tsc -p tsconfig.node.json",
56
+ prepare: "npm run build",
57
+ prepack: "npm run build"
58
+ },
59
+ dependencies: {
60
+ "@codemirror/lang-markdown": "^6.5.0",
61
+ "@codemirror/language": "^6.12.3",
62
+ "@codemirror/state": "^6.6.0",
63
+ "@codemirror/view": "^6.43.1",
64
+ "@hono/node-server": "^2.0.4",
65
+ "@lezer/highlight": "^1.2.3",
66
+ "@radix-ui/react-icons": "^1.3.2",
67
+ chokidar: "^5.0.0",
68
+ codemirror: "^6.0.2",
69
+ commander: "^15.0.0",
70
+ diff: "^9.0.0",
71
+ hono: "^4.12.25",
72
+ "mdast-util-to-markdown": "^2.1.2",
73
+ "radix-ui": "^1.5.0",
74
+ react: "^19.2.7",
75
+ "react-dom": "^19.2.7",
76
+ "remark-gfm": "^4.0.1",
77
+ "remark-parse": "^11.0.0",
78
+ "remark-stringify": "^11.0.0",
79
+ "slot-text": "^0.2.2",
80
+ unified: "^11.0.5",
81
+ "unist-util-visit": "^5.1.0",
82
+ zustand: "^5.0.14"
83
+ },
84
+ devDependencies: {
85
+ "@types/mdast": "^4.0.4",
86
+ "@types/node": "^25.9.3",
87
+ "@types/react": "^19.2.17",
88
+ "@types/react-dom": "^19.2.3",
89
+ "@types/unist": "^3.0.3",
90
+ "@vitejs/plugin-react": "^6.0.2",
91
+ concurrently: "^10.0.3",
92
+ jsdom: "^29.1.1",
93
+ tsup: "^8.5.1",
94
+ tsx: "^4.22.4",
95
+ typescript: "^6.0.3",
96
+ vite: "^8.0.16",
97
+ vitest: "^4.1.8"
98
+ }
99
+ };
100
+
101
+ // src/server/paths.ts
102
+ import { fileURLToPath } from "url";
103
+ import path from "path";
104
+ import os from "os";
105
+ import fs from "fs";
106
+ var pkgRoot = fileURLToPath(new URL("../../", import.meta.url));
107
+ var uiDir = path.join(pkgRoot, "dist", "ui");
108
+ var themesDir = path.join(pkgRoot, "themes");
109
+ function configDir() {
110
+ return process.env.MASTERMIND_CONFIG_DIR ?? path.join(os.homedir(), ".config", "mastermind");
111
+ }
112
+ function ensureConfigDir() {
113
+ const dir = configDir();
114
+ fs.mkdirSync(dir, { recursive: true });
115
+ return dir;
116
+ }
117
+ function stateFilePath() {
118
+ return path.join(configDir(), "server.json");
119
+ }
120
+ function serverLogPath() {
121
+ return path.join(configDir(), "server.log");
122
+ }
123
+ function configFilePath() {
124
+ return path.join(configDir(), "config.json");
125
+ }
126
+
127
+ // src/server/statefile.ts
128
+ import fs2 from "fs";
129
+ import path2 from "path";
130
+ function readServerState() {
131
+ try {
132
+ const raw = fs2.readFileSync(stateFilePath(), "utf8");
133
+ const parsed = JSON.parse(raw);
134
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.port === "number" && typeof parsed.pid === "number" && typeof parsed.version === "string" && typeof parsed.startedAt === "number") {
135
+ return parsed;
136
+ }
137
+ return null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+ function clearServerState(onlyIfPid) {
143
+ if (onlyIfPid !== void 0) {
144
+ const current = readServerState();
145
+ if (current && current.pid !== onlyIfPid) return;
146
+ }
147
+ try {
148
+ fs2.unlinkSync(stateFilePath());
149
+ } catch {
150
+ }
151
+ }
152
+ function pidIsAlive(pid) {
153
+ try {
154
+ process.kill(pid, 0);
155
+ return true;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ // src/cli/daemon.ts
162
+ import { spawn } from "child_process";
163
+ import fs3 from "fs";
164
+ import { fileURLToPath as fileURLToPath2 } from "url";
165
+
166
+ // src/cli/http.ts
167
+ function isConnRefused(err) {
168
+ const cause = err.cause;
169
+ if (!cause) return false;
170
+ const code = cause.code;
171
+ if (code === "ECONNREFUSED") return true;
172
+ const errors = cause.errors;
173
+ return Array.isArray(errors) && errors.some((e) => e.code === "ECONNREFUSED");
174
+ }
175
+ async function probeHealth(port) {
176
+ try {
177
+ const res = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(1500) });
178
+ if (!res.ok) return "foreign";
179
+ const j = await res.json();
180
+ if (j.ok === true && typeof j.pid === "number" && typeof j.version === "string") {
181
+ return j;
182
+ }
183
+ return "foreign";
184
+ } catch (err) {
185
+ return isConnRefused(err) ? "free" : "foreign";
186
+ }
187
+ }
188
+ async function postJson(port, pathName, body) {
189
+ const res = await fetch(`http://127.0.0.1:${port}${pathName}`, {
190
+ method: "POST",
191
+ headers: { "content-type": "application/json" },
192
+ body: JSON.stringify(body),
193
+ signal: AbortSignal.timeout(5e3)
194
+ });
195
+ if (!res.ok) {
196
+ let detail = `${res.status}`;
197
+ try {
198
+ const j = await res.json();
199
+ if (j.error) detail = j.error;
200
+ } catch {
201
+ }
202
+ throw new Error(detail);
203
+ }
204
+ return await res.json();
205
+ }
206
+ async function postNoContent(port, pathName, body) {
207
+ const res = await fetch(`http://127.0.0.1:${port}${pathName}`, {
208
+ method: "POST",
209
+ headers: { "content-type": "application/json" },
210
+ body: JSON.stringify(body),
211
+ signal: AbortSignal.timeout(5e3)
212
+ });
213
+ if (!res.ok) {
214
+ let detail = `${res.status}`;
215
+ try {
216
+ const j = await res.json();
217
+ if (j.error) detail = j.error;
218
+ } catch {
219
+ }
220
+ throw new Error(detail);
221
+ }
222
+ }
223
+ async function getJson(port, pathName) {
224
+ const res = await fetch(`http://127.0.0.1:${port}${pathName}`, { signal: AbortSignal.timeout(5e3) });
225
+ if (!res.ok) throw new Error(`${res.status}`);
226
+ return await res.json();
227
+ }
228
+ async function putJson(port, pathName, body) {
229
+ const res = await fetch(`http://127.0.0.1:${port}${pathName}`, {
230
+ method: "PUT",
231
+ headers: { "content-type": "application/json" },
232
+ body: JSON.stringify(body),
233
+ signal: AbortSignal.timeout(5e3)
234
+ });
235
+ if (!res.ok) {
236
+ let detail = `${res.status}`;
237
+ try {
238
+ const j = await res.json();
239
+ if (j.error) detail = j.error;
240
+ } catch {
241
+ }
242
+ throw new Error(detail);
243
+ }
244
+ return await res.json();
245
+ }
246
+ async function requestShutdown(port) {
247
+ try {
248
+ await fetch(`http://127.0.0.1:${port}/api/admin/shutdown`, {
249
+ method: "POST",
250
+ signal: AbortSignal.timeout(1500)
251
+ });
252
+ } catch {
253
+ }
254
+ }
255
+ function sleep(ms) {
256
+ return new Promise((r) => setTimeout(r, ms));
257
+ }
258
+
259
+ // src/cli/daemon.ts
260
+ var serverEntry = fileURLToPath2(new URL("../server/index.js", import.meta.url));
261
+ var CliError = class extends Error {
262
+ constructor(message, exitCode) {
263
+ super(message);
264
+ this.exitCode = exitCode;
265
+ }
266
+ exitCode;
267
+ };
268
+ function spawnDaemon(opts) {
269
+ ensureConfigDir();
270
+ const logPath = serverLogPath();
271
+ try {
272
+ const st = fs3.statSync(logPath);
273
+ if (st.size > 5 * 1024 * 1024) fs3.renameSync(logPath, `${logPath}.1`);
274
+ } catch {
275
+ }
276
+ const logFd = fs3.openSync(logPath, "a");
277
+ const child = spawn(process.execPath, [serverEntry], {
278
+ detached: true,
279
+ stdio: ["ignore", logFd, logFd],
280
+ cwd: pkgRoot,
281
+ env: {
282
+ ...process.env,
283
+ MASTERMIND_PORT: opts.port !== void 0 ? String(opts.port) : "",
284
+ MASTERMIND_PINNED: opts.pinned ? "1" : ""
285
+ }
286
+ });
287
+ child.unref();
288
+ fs3.closeSync(logFd);
289
+ }
290
+ async function awaitReady(deadlineMs) {
291
+ const until = Date.now() + deadlineMs;
292
+ while (Date.now() < until) {
293
+ const state = readServerState();
294
+ if (state && state.version === package_default.version) {
295
+ const probe = await probeHealth(state.port);
296
+ if (probe !== "free" && probe !== "foreign" && probe.version === package_default.version) return state.port;
297
+ }
298
+ await sleep(120);
299
+ }
300
+ throw new CliError(`daemon did not become ready \u2014 check ${serverLogPath()}`, 1);
301
+ }
302
+ async function shutdownAndWait(port) {
303
+ await requestShutdown(port);
304
+ const until = Date.now() + 4e3;
305
+ while (Date.now() < until) {
306
+ if (await probeHealth(port) === "free") return;
307
+ await sleep(100);
308
+ }
309
+ }
310
+ async function ensureServer(opts = {}) {
311
+ const pinned = opts.pinnedPort;
312
+ const state = readServerState();
313
+ if (state) {
314
+ const probe = await probeHealth(state.port);
315
+ if (probe !== "free" && probe !== "foreign") {
316
+ if (probe.version !== package_default.version) {
317
+ process.stderr.write(`mastermind: restarting daemon (v${probe.version} \u2192 v${package_default.version})
318
+ `);
319
+ await shutdownAndWait(state.port);
320
+ } else if (pinned !== void 0 && state.port !== pinned) {
321
+ process.stderr.write(`mastermind: daemon already running on port ${state.port}; reusing it (ignoring --port ${pinned})
322
+ `);
323
+ return state.port;
324
+ } else {
325
+ return state.port;
326
+ }
327
+ } else {
328
+ if (pidIsAlive(state.pid)) {
329
+ process.stderr.write(`mastermind: unresponsive daemon (pid ${state.pid}) \u2014 abandoning it
330
+ `);
331
+ }
332
+ clearServerState(state.pid);
333
+ }
334
+ }
335
+ if (pinned !== void 0) {
336
+ const probe = await probeHealth(pinned);
337
+ if (probe === "foreign") {
338
+ throw new CliError(`port ${pinned} is in use by something that isn't mastermind`, 2);
339
+ }
340
+ if (probe !== "free") {
341
+ if (probe.version === package_default.version) return pinned;
342
+ await shutdownAndWait(pinned);
343
+ }
344
+ spawnDaemon({ port: pinned, pinned: true });
345
+ } else {
346
+ spawnDaemon({ pinned: false });
347
+ }
348
+ return awaitReady(6e3);
349
+ }
350
+
351
+ // src/cli/sse.ts
352
+ async function* sseRecords(url, signal) {
353
+ const res = await fetch(url, { headers: { accept: "text/event-stream" }, signal });
354
+ if (!res.ok || !res.body) throw new Error(`sse connect failed: ${res.status}`);
355
+ const decoder = new TextDecoder();
356
+ let buf = "";
357
+ for await (const chunk of res.body) {
358
+ buf += decoder.decode(chunk, { stream: true });
359
+ let idx;
360
+ while ((idx = buf.indexOf("\n\n")) !== -1) {
361
+ const record = buf.slice(0, idx);
362
+ buf = buf.slice(idx + 2);
363
+ if (record.startsWith(":")) continue;
364
+ let event = "message";
365
+ let data = "";
366
+ for (const line of record.split("\n")) {
367
+ if (line.startsWith("event:")) event = line.slice(6).trim();
368
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
369
+ }
370
+ yield { event, data };
371
+ }
372
+ }
373
+ }
374
+
375
+ // src/cli/wait.ts
376
+ var CLOSE_MESSAGES = {
377
+ "tabs-closed": "tab closed without hand-back",
378
+ "never-opened": "browser never connected",
379
+ shutdown: "server shut down"
380
+ };
381
+ async function drainAssistPending(port, sessionId) {
382
+ try {
383
+ const res = await fetch(`http://127.0.0.1:${port}/api/assist/pending?sessionId=${sessionId}`, {
384
+ signal: AbortSignal.timeout(2e3)
385
+ });
386
+ if (!res.ok) return;
387
+ const { requests } = await res.json();
388
+ for (const req of requests) process.stdout.write(`mastermind-assist: ${JSON.stringify(req)}
389
+ `);
390
+ } catch {
391
+ }
392
+ }
393
+ async function serveAssist(port, sessionId) {
394
+ process.on("SIGINT", () => process.exit(130));
395
+ process.on("SIGTERM", () => process.exit(143));
396
+ process.stderr.write("mastermind: assist listener ready \u2014 answer with `mastermind assist-result|assist-error`\n");
397
+ let attempts = 0;
398
+ for (; ; ) {
399
+ try {
400
+ await drainAssistPending(port, sessionId);
401
+ const url = `http://127.0.0.1:${port}/api/sessions/${sessionId}/events?role=cli&assist=1`;
402
+ for await (const evt of sseRecords(url)) {
403
+ attempts = 0;
404
+ if (evt.event === "assist-request") {
405
+ process.stdout.write(`mastermind-assist: ${evt.data}
406
+ `);
407
+ } else if (evt.event === "session-closed") {
408
+ const { reason } = JSON.parse(evt.data);
409
+ process.stderr.write(`mastermind: ${CLOSE_MESSAGES[reason] ?? reason}
410
+ `);
411
+ process.exit(0);
412
+ }
413
+ }
414
+ } catch {
415
+ }
416
+ attempts++;
417
+ if (await probeHealth(port) === "free") {
418
+ process.stderr.write("mastermind: daemon is gone\n");
419
+ process.exit(1);
420
+ }
421
+ await sleep(Math.min(1e3 * attempts, 1e4));
422
+ }
423
+ }
424
+ async function waitForHandback(port, sessionId, opts = {}) {
425
+ process.on("SIGINT", () => process.exit(130));
426
+ process.on("SIGTERM", () => process.exit(143));
427
+ let attempts = 0;
428
+ for (; ; ) {
429
+ try {
430
+ const q = opts.serveAssist ? "role=cli&assist=1" : "role=cli";
431
+ const url = `http://127.0.0.1:${port}/api/sessions/${sessionId}/events?${q}`;
432
+ if (opts.serveAssist) await drainAssistPending(port, sessionId);
433
+ for await (const evt of sseRecords(url)) {
434
+ attempts = 0;
435
+ if (evt.event === "handback") {
436
+ const payload = JSON.parse(evt.data);
437
+ process.stdout.write(`${payload.summaryLine}
438
+ `);
439
+ process.exit(0);
440
+ }
441
+ if (evt.event === "session-closed") {
442
+ const { reason } = JSON.parse(evt.data);
443
+ process.stderr.write(`mastermind: ${CLOSE_MESSAGES[reason] ?? reason}
444
+ `);
445
+ process.exit(1);
446
+ }
447
+ if (opts.serveAssist && evt.event === "assist-request") {
448
+ process.stdout.write(`mastermind-assist: ${evt.data}
449
+ `);
450
+ }
451
+ }
452
+ } catch {
453
+ }
454
+ attempts++;
455
+ if (attempts > 3) {
456
+ process.stderr.write("mastermind: lost connection to the daemon\n");
457
+ process.exit(1);
458
+ }
459
+ await sleep(1e3 * attempts);
460
+ const probe = await probeHealth(port);
461
+ if (probe === "free" || probe === "foreign") {
462
+ process.stderr.write("mastermind: daemon is gone\n");
463
+ process.exit(1);
464
+ }
465
+ const sessionAlive = await fetch(`http://127.0.0.1:${port}/api/sessions/${sessionId}`, {
466
+ signal: AbortSignal.timeout(2e3)
467
+ }).then((r) => r.ok).catch(() => false);
468
+ if (!sessionAlive) {
469
+ process.stderr.write("mastermind: session is gone\n");
470
+ process.exit(1);
471
+ }
472
+ }
473
+ }
474
+
475
+ // src/server/config.ts
476
+ import fs4 from "fs";
477
+ import path3 from "path";
478
+
479
+ // src/shared/constants.ts
480
+ var DAEMON_IDLE_EXIT_MS = 30 * 6e4;
481
+ var DEFAULT_AUTHOR_TAG = "ke";
482
+
483
+ // src/server/config.ts
484
+ var DEFAULT_CONFIG = {
485
+ version: 1,
486
+ theme: "grid",
487
+ fontSize: 16,
488
+ lineHeight: 1.6,
489
+ contentWidth: 736,
490
+ authorTag: DEFAULT_AUTHOR_TAG,
491
+ typeSet: "grid",
492
+ monoFont: "geist",
493
+ codeTheme: "none",
494
+ uiLang: "en",
495
+ langPair: { a: "English", b: "Simplified Chinese" },
496
+ browser: "",
497
+ grain: {}
498
+ };
499
+ function readConfig() {
500
+ try {
501
+ const raw = fs4.readFileSync(configFilePath(), "utf8");
502
+ const parsed = JSON.parse(raw);
503
+ return { ...DEFAULT_CONFIG, ...parsed, version: 1 };
504
+ } catch {
505
+ return { ...DEFAULT_CONFIG };
506
+ }
507
+ }
508
+ function persist(config) {
509
+ const dir = ensureConfigDir();
510
+ const tmp = path3.join(dir, `.config.json.${process.pid}.tmp`);
511
+ fs4.writeFileSync(tmp, JSON.stringify(config, null, 2));
512
+ fs4.renameSync(tmp, configFilePath());
513
+ }
514
+ function updateConfig(patch) {
515
+ const next = { ...readConfig(), ...patch, version: 1 };
516
+ persist(next);
517
+ return next;
518
+ }
519
+
520
+ // src/shared/critic/scanner.ts
521
+ var OPENERS = [
522
+ ["{++", "ins"],
523
+ ["{--", "del"],
524
+ ["{~~", "sub"],
525
+ ["{==", "highlight"],
526
+ ["{>>", "comment"]
527
+ ];
528
+ var CLOSERS = {
529
+ ins: "++}",
530
+ del: "--}",
531
+ sub: "~~}",
532
+ highlight: "==}",
533
+ comment: "<<}"
534
+ };
535
+ function blankBreaks(text) {
536
+ const out = [];
537
+ const re = /\r?\n[ \t]*\r?\n/g;
538
+ let m;
539
+ while ((m = re.exec(text)) !== null) {
540
+ out.push(m.index);
541
+ re.lastIndex = m.index + 1;
542
+ }
543
+ return out;
544
+ }
545
+ function makeExcludeLookup(exclude) {
546
+ const sorted = [...exclude].sort((a, b) => a.start - b.start);
547
+ return (pos) => {
548
+ let lo = 0;
549
+ let hi = sorted.length - 1;
550
+ while (lo <= hi) {
551
+ const mid = lo + hi >> 1;
552
+ const r = sorted[mid];
553
+ if (pos < r.start) hi = mid - 1;
554
+ else if (pos >= r.end) lo = mid + 1;
555
+ else return r;
556
+ }
557
+ return null;
558
+ };
559
+ }
560
+ function escapedAt(text, bracePos) {
561
+ let backslashes = 0;
562
+ while (bracePos - 1 - backslashes >= 0 && text[bracePos - 1 - backslashes] === "\\") backslashes++;
563
+ return backslashes % 2 === 1;
564
+ }
565
+ function scan(text, exclude = []) {
566
+ const spans = [];
567
+ const breaks = blankBreaks(text);
568
+ const excludeAt = makeExcludeLookup(exclude);
569
+ let breakIdx = 0;
570
+ const findToken = (token, from, limit) => {
571
+ let k = from;
572
+ while (k < limit) {
573
+ const j = text.indexOf(token, k);
574
+ if (j === -1 || j >= limit) return -1;
575
+ const ex = excludeAt(j);
576
+ if (ex) {
577
+ k = ex.end;
578
+ continue;
579
+ }
580
+ return j;
581
+ }
582
+ return -1;
583
+ };
584
+ let i = 0;
585
+ while (i < text.length) {
586
+ const brace = text.indexOf("{", i);
587
+ if (brace === -1) break;
588
+ i = brace;
589
+ const ex = excludeAt(i);
590
+ if (ex) {
591
+ i = ex.end;
592
+ continue;
593
+ }
594
+ if (escapedAt(text, i)) {
595
+ i += 1;
596
+ continue;
597
+ }
598
+ const opener = OPENERS.find(([tok]) => text.startsWith(tok, i));
599
+ if (!opener) {
600
+ i += 1;
601
+ continue;
602
+ }
603
+ const kind = opener[1];
604
+ const innerStart = i + 3;
605
+ while (breakIdx < breaks.length && breaks[breakIdx] < innerStart) breakIdx++;
606
+ const limit = breakIdx < breaks.length ? breaks[breakIdx] : text.length;
607
+ if (kind === "sub") {
608
+ const firstCloser = findToken(CLOSERS.sub, innerStart, limit);
609
+ const sep = findToken("~>", innerStart, limit);
610
+ if (firstCloser === -1 || sep === -1 || sep >= firstCloser) {
611
+ i += 1;
612
+ continue;
613
+ }
614
+ spans.push({
615
+ kind,
616
+ start: i,
617
+ end: firstCloser + 3,
618
+ innerStart,
619
+ innerEnd: firstCloser,
620
+ oldStart: innerStart,
621
+ oldEnd: sep,
622
+ newStart: sep + 2,
623
+ newEnd: firstCloser
624
+ });
625
+ i = firstCloser + 3;
626
+ continue;
627
+ }
628
+ const closer = findToken(CLOSERS[kind], innerStart, limit);
629
+ if (closer === -1) {
630
+ i += 1;
631
+ continue;
632
+ }
633
+ spans.push({ kind, start: i, end: closer + 3, innerStart, innerEnd: closer });
634
+ i = closer + 3;
635
+ }
636
+ return spans;
637
+ }
638
+
639
+ // src/shared/markdown/processor.ts
640
+ import remarkGfm from "remark-gfm";
641
+ import remarkParse from "remark-parse";
642
+ import { unified } from "unified";
643
+ var processor = unified().use(remarkParse).use(remarkGfm).freeze();
644
+ function parseMarkdown(text) {
645
+ const tree = processor.parse(text);
646
+ return processor.runSync(tree);
647
+ }
648
+
649
+ // src/shared/markdown/exclusions.ts
650
+ var EXCLUDED_TYPES = /* @__PURE__ */ new Set(["code", "inlineCode", "html"]);
651
+ function codeRanges(text) {
652
+ const tree = parseMarkdown(text);
653
+ const out = [];
654
+ const visit = (node) => {
655
+ const start = node.position?.start?.offset;
656
+ const end = node.position?.end?.offset;
657
+ if (EXCLUDED_TYPES.has(node.type) && start !== void 0 && end !== void 0) {
658
+ out.push({ start, end });
659
+ return;
660
+ }
661
+ const children = node.children;
662
+ if (children) for (const child of children) visit(child);
663
+ };
664
+ visit(tree);
665
+ return out.sort((a, b) => a.start - b.start);
666
+ }
667
+
668
+ // src/shared/blocks.ts
669
+ var UNTRANSLATABLE = /* @__PURE__ */ new Set(["code", "html", "thematicBreak", "definition"]);
670
+ async function hashBlock(kind, text) {
671
+ const data = new TextEncoder().encode(`${kind}\0${text}`);
672
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", data);
673
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
674
+ }
675
+ async function segmentBlocks(text) {
676
+ const tree = parseMarkdown(text);
677
+ const out = [];
678
+ const push = async (node, kind) => {
679
+ const start = node.position?.start?.offset;
680
+ const end = node.position?.end?.offset;
681
+ if (start === void 0 || end === void 0 || end <= start) return;
682
+ const slice = text.slice(start, end);
683
+ out.push({
684
+ hash: await hashBlock(kind, slice),
685
+ text: slice,
686
+ start,
687
+ end,
688
+ kind,
689
+ translatable: !UNTRANSLATABLE.has(kind)
690
+ });
691
+ };
692
+ for (const node of tree.children) {
693
+ if (node.type === "list") {
694
+ for (const item of node.children) await push(item, "listItem");
695
+ continue;
696
+ }
697
+ await push(node, node.type);
698
+ }
699
+ return out;
700
+ }
701
+ function validateTranslatedBlock(original, translated) {
702
+ const kinds = (t) => scan(t, codeRanges(t)).map((s) => s.kind).join(",");
703
+ return kinds(original) === kinds(translated);
704
+ }
705
+ function detectDocScript(text) {
706
+ const sample = text.slice(0, 4e3);
707
+ let cjk = 0;
708
+ let letters = 0;
709
+ for (const ch of sample) {
710
+ const code = ch.codePointAt(0);
711
+ if (code >= 19968 && code <= 40959) cjk++;
712
+ if (code >= 65 && code <= 122 || code >= 19968 && code <= 40959) letters++;
713
+ }
714
+ return letters > 0 && cjk / letters > 0.25 ? "cjk" : "latin";
715
+ }
716
+
717
+ // src/shared/languages.ts
718
+ var LANGUAGES = [
719
+ { code: "en", name: "English", native: "English" },
720
+ { code: "zh-CN", name: "Simplified Chinese", native: "\u7B80\u4F53\u4E2D\u6587", cjk: true },
721
+ { code: "zh-TW", name: "Traditional Chinese", native: "\u7E41\u9AD4\u4E2D\u6587", cjk: true },
722
+ { code: "ja", name: "Japanese", native: "\u65E5\u672C\u8A9E", cjk: true },
723
+ { code: "ko", name: "Korean", native: "\uD55C\uAD6D\uC5B4", cjk: true },
724
+ { code: "es", name: "Spanish", native: "Espa\xF1ol" },
725
+ { code: "fr", name: "French", native: "Fran\xE7ais" },
726
+ { code: "de", name: "German", native: "Deutsch" },
727
+ { code: "it", name: "Italian", native: "Italiano" },
728
+ { code: "pt", name: "Portuguese", native: "Portugu\xEAs" },
729
+ { code: "ru", name: "Russian", native: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" },
730
+ { code: "ar", name: "Arabic", native: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629" },
731
+ { code: "hi", name: "Hindi", native: "\u0939\u093F\u0928\u094D\u0926\u0940" },
732
+ { code: "bn", name: "Bengali", native: "\u09AC\u09BE\u0982\u09B2\u09BE" },
733
+ { code: "pa", name: "Punjabi", native: "\u0A2A\u0A70\u0A1C\u0A3E\u0A2C\u0A40" },
734
+ { code: "id", name: "Indonesian", native: "Bahasa Indonesia" },
735
+ { code: "ms", name: "Malay", native: "Bahasa Melayu" },
736
+ { code: "vi", name: "Vietnamese", native: "Ti\u1EBFng Vi\u1EC7t" },
737
+ { code: "th", name: "Thai", native: "\u0E44\u0E17\u0E22" },
738
+ { code: "tr", name: "Turkish", native: "T\xFCrk\xE7e" },
739
+ { code: "nl", name: "Dutch", native: "Nederlands" },
740
+ { code: "pl", name: "Polish", native: "Polski" },
741
+ { code: "uk", name: "Ukrainian", native: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430" },
742
+ { code: "sv", name: "Swedish", native: "Svenska" },
743
+ { code: "fi", name: "Finnish", native: "Suomi" },
744
+ { code: "el", name: "Greek", native: "\u0395\u03BB\u03BB\u03B7\u03BD\u03B9\u03BA\u03AC" },
745
+ { code: "he", name: "Hebrew", native: "\u05E2\u05D1\u05E8\u05D9\u05EA" },
746
+ { code: "fa", name: "Persian", native: "\u0641\u0627\u0631\u0633\u06CC" },
747
+ { code: "cs", name: "Czech", native: "\u010Ce\u0161tina" },
748
+ { code: "ro", name: "Romanian", native: "Rom\xE2n\u0103" },
749
+ { code: "hu", name: "Hungarian", native: "Magyar" }
750
+ ];
751
+ var norm = (s) => s.trim().toLowerCase();
752
+ function findLanguage(value) {
753
+ const v = norm(value);
754
+ if (!v) return void 0;
755
+ return LANGUAGES.find((l) => norm(l.name) === v || norm(l.code) === v || norm(l.native) === v);
756
+ }
757
+ function isCjkLang(value) {
758
+ const hit = findLanguage(value);
759
+ if (hit) return !!hit.cjk;
760
+ return /^(zh|ja|ko|cjk|中文|日本|한국)/i.test(value.trim()) || /[一-鿿぀-ヿ가-힯]/.test(value);
761
+ }
762
+
763
+ // src/shared/translate-direction.ts
764
+ function pickTarget(source, pair) {
765
+ const docIsCjk = detectDocScript(source) === "cjk";
766
+ const aIsCjk = isCjkLang(pair.a);
767
+ if (docIsCjk) {
768
+ return aIsCjk ? { sourceLang: pair.a, targetLang: pair.b } : { sourceLang: pair.b, targetLang: pair.a };
769
+ }
770
+ return aIsCjk ? { sourceLang: pair.b, targetLang: pair.a } : { sourceLang: pair.a, targetLang: pair.b };
771
+ }
772
+
773
+ // src/server/translate/cache.ts
774
+ import path4 from "path";
775
+ import fs5 from "fs/promises";
776
+ function cacheFile(realPath, targetLang) {
777
+ const safeLang = targetLang.replace(/[^a-zA-Z0-9_-]/g, "_") || "x";
778
+ return path4.join(path4.dirname(realPath), ".mastermind", "translations", `${path4.basename(realPath)}.${safeLang}.json`);
779
+ }
780
+ async function loadCache(realPath, targetLang) {
781
+ try {
782
+ const raw = await fs5.readFile(cacheFile(realPath, targetLang), "utf8");
783
+ const parsed = JSON.parse(raw);
784
+ return parsed && typeof parsed === "object" ? parsed : {};
785
+ } catch {
786
+ return {};
787
+ }
788
+ }
789
+ async function saveCache(realPath, targetLang, entries) {
790
+ if (Object.keys(entries).length === 0) return;
791
+ const file = cacheFile(realPath, targetLang);
792
+ try {
793
+ await fs5.mkdir(path4.dirname(file), { recursive: true });
794
+ const merged = { ...await loadCache(realPath, targetLang), ...entries };
795
+ await fs5.writeFile(file, JSON.stringify(merged));
796
+ } catch {
797
+ }
798
+ }
799
+
800
+ // src/server/translate/pretranslate.ts
801
+ async function planPretranslate(realPath, source, langPair) {
802
+ const { sourceLang, targetLang } = pickTarget(source, langPair);
803
+ const cached = await loadCache(realPath, targetLang);
804
+ const blocks = (await segmentBlocks(source)).filter((b) => b.translatable && cached[b.hash] === void 0).map((b) => ({ hash: b.hash, text: b.text }));
805
+ return { sourceLang, targetLang, cachePath: cacheFile(realPath, targetLang), blocks };
806
+ }
807
+ async function writePretranslations(realPath, source, langPair, entries) {
808
+ const { targetLang } = pickTarget(source, langPair);
809
+ const originals = new Map((await segmentBlocks(source)).map((b) => [b.hash, b.text]));
810
+ const valid = {};
811
+ let skippedUnknownHash = 0;
812
+ let skippedMarkMismatch = 0;
813
+ for (const { hash, text } of entries) {
814
+ const original = originals.get(hash);
815
+ if (original === void 0) skippedUnknownHash++;
816
+ else if (!validateTranslatedBlock(original, text)) skippedMarkMismatch++;
817
+ else valid[hash] = text;
818
+ }
819
+ await saveCache(realPath, targetLang, valid);
820
+ return {
821
+ targetLang,
822
+ cachePath: cacheFile(realPath, targetLang),
823
+ written: Object.keys(valid).length,
824
+ skipped: skippedUnknownHash + skippedMarkMismatch,
825
+ skippedUnknownHash,
826
+ skippedMarkMismatch
827
+ };
828
+ }
829
+
830
+ // src/cli/config-edit.ts
831
+ var CONFIG_KEYS = /* @__PURE__ */ new Set([
832
+ "theme",
833
+ "fontSize",
834
+ "lineHeight",
835
+ "contentWidth",
836
+ "authorTag",
837
+ "typeSet",
838
+ "monoFont",
839
+ "codeTheme",
840
+ "uiLang",
841
+ "langPair",
842
+ "browser",
843
+ "grain"
844
+ ]);
845
+ var NUMERIC_KEYS = /* @__PURE__ */ new Set(["fontSize", "lineHeight", "contentWidth"]);
846
+ function parseAssignment(arg) {
847
+ const eq = arg.indexOf("=");
848
+ if (eq < 1) throw new Error(`expected key=value, got: ${arg}`);
849
+ return { key: arg.slice(0, eq), value: arg.slice(eq + 1) };
850
+ }
851
+ function coerceConfigValue(key, value) {
852
+ if (!NUMERIC_KEYS.has(key.split(".")[0])) return value;
853
+ const n = Number(value);
854
+ if (value.trim() === "" || !Number.isFinite(n) || n <= 0) {
855
+ throw new Error(`${key} must be a positive number, got: ${value === "" ? "(empty)" : value}`);
856
+ }
857
+ return n;
858
+ }
859
+ function setDotted(obj, dotted, value) {
860
+ const parts = dotted.split(".");
861
+ let cur = obj;
862
+ for (let i = 0; i < parts.length - 1; i++) {
863
+ const k = parts[i];
864
+ const existing = cur[k];
865
+ if (existing === void 0 || existing === null) cur[k] = {};
866
+ else if (typeof existing !== "object") {
867
+ throw new Error(`cannot set ${dotted}: ${parts.slice(0, i + 1).join(".")} is not a nested object`);
868
+ }
869
+ cur = cur[k];
870
+ }
871
+ cur[parts[parts.length - 1]] = value;
872
+ }
873
+ function getDotted(obj, dotted) {
874
+ return dotted.split(".").reduce((acc, k) => acc == null ? void 0 : acc[k], obj);
875
+ }
876
+ function applyAssignments(current, assignments) {
877
+ const next = structuredClone(current);
878
+ for (const { key, value } of assignments) {
879
+ const top = key.split(".")[0];
880
+ if (!CONFIG_KEYS.has(top)) throw new Error(`unknown config key: ${top}`);
881
+ setDotted(next, key, coerceConfigValue(key, value));
882
+ }
883
+ const patch = {};
884
+ for (const top of new Set(assignments.map((a) => a.key.split(".")[0]))) patch[top] = next[top];
885
+ return { patch };
886
+ }
887
+
888
+ // src/cli/browser-open.ts
889
+ function browserOpenArgs(url, override, configBrowser) {
890
+ const app = (override ?? configBrowser).trim();
891
+ return app ? ["-a", app, url] : [url];
892
+ }
893
+
894
+ // src/cli/install-agents.ts
895
+ var MM_BEGIN = "<!-- mastermind:begin -->";
896
+ var MM_END = "<!-- mastermind:end -->";
897
+ var AGENT_TARGETS = [
898
+ { id: "claude", dir: ".claude", globalFile: "CLAUDE.md", version: false, license: false },
899
+ { id: "cursor", dir: ".cursor", globalFile: null, version: true, license: true },
900
+ { id: "gemini", dir: ".gemini", globalFile: "GEMINI.md", version: true, license: false },
901
+ { id: "agents", dir: ".agents", globalFile: null, version: false, license: false }
902
+ ];
903
+ function escapeRe(s) {
904
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
905
+ }
906
+ function stripFrontmatter(md) {
907
+ if (!md.startsWith("---\n")) return md;
908
+ const end = md.indexOf("\n---", 4);
909
+ if (end === -1) return md;
910
+ const after = md.indexOf("\n", end + 1);
911
+ return after === -1 ? "" : md.slice(after + 1).replace(/^\n+/, "");
912
+ }
913
+ function skillFile(t, name, description, body, version) {
914
+ const lines = ["---", `name: ${name}`, `description: ${description.replace(/\n+/g, " ").trim()}`];
915
+ if (t.version) lines.push(`version: ${version}`);
916
+ if (t.license) lines.push("license: Apache-2.0");
917
+ lines.push("---", "");
918
+ return `${lines.join("\n")}
919
+ ${body.trim()}
920
+ `;
921
+ }
922
+ function wrapGlobalBlock(body) {
923
+ return `${MM_BEGIN}
924
+ ${body.trim()}
925
+ ${MM_END}`;
926
+ }
927
+ function upsertMarkedBlock(existing, body) {
928
+ const block = wrapGlobalBlock(body);
929
+ const re = new RegExp(`${escapeRe(MM_BEGIN)}[\\s\\S]*?${escapeRe(MM_END)}`);
930
+ if (re.test(existing)) return existing.replace(re, block);
931
+ if (existing.trim() === "") return `${block}
932
+ `;
933
+ return `${existing.replace(/\n*$/, "")}
934
+
935
+ ${block}
936
+ `;
937
+ }
938
+ function removeMarkedBlock(existing) {
939
+ const re = new RegExp(`\\n*${escapeRe(MM_BEGIN)}[\\s\\S]*?${escapeRe(MM_END)}\\n*`, "g");
940
+ return existing.replace(re, "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
941
+ }
942
+
943
+ // src/cli/index.ts
944
+ var program = new Command();
945
+ function die(code, message) {
946
+ process.stderr.write(`mastermind: ${message}
947
+ `);
948
+ process.exit(code);
949
+ }
950
+ function parsePort(value) {
951
+ if (value === void 0) return void 0;
952
+ const n = Number(value);
953
+ if (!Number.isInteger(n) || n < 1 || n > 65535) die(2, `invalid port: ${value}`);
954
+ return n;
955
+ }
956
+ async function openBrowser(url, override) {
957
+ if (process.platform !== "darwin") return;
958
+ const args = browserOpenArgs(url, override, readConfig().browser);
959
+ if (args.length === 1) {
960
+ spawn2("open", args, { stdio: "ignore", detached: true }).unref();
961
+ return;
962
+ }
963
+ const ok = await new Promise((resolve) => {
964
+ const child = spawn2("open", args, { stdio: "ignore" });
965
+ child.on("error", () => resolve(false));
966
+ child.on("exit", (code) => resolve(code === 0));
967
+ });
968
+ if (!ok) {
969
+ process.stderr.write(`mastermind: couldn't open ${args[1]} \u2014 using the default browser
970
+ `);
971
+ spawn2("open", [url], { stdio: "ignore", detached: true }).unref();
972
+ }
973
+ }
974
+ async function createSession(port, filePath) {
975
+ return postJson(port, "/api/sessions", { path: filePath });
976
+ }
977
+ async function createWorkspace(port, dir) {
978
+ return postJson(port, "/api/workspaces", { root: dir });
979
+ }
980
+ program.name("mastermind").description("Local-first markdown review for humans and AI agents \u2014 the file is the protocol").version(package_default.version).option("--port <n>", "port override (default 5173 or next free)");
981
+ program.command("open").argument("<file>", "markdown file to open").option("--wait", 'block until the user clicks "Save & hand back", then exit 0').option("--serve-assist", "answer translation/suggestion requests for this session (on by default with --wait)").option("--no-assist", "with --wait, review only \u2014 do not serve the assist channel (translation/suggestions)").option("--no-browser", "print the URL without opening a browser tab").option("--in <browser>", 'open in a specific browser app, e.g. "Google Chrome" (overrides the configured default)').description("start the server if not running and open a browser tab for this file").action(async (file, opts) => {
982
+ const pinnedPort = parsePort(program.opts().port);
983
+ const abs = path5.resolve(file);
984
+ let st;
985
+ try {
986
+ st = fs6.statSync(abs);
987
+ } catch {
988
+ die(2, `file not found: ${abs}`);
989
+ }
990
+ if (!st.isFile()) die(2, `not a file: ${abs}`);
991
+ const port = await ensureServer({ pinnedPort });
992
+ const session = await createSession(port, abs);
993
+ process.stdout.write(`${session.url}
994
+ `);
995
+ if (opts.browser) await openBrowser(session.url, opts.in);
996
+ const serve = !!opts.serveAssist || !!opts.wait && opts.assist;
997
+ if (opts.wait) {
998
+ await waitForHandback(port, session.sessionId, { serveAssist: serve });
999
+ } else if (serve) {
1000
+ await serveAssist(port, session.sessionId);
1001
+ }
1002
+ process.exit(0);
1003
+ });
1004
+ program.command("workspace").alias("ws").argument("[dir]", "directory to browse (default: current directory)").option("--no-browser", "print the URL without opening a browser tab").description("open a file-tree workspace rooted at a directory").action(async (dirArg, opts) => {
1005
+ const pinnedPort = parsePort(program.opts().port);
1006
+ const abs = path5.resolve(dirArg ?? process.cwd());
1007
+ let st;
1008
+ try {
1009
+ st = fs6.statSync(abs);
1010
+ } catch {
1011
+ die(2, `directory not found: ${abs}`);
1012
+ }
1013
+ if (!st.isDirectory()) die(2, `not a directory: ${abs}`);
1014
+ const port = await ensureServer({ pinnedPort });
1015
+ const ws = await createWorkspace(port, abs);
1016
+ process.stdout.write(`${ws.url}
1017
+ `);
1018
+ if (opts.browser) await openBrowser(ws.url);
1019
+ process.exit(0);
1020
+ });
1021
+ program.command("assist").argument("<file>", "markdown file under review").description("listen for translation/suggestion requests and stream them as JSON lines (agent-channel)").action(async (file) => {
1022
+ const pinnedPort = parsePort(program.opts().port);
1023
+ const abs = path5.resolve(file);
1024
+ if (!fs6.existsSync(abs)) die(2, `file not found: ${abs}`);
1025
+ const port = await ensureServer({ pinnedPort });
1026
+ const session = await createSession(port, abs);
1027
+ await serveAssist(port, session.sessionId);
1028
+ });
1029
+ program.command("assist-result").argument("<id>", "the request id from the assist-request line").option("--blocks <json>", "translate result: JSON [{hash,text}]").option("--markup <md>", "suggest result: the selection rewritten with CriticMarkup").description("deliver an agent-channel result back to Mastermind").action(async (id, opts) => {
1030
+ const state = readServerState();
1031
+ if (!state) die(1, "no daemon running");
1032
+ let payload;
1033
+ if (opts.blocks !== void 0) {
1034
+ let blocks;
1035
+ try {
1036
+ blocks = JSON.parse(opts.blocks);
1037
+ } catch {
1038
+ die(2, "--blocks must be valid JSON");
1039
+ }
1040
+ payload = { kind: "translate", blocks };
1041
+ } else if (opts.markup !== void 0) {
1042
+ payload = { kind: "suggest", markup: opts.markup };
1043
+ } else {
1044
+ die(2, "provide --blocks or --markup");
1045
+ }
1046
+ try {
1047
+ await postNoContent(state.port, `/api/assist/${id}/result`, payload);
1048
+ process.exit(0);
1049
+ } catch (err) {
1050
+ die(1, `result rejected: ${err instanceof Error ? err.message : String(err)}`);
1051
+ }
1052
+ });
1053
+ program.command("assist-error").argument("<id>", "the request id").option("--reason <text>", "why the request could not be fulfilled").description("decline an agent-channel request").action(async (id, opts) => {
1054
+ const state = readServerState();
1055
+ if (!state) die(1, "no daemon running");
1056
+ try {
1057
+ await postNoContent(state.port, `/api/assist/${id}/error`, { reason: opts.reason ?? "declined" });
1058
+ process.exit(0);
1059
+ } catch (err) {
1060
+ die(1, `error rejected: ${err instanceof Error ? err.message : String(err)}`);
1061
+ }
1062
+ });
1063
+ program.command("new").argument("[path]", "optional path for the new draft").option("--no-browser", "print the URL without opening a browser tab").description("create a blank draft and open it (prompts for a name on first save)").action(async (pathArg, opts) => {
1064
+ const pinnedPort = parsePort(program.opts().port);
1065
+ const port = await ensureServer({ pinnedPort });
1066
+ let session;
1067
+ if (pathArg) {
1068
+ const abs = path5.resolve(pathArg);
1069
+ if (!fs6.existsSync(abs)) {
1070
+ fs6.mkdirSync(path5.dirname(abs), { recursive: true });
1071
+ fs6.writeFileSync(abs, "", { flag: "wx" });
1072
+ }
1073
+ session = await postJson(port, "/api/sessions", { path: abs });
1074
+ } else {
1075
+ session = await postJson(port, "/api/sessions", { draft: true, dir: process.cwd() });
1076
+ }
1077
+ process.stdout.write(`${session.url}
1078
+ `);
1079
+ if (opts.browser) await openBrowser(session.url);
1080
+ process.exit(0);
1081
+ });
1082
+ program.command("status").description("show daemon status").action(async () => {
1083
+ const state = readServerState();
1084
+ if (!state) {
1085
+ process.stdout.write("mastermind: no daemon registered\n");
1086
+ process.exit(0);
1087
+ }
1088
+ const probe = await probeHealth(state.port);
1089
+ if (probe === "free" || probe === "foreign") {
1090
+ process.stdout.write(
1091
+ `mastermind: statefile names pid ${state.pid} on port ${state.port}, but no healthy daemon answered (${probe})
1092
+ `
1093
+ );
1094
+ process.exit(1);
1095
+ }
1096
+ const uptime = Math.round((Date.now() - probe.startedAt) / 1e3);
1097
+ process.stdout.write(
1098
+ `mastermind v${probe.version} \u2014 pid ${probe.pid}, http://127.0.0.1:${state.port}, up ${uptime}s
1099
+ log: ${serverLogPath()}
1100
+ `
1101
+ );
1102
+ process.exit(0);
1103
+ });
1104
+ program.command("stop").description("shut the daemon down").action(async () => {
1105
+ const state = readServerState();
1106
+ if (!state) {
1107
+ process.stdout.write("mastermind: no daemon registered\n");
1108
+ process.exit(0);
1109
+ }
1110
+ await requestShutdown(state.port);
1111
+ const until = Date.now() + 4e3;
1112
+ while (Date.now() < until) {
1113
+ if (await probeHealth(state.port) === "free") {
1114
+ process.stdout.write("mastermind: daemon stopped\n");
1115
+ process.exit(0);
1116
+ }
1117
+ await sleep(100);
1118
+ }
1119
+ die(1, `daemon did not stop \u2014 check ${serverLogPath()}`);
1120
+ });
1121
+ async function readStdin() {
1122
+ const chunks = [];
1123
+ for await (const chunk of process.stdin) chunks.push(chunk);
1124
+ return Buffer.concat(chunks).toString("utf8");
1125
+ }
1126
+ program.command("translate-blocks").argument("<file>", "markdown file to pre-translate").option("--save", "read translated [{hash,text}] JSON from stdin and write it to the on-disk cache").description("offline pre-translate: emit the blocks to translate, or (--save) cache the agent's translations").action(async (file, opts) => {
1127
+ const abs = path5.resolve(file);
1128
+ let source;
1129
+ try {
1130
+ source = fs6.readFileSync(abs, "utf8");
1131
+ } catch {
1132
+ die(2, `file not found: ${abs}`);
1133
+ }
1134
+ const { langPair } = readConfig();
1135
+ if (opts.save) {
1136
+ let entries;
1137
+ try {
1138
+ const parsed = JSON.parse(await readStdin());
1139
+ if (!Array.isArray(parsed)) throw new Error("expected a JSON array");
1140
+ const ok = parsed.every(
1141
+ (e) => e !== null && typeof e === "object" && typeof e.hash === "string" && typeof e.text === "string"
1142
+ );
1143
+ if (!ok) throw new Error("each element must be { hash: string, text: string }");
1144
+ entries = parsed;
1145
+ } catch (err) {
1146
+ die(2, `--save expects [{hash,text}] JSON on stdin: ${err instanceof Error ? err.message : String(err)}`);
1147
+ }
1148
+ const res = await writePretranslations(abs, source, langPair, entries);
1149
+ const skips = [];
1150
+ if (res.skippedUnknownHash) skips.push(`${res.skippedUnknownHash} stale/unknown-hash`);
1151
+ if (res.skippedMarkMismatch) skips.push(`${res.skippedMarkMismatch} mismatched-CriticMarkup`);
1152
+ process.stdout.write(
1153
+ `mastermind: cached ${res.written} block(s) \u2192 ${res.cachePath}${skips.length ? ` (skipped ${skips.join(", ")})` : ""}
1154
+ `
1155
+ );
1156
+ process.exit(0);
1157
+ }
1158
+ process.stdout.write(`${JSON.stringify(await planPretranslate(abs, source, langPair))}
1159
+ `);
1160
+ process.exit(0);
1161
+ });
1162
+ async function liveDaemonPort() {
1163
+ const state = readServerState();
1164
+ if (!state) return null;
1165
+ const probe = await probeHealth(state.port);
1166
+ return typeof probe === "object" ? state.port : null;
1167
+ }
1168
+ var configCmd = program.command("config").description("read or write Mastermind preferences");
1169
+ configCmd.command("get [key]").description("print the whole config as JSON, or one dotted key (e.g. langPair.a)").action((key) => {
1170
+ const cfg = readConfig();
1171
+ if (key === void 0) {
1172
+ process.stdout.write(`${JSON.stringify(cfg, null, 2)}
1173
+ `);
1174
+ } else {
1175
+ const v = getDotted(cfg, key);
1176
+ if (v === void 0) die(2, `unknown config key: ${key}`);
1177
+ process.stdout.write(`${typeof v === "string" ? v : JSON.stringify(v)}
1178
+ `);
1179
+ }
1180
+ process.exit(0);
1181
+ });
1182
+ configCmd.command("set <assignments...>").description('set dotted key=value pairs, e.g. langPair.a=English theme=nacht browser="Google Chrome"').action(async (raw) => {
1183
+ let assignments;
1184
+ try {
1185
+ assignments = raw.map(parseAssignment);
1186
+ } catch (err) {
1187
+ die(2, err instanceof Error ? err.message : String(err));
1188
+ }
1189
+ const port = await liveDaemonPort();
1190
+ const current = port ? await getJson(port, "/api/config") : readConfig();
1191
+ let patch;
1192
+ try {
1193
+ patch = applyAssignments(current, assignments).patch;
1194
+ } catch (err) {
1195
+ die(2, err instanceof Error ? err.message : String(err));
1196
+ }
1197
+ try {
1198
+ if (port) await putJson(port, "/api/config", patch);
1199
+ else updateConfig(patch);
1200
+ } catch (err) {
1201
+ die(1, `config set failed: ${err instanceof Error ? err.message : String(err)}`);
1202
+ }
1203
+ process.stdout.write(`mastermind: set ${assignments.map((a) => a.key).join(", ")}
1204
+ `);
1205
+ process.exit(0);
1206
+ });
1207
+ var MM_DESC = "Visualize and review a Markdown file in Mastermind \u2014 /mastermind [setup|demo|<file>]. Always pre-translates the doc into both reading languages first.";
1208
+ var MASTER_DESC = "Alias of /mastermind: translate a file into both reading languages, then open it in Mastermind.";
1209
+ function readCanonical(rel) {
1210
+ return fs6.readFileSync(path5.join(pkgRoot, rel), "utf8");
1211
+ }
1212
+ function installAgents(opts) {
1213
+ const home = os2.homedir();
1214
+ const mmBody = stripFrontmatter(readCanonical(".claude/skills/mastermind/SKILL.md"));
1215
+ const masterBody = stripFrontmatter(readCanonical(".claude/skills/master/SKILL.md"));
1216
+ const demo = readCanonical(".claude/skills/mastermind/reference/demo.md");
1217
+ const globalBody = readCanonical("assets/agent/global.md");
1218
+ const done = [];
1219
+ for (const t of AGENT_TARGETS) {
1220
+ const base = path5.join(home, t.dir);
1221
+ if (!opts.all && !fs6.existsSync(base)) continue;
1222
+ const mmDir = path5.join(base, "skills", "mastermind");
1223
+ fs6.mkdirSync(path5.join(mmDir, "reference"), { recursive: true });
1224
+ fs6.writeFileSync(path5.join(mmDir, "SKILL.md"), skillFile(t, "mastermind", MM_DESC, mmBody, package_default.version));
1225
+ fs6.writeFileSync(path5.join(mmDir, "reference", "demo.md"), demo);
1226
+ const masterDir = path5.join(base, "skills", "master");
1227
+ fs6.mkdirSync(masterDir, { recursive: true });
1228
+ fs6.writeFileSync(path5.join(masterDir, "SKILL.md"), skillFile(t, "master", MASTER_DESC, masterBody, package_default.version));
1229
+ let note = ` ${t.id}: skills/{mastermind,master}`;
1230
+ if (opts.global && t.globalFile) {
1231
+ const gf = path5.join(base, t.globalFile);
1232
+ const existing = fs6.existsSync(gf) ? fs6.readFileSync(gf, "utf8") : "";
1233
+ fs6.writeFileSync(gf, upsertMarkedBlock(existing, globalBody));
1234
+ note += ` + global (${t.globalFile})`;
1235
+ } else if (opts.global && !t.globalFile) {
1236
+ note += ` + global skipped (no known memory file for ${t.id}; see AGENT_SETUP.md)`;
1237
+ }
1238
+ done.push(`${note} \u2192 ${base}`);
1239
+ }
1240
+ if (done.length === 0) {
1241
+ process.stdout.write(
1242
+ "mastermind: no agent config dirs found (~/.claude, ~/.cursor, ~/.gemini, ~/.agents). Re-run with --all to create them.\n"
1243
+ );
1244
+ return;
1245
+ }
1246
+ process.stdout.write(`mastermind: installed for ${done.length} agent(s):
1247
+ ${done.join("\n")}
1248
+ `);
1249
+ if (opts.global) process.stdout.write(" (undo the global block + skills with `mastermind uninstall-agents`)\n");
1250
+ }
1251
+ function uninstallAgents() {
1252
+ const home = os2.homedir();
1253
+ const removed = [];
1254
+ for (const t of AGENT_TARGETS) {
1255
+ const base = path5.join(home, t.dir);
1256
+ for (const name of ["mastermind", "master"]) {
1257
+ const dir = path5.join(base, "skills", name);
1258
+ if (fs6.existsSync(dir)) {
1259
+ fs6.rmSync(dir, { recursive: true, force: true });
1260
+ removed.push(` ${t.id}: skills/${name}`);
1261
+ }
1262
+ }
1263
+ if (t.globalFile) {
1264
+ const gf = path5.join(base, t.globalFile);
1265
+ if (fs6.existsSync(gf)) fs6.writeFileSync(gf, removeMarkedBlock(fs6.readFileSync(gf, "utf8")));
1266
+ }
1267
+ }
1268
+ process.stdout.write(removed.length ? `mastermind: removed:
1269
+ ${removed.join("\n")}
1270
+ ` : "mastermind: nothing to remove\n");
1271
+ }
1272
+ program.command("install-agents").option("--all", "install for every supported agent, creating its config dir if absent").option("--no-global", "install the skills only \u2014 skip the always-translate-first / plan\u2192visualize global instruction block").description("install the mastermind + master skills (and a global instruction block) for your coding agents").action((opts) => {
1273
+ installAgents({ all: !!opts.all, global: opts.global });
1274
+ process.exit(0);
1275
+ });
1276
+ program.command("uninstall-agents").description("remove the mastermind/master skills and the global instruction block this tool installed").action(() => {
1277
+ uninstallAgents();
1278
+ process.exit(0);
1279
+ });
1280
+ program.parseAsync().catch((err) => {
1281
+ if (err instanceof CliError) die(err.exitCode, err.message);
1282
+ die(1, err instanceof Error ? err.message : String(err));
1283
+ });
1284
+ //# sourceMappingURL=index.js.map