@fenglimg/fabric-cli 2.0.0-rc.13 → 2.0.0-rc.15

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.
@@ -10,7 +10,12 @@ var ALL_PATHS_SENTINEL = "**";
10
10
  var planContextHintCommand = defineCommand({
11
11
  meta: {
12
12
  name: "plan-context-hint",
13
- description: "Emit versioned knowledge hint JSON to stdout. Used by rc.6 hooks and the fabric-import skill."
13
+ description: "Emit versioned knowledge hint JSON to stdout. Used by rc.6 hooks and the fabric-import skill.",
14
+ // rc.15 TASK-004 (C8): hidden from `fab --help` listing. The command stays
15
+ // callable so hook scripts and the fabric-import skill can still invoke
16
+ // it via `fab plan-context-hint ...`; it just no longer appears in the
17
+ // top-level usage banner alongside install/doctor/serve/uninstall/config.
18
+ hidden: true
14
19
  },
15
20
  args: {
16
21
  paths: {
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ hasActionHint,
3
4
  paint,
5
+ renderFabricError,
4
6
  symbol
5
- } from "./chunk-WWNXR34K.js";
7
+ } from "./chunk-G2CIOLD4.js";
8
+ import {
9
+ t
10
+ } from "./chunk-6ICJICVU.js";
6
11
  import {
7
12
  createDebugLogger,
8
13
  resolveDevMode
9
14
  } from "./chunk-OBQU6NHO.js";
10
- import {
11
- t
12
- } from "./chunk-6ICJICVU.js";
13
15
 
14
16
  // src/commands/serve.ts
15
17
  import { defineCommand } from "citty";
@@ -39,11 +41,6 @@ var serveCommand = defineCommand({
39
41
  type: "boolean",
40
42
  description: t("cli.serve.args.debug.description"),
41
43
  default: false
42
- },
43
- force: {
44
- type: "boolean",
45
- description: t("cli.serve.args.force.description"),
46
- default: false
47
44
  }
48
45
  },
49
46
  async run({ args }) {
@@ -55,7 +52,15 @@ var serveCommand = defineCommand({
55
52
  const authToken = readAuthTokenFromEnv();
56
53
  const host = validateHost(requestedHost, authToken);
57
54
  const projectRoot = resolution.target;
58
- acquireLock(projectRoot, { force: args.force });
55
+ try {
56
+ acquireLock(projectRoot);
57
+ } catch (err) {
58
+ if (hasActionHint(err)) {
59
+ renderFabricError(err);
60
+ process.exit(1);
61
+ }
62
+ throw err;
63
+ }
59
64
  process.on("exit", () => {
60
65
  releaseLock(projectRoot);
61
66
  });
@@ -1,8 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- detectClientSupports,
4
- resolveClients
5
- } from "./chunk-OHWQNSLH.js";
6
2
  import {
7
3
  FABRIC_HOOK_COMMAND_PATHS,
8
4
  FABRIC_SECTION_REGEX,
@@ -11,21 +7,27 @@ import {
11
7
  HOOK_SCRIPT_DESTINATIONS,
12
8
  SECTION_TARGETS,
13
9
  SKILL_DESTINATIONS
14
- } from "./chunk-X7QPY5KH.js";
10
+ } from "./chunk-UTF4YBDN.js";
15
11
  import {
16
- paint
17
- } from "./chunk-WWNXR34K.js";
12
+ detectClientSupports,
13
+ resolveClients
14
+ } from "./chunk-SKSYUHKK.js";
18
15
  import {
19
- createDebugLogger,
20
- resolveDevMode
21
- } from "./chunk-OBQU6NHO.js";
16
+ hasActionHint,
17
+ paint,
18
+ renderFabricError
19
+ } from "./chunk-G2CIOLD4.js";
22
20
  import {
23
21
  t
24
22
  } from "./chunk-6ICJICVU.js";
23
+ import {
24
+ createDebugLogger,
25
+ resolveDevMode
26
+ } from "./chunk-OBQU6NHO.js";
25
27
 
26
28
  // src/commands/uninstall.ts
27
29
  import { existsSync as existsSync2, statSync } from "fs";
28
- import { readdir as readdir2, rm as rm2 } from "fs/promises";
30
+ import { rm as rm2 } from "fs/promises";
29
31
  import { homedir } from "os";
30
32
  import { isAbsolute, join as join2, relative, resolve, sep } from "path";
31
33
  import { cancel, confirm, group, intro, isCancel, log, note, outro } from "@clack/prompts";
@@ -80,37 +82,34 @@ async function removeHookScripts(step, rels, projectRoot) {
80
82
  }
81
83
  return results;
82
84
  }
83
- async function unmergeClaudeCodeHookConfig(projectRoot, opts = {}) {
85
+ async function unmergeClaudeCodeHookConfig(projectRoot) {
84
86
  return unmergeHookConfig({
85
87
  step: "claude-hook-config",
86
88
  projectRoot,
87
89
  configRel: HOOK_CONFIG_TARGETS.claudeCode,
88
90
  arrayPaths: [...HOOK_CONFIG_ARRAY_PATHS.claudeCode],
89
91
  fabricCommands: Object.values(FABRIC_HOOK_COMMAND_PATHS.claudeCode),
90
- extractCommands: extractClaudeCommands,
91
- cleanEmpties: opts.cleanEmpties === true
92
+ extractCommands: extractClaudeCommands
92
93
  });
93
94
  }
94
- async function unmergeCodexHookConfig(projectRoot, opts = {}) {
95
+ async function unmergeCodexHookConfig(projectRoot) {
95
96
  return unmergeHookConfig({
96
97
  step: "codex-hook-config",
97
98
  projectRoot,
98
99
  configRel: HOOK_CONFIG_TARGETS.codex,
99
100
  arrayPaths: [...HOOK_CONFIG_ARRAY_PATHS.codex],
100
101
  fabricCommands: Object.values(FABRIC_HOOK_COMMAND_PATHS.codex),
101
- extractCommands: extractFlatCommands,
102
- cleanEmpties: opts.cleanEmpties === true
102
+ extractCommands: extractFlatCommands
103
103
  });
104
104
  }
105
- async function unmergeCursorHookConfig(projectRoot, opts = {}) {
105
+ async function unmergeCursorHookConfig(projectRoot) {
106
106
  return unmergeHookConfig({
107
107
  step: "cursor-hook-config",
108
108
  projectRoot,
109
109
  configRel: HOOK_CONFIG_TARGETS.cursor,
110
110
  arrayPaths: [...HOOK_CONFIG_ARRAY_PATHS.cursor],
111
111
  fabricCommands: Object.values(FABRIC_HOOK_COMMAND_PATHS.cursor),
112
- extractCommands: extractFlatCommands,
113
- cleanEmpties: opts.cleanEmpties === true
112
+ extractCommands: extractFlatCommands
114
113
  });
115
114
  }
116
115
  async function stripFabricKnowledgeBaseSection(projectRoot) {
@@ -169,7 +168,7 @@ async function stripFabricKnowledgeBaseSection(projectRoot) {
169
168
  }
170
169
  return results;
171
170
  }
172
- async function uninstallBootstrapStage(projectRoot, opts = {}) {
171
+ async function uninstallBootstrapStage(projectRoot, _opts = {}) {
173
172
  const results = [];
174
173
  await runAndCollect(
175
174
  results,
@@ -181,19 +180,19 @@ async function uninstallBootstrapStage(projectRoot, opts = {}) {
181
180
  results,
182
181
  "cursor-hook-config",
183
182
  projectRoot,
184
- () => unmergeCursorHookConfig(projectRoot, opts)
183
+ () => unmergeCursorHookConfig(projectRoot)
185
184
  );
186
185
  await runAndCollectOne(
187
186
  results,
188
187
  "codex-hook-config",
189
188
  projectRoot,
190
- () => unmergeCodexHookConfig(projectRoot, opts)
189
+ () => unmergeCodexHookConfig(projectRoot)
191
190
  );
192
191
  await runAndCollectOne(
193
192
  results,
194
193
  "claude-hook-config",
195
194
  projectRoot,
196
- () => unmergeClaudeCodeHookConfig(projectRoot, opts)
195
+ () => unmergeClaudeCodeHookConfig(projectRoot)
197
196
  );
198
197
  await runAndCollect(
199
198
  results,
@@ -339,7 +338,7 @@ async function unmergeHookConfig(args) {
339
338
  }
340
339
  const next = JSON.parse(JSON.stringify(parsed));
341
340
  for (const dotted of args.arrayPaths) {
342
- pruneArrayAtPath(next, dotted, args.fabricCommands, args.extractCommands, args.cleanEmpties);
341
+ pruneArrayAtPath(next, dotted, args.fabricCommands, args.extractCommands);
343
342
  }
344
343
  if (jsonEqual(parsed, next)) {
345
344
  return { step: args.step, path: target, status: "skipped", message: "no-fabric-entries" };
@@ -356,7 +355,7 @@ async function unmergeHookConfig(args) {
356
355
  };
357
356
  }
358
357
  }
359
- function pruneArrayAtPath(root, path, fabricCommands, extractCommands, cleanEmpties) {
358
+ function pruneArrayAtPath(root, path, fabricCommands, extractCommands) {
360
359
  const keys = path.split(".");
361
360
  const chain = [];
362
361
  let cursor = root;
@@ -384,7 +383,7 @@ function pruneArrayAtPath(root, path, fabricCommands, extractCommands, cleanEmpt
384
383
  });
385
384
  const leaf = chain[chain.length - 1];
386
385
  leaf.parent[leaf.key] = filtered;
387
- if (!cleanEmpties || filtered.length > 0) {
386
+ if (filtered.length > 0) {
388
387
  return;
389
388
  }
390
389
  for (let i = chain.length - 1; i >= 0; i--) {
@@ -446,59 +445,24 @@ var uninstallCommand = defineCommand({
446
445
  description: t("cli.uninstall.description")
447
446
  },
448
447
  args: {
449
- target: {
450
- type: "string",
451
- description: t("cli.uninstall.args.target.description")
452
- },
453
448
  debug: {
454
449
  type: "boolean",
455
450
  description: t("cli.uninstall.args.debug.description"),
456
451
  default: false
457
452
  },
458
- force: {
453
+ "dry-run": {
459
454
  type: "boolean",
460
- description: t("cli.uninstall.args.force.description"),
455
+ description: t("cli.uninstall.args.dry-run.description"),
461
456
  default: false
462
457
  },
458
+ target: {
459
+ type: "string",
460
+ description: t("cli.uninstall.args.target.description")
461
+ },
463
462
  yes: {
464
463
  type: "boolean",
465
464
  description: t("cli.uninstall.args.yes.description"),
466
465
  default: false
467
- },
468
- plan: {
469
- type: "boolean",
470
- description: t("cli.uninstall.args.plan.description"),
471
- default: false
472
- },
473
- bootstrap: {
474
- type: "boolean",
475
- default: true,
476
- negativeDescription: t("cli.uninstall.flags.no-bootstrap")
477
- },
478
- mcp: {
479
- type: "boolean",
480
- default: true,
481
- negativeDescription: t("cli.uninstall.flags.no-mcp")
482
- },
483
- scaffold: {
484
- type: "boolean",
485
- default: true,
486
- negativeDescription: t("cli.uninstall.flags.no-scaffold")
487
- },
488
- interactive: {
489
- type: "boolean",
490
- description: t("cli.uninstall.flags.interactive"),
491
- default: true
492
- },
493
- purge: {
494
- type: "boolean",
495
- description: t("cli.uninstall.flags.purge"),
496
- default: false
497
- },
498
- "clean-empties": {
499
- type: "boolean",
500
- description: t("cli.uninstall.flags.clean-empties"),
501
- default: false
502
466
  }
503
467
  },
504
468
  async run({ args }) {
@@ -514,7 +478,15 @@ async function runUninstallCommand(args) {
514
478
  for (const step of resolution.chain) {
515
479
  logger(step);
516
480
  }
517
- checkLockOrThrow(intent.target, { force: args.force });
481
+ try {
482
+ checkLockOrThrow(intent.target);
483
+ } catch (err) {
484
+ if (hasActionHint(err)) {
485
+ renderFabricError(err);
486
+ process.exit(1);
487
+ }
488
+ throw err;
489
+ }
518
490
  const supports = detectClientSupports(intent.target);
519
491
  const basePlan = await buildUninstallExecutionPlan(intent.target, {
520
492
  ...intent.options
@@ -525,7 +497,7 @@ async function runUninstallCommand(args) {
525
497
  interactive: intent.interactiveSummary && !intent.wizardEnabled,
526
498
  supports
527
499
  };
528
- const finalPlan = intent.wizardEnabled ? await resolveUninstallExecutionPlanWithWizard(planWithSupports, args, createDefaultUninstallWizardAdapter()) : planWithSupports;
500
+ const finalPlan = intent.wizardEnabled ? await resolveUninstallExecutionPlanWithWizard(planWithSupports, createDefaultUninstallWizardAdapter()) : planWithSupports;
529
501
  if (finalPlan === null) {
530
502
  process.exitCode = 130;
531
503
  return;
@@ -541,7 +513,7 @@ async function runUninstallCommand(args) {
541
513
  }))
542
514
  };
543
515
  }
544
- if (intent.interactiveSummary && !intent.wizardEnabled && args.yes !== true && args.force !== true) {
516
+ if (intent.interactiveSummary && !intent.wizardEnabled && args.yes !== true) {
545
517
  const proceed = await confirmDestructive(finalPlan);
546
518
  if (!proceed) {
547
519
  process.exitCode = 130;
@@ -555,25 +527,19 @@ async function runUninstallCommand(args) {
555
527
  function resolveUninstallCliIntent(args, targetInput) {
556
528
  const target = normalizeTarget(targetInput);
557
529
  const terminalInteractive = isInteractiveUninstall();
558
- const planOnly = args.plan === true;
530
+ const planOnly = args["dry-run"] === true;
559
531
  const options = {
560
- force: args.force,
561
- skipBootstrap: args.bootstrap === false,
562
- skipMcp: args.mcp === false,
563
- skipScaffold: args.scaffold === false,
564
- planOnly,
565
- purge: args.purge === true,
566
- cleanEmpties: args["clean-empties"] === true
532
+ planOnly
567
533
  };
568
534
  return {
569
535
  target,
570
536
  options,
571
- interactiveSummary: args.interactive !== false && terminalInteractive,
537
+ interactiveSummary: terminalInteractive,
572
538
  wizardEnabled: shouldUseUninstallWizard(args, terminalInteractive) && !planOnly
573
539
  };
574
540
  }
575
541
  function shouldUseUninstallWizard(args, terminalInteractive = isInteractiveUninstall()) {
576
- return terminalInteractive && args.interactive !== false && args.yes !== true;
542
+ return terminalInteractive && args.yes !== true;
577
543
  }
578
544
  async function buildUninstallExecutionPlan(target, options = {}) {
579
545
  const scaffold = buildUninstallFabricPlan(target, options);
@@ -605,13 +571,6 @@ function buildUninstallFabricPlan(target, options = {}) {
605
571
  const gk = join2(fabricDir, "knowledge", sub, ".gitkeep");
606
572
  entries.push({ path: gk, kind: "gitkeep", absent: !existsSync2(gk) });
607
573
  }
608
- if (options.purge === true) {
609
- for (const sub of KNOWLEDGE_SUBDIRS) {
610
- const subdir = join2(fabricDir, "knowledge", sub);
611
- entries.push({ path: subdir, kind: "knowledge-subdir", absent: !existsSync2(subdir) });
612
- }
613
- entries.push({ path: fabricDir, kind: "fabric-dir", absent: !existsSync2(fabricDir) });
614
- }
615
574
  const safeEntries = entries.filter((entry) => !isInsidePersonalRoot(entry.path, personalKnowledgeDir));
616
575
  return {
617
576
  target: absTarget,
@@ -623,9 +582,7 @@ function buildUninstallFabricPlan(target, options = {}) {
623
582
  }
624
583
  async function executeUninstallFabricPlan(plan) {
625
584
  const results = [];
626
- const fabricDirEntry = plan.entries.find((entry) => entry.kind === "fabric-dir");
627
- const otherEntries = plan.entries.filter((entry) => entry.kind !== "fabric-dir");
628
- for (const entry of otherEntries) {
585
+ for (const entry of plan.entries) {
629
586
  if (entry.absent) {
630
587
  results.push({
631
588
  step: scaffoldStepLabel(entry.kind),
@@ -636,7 +593,7 @@ async function executeUninstallFabricPlan(plan) {
636
593
  continue;
637
594
  }
638
595
  try {
639
- await rm2(entry.path, { recursive: entry.kind === "knowledge-subdir", force: true });
596
+ await rm2(entry.path, { force: true });
640
597
  results.push({ step: scaffoldStepLabel(entry.kind), path: entry.path, status: "removed" });
641
598
  } catch (error) {
642
599
  results.push({
@@ -647,39 +604,6 @@ async function executeUninstallFabricPlan(plan) {
647
604
  });
648
605
  }
649
606
  }
650
- if (fabricDirEntry !== void 0) {
651
- const path = fabricDirEntry.path;
652
- if (!existsSync2(path)) {
653
- results.push({
654
- step: "fabric-dir",
655
- path,
656
- status: "skipped",
657
- message: "absent"
658
- });
659
- } else {
660
- try {
661
- const entries = await readdir2(path);
662
- if (entries.length > 0) {
663
- results.push({
664
- step: "fabric-dir",
665
- path,
666
- status: "skipped",
667
- message: "not-empty"
668
- });
669
- } else {
670
- await rm2(path, { recursive: true, force: true });
671
- results.push({ step: "fabric-dir", path, status: "removed" });
672
- }
673
- } catch (error) {
674
- results.push({
675
- step: "fabric-dir",
676
- path,
677
- status: "error",
678
- message: error instanceof Error ? error.message : String(error)
679
- });
680
- }
681
- }
682
- }
683
607
  return results;
684
608
  }
685
609
  function scaffoldStepLabel(kind) {
@@ -688,10 +612,6 @@ function scaffoldStepLabel(kind) {
688
612
  return "scaffold-state";
689
613
  case "gitkeep":
690
614
  return "scaffold-gitkeep";
691
- case "knowledge-subdir":
692
- return "scaffold-knowledge";
693
- case "fabric-dir":
694
- return "fabric-dir";
695
615
  }
696
616
  }
697
617
  async function uninstallMcpClients(target, options = {}) {
@@ -799,7 +719,7 @@ async function executeUninstallStage(plan, stageName) {
799
719
  case "scaffold":
800
720
  return executeUninstallFabricPlan(plan.scaffold);
801
721
  case "bootstrap": {
802
- const opts = { cleanEmpties: plan.options.cleanEmpties === true };
722
+ const opts = {};
803
723
  return uninstallBootstrapStage(plan.target, opts);
804
724
  }
805
725
  case "mcp": {
@@ -812,12 +732,12 @@ async function uninstallFabric(target, options = {}) {
812
732
  const plan = await buildUninstallExecutionPlan(target, options);
813
733
  return executeUninstallExecutionPlan(plan);
814
734
  }
815
- async function resolveUninstallExecutionPlanWithWizard(basePlan, args, wizardAdapter) {
735
+ async function resolveUninstallExecutionPlanWithWizard(basePlan, wizardAdapter) {
816
736
  const selection = await wizardAdapter.run({
817
737
  target: basePlan.target,
818
738
  options: basePlan.options,
819
739
  supports: basePlan.supports,
820
- lockedStages: collectLockedWizardStages(args)
740
+ lockedStages: []
821
741
  });
822
742
  if (selection === null) {
823
743
  return null;
@@ -826,9 +746,7 @@ async function resolveUninstallExecutionPlanWithWizard(basePlan, args, wizardAda
826
746
  ...basePlan.options,
827
747
  skipScaffold: !selection.scaffold,
828
748
  skipBootstrap: !selection.bootstrap,
829
- skipMcp: !selection.mcp,
830
- purge: selection.purge,
831
- cleanEmpties: selection.cleanEmpties
749
+ skipMcp: !selection.mcp
832
750
  };
833
751
  const rebuilt = await buildUninstallExecutionPlan(basePlan.target, nextOptions);
834
752
  return {
@@ -879,18 +797,6 @@ function createDefaultUninstallWizardAdapter() {
879
797
  defaultValue: formatPromptDefault(!context.options.skipMcp)
880
798
  }),
881
799
  initialValue: !context.options.skipMcp
882
- }),
883
- purge: async () => confirmInGroup({
884
- message: t("cli.uninstall.wizard.purge", {
885
- defaultValue: formatPromptDefault(context.options.purge === true)
886
- }),
887
- initialValue: context.options.purge === true
888
- }),
889
- cleanEmpties: async () => confirmInGroup({
890
- message: t("cli.uninstall.wizard.clean-empties", {
891
- defaultValue: formatPromptDefault(context.options.cleanEmpties === true)
892
- }),
893
- initialValue: context.options.cleanEmpties === true
894
800
  })
895
801
  },
896
802
  {
@@ -910,9 +816,7 @@ function createDefaultUninstallWizardAdapter() {
910
816
  ...context.options,
911
817
  skipScaffold: !groupedSelection.scaffold,
912
818
  skipBootstrap: !groupedSelection.bootstrap,
913
- skipMcp: !groupedSelection.mcp,
914
- purge: groupedSelection.purge,
915
- cleanEmpties: groupedSelection.cleanEmpties
819
+ skipMcp: !groupedSelection.mcp
916
820
  };
917
821
  log.step(t("cli.uninstall.wizard.step.review"));
918
822
  printUninstallPlanSummary(context.target, previewOptions, context.supports);
@@ -939,13 +843,6 @@ async function confirmInGroup(options) {
939
843
  }
940
844
  return result;
941
845
  }
942
- function collectLockedWizardStages(args) {
943
- const locked = [];
944
- if (args.scaffold === false) locked.push("scaffold");
945
- if (args.bootstrap === false) locked.push("bootstrap");
946
- if (args.mcp === false) locked.push("mcp");
947
- return locked;
948
- }
949
846
  async function confirmDestructive(plan) {
950
847
  printUninstallPlanSummary(plan.target, plan.options, plan.supports);
951
848
  const answer = await confirm({
@@ -964,9 +861,7 @@ function printUninstallPlanPreview(plan) {
964
861
  t("cli.uninstall.plan.preview-result", {
965
862
  scaffold: yesNoLabel(!plan.options.skipScaffold),
966
863
  bootstrap: yesNoLabel(!plan.options.skipBootstrap),
967
- mcp: yesNoLabel(!plan.options.skipMcp),
968
- purge: yesNoLabel(plan.options.purge === true),
969
- cleanEmpties: yesNoLabel(plan.options.cleanEmpties === true)
864
+ mcp: yesNoLabel(!plan.options.skipMcp)
970
865
  })
971
866
  );
972
867
  if (!plan.options.skipScaffold && plan.scaffold.entries.length > 0) {
@@ -984,9 +879,7 @@ function printUninstallPlanSummary(target, options, supports) {
984
879
  t("cli.uninstall.plan.actions", {
985
880
  scaffold: yesNoLabel(!options.skipScaffold),
986
881
  bootstrap: yesNoLabel(!options.skipBootstrap),
987
- mcp: yesNoLabel(!options.skipMcp),
988
- purge: yesNoLabel(options.purge === true),
989
- cleanEmpties: yesNoLabel(options.cleanEmpties === true)
882
+ mcp: yesNoLabel(!options.skipMcp)
990
883
  })
991
884
  );
992
885
  const detected = supports.filter((support) => support.detected);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-cli",
3
- "version": "2.0.0-rc.13",
3
+ "version": "2.0.0-rc.15",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "fab": "dist/index.js",
@@ -20,8 +20,8 @@
20
20
  "tree-sitter-javascript": "^0.25.0",
21
21
  "tree-sitter-typescript": "^0.23.2",
22
22
  "web-tree-sitter": "^0.26.8",
23
- "@fenglimg/fabric-shared": "2.0.0-rc.13",
24
- "@fenglimg/fabric-server": "2.0.0-rc.13"
23
+ "@fenglimg/fabric-server": "2.0.0-rc.15",
24
+ "@fenglimg/fabric-shared": "2.0.0-rc.15"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.15.0",
@@ -27,11 +27,15 @@ config (`~/.codex/config.toml`) is TOML.
27
27
 
28
28
  ## cursor-hooks.json
29
29
 
30
- Written to (or merged into) the user repo's `.cursor/hooks.json`. Mirrors the
31
- Codex `events.Stop[]` envelope shape — Cursor's hook event vocabulary is
32
- not stable across releases, so the canonical Stop-on-tool-finish lifecycle hook
33
- is the only entry we register today. SessionStart / PreToolUse slots are left
34
- unfilled for rc.6 to add when their semantics stabilise.
30
+ Written to (or merged into) the user repo's `.cursor/hooks.json`. Schema
31
+ authoritative source: https://cursor.com/cn/docs/hooks. Top-level requires
32
+ `version: 1` (number literal, NOT string) and a `hooks` object (NOT `events`)
33
+ keyed by camelCase event names: `stop`, `sessionStart`, `preToolUse`. Per-entry
34
+ shape stays flat (Codex-style): `{command, matcher?, type?, timeout?,
35
+ loop_limit?, failClosed?}`. rc.14 TASK-001 corrected rc.13's wrong top-level
36
+ envelope (was `{events: {Stop, SessionStart, PreToolUse}}` PascalCase, which
37
+ Cursor rejects with "Config version must be a number; Config hooks must be an
38
+ object").
35
39
 
36
40
  ## fabric-hint.cjs script paths
37
41
 
@@ -1,16 +1,13 @@
1
1
  {
2
- "events": {
3
- "Stop": [
4
- {
5
- "command": ".cursor/hooks/fabric-hint.cjs"
6
- }
2
+ "version": 1,
3
+ "hooks": {
4
+ "stop": [
5
+ { "command": ".cursor/hooks/fabric-hint.cjs" }
7
6
  ],
8
- "SessionStart": [
9
- {
10
- "command": ".cursor/hooks/knowledge-hint-broad.cjs"
11
- }
7
+ "sessionStart": [
8
+ { "command": ".cursor/hooks/knowledge-hint-broad.cjs" }
12
9
  ],
13
- "PreToolUse": [
10
+ "preToolUse": [
14
11
  {
15
12
  "matcher": "Edit|Write|MultiEdit",
16
13
  "command": ".cursor/hooks/knowledge-hint-narrow.cjs"