@fenglimg/fabric-cli 2.2.0-rc.1 → 2.2.0-rc.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.
Files changed (35) hide show
  1. package/dist/chunk-5LQIHYFC.js +64 -0
  2. package/dist/chunk-5ZUMLCD5.js +248 -0
  3. package/dist/chunk-EOT63RDH.js +36 -0
  4. package/dist/{chunk-AOE6AYI7.js → chunk-F6ITRM7T.js} +2 -2
  5. package/dist/{chunk-WU6GAPKH.js → chunk-H3FE6VIK.js} +3 -5
  6. package/dist/chunk-XCBVSGCS.js +25 -0
  7. package/dist/{chunk-2R55HNVD.js → chunk-XHHCRDIR.js} +71 -6
  8. package/dist/{config-XYRBZJDU.js → config-VJMXCLXW.js} +1 -1
  9. package/dist/{doctor-YONYXDX6.js → doctor-J4O3X54I.js} +118 -7
  10. package/dist/index.js +13 -12
  11. package/dist/{install-74ANPCCP.js → install-BULNDUIM.js} +159 -80
  12. package/dist/{plan-context-hint-FC6P3WFE.js → plan-context-hint-CHVZGOZ5.js} +21 -8
  13. package/dist/{scope-explain-CDIZESP5.js → scope-explain-BWRWBCCP.js} +14 -4
  14. package/dist/{status-GLQWLWH6.js → status-PANEGKU2.js} +17 -6
  15. package/dist/store-66NK2FTQ.js +443 -0
  16. package/dist/{sync-UJ4BBCZJ.js → sync-EA5HZMXM.js} +165 -21
  17. package/dist/{uninstall-C3QXKOO6.js → uninstall-F75MPKQC.js} +27 -1
  18. package/dist/{whoami-2MLO4Y37.js → whoami-66YKY5DZ.js} +16 -5
  19. package/package.json +3 -3
  20. package/templates/hooks/cite-policy-evict.cjs +412 -160
  21. package/templates/hooks/configs/claude-code.json +17 -2
  22. package/templates/hooks/configs/codex-hooks.json +14 -2
  23. package/templates/hooks/configs/cursor-hooks.json +14 -2
  24. package/templates/hooks/fabric-hint.cjs +151 -15
  25. package/templates/hooks/knowledge-hint-broad.cjs +12 -1
  26. package/templates/hooks/knowledge-hint-narrow.cjs +54 -1
  27. package/templates/hooks/post-tooluse-mutation.cjs +285 -0
  28. package/templates/hooks/session-end-marker.cjs +140 -0
  29. package/templates/skills/fabric-archive/SKILL.md +7 -1
  30. package/dist/chunk-4R2CYEA4.js +0 -116
  31. package/dist/chunk-L4Q55UC4.js +0 -52
  32. package/dist/chunk-LFIKMVY7.js +0 -27
  33. package/dist/chunk-RYAFBNES.js +0 -33
  34. package/dist/chunk-T5RPGCCM.js +0 -40
  35. package/dist/store-XB3ADT65.js +0 -144
@@ -13,6 +13,8 @@ import {
13
13
  installHookLibs,
14
14
  installKnowledgeHintBroadHook,
15
15
  installKnowledgeHintNarrowHook,
16
+ installPostTooluseMutationHook,
17
+ installSessionEndMarkerHook,
16
18
  installSharedSkillLib,
17
19
  mergeClaudeCodeHookConfig,
18
20
  mergeCodexHookConfig,
@@ -22,7 +24,7 @@ import {
22
24
  writeCodexBootstrapManagedBlock,
23
25
  writeCursorBootstrapManagedBlock,
24
26
  writeFabricAgentsSnapshot
25
- } from "./chunk-2R55HNVD.js";
27
+ } from "./chunk-XHHCRDIR.js";
26
28
  import {
27
29
  displayWidth,
28
30
  padEnd,
@@ -34,7 +36,7 @@ import {
34
36
  } from "./chunk-COI5VDFU.js";
35
37
  import {
36
38
  installMcpClients
37
- } from "./chunk-AOE6AYI7.js";
39
+ } from "./chunk-F6ITRM7T.js";
38
40
  import {
39
41
  detectClientSupports
40
42
  } from "./chunk-XC5RUHLK.js";
@@ -42,19 +44,23 @@ import {
42
44
  getProjectTranslator,
43
45
  t
44
46
  } from "./chunk-2CY4BMTH.js";
47
+ import {
48
+ syncStoreAliasLinks,
49
+ unboundAvailableStores
50
+ } from "./chunk-5ZUMLCD5.js";
45
51
  import {
46
52
  globalConfigPath,
47
53
  loadGlobalConfig,
48
54
  resolveGlobalRoot,
49
55
  saveGlobalConfig
50
- } from "./chunk-RYAFBNES.js";
56
+ } from "./chunk-XCBVSGCS.js";
51
57
 
52
58
  // src/commands/install.ts
53
59
  import { randomUUID as randomUUID2 } from "crypto";
54
60
  import { homedir } from "os";
55
61
  import * as childProcess from "child_process";
56
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync4, writeFileSync } from "fs";
57
- import { dirname, isAbsolute as isAbsolute3, join as join6, resolve as resolve3 } from "path";
62
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync4, writeFileSync as writeFileSync2 } from "fs";
63
+ import { dirname, isAbsolute as isAbsolute3, join as join7, resolve as resolve3 } from "path";
58
64
  import { cancel, confirm, group, intro, isCancel, log, note, outro, select } from "@clack/prompts";
59
65
  import { defaultAgentsMetaCounters } from "@fenglimg/fabric-shared";
60
66
  import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
@@ -79,6 +85,8 @@ async function installHooks(target, _options = {}) {
79
85
  results.push(...await runStep(() => installKnowledgeHintBroadHook(normalizedTarget)));
80
86
  results.push(...await runStep(() => installKnowledgeHintNarrowHook(normalizedTarget)));
81
87
  results.push(...await runStep(() => installCitePolicyEvictHook(normalizedTarget)));
88
+ results.push(...await runStep(() => installSessionEndMarkerHook(normalizedTarget)));
89
+ results.push(...await runStep(() => installPostTooluseMutationHook(normalizedTarget)));
82
90
  results.push(...await runStep(() => installHookLibs(normalizedTarget)));
83
91
  results.push(await runSingleStep("claude-hook-config", () => mergeClaudeCodeHookConfig(normalizedTarget)));
84
92
  results.push(await runSingleStep("codex-hook-config", () => mergeCodexHookConfig(normalizedTarget)));
@@ -94,7 +102,10 @@ function validateHookPaths(projectRoot) {
94
102
  const scripts = [
95
103
  { stepSuffix: "", hookFile: "fabric-hint.cjs" },
96
104
  { stepSuffix: "-broad", hookFile: "knowledge-hint-broad.cjs" },
97
- { stepSuffix: "-narrow", hookFile: "knowledge-hint-narrow.cjs" }
105
+ { stepSuffix: "-narrow", hookFile: "knowledge-hint-narrow.cjs" },
106
+ // lifecycle-refactor W2-T2/T3: SessionEnd + PostToolUse marker hooks.
107
+ { stepSuffix: "-session-end", hookFile: "session-end-marker.cjs" },
108
+ { stepSuffix: "-post-tooluse", hookFile: "post-tooluse-mutation.cjs" }
98
109
  ];
99
110
  const clients = [
100
111
  {
@@ -197,12 +208,50 @@ function assertExistingDirectory(target) {
197
208
  }
198
209
  }
199
210
 
211
+ // src/install/semantic-search.ts
212
+ import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
213
+ import { join as join2 } from "path";
214
+ var DEFAULT_EMBED_MODEL_PIN = "fast-bge-small-zh-v1.5";
215
+ function enableSemanticSearch(projectRoot, opts = {}) {
216
+ const model = typeof opts.model === "string" && opts.model.length > 0 ? opts.model : DEFAULT_EMBED_MODEL_PIN;
217
+ const configPath = join2(projectRoot, "fabric.config.json");
218
+ let existing = {};
219
+ if (existsSync2(configPath)) {
220
+ try {
221
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
222
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
223
+ existing = parsed;
224
+ }
225
+ } catch {
226
+ existing = {};
227
+ }
228
+ }
229
+ const alreadyEnabled = existing.embed_enabled === true && existing.embed_model === model;
230
+ if (alreadyEnabled) {
231
+ return { configPath, model, alreadyEnabled: true, changed: false };
232
+ }
233
+ const merged = { ...existing, embed_enabled: true, embed_model: model };
234
+ writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
235
+ return { configPath, model, alreadyEnabled: false, changed: true };
236
+ }
237
+ function renderSemanticSearchInstructions(model) {
238
+ return [
239
+ "\u8BED\u4E49\u641C\u7D22\u5DF2\u542F\u7528 (embed_enabled=true, embed_model=" + model + ")\u3002\u8FD8\u9700\u4E24\u6B65 (\u4E00\u6B21\u6027):",
240
+ " 1. \u5B89\u88C5\u53EF\u9009 embedder (\u88C5\u5230 MCP server \u89E3\u6790\u6A21\u5757\u7684\u4F4D\u7F6E \u2014 \u5168\u5C40\u5B89\u88C5\u5373\u5168\u5C40):",
241
+ " npm i -g fastembed",
242
+ " 2. \u9884\u70ED\u6A21\u578B\u7F13\u5B58 (\u9996\u8DD1\u4F1A\u8054\u7F51\u4E0B\u8F7D\u6A21\u578B\u6743\u91CD ~\u6570\u5341-\u6570\u767E MB, \u4E0D\u4E0A\u4F20\u4EFB\u4F55 KB \u6570\u636E):",
243
+ " export FABRIC_EMBED_CACHE_DIR=~/.cache/fabric-embed # \u4E25\u683C\u79BB\u7EBF\u8005\u9884\u5148\u653E\u597D\u6743\u91CD",
244
+ " \u6CE8: \u5207\u6362 embed_model \u540E\u5DF2\u6709\u5411\u91CF\u7EF4\u5EA6/\u8BED\u4E49\u53D8\u5316, \u4E0B\u6B21 recall \u4F1A\u6309\u65B0\u6A21\u578B\u91CD\u65B0\u5D4C\u5165 (doc \u5411\u91CF\u6309\u6587\u672C\u7F13\u5B58, \u81EA\u52A8\u5931\u914D\u91CD\u7B97)\u3002",
245
+ " \u5173\u95ED: \u7F16\u8F91 fabric.config.json \u8BBE embed_enabled=false\u3002"
246
+ ];
247
+ }
248
+
200
249
  // src/install/run-global-install.ts
201
250
  import { execFileSync as execFileSync2 } from "child_process";
202
251
  import { randomUUID } from "crypto";
203
252
  import { mkdirSync, mkdtempSync, renameSync } from "fs";
204
253
  import { tmpdir } from "os";
205
- import { join as join3 } from "path";
254
+ import { join as join4 } from "path";
206
255
  import { STORES_ROOT_DIR as STORES_ROOT_DIR2, addMountedStore, readStoreIdentity } from "@fenglimg/fabric-shared";
207
256
  import { GenericIOError } from "@fenglimg/fabric-shared/errors";
208
257
 
@@ -229,7 +278,7 @@ function deriveUid(opts = {}) {
229
278
 
230
279
  // src/install/install-global.ts
231
280
  import { rmSync } from "fs";
232
- import { join as join2 } from "path";
281
+ import { join as join3 } from "path";
233
282
  import {
234
283
  STORES_ROOT_DIR,
235
284
  globalConfigSchema,
@@ -288,7 +337,7 @@ async function installGlobalCore(options) {
288
337
  };
289
338
  }
290
339
  const alias = options.personalAlias ?? "personal";
291
- const personalDir = join2(options.globalRoot, STORES_ROOT_DIR, options.personalStoreUuid);
340
+ const personalDir = join3(options.globalRoot, STORES_ROOT_DIR, options.personalStoreUuid);
292
341
  let config = null;
293
342
  const receipt = await runInstallTransaction([
294
343
  {
@@ -339,10 +388,10 @@ function gitClone(url, dest) {
339
388
  }
340
389
  }
341
390
  function mountStoreFromRemote(url, globalRoot) {
342
- const storesRoot = join3(globalRoot, STORES_ROOT_DIR2);
391
+ const storesRoot = join4(globalRoot, STORES_ROOT_DIR2);
343
392
  mkdirSync(storesRoot, { recursive: true });
344
- const tmp = mkdtempSync(join3(tmpdir(), "fabric-clone-"));
345
- const cloneDest = join3(tmp, "store");
393
+ const tmp = mkdtempSync(join4(tmpdir(), "fabric-clone-"));
394
+ const cloneDest = join4(tmp, "store");
346
395
  gitClone(url, cloneDest);
347
396
  const identity = readStoreIdentity(cloneDest);
348
397
  if (identity === null) {
@@ -350,7 +399,7 @@ function mountStoreFromRemote(url, globalRoot) {
350
399
  actionHint: "verify the url points to a repository created by `fabric` (it must contain a store.json at its root); if you meant to mount a different store, re-run with the correct url"
351
400
  });
352
401
  }
353
- const finalDir = join3(storesRoot, identity.store_uuid);
402
+ const finalDir = join4(storesRoot, identity.store_uuid);
354
403
  renameSync(cloneDest, finalDir);
355
404
  const config = loadGlobalConfig(globalRoot);
356
405
  if (config === null) {
@@ -384,23 +433,24 @@ async function runGlobalInstall(options = {}, globalRoot = resolveGlobalRoot())
384
433
  if (options.url !== void 0) {
385
434
  mountStoreFromRemote(options.url, globalRoot);
386
435
  }
436
+ syncStoreAliasLinks(globalRoot);
387
437
  }
388
438
 
389
439
  // src/lib/detect-language.ts
390
- import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "fs";
391
- import { join as join4 } from "path";
440
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
441
+ import { join as join5 } from "path";
392
442
  function detectExistingLanguage(target) {
393
443
  const ZH_CN_RATIO_THRESHOLD = 0.3;
394
444
  const samples = [];
395
- const readmePath = join4(target, "README.md");
396
- if (existsSync2(readmePath)) {
445
+ const readmePath = join5(target, "README.md");
446
+ if (existsSync3(readmePath)) {
397
447
  try {
398
- samples.push(readFileSync(readmePath, "utf8"));
448
+ samples.push(readFileSync2(readmePath, "utf8"));
399
449
  } catch {
400
450
  }
401
451
  }
402
- const docsDir = join4(target, "docs");
403
- if (existsSync2(docsDir)) {
452
+ const docsDir = join5(target, "docs");
453
+ if (existsSync3(docsDir)) {
404
454
  try {
405
455
  const stat = statSync2(docsDir);
406
456
  if (stat.isDirectory()) {
@@ -408,7 +458,7 @@ function detectExistingLanguage(target) {
408
458
  if (!entry.isFile()) continue;
409
459
  if (!/\.(md|mdx|txt)$/iu.test(entry.name)) continue;
410
460
  try {
411
- samples.push(readFileSync(join4(docsDir, entry.name), "utf8"));
461
+ samples.push(readFileSync2(join5(docsDir, entry.name), "utf8"));
412
462
  } catch {
413
463
  }
414
464
  }
@@ -441,9 +491,9 @@ function detectExistingLanguage(target) {
441
491
 
442
492
  // src/scanner/forensic.ts
443
493
  import { execFileSync as execFileSync3 } from "child_process";
444
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
494
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
445
495
  import { createRequire } from "module";
446
- import { basename, extname, isAbsolute as isAbsolute2, join as join5, posix, relative, resolve as resolve2, sep } from "path";
496
+ import { basename, extname, isAbsolute as isAbsolute2, join as join6, posix, relative, resolve as resolve2, sep } from "path";
447
497
  import {
448
498
  buildScanRecommendations,
449
499
  forensicReportSchema
@@ -588,7 +638,7 @@ function buildTopology(root) {
588
638
  continue;
589
639
  }
590
640
  for (const entry of readdirSync2(current, { withFileTypes: true })) {
591
- const absolutePath = join5(current, entry.name);
641
+ const absolutePath = join6(current, entry.name);
592
642
  const relativePath = toPosixPath(relative(root, absolutePath));
593
643
  if (relativePath.length === 0) {
594
644
  continue;
@@ -627,7 +677,7 @@ function buildTopology(root) {
627
677
  };
628
678
  }
629
679
  function assertExistingDirectory2(target) {
630
- if (!existsSync3(target) || !statSync3(target).isDirectory()) {
680
+ if (!existsSync4(target) || !statSync3(target).isDirectory()) {
631
681
  throw new Error(`Target must be an existing directory: ${target}`);
632
682
  }
633
683
  }
@@ -676,7 +726,7 @@ function getEntryPointReason(relativePath) {
676
726
  async function buildCodeSamples(target, entryPoints, frameworkKind, topology, packageDependencies) {
677
727
  const samples = [];
678
728
  for (const entryPoint of entryPoints.slice(0, SAMPLE_LIMIT)) {
679
- const absolutePath = join5(target, ...entryPoint.path.split("/"));
729
+ const absolutePath = join6(target, ...entryPoint.path.split("/"));
680
730
  const sample = readFirstLines(absolutePath, SAMPLE_LINE_LIMIT);
681
731
  const patternAnalysis = await inferPatternHint(entryPoint.path, sample.snippet, {
682
732
  frameworkKind,
@@ -696,7 +746,7 @@ async function buildCodeSamples(target, entryPoints, frameworkKind, topology, pa
696
746
  }
697
747
  function readFirstLines(path, lineLimit) {
698
748
  try {
699
- const lines = readFileSync2(path, "utf8").split(/\r?\n/);
749
+ const lines = readFileSync3(path, "utf8").split(/\r?\n/);
700
750
  if (lines.at(-1) === "") {
701
751
  lines.pop();
702
752
  }
@@ -713,12 +763,12 @@ function readFirstLines(path, lineLimit) {
713
763
  }
714
764
  }
715
765
  function readPackageDependencies(target) {
716
- const packageJsonPath = join5(target, "package.json");
717
- if (!existsSync3(packageJsonPath)) {
766
+ const packageJsonPath = join6(target, "package.json");
767
+ if (!existsSync4(packageJsonPath)) {
718
768
  return /* @__PURE__ */ new Map();
719
769
  }
720
770
  try {
721
- const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
771
+ const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
722
772
  return new Map([
723
773
  ...Object.entries(packageJson.dependencies ?? {}),
724
774
  ...Object.entries(packageJson.devDependencies ?? {}),
@@ -1054,16 +1104,16 @@ function scoreFrameworkConfidence(input) {
1054
1104
  return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "LOW";
1055
1105
  }
1056
1106
  function readReadmeInfo(target) {
1057
- const readmePath = join5(target, "README.md");
1058
- const hasContributing = existsSync3(join5(target, "CONTRIBUTING.md"));
1059
- if (!existsSync3(readmePath)) {
1107
+ const readmePath = join6(target, "README.md");
1108
+ const hasContributing = existsSync4(join6(target, "CONTRIBUTING.md"));
1109
+ if (!existsSync4(readmePath)) {
1060
1110
  return {
1061
1111
  quality: "missing",
1062
1112
  line_count: 0,
1063
1113
  has_contributing: hasContributing
1064
1114
  };
1065
1115
  }
1066
- const readme = readFileSync2(readmePath, "utf8");
1116
+ const readme = readFileSync3(readmePath, "utf8");
1067
1117
  const wordCount = readme.trim().split(/\s+/).filter(Boolean).length;
1068
1118
  return {
1069
1119
  quality: wordCount >= 200 ? "ok" : "stub",
@@ -1529,10 +1579,10 @@ function buildSkillRecommendations(frameworkKind, topology, readme, projectRoot)
1529
1579
  );
1530
1580
  }
1531
1581
  function readProjectName(target) {
1532
- const packageJsonPath = join5(target, "package.json");
1533
- if (existsSync3(packageJsonPath)) {
1582
+ const packageJsonPath = join6(target, "package.json");
1583
+ if (existsSync4(packageJsonPath)) {
1534
1584
  try {
1535
- const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
1585
+ const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
1536
1586
  if (packageJson.name !== void 0 && packageJson.name.trim().length > 0) {
1537
1587
  return packageJson.name;
1538
1588
  }
@@ -1543,7 +1593,7 @@ function readProjectName(target) {
1543
1593
  return basename(target);
1544
1594
  }
1545
1595
  function getCliVersion() {
1546
- return true ? "2.2.0-rc.1" : "unknown";
1596
+ return true ? "2.2.0-rc.3" : "unknown";
1547
1597
  }
1548
1598
  function sortRecord(record) {
1549
1599
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
@@ -1553,7 +1603,7 @@ function toPosixPath(path) {
1553
1603
  }
1554
1604
 
1555
1605
  // src/commands/install.ts
1556
- var LOCAL_FABRIC_SERVER_PATH = join6("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
1606
+ var LOCAL_FABRIC_SERVER_PATH = join7("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
1557
1607
  var FABRIC_SERVER_PACKAGE = "@fenglimg/fabric-server";
1558
1608
  var INIT_WIZARD_GROUP_CANCELLED = /* @__PURE__ */ Symbol("init-wizard-group-cancelled");
1559
1609
  var installCommand = defineCommand({
@@ -1599,6 +1649,15 @@ var installCommand = defineCommand({
1599
1649
  url: {
1600
1650
  type: "string",
1601
1651
  description: "With --global: clone + mount this shared store remote"
1652
+ },
1653
+ "enable-embed": {
1654
+ type: "boolean",
1655
+ description: t("cli.install.args.enable-embed.description"),
1656
+ default: false
1657
+ },
1658
+ "embed-model": {
1659
+ type: "string",
1660
+ description: t("cli.install.args.embed-model.description")
1602
1661
  }
1603
1662
  },
1604
1663
  async run({ args }) {
@@ -1608,8 +1667,8 @@ var installCommand = defineCommand({
1608
1667
  var install_default = installCommand;
1609
1668
  async function runSkillsOnlyRefresh(targetInput) {
1610
1669
  const target = normalizeTarget3(targetInput);
1611
- const metaPath = join6(target, ".fabric", "agents.meta.json");
1612
- if (!existsSync4(metaPath)) {
1670
+ const metaPath = join7(target, ".fabric", "agents.meta.json");
1671
+ if (!existsSync5(metaPath)) {
1613
1672
  const message = t("cli.install.force-skills-only.uninitialised.message");
1614
1673
  const hint = t("cli.install.force-skills-only.uninitialised.hint");
1615
1674
  process.stderr.write(`${message}
@@ -1656,8 +1715,8 @@ ${hint}
1656
1715
  }
1657
1716
  async function runHooksOnlyRefresh(targetInput) {
1658
1717
  const target = normalizeTarget3(targetInput);
1659
- const metaPath = join6(target, ".fabric", "agents.meta.json");
1660
- if (!existsSync4(metaPath)) {
1718
+ const metaPath = join7(target, ".fabric", "agents.meta.json");
1719
+ if (!existsSync5(metaPath)) {
1661
1720
  const message = t("cli.install.force-hooks-only.uninitialised.message");
1662
1721
  const hint = t("cli.install.force-hooks-only.uninitialised.hint");
1663
1722
  process.stderr.write(`${message}
@@ -1703,6 +1762,10 @@ async function runInitCommand(args) {
1703
1762
  for (const step of resolution.chain) {
1704
1763
  logger(step);
1705
1764
  }
1765
+ if (loadGlobalConfig() === null) {
1766
+ logger("no global Fabric config found \u2014 minting ~/.fabric (uid + personal store)");
1767
+ await runGlobalInstall({});
1768
+ }
1706
1769
  const supports = detectClientSupports(intent.target);
1707
1770
  const basePlan = await buildInitExecutionPlan({
1708
1771
  target: intent.target,
@@ -1723,6 +1786,27 @@ async function runInitCommand(args) {
1723
1786
  console.log(t("cli.install.next-steps"));
1724
1787
  console.log("");
1725
1788
  console.log(paint.muted("More: docs/surfaces.md explains when to use CLI vs Skill vs MCP."));
1789
+ const unboundStores = unboundAvailableStores(resolution.target);
1790
+ if (unboundStores.length > 0) {
1791
+ console.log("");
1792
+ console.log(
1793
+ t("cli.install.store-bind-nudge", {
1794
+ aliases: unboundStores.map((s) => `'${s.alias}'`).join(", "),
1795
+ first: unboundStores[0].alias
1796
+ })
1797
+ );
1798
+ }
1799
+ if (args["enable-embed"] === true) {
1800
+ const enabled = enableSemanticSearch(resolution.target, { model: args["embed-model"] });
1801
+ console.log("");
1802
+ if (enabled.alreadyEnabled) {
1803
+ console.log(paint.muted(`\u8BED\u4E49\u641C\u7D22\u5DF2\u662F\u542F\u7528\u72B6\u6001 (embed_model=${enabled.model})\uFF0C\u672A\u6539\u52A8 ${enabled.configPath}\u3002`));
1804
+ } else {
1805
+ for (const line of renderSemanticSearchInstructions(enabled.model)) {
1806
+ console.log(line);
1807
+ }
1808
+ }
1809
+ }
1726
1810
  }
1727
1811
  return result;
1728
1812
  }
@@ -1739,14 +1823,14 @@ var FABRIC_GITIGNORE_CONTENT = [
1739
1823
  ""
1740
1824
  ].join("\n");
1741
1825
  function writeDefaultGitignore(fabricDir) {
1742
- const target = join6(fabricDir, ".gitignore");
1743
- if (existsSync4(target)) return;
1826
+ const target = join7(fabricDir, ".gitignore");
1827
+ if (existsSync5(target)) return;
1744
1828
  mkdirSync2(fabricDir, { recursive: true });
1745
- writeFileSync(target, FABRIC_GITIGNORE_CONTENT, "utf8");
1829
+ writeFileSync2(target, FABRIC_GITIGNORE_CONTENT, "utf8");
1746
1830
  }
1747
1831
  function writeDefaultFabricConfig(fabricDir, targetRoot) {
1748
- const target = join6(fabricDir, "fabric-config.json");
1749
- if (existsSync4(target)) return;
1832
+ const target = join7(fabricDir, "fabric-config.json");
1833
+ if (existsSync5(target)) return;
1750
1834
  const detectedLanguage = detectExistingLanguage(targetRoot);
1751
1835
  const FABRIC_CONFIG_DEFAULTS = {
1752
1836
  // Scan/import language policy. Fixated at init time by probing
@@ -1808,7 +1892,7 @@ function writeDefaultFabricConfig(fabricDir, targetRoot) {
1808
1892
  review_stale_pending_days: 14
1809
1893
  };
1810
1894
  mkdirSync2(fabricDir, { recursive: true });
1811
- writeFileSync(target, JSON.stringify(FABRIC_CONFIG_DEFAULTS, null, 2) + "\n", "utf8");
1895
+ writeFileSync2(target, JSON.stringify(FABRIC_CONFIG_DEFAULTS, null, 2) + "\n", "utf8");
1812
1896
  log.info(
1813
1897
  `Detected and fixated fabric_language = ${detectedLanguage}; edit ${target} to override.`
1814
1898
  );
@@ -1885,7 +1969,7 @@ async function executeInitExecutionPlan(plan) {
1885
1969
  finalSupports: plan.supports
1886
1970
  };
1887
1971
  }
1888
- if (existsSync4(plan.scaffold.fabricDir) && !statSync4(plan.scaffold.fabricDir).isDirectory()) {
1972
+ if (existsSync5(plan.scaffold.fabricDir) && !statSync4(plan.scaffold.fabricDir).isDirectory()) {
1889
1973
  throw new Error(
1890
1974
  t("cli.install.diff.drift-abort", { path: plan.scaffold.fabricDir })
1891
1975
  );
@@ -1938,16 +2022,16 @@ function resolvePersonalFabricRoot() {
1938
2022
  }
1939
2023
  async function buildInitFabricPlan(target, options) {
1940
2024
  assertExistingDirectory3(target);
1941
- const fabricDir = join6(target, ".fabric");
1942
- const agentsMdPath = join6(target, "AGENTS.md");
1943
- const agentsMdAction = existsSync4(agentsMdPath) ? "preserved" : "created";
1944
- const knowledgeDir = join6(fabricDir, "knowledge");
1945
- const personalKnowledgeDir = join6(resolvePersonalFabricRoot(), ".fabric", "knowledge");
1946
- const forensicPath = join6(fabricDir, "forensic.json");
1947
- const eventsPath = join6(fabricDir, "events.jsonl");
1948
- const metaPath = join6(fabricDir, "agents.meta.json");
2025
+ const fabricDir = join7(target, ".fabric");
2026
+ const agentsMdPath = join7(target, "AGENTS.md");
2027
+ const agentsMdAction = existsSync5(agentsMdPath) ? "preserved" : "created";
2028
+ const knowledgeDir = join7(fabricDir, "knowledge");
2029
+ const personalKnowledgeDir = join7(resolvePersonalFabricRoot(), ".fabric", "knowledge");
2030
+ const forensicPath = join7(fabricDir, "forensic.json");
2031
+ const eventsPath = join7(fabricDir, "events.jsonl");
2032
+ const metaPath = join7(fabricDir, "agents.meta.json");
1949
2033
  const replaceFabricDir = shouldReplaceWritableDirectory(fabricDir, options);
1950
- const knowledgeDirAction = existsSync4(knowledgeDir) ? "overwritten" : "created";
2034
+ const knowledgeDirAction = existsSync5(knowledgeDir) ? "overwritten" : "created";
1951
2035
  const metaClassification = classifyFreshPath(metaPath, "structural");
1952
2036
  const eventsClassification = classifyFreshPath(eventsPath, "presence");
1953
2037
  const forensicClassification = classifyFreshPath(forensicPath, "always-rewrite");
@@ -1997,20 +2081,13 @@ async function executeInitFabricPlan(plan) {
1997
2081
  writeDefaultGitignore(plan.fabricDir);
1998
2082
  mkdirSync2(plan.knowledgeDir, { recursive: true });
1999
2083
  for (const sub of KNOWLEDGE_SUBDIRS) {
2000
- const teamSubDir = join6(plan.knowledgeDir, sub);
2084
+ const teamSubDir = join7(plan.knowledgeDir, sub);
2001
2085
  mkdirSync2(teamSubDir, { recursive: true });
2002
- const teamGitkeep = join6(teamSubDir, ".gitkeep");
2003
- if (!existsSync4(teamGitkeep)) {
2004
- writeFileSync(teamGitkeep, "", "utf8");
2086
+ const teamGitkeep = join7(teamSubDir, ".gitkeep");
2087
+ if (!existsSync5(teamGitkeep)) {
2088
+ writeFileSync2(teamGitkeep, "", "utf8");
2005
2089
  }
2006
2090
  }
2007
- try {
2008
- mkdirSync2(plan.personalKnowledgeDir, { recursive: true });
2009
- for (const sub of KNOWLEDGE_SUBDIRS) {
2010
- mkdirSync2(join6(plan.personalKnowledgeDir, sub), { recursive: true });
2011
- }
2012
- } catch {
2013
- }
2014
2091
  if (plan.metaState === "missing") {
2015
2092
  preparePlannedPath(plan.metaPath, plan.metaAction);
2016
2093
  await atomicWriteJson(plan.metaPath, plan.meta);
@@ -2018,11 +2095,11 @@ async function executeInitFabricPlan(plan) {
2018
2095
  if (plan.eventsState === "missing") {
2019
2096
  preparePlannedPath(plan.eventsPath, plan.eventsAction);
2020
2097
  mkdirSync2(dirname(plan.eventsPath), { recursive: true });
2021
- writeFileSync(plan.eventsPath, "", "utf8");
2098
+ writeFileSync2(plan.eventsPath, "", "utf8");
2022
2099
  }
2023
2100
  preparePlannedPath(plan.forensicPath, plan.forensicAction);
2024
2101
  await atomicWriteJson(plan.forensicPath, plan.forensicReport);
2025
- if (existsSync4(plan.eventsPath)) {
2102
+ if (existsSync5(plan.eventsPath)) {
2026
2103
  const applied = [];
2027
2104
  const canonical = [];
2028
2105
  const drifted = [];
@@ -2183,6 +2260,8 @@ async function executeInitStagePlan(plan, stageName) {
2183
2260
  installResults.push(...await runBestEffort("hook-broad-script", () => installKnowledgeHintBroadHook(plan.target)));
2184
2261
  installResults.push(...await runBestEffort("hook-narrow-script", () => installKnowledgeHintNarrowHook(plan.target)));
2185
2262
  installResults.push(...await runBestEffort("hook-cite-policy-evict-script", () => installCitePolicyEvictHook(plan.target)));
2263
+ installResults.push(...await runBestEffort("hook-session-end-script", () => installSessionEndMarkerHook(plan.target)));
2264
+ installResults.push(...await runBestEffort("hook-post-tooluse-script", () => installPostTooluseMutationHook(plan.target)));
2186
2265
  installResults.push(...await runBestEffort("hook-lib", () => installHookLibs(plan.target)));
2187
2266
  installResults.push(await runBestEffortSingle("claude-hook-config", () => mergeClaudeCodeHookConfig(plan.target)));
2188
2267
  installResults.push(await runBestEffortSingle("codex-hook-config", () => mergeCodexHookConfig(plan.target)));
@@ -2238,7 +2317,7 @@ async function executeInitStagePlan(plan, stageName) {
2238
2317
  }
2239
2318
  }
2240
2319
  function shouldReplaceWritableDirectory(path, _options) {
2241
- if (!existsSync4(path)) {
2320
+ if (!existsSync5(path)) {
2242
2321
  return false;
2243
2322
  }
2244
2323
  if (statSync4(path).isDirectory()) {
@@ -2247,7 +2326,7 @@ function shouldReplaceWritableDirectory(path, _options) {
2247
2326
  return false;
2248
2327
  }
2249
2328
  function classifyFreshPath(path, strategy) {
2250
- if (!existsSync4(path)) {
2329
+ if (!existsSync5(path)) {
2251
2330
  return { path, state: "missing" };
2252
2331
  }
2253
2332
  let stat;
@@ -2267,7 +2346,7 @@ function classifyFreshPath(path, strategy) {
2267
2346
  return { path, state: "present-canonical" };
2268
2347
  }
2269
2348
  try {
2270
- const raw = readFileSync3(path, "utf8");
2349
+ const raw = readFileSync4(path, "utf8");
2271
2350
  const parsed = JSON.parse(raw);
2272
2351
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2273
2352
  return { path, state: "user-modified", reason: "not a JSON object" };
@@ -2296,7 +2375,7 @@ function formatDiffFileState(state) {
2296
2375
  }
2297
2376
  function preparePlannedPath(path, action) {
2298
2377
  mkdirSync2(dirname(path), { recursive: true });
2299
- if (action === "overwritten" && existsSync4(path)) {
2378
+ if (action === "overwritten" && existsSync5(path)) {
2300
2379
  rmSync2(path, { recursive: true, force: true });
2301
2380
  }
2302
2381
  }
@@ -2450,19 +2529,19 @@ function normalizeTarget3(targetInput) {
2450
2529
  return isAbsolute3(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
2451
2530
  }
2452
2531
  function assertExistingDirectory3(target) {
2453
- if (!existsSync4(target) || !statSync4(target).isDirectory()) {
2532
+ if (!existsSync5(target) || !statSync4(target).isDirectory()) {
2454
2533
  throw new Error(`Target must be an existing directory: ${target}`);
2455
2534
  }
2456
2535
  }
2457
2536
  function detectPackageManager(cwd) {
2458
2537
  const workspaceRoot = resolve3(cwd);
2459
- if (existsSync4(join6(workspaceRoot, "pnpm-lock.yaml"))) {
2538
+ if (existsSync5(join7(workspaceRoot, "pnpm-lock.yaml"))) {
2460
2539
  return "pnpm";
2461
2540
  }
2462
- if (existsSync4(join6(workspaceRoot, "yarn.lock"))) {
2541
+ if (existsSync5(join7(workspaceRoot, "yarn.lock"))) {
2463
2542
  return "yarn";
2464
2543
  }
2465
- if (existsSync4(join6(workspaceRoot, "package-lock.json"))) {
2544
+ if (existsSync5(join7(workspaceRoot, "package-lock.json"))) {
2466
2545
  return "npm";
2467
2546
  }
2468
2547
  return "npm";
@@ -56,16 +56,29 @@ async function runPlanContextHint(opts) {
56
56
  const targetPaths = all ? [ALL_PATHS_SENTINEL] : explicitPaths.length > 0 ? explicitPaths : [ALL_PATHS_SENTINEL];
57
57
  const resolution = resolveDevMode(opts.target, process.cwd());
58
58
  const result = await planContext(resolution.target, {
59
- paths: targetPaths
59
+ paths: targetPaths,
60
+ // lifecycle-refactor W3-T2 (§7 图谱消费 / §5): default-enable graph二阶召回 for
61
+ // the hint path. planContext appends the one-hop `related` neighbours that
62
+ // ranked outside top_k of the surfaced set and reports them in
63
+ // `related_appended` (appended id → source id). Honest no-op when the
64
+ // surfaced set declares no in-corpus related edge.
65
+ include_related: true
60
66
  });
61
67
  const candidates = result.candidates;
62
- const entries = candidates.map((item) => ({
63
- id: item.stable_id,
64
- type: item.description.knowledge_type ?? "",
65
- maturity: item.description.maturity ?? "",
66
- summary: item.description.summary,
67
- relevance_scope: item.description.relevance_scope ?? "broad"
68
- }));
68
+ const relatedAppended = result.related_appended ?? {};
69
+ const entries = candidates.map((item) => {
70
+ const relatedTo = relatedAppended[item.stable_id];
71
+ return {
72
+ id: item.stable_id,
73
+ type: item.description.knowledge_type ?? "",
74
+ maturity: item.description.maturity ?? "",
75
+ summary: item.description.summary,
76
+ relevance_scope: item.description.relevance_scope ?? "broad",
77
+ // Only set when this entry was pulled in via a graph edge — its presence
78
+ // is the honest signal, never synthesized for ordinarily-ranked entries.
79
+ ...typeof relatedTo === "string" ? { related_to: relatedTo } : {}
80
+ };
81
+ });
69
82
  let narrow_count = 0;
70
83
  let broad_only_count = 0;
71
84
  for (const e of entries) {
@@ -1,15 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scopeExplain
4
- } from "./chunk-L4Q55UC4.js";
4
+ } from "./chunk-EOT63RDH.js";
5
5
  import {
6
6
  getProjectTranslator
7
7
  } from "./chunk-2CY4BMTH.js";
8
- import "./chunk-LFIKMVY7.js";
9
- import "./chunk-RYAFBNES.js";
10
8
 
11
9
  // src/commands/scope-explain.ts
12
10
  import { defineCommand } from "citty";
11
+ import { FabricError } from "@fenglimg/fabric-shared/errors";
13
12
  var scope_explain_default = defineCommand({
14
13
  meta: {
15
14
  name: "scope-explain",
@@ -24,7 +23,18 @@ var scope_explain_default = defineCommand({
24
23
  },
25
24
  run({ args }) {
26
25
  const projectRoot = process.cwd();
27
- const result = scopeExplain(projectRoot, args.scope);
26
+ let result;
27
+ try {
28
+ result = scopeExplain(projectRoot, args.scope);
29
+ } catch (error) {
30
+ if (error instanceof FabricError) {
31
+ console.error(`${error.message}
32
+ \u2192 ${error.actionHint}`);
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ throw error;
37
+ }
28
38
  if (result === null) {
29
39
  console.log(getProjectTranslator(projectRoot)("cli.cmd.no-global-config"));
30
40
  return;