memento-mori-jester 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,789 @@
1
+ #!/usr/bin/env node
2
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { stdin as input, stdout as output } from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { configPresetNames, defaultUserConfig, findConfigPath, loadConfig, validateConfig, writeDefaultConfig } from "./config.js";
7
+ import { review, reviewCommand } from "./core.js";
8
+ import { formatReview } from "./format.js";
9
+ import { hookNames, hookStatus, installHook, isHookName, shellCommandPrefixForLocalCli, shellQuote, uninstallHook } from "./hooks.js";
10
+ import { reviewKinds, tones } from "./types.js";
11
+ const packageSpecDefault = "memento-mori-jester@latest";
12
+ const args = process.argv.slice(2);
13
+ main(args).catch((error) => {
14
+ console.error(error instanceof Error ? error.message : String(error));
15
+ process.exitCode = 1;
16
+ });
17
+ async function main(argv) {
18
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
19
+ output.write(helpText());
20
+ return;
21
+ }
22
+ if (argv[0] === "mcp-server") {
23
+ await import("./server.js");
24
+ return;
25
+ }
26
+ if (argv[0] === "mcp-config") {
27
+ const setupOptions = parseSetupOptions(argv.slice(1));
28
+ output.write(`${JSON.stringify(mcpConfigSnippet(setupOptions), null, 2)}\n`);
29
+ return;
30
+ }
31
+ if (argv[0] === "init") {
32
+ const setupOptions = parseSetupOptions(argv.slice(1));
33
+ output.write(renderInit(setupOptions));
34
+ return;
35
+ }
36
+ if (argv[0] === "bootstrap") {
37
+ output.write(await handleBootstrap(argv.slice(1)));
38
+ return;
39
+ }
40
+ if (argv[0] === "config") {
41
+ output.write(await handleConfigCommand(argv.slice(1)));
42
+ return;
43
+ }
44
+ if (argv[0] === "doctor") {
45
+ const doctorOptions = parseConfigCommandOptions(argv.slice(1));
46
+ const result = await renderDoctor(doctorOptions);
47
+ output.write(result.text);
48
+ if (!result.ok) {
49
+ process.exitCode = 1;
50
+ }
51
+ return;
52
+ }
53
+ if (argv[0] === "install-hook") {
54
+ const result = await handleInstallHook(argv.slice(1));
55
+ output.write(`${result.message}\n`);
56
+ return;
57
+ }
58
+ if (argv[0] === "uninstall-hook") {
59
+ const result = await handleUninstallHook(argv.slice(1));
60
+ output.write(`${result.message}\n`);
61
+ return;
62
+ }
63
+ if (argv[0] === "hook-status") {
64
+ output.write(await renderHookStatus());
65
+ return;
66
+ }
67
+ const { command, rest } = splitCommand(argv);
68
+ const options = parseOptions(rest);
69
+ const kind = resolveKind(command, options.kind);
70
+ const content = await resolveContent(options, rest);
71
+ if (!content.trim()) {
72
+ throw new Error("Nothing to review. Pass text, use --file, or pipe content on stdin.");
73
+ }
74
+ const loadedConfig = await loadConfig({
75
+ configPath: options.configPath,
76
+ search: !options.noConfig
77
+ });
78
+ const inputForReview = {
79
+ kind,
80
+ content,
81
+ subject: options.subject,
82
+ context: options.context,
83
+ tone: options.tone,
84
+ intensity: options.intensity,
85
+ riskTolerance: options.riskTolerance,
86
+ config: loadedConfig.config
87
+ };
88
+ const result = review(inputForReview);
89
+ output.write(options.json ? `${JSON.stringify(result, null, 2)}\n` : `${formatReview(result)}\n`);
90
+ if (options.failOn === "block" && result.verdict === "block") {
91
+ process.exitCode = 2;
92
+ }
93
+ else if (options.failOn === "caution" && result.verdict !== "pass") {
94
+ process.exitCode = result.verdict === "block" ? 2 : 1;
95
+ }
96
+ }
97
+ function splitCommand(argv) {
98
+ const [first, ...rest] = argv;
99
+ if (reviewKinds.includes(first) || first === "review") {
100
+ return { command: first, rest };
101
+ }
102
+ return { command: "review", rest: argv };
103
+ }
104
+ function resolveKind(command, optionKind) {
105
+ if (command !== "review" && reviewKinds.includes(command)) {
106
+ return command;
107
+ }
108
+ return optionKind ?? "plan";
109
+ }
110
+ function parseOptions(argv) {
111
+ const options = { json: false, noConfig: false };
112
+ for (let index = 0; index < argv.length; index += 1) {
113
+ const arg = argv[index];
114
+ const next = argv[index + 1];
115
+ if (arg === "--json") {
116
+ options.json = true;
117
+ }
118
+ else if (arg === "--kind") {
119
+ options.kind = parseKind(requireValue(arg, next));
120
+ index += 1;
121
+ }
122
+ else if (arg === "--tone") {
123
+ options.tone = parseTone(requireValue(arg, next));
124
+ index += 1;
125
+ }
126
+ else if (arg === "--intensity") {
127
+ options.intensity = Number.parseInt(requireValue(arg, next), 10);
128
+ index += 1;
129
+ }
130
+ else if (arg === "--risk") {
131
+ options.riskTolerance = parseRisk(requireValue(arg, next));
132
+ index += 1;
133
+ }
134
+ else if (arg === "--fail-on") {
135
+ options.failOn = parseFailOn(requireValue(arg, next));
136
+ index += 1;
137
+ }
138
+ else if (arg === "--subject") {
139
+ options.subject = requireValue(arg, next);
140
+ index += 1;
141
+ }
142
+ else if (arg === "--context") {
143
+ options.context = requireValue(arg, next);
144
+ index += 1;
145
+ }
146
+ else if (arg === "--file") {
147
+ options.file = requireValue(arg, next);
148
+ index += 1;
149
+ }
150
+ else if (arg === "--config") {
151
+ options.configPath = requireValue(arg, next);
152
+ index += 1;
153
+ }
154
+ else if (arg === "--no-config") {
155
+ options.noConfig = true;
156
+ }
157
+ }
158
+ return options;
159
+ }
160
+ function parseConfigCommandOptions(argv) {
161
+ const options = {
162
+ json: false,
163
+ force: false,
164
+ noConfig: false,
165
+ preset: "default"
166
+ };
167
+ for (let index = 0; index < argv.length; index += 1) {
168
+ const arg = argv[index];
169
+ const next = argv[index + 1];
170
+ if (arg === "--json") {
171
+ options.json = true;
172
+ }
173
+ else if (arg === "--force") {
174
+ options.force = true;
175
+ }
176
+ else if (arg === "--path") {
177
+ options.path = requireValue(arg, next);
178
+ index += 1;
179
+ }
180
+ else if (arg === "--config") {
181
+ options.configPath = requireValue(arg, next);
182
+ index += 1;
183
+ }
184
+ else if (arg === "--no-config") {
185
+ options.noConfig = true;
186
+ }
187
+ else if (arg === "--preset") {
188
+ options.preset = parseConfigPreset(requireValue(arg, next));
189
+ index += 1;
190
+ }
191
+ }
192
+ return options;
193
+ }
194
+ function parseSetupOptions(argv) {
195
+ const options = {
196
+ mode: "npx",
197
+ agent: "generic",
198
+ packageSpec: packageSpecDefault,
199
+ tone: "court_jester",
200
+ intensity: 3,
201
+ riskTolerance: "medium",
202
+ json: false
203
+ };
204
+ for (let index = 0; index < argv.length; index += 1) {
205
+ const arg = argv[index];
206
+ const next = argv[index + 1];
207
+ if (arg === "--json") {
208
+ options.json = true;
209
+ }
210
+ else if (arg === "--mode") {
211
+ options.mode = parseSetupMode(requireValue(arg, next));
212
+ index += 1;
213
+ }
214
+ else if (arg === "--agent") {
215
+ options.agent = parseAgent(requireValue(arg, next));
216
+ index += 1;
217
+ }
218
+ else if (arg === "--package") {
219
+ options.packageSpec = requireValue(arg, next);
220
+ index += 1;
221
+ }
222
+ else if (arg === "--tone") {
223
+ options.tone = parseTone(requireValue(arg, next));
224
+ index += 1;
225
+ }
226
+ else if (arg === "--intensity") {
227
+ options.intensity = Number.parseInt(requireValue(arg, next), 10);
228
+ index += 1;
229
+ }
230
+ else if (arg === "--risk") {
231
+ options.riskTolerance = parseRisk(requireValue(arg, next));
232
+ index += 1;
233
+ }
234
+ else if (!arg.startsWith("--")) {
235
+ if (isSetupMode(arg)) {
236
+ options.mode = arg;
237
+ }
238
+ else if (isAgent(arg)) {
239
+ options.agent = arg;
240
+ }
241
+ }
242
+ }
243
+ return options;
244
+ }
245
+ async function resolveContent(options, argv) {
246
+ if (options.file) {
247
+ return readFile(options.file, "utf8");
248
+ }
249
+ const positional = collectPositional(argv);
250
+ if (positional.length > 0) {
251
+ return positional.join(" ");
252
+ }
253
+ if (!input.isTTY) {
254
+ return readStdin();
255
+ }
256
+ return "";
257
+ }
258
+ function collectPositional(argv) {
259
+ const positional = [];
260
+ let afterSeparator = false;
261
+ for (let index = 0; index < argv.length; index += 1) {
262
+ const arg = argv[index];
263
+ if (arg === "--") {
264
+ afterSeparator = true;
265
+ continue;
266
+ }
267
+ if (!afterSeparator && isKnownOption(arg)) {
268
+ index += optionHasValue(arg) ? 1 : 0;
269
+ continue;
270
+ }
271
+ positional.push(arg);
272
+ }
273
+ return positional;
274
+ }
275
+ function optionHasValue(arg) {
276
+ return ["--kind", "--tone", "--intensity", "--risk", "--fail-on", "--subject", "--context", "--file", "--config", "--path", "--preset"].includes(arg);
277
+ }
278
+ function isKnownOption(arg) {
279
+ return optionHasValue(arg) || ["--json", "--no-config", "--force"].includes(arg);
280
+ }
281
+ function readStdin() {
282
+ return new Promise((resolve, reject) => {
283
+ let data = "";
284
+ input.setEncoding("utf8");
285
+ input.on("data", (chunk) => {
286
+ data += chunk;
287
+ });
288
+ input.on("end", () => resolve(data));
289
+ input.on("error", reject);
290
+ });
291
+ }
292
+ function parseKind(value) {
293
+ if (reviewKinds.includes(value)) {
294
+ return value;
295
+ }
296
+ throw new Error(`Unknown review kind "${value}". Use one of: ${reviewKinds.join(", ")}`);
297
+ }
298
+ function parseTone(value) {
299
+ if (tones.includes(value)) {
300
+ return value;
301
+ }
302
+ throw new Error(`Unknown tone "${value}". Use one of: ${tones.join(", ")}`);
303
+ }
304
+ function parseRisk(value) {
305
+ if (value === "low" || value === "medium" || value === "high") {
306
+ return value;
307
+ }
308
+ throw new Error('Unknown risk tolerance. Use "low", "medium", or "high".');
309
+ }
310
+ function parseFailOn(value) {
311
+ if (value === "caution" || value === "block") {
312
+ return value;
313
+ }
314
+ throw new Error('Unknown fail threshold. Use "caution" or "block".');
315
+ }
316
+ function parseConfigPreset(value) {
317
+ if (configPresetNames.includes(value)) {
318
+ return value;
319
+ }
320
+ throw new Error(`Unknown config preset "${value}". Use one of: ${configPresetNames.join(", ")}`);
321
+ }
322
+ function parseSetupMode(value) {
323
+ if (isSetupMode(value)) {
324
+ return value;
325
+ }
326
+ throw new Error('Unknown setup mode. Use "npx", "global", or "local".');
327
+ }
328
+ function isSetupMode(value) {
329
+ return value === "npx" || value === "global" || value === "local";
330
+ }
331
+ function parseAgent(value) {
332
+ if (isAgent(value)) {
333
+ return value;
334
+ }
335
+ throw new Error('Unknown agent target. Use "generic", "claude", or "codex".');
336
+ }
337
+ function isAgent(value) {
338
+ return value === "generic" || value === "claude" || value === "codex";
339
+ }
340
+ function requireValue(flag, value) {
341
+ if (!value || value.startsWith("--")) {
342
+ throw new Error(`Missing value for ${flag}.`);
343
+ }
344
+ return value;
345
+ }
346
+ function mcpConfigSnippet(options) {
347
+ return {
348
+ mcpServers: {
349
+ "memento-mori-jester": {
350
+ ...mcpCommandSpec(options)
351
+ }
352
+ }
353
+ };
354
+ }
355
+ function mcpCommandSpec(options) {
356
+ if (options.mode === "local") {
357
+ return {
358
+ command: "node",
359
+ args: [serverPath()]
360
+ };
361
+ }
362
+ if (options.mode === "global") {
363
+ return {
364
+ command: "memento-mori-jester-mcp",
365
+ args: []
366
+ };
367
+ }
368
+ return {
369
+ command: "npx",
370
+ args: ["-y", options.packageSpec, "mcp-server"]
371
+ };
372
+ }
373
+ function serverPath() {
374
+ return join(dirname(fileURLToPath(import.meta.url)), "server.js");
375
+ }
376
+ function cliPath() {
377
+ return fileURLToPath(import.meta.url);
378
+ }
379
+ function renderInit(options) {
380
+ const cliCommand = renderCliCommand(options);
381
+ const config = JSON.stringify(mcpConfigSnippet(options), null, 2);
382
+ const agentLine = options.agent === "generic"
383
+ ? "Paste this into any MCP client that accepts the standard mcpServers JSON shape."
384
+ : `Use this for ${options.agent}; if its config format differs, keep the command and args values.`;
385
+ return `Memento Mori Jester setup
386
+
387
+ Try it now:
388
+ ${cliCommand} command "git reset --hard"
389
+
390
+ MCP config (${options.mode} mode):
391
+ ${config}
392
+
393
+ ${agentLine}
394
+
395
+ Suggested agent instruction:
396
+ Before risky commands, final answers, commits, or large edits, call the Memento Mori Jester. Treat BLOCK as requiring a changed plan, and CAUTION as requiring at least one concrete verification step.
397
+
398
+ Useful next checks:
399
+ ${cliCommand} doctor
400
+ ${cliCommand} config init
401
+ ${cliCommand} install-hook pre-commit
402
+ ${cliCommand} plan "I will just refactor auth and ship it"
403
+ `;
404
+ }
405
+ function renderCliCommand(options) {
406
+ if (options.mode === "global") {
407
+ return "jester";
408
+ }
409
+ if (options.mode === "local") {
410
+ return `node "${cliPath()}"`;
411
+ }
412
+ return `npx -y ${options.packageSpec}`;
413
+ }
414
+ async function handleConfigCommand(argv) {
415
+ const [subcommand = "show"] = argv;
416
+ const options = parseConfigCommandOptions(argv.slice(1));
417
+ if (subcommand === "init") {
418
+ const path = await writeDefaultConfig({
419
+ path: options.path,
420
+ force: options.force,
421
+ preset: options.preset
422
+ });
423
+ return `Wrote ${path}\n`;
424
+ }
425
+ if (subcommand === "presets") {
426
+ return `${configPresetNames.join("\n")}\n`;
427
+ }
428
+ if (subcommand === "show") {
429
+ const loaded = await loadConfig({
430
+ configPath: options.configPath,
431
+ search: !options.noConfig
432
+ });
433
+ if (options.json) {
434
+ return `${JSON.stringify(loaded, null, 2)}\n`;
435
+ }
436
+ const label = loaded.path ? `Loaded ${loaded.path}` : "No config file found; using built-in defaults.";
437
+ return `${label}\n${JSON.stringify({ ...defaultUserConfig(), ...loaded.config }, null, 2)}\n`;
438
+ }
439
+ if (subcommand === "validate") {
440
+ const result = await validateConfig({
441
+ configPath: options.configPath,
442
+ search: !options.noConfig
443
+ });
444
+ if (options.json) {
445
+ return `${JSON.stringify(result, null, 2)}\n`;
446
+ }
447
+ if (result.ok) {
448
+ return `Config valid: ${result.path}\n`;
449
+ }
450
+ process.exitCode = 1;
451
+ return `Config invalid${result.path ? `: ${result.path}` : ""}\n${result.issues.map((issue) => `- ${issue}`).join("\n")}\n`;
452
+ }
453
+ throw new Error('Unknown config command. Use "jester config init", "jester config show", "jester config validate", or "jester config presets".');
454
+ }
455
+ async function handleBootstrap(argv) {
456
+ const options = parseBootstrapOptions(argv);
457
+ const configFile = await ensureBootstrapConfig(options);
458
+ const mcpFile = await writeStarterFile({
459
+ relativePath: "memento-mori.mcp.json",
460
+ content: `${JSON.stringify(mcpConfigSnippet(options), null, 2)}\n`,
461
+ force: options.force
462
+ });
463
+ const instructionsFile = await writeStarterFile({
464
+ relativePath: "MEMENTO_MORI.md",
465
+ content: renderBootstrapInstructions(options),
466
+ force: options.force
467
+ });
468
+ const loaded = await loadConfig({ configPath: configFile.path, search: false });
469
+ const failOn = loaded.config.hookFailOn ?? "block";
470
+ const hooks = [];
471
+ for (const hook of options.hooks) {
472
+ hooks.push(await installHook({
473
+ hook,
474
+ commandPrefix: hookCommandPrefix(options),
475
+ failOn,
476
+ force: options.force
477
+ }));
478
+ }
479
+ const result = {
480
+ ok: true,
481
+ mode: options.mode,
482
+ agent: options.agent,
483
+ preset: options.preset,
484
+ files: [configFile, mcpFile, instructionsFile],
485
+ hooks,
486
+ nextSteps: [
487
+ `${renderCliCommand(options)} doctor`,
488
+ `${renderCliCommand(options)} config validate`,
489
+ "Add memento-mori.mcp.json to your MCP client, or copy the command and args from it."
490
+ ]
491
+ };
492
+ if (options.json) {
493
+ return `${JSON.stringify(result, null, 2)}\n`;
494
+ }
495
+ const lines = [
496
+ "Memento Mori Jester bootstrap",
497
+ "",
498
+ "Files:",
499
+ ...result.files.map((file) => ` ${file.changed ? "wrote" : "kept"} ${file.path}`),
500
+ ""
501
+ ];
502
+ if (hooks.length > 0) {
503
+ lines.push("Hooks:", ...hooks.map((hook) => ` ${hook.message}`), "");
504
+ }
505
+ lines.push("Next:", ...result.nextSteps.map((step) => ` ${step}`), "");
506
+ return lines.join("\n");
507
+ }
508
+ async function handleInstallHook(argv) {
509
+ const options = await parseHookCommandOptions(argv);
510
+ const loaded = await loadConfig({
511
+ configPath: options.configPath,
512
+ search: !options.noConfig
513
+ });
514
+ const failOn = options.failOn ?? loaded.config.hookFailOn ?? "block";
515
+ return installHook({
516
+ hook: options.hook,
517
+ commandPrefix: hookCommandPrefix(options.setup),
518
+ failOn,
519
+ force: options.force
520
+ });
521
+ }
522
+ async function handleUninstallHook(argv) {
523
+ const options = await parseHookCommandOptions(argv);
524
+ return uninstallHook(options.hook, { force: options.force });
525
+ }
526
+ async function parseHookCommandOptions(argv) {
527
+ const setup = parseSetupOptions(argv);
528
+ let hook;
529
+ let failOn;
530
+ let force = false;
531
+ let configPath;
532
+ let noConfig = false;
533
+ for (let index = 0; index < argv.length; index += 1) {
534
+ const arg = argv[index];
535
+ const next = argv[index + 1];
536
+ if (isHookName(arg)) {
537
+ hook = arg;
538
+ }
539
+ else if (arg === "--fail-on") {
540
+ failOn = parseFailOn(requireValue(arg, next));
541
+ index += 1;
542
+ }
543
+ else if (arg === "--force") {
544
+ force = true;
545
+ }
546
+ else if (arg === "--config") {
547
+ configPath = requireValue(arg, next);
548
+ index += 1;
549
+ }
550
+ else if (arg === "--no-config") {
551
+ noConfig = true;
552
+ }
553
+ }
554
+ if (!hook) {
555
+ throw new Error(`Missing hook name. Use one of: ${hookNames.join(", ")}`);
556
+ }
557
+ return {
558
+ hook,
559
+ setup,
560
+ failOn,
561
+ force,
562
+ configPath,
563
+ noConfig
564
+ };
565
+ }
566
+ function parseBootstrapOptions(argv) {
567
+ const setup = parseSetupOptions(argv);
568
+ let preset = "default";
569
+ let force = false;
570
+ const hooks = [];
571
+ for (let index = 0; index < argv.length; index += 1) {
572
+ const arg = argv[index];
573
+ const next = argv[index + 1];
574
+ if (arg === "--preset") {
575
+ preset = parseConfigPreset(requireValue(arg, next));
576
+ index += 1;
577
+ }
578
+ else if (arg === "--force") {
579
+ force = true;
580
+ }
581
+ else if (arg === "--hook") {
582
+ const hook = requireValue(arg, next);
583
+ if (!isHookName(hook)) {
584
+ throw new Error(`Unknown hook "${hook}". Use one of: ${hookNames.join(", ")}`);
585
+ }
586
+ hooks.push(hook);
587
+ index += 1;
588
+ }
589
+ }
590
+ return {
591
+ ...setup,
592
+ preset,
593
+ force,
594
+ hooks: [...new Set(hooks)]
595
+ };
596
+ }
597
+ async function renderHookStatus() {
598
+ const statuses = await hookStatus();
599
+ return `${statuses.map((status) => `${status.hook}: ${status.message} (${status.path})`).join("\n")}\n`;
600
+ }
601
+ function hookCommandPrefix(options) {
602
+ if (options.mode === "local") {
603
+ return shellCommandPrefixForLocalCli(cliPath());
604
+ }
605
+ if (options.mode === "global") {
606
+ return "jester";
607
+ }
608
+ return `npx -y ${shellQuote(options.packageSpec)}`;
609
+ }
610
+ async function renderDoctor(options) {
611
+ let configCheck;
612
+ try {
613
+ const loaded = await loadConfig({
614
+ configPath: options.configPath,
615
+ search: !options.noConfig
616
+ });
617
+ configCheck = {
618
+ name: "config",
619
+ ok: true,
620
+ detail: loaded.path ? `Loaded ${loaded.path}.` : "No config file found; using built-in defaults."
621
+ };
622
+ }
623
+ catch (error) {
624
+ configCheck = {
625
+ name: "config",
626
+ ok: false,
627
+ detail: error instanceof Error ? error.message : String(error)
628
+ };
629
+ }
630
+ const checks = [
631
+ {
632
+ name: "node-version",
633
+ ok: nodeMajorVersion() >= 20,
634
+ detail: `Node ${process.version}; required >=20.`
635
+ },
636
+ {
637
+ name: "mcp-server-file",
638
+ ok: await fileExists(serverPath()),
639
+ detail: serverPath()
640
+ },
641
+ {
642
+ name: "review-engine",
643
+ ok: reviewCommand("git reset --hard").verdict === "block",
644
+ detail: "Dangerous git command is blocked."
645
+ },
646
+ configCheck
647
+ ];
648
+ const ok = checks.every((check) => check.ok);
649
+ if (options.json) {
650
+ return {
651
+ ok,
652
+ text: `${JSON.stringify({ ok, checks }, null, 2)}\n`
653
+ };
654
+ }
655
+ const lines = [
656
+ "Memento Mori Jester doctor",
657
+ "",
658
+ ...checks.map((check) => `${check.ok ? "PASS" : "FAIL"} ${check.name}: ${check.detail}`),
659
+ "",
660
+ ok
661
+ ? "The fool is fit for court."
662
+ : "Something needs fixing before the fool can be trusted with sharp objects.",
663
+ ""
664
+ ];
665
+ return { ok, text: lines.join("\n") };
666
+ }
667
+ async function ensureBootstrapConfig(options) {
668
+ const existing = await findConfigPath();
669
+ if (existing && !options.force) {
670
+ return {
671
+ path: existing,
672
+ changed: false,
673
+ message: "Kept existing config file."
674
+ };
675
+ }
676
+ const path = await writeDefaultConfig({ force: options.force, preset: options.preset });
677
+ return {
678
+ path,
679
+ changed: true,
680
+ message: "Wrote project config."
681
+ };
682
+ }
683
+ async function writeStarterFile(options) {
684
+ const path = resolve(process.cwd(), options.relativePath);
685
+ const exists = await fileExists(path);
686
+ if (exists && !options.force) {
687
+ return {
688
+ path,
689
+ changed: false,
690
+ message: `Kept existing ${options.relativePath}.`
691
+ };
692
+ }
693
+ await mkdir(dirname(path), { recursive: true });
694
+ await writeFile(path, options.content, "utf8");
695
+ return {
696
+ path,
697
+ changed: true,
698
+ message: `Wrote ${options.relativePath}.`
699
+ };
700
+ }
701
+ function renderBootstrapInstructions(options) {
702
+ const cliCommand = renderCliCommand(options);
703
+ return `# Memento Mori Jester
704
+
705
+ ## Agent Instruction
706
+
707
+ Before risky commands, final answers, commits, or large edits, call the Memento Mori Jester. Treat BLOCK as requiring a changed plan, and CAUTION as requiring at least one concrete verification step.
708
+
709
+ ## MCP
710
+
711
+ Use \`memento-mori.mcp.json\` with an MCP client, or copy the command and args from it into the client's config.
712
+
713
+ ## Local Checks
714
+
715
+ \`\`\`powershell
716
+ ${cliCommand} doctor
717
+ ${cliCommand} config validate
718
+ ${cliCommand} command "git reset --hard"
719
+ git diff | ${cliCommand} diff --fail-on block
720
+ \`\`\`
721
+
722
+ ## Git Hooks
723
+
724
+ \`\`\`powershell
725
+ ${cliCommand} install-hook pre-commit
726
+ ${cliCommand} install-hook pre-push --fail-on caution
727
+ \`\`\`
728
+ `;
729
+ }
730
+ function nodeMajorVersion() {
731
+ return Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
732
+ }
733
+ async function fileExists(path) {
734
+ try {
735
+ await access(path);
736
+ return true;
737
+ }
738
+ catch {
739
+ return false;
740
+ }
741
+ }
742
+ function helpText() {
743
+ return `Memento Mori Jester
744
+
745
+ Usage:
746
+ jester plan "I will just refactor auth and ship it"
747
+ jester command "Remove-Item .\\\\dist -Recurse -Force"
748
+ git diff | jester diff --fail-on block
749
+ jester final --file final-answer.txt --tone professional
750
+ jester init
751
+ jester bootstrap --preset node
752
+ jester doctor
753
+ jester config init
754
+ jester config init --preset security
755
+ jester config show
756
+ jester config validate
757
+ jester config presets
758
+ jester install-hook pre-commit
759
+ jester install-hook pre-push --fail-on caution
760
+ jester hook-status
761
+ jester mcp-config --mode npx
762
+ jester mcp-server
763
+
764
+ Options:
765
+ --kind <plan|command|diff|final> Review kind when using "review"
766
+ --tone <gentle_stoic|court_jester|absolute_menace|professional>
767
+ --intensity <1-5>
768
+ --risk <low|medium|high>
769
+ --fail-on <caution|block> Set a non-zero exit code at this verdict
770
+ --subject <text>
771
+ --context <text>
772
+ --file <path>
773
+ --config <path> Use a specific jester config file
774
+ --no-config Ignore jester.config.json discovery
775
+ --preset <default|node|python|security>
776
+ --json
777
+
778
+ Setup options:
779
+ --mode <npx|global|local> MCP command style; default is npx
780
+ --agent <generic|claude|codex> Label the generated setup guidance
781
+ --package <npm-or-git-spec> Package spec used by npx mode
782
+ --hook <pre-commit|pre-push> Install a hook during bootstrap; repeatable
783
+
784
+ Hook options:
785
+ --fail-on <caution|block> Hook failure threshold; defaults to config hookFailOn or block
786
+ --force Replace existing hooks or bootstrap files
787
+ `;
788
+ }
789
+ //# sourceMappingURL=cli.js.map