@fenglimg/fabric-cli 2.2.0-rc.1 → 2.2.0-rc.10

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 (83) hide show
  1. package/README.md +8 -5
  2. package/dist/chunk-27HK6H5Y.js +69 -0
  3. package/dist/{chunk-AOE6AYI7.js → chunk-2KBCTMID.js} +31 -8
  4. package/dist/chunk-3D7B2UAZ.js +149 -0
  5. package/dist/{chunk-XC5RUHLK.js → chunk-3IOLS5EK.js} +23 -38
  6. package/dist/{plan-context-hint-FC6P3WFE.js → chunk-722JU5BP.js} +52 -12
  7. package/dist/{chunk-2R55HNVD.js → chunk-7ZDXBOOU.js} +234 -206
  8. package/dist/{doctor-YONYXDX6.js → chunk-E7HJUU34.js} +215 -52
  9. package/dist/chunk-EOT63RDH.js +36 -0
  10. package/dist/chunk-FNHDQTPC.js +16 -0
  11. package/dist/{chunk-2CY4BMTH.js → chunk-HORSMSZL.js} +9 -5
  12. package/dist/{chunk-BO4XIZWZ.js → chunk-NLNH64A3.js} +5 -18
  13. package/dist/{chunk-WU6GAPKH.js → chunk-PTGQAZEW.js} +12 -4
  14. package/dist/chunk-QFIVFZRH.js +13 -0
  15. package/dist/chunk-QPAW6IYT.js +387 -0
  16. package/dist/{chunk-COI5VDFU.js → chunk-WA3DYGSY.js} +1 -2
  17. package/dist/{config-XYRBZJDU.js → config-A3LTECAY.js} +4 -3
  18. package/dist/context-UJCGYOT6.js +117 -0
  19. package/dist/doctor-MDTZWKBK.js +24 -0
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +133 -22
  22. package/dist/info-7FKBTMVO.js +139 -0
  23. package/dist/install-v2-RINEA24K.js +3279 -0
  24. package/dist/{metrics-RER6NLFC.js → metrics-HMFH4YHK.js} +1 -1
  25. package/dist/{onboard-coverage-JWQWDZW7.js → onboard-coverage-XSG77LL3.js} +48 -27
  26. package/dist/plan-context-hint-5TNGH3R4.js +12 -0
  27. package/dist/{scope-explain-CDIZESP5.js → scope-explain-HLJZ2M33.js} +17 -6
  28. package/dist/status-4R3TM4FJ.js +37 -0
  29. package/dist/store-HOCORVL3.js +563 -0
  30. package/dist/{sync-UJ4BBCZJ.js → sync-DT5UJMMR.js} +197 -30
  31. package/dist/{uninstall-C3QXKOO6.js → uninstall-IFN2KYBK.js} +97 -140
  32. package/dist/whoami-ITGEFWH4.js +49 -0
  33. package/package.json +7 -5
  34. package/templates/hooks/cite-policy-evict.cjs +412 -160
  35. package/templates/hooks/configs/README.md +14 -27
  36. package/templates/hooks/configs/claude-code.json +17 -2
  37. package/templates/hooks/configs/codex-hooks.json +15 -3
  38. package/templates/hooks/fabric-hint.cjs +477 -176
  39. package/templates/hooks/knowledge-hint-broad.cjs +577 -274
  40. package/templates/hooks/knowledge-hint-narrow.cjs +113 -73
  41. package/templates/hooks/lib/banner-i18n.cjs +31 -0
  42. package/templates/hooks/lib/bindings-snapshot-reader.cjs +118 -7
  43. package/templates/hooks/lib/cite-line-parser.cjs +12 -20
  44. package/templates/hooks/lib/client-adapter.cjs +66 -7
  45. package/templates/hooks/lib/nudge-policy.cjs +117 -0
  46. package/templates/hooks/lib/state-store.cjs +60 -0
  47. package/templates/hooks/post-tooluse-mutation.cjs +386 -0
  48. package/templates/hooks/session-end-marker.cjs +140 -0
  49. package/templates/skills/fabric/SKILL.md +100 -0
  50. package/templates/skills/fabric-archive/SKILL.md +35 -24
  51. package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
  52. package/templates/skills/fabric-archive/ref/i18n-policy.md +2 -3
  53. package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +2 -3
  54. package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +1 -1
  55. package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +1 -1
  56. package/templates/skills/fabric-archive/ref/phase-3-6-related-edges.md +18 -0
  57. package/templates/skills/fabric-archive/ref/phase-3-7-semantic-scope.md +47 -0
  58. package/templates/skills/fabric-audit/SKILL.md +13 -3
  59. package/templates/skills/fabric-connect/SKILL.md +3 -3
  60. package/templates/skills/fabric-import/SKILL.md +7 -7
  61. package/templates/skills/fabric-import/ref/i18n-policy.md +2 -3
  62. package/templates/skills/fabric-import/ref/state-recovery.md +1 -2
  63. package/templates/skills/fabric-review/SKILL.md +14 -5
  64. package/templates/skills/fabric-review/ref/cite-contract.md +1 -1
  65. package/templates/skills/fabric-review/ref/i18n-policy.md +2 -3
  66. package/templates/skills/fabric-review/ref/output-contract.md +1 -1
  67. package/templates/skills/fabric-review/ref/per-mode-flows.md +2 -2
  68. package/templates/skills/fabric-review/ref/worked-examples.md +1 -1
  69. package/templates/skills/fabric-store/SKILL.md +1 -1
  70. package/templates/skills/fabric-sync/SKILL.md +1 -1
  71. package/templates/skills/lib/shared-policy.md +2 -2
  72. package/dist/chunk-4R2CYEA4.js +0 -116
  73. package/dist/chunk-L4Q55UC4.js +0 -52
  74. package/dist/chunk-LFIKMVY7.js +0 -27
  75. package/dist/chunk-RYAFBNES.js +0 -33
  76. package/dist/chunk-T5RPGCCM.js +0 -40
  77. package/dist/install-74ANPCCP.js +0 -2737
  78. package/dist/status-GLQWLWH6.js +0 -23
  79. package/dist/store-XB3ADT65.js +0 -144
  80. package/dist/whoami-2MLO4Y37.js +0 -36
  81. package/templates/hooks/configs/cursor-hooks.json +0 -18
  82. package/templates/hooks/lib/cite-contract-reminder.cjs +0 -179
  83. package/templates/hooks/lib/summary-fallback.cjs +0 -210
package/README.md CHANGED
@@ -7,19 +7,22 @@
7
7
  1. 在 monorepo 根目录运行 `pnpm install`。
8
8
  2. 用 `pnpm --filter @fenglimg/fabric-cli build` 构建 CLI。
9
9
  3. 在目标项目运行 `fabric install`,完成一站式安装。
10
- 4. 启动 `fabric serve`,再去客户端里验证 `fab_plan_context` `fab_get_knowledge_sections`。
10
+ 4. 重启 Claude Code / Codex CLI,在客户端里验证 `fab_recall`。
11
11
 
12
- `fabric install` 会自动准备 bootstrap、MCP 配置和 git hooks。公共命令面只保留 `install`、`doctor`、`serve`、`uninstall`、`config`(rc.23 起移除了 baseline scan 机制,知识库唯一合法来源是 Skill 路径:`fabric-archive` / `fabric-import` / `fabric-review`)。
12
+ `fabric install` 会自动准备 bootstrap、MCP stdio 配置和 git hooks。当前公共命令面包括 `install`、`store`、`sync`、`info`、`doctor`、`uninstall`、`config`;`metrics`、`plan-context-hint`、`onboard-coverage` hidden/internal 命令;`whoami` / `status` / `scope-explain` 作为 deprecated alias 保留到 v3。`fabric serve` 已 quarantine 到 `packages/server-http-experimental/`,主线不再注册。
13
13
 
14
14
  ## 常用命令
15
15
 
16
16
  - `fabric install`
17
17
  - `fabric doctor`
18
18
  - `fabric doctor --json`
19
- - `fabric doctor --strict`
20
19
  - `fabric doctor --fix`
21
- - `fabric serve`
20
+ - `fabric doctor --fix-knowledge`
21
+ - `fabric store list`
22
+ - `fabric sync`
23
+ - `fabric info`
24
+ - `fabric metrics`(hidden/internal)
22
25
  - `fabric uninstall`
23
26
  - `fabric config`(rc.16 起将提供配置面板;当前为占位提示)
24
27
 
25
- `fabric doctor --fix` 只修复确定性的派生状态,例如 `.fabric/agents.meta.json`、`.fabric/.cache/knowledge-test.index.json`、缺失的 `.fabric/events.jsonl` 和 stale hashes;语义冲突、缺失 rule section、未完成的初始化确认仍需要人工处理。
28
+ `fabric doctor --fix` 只修复确定性的派生状态,例如 `.fabric/agents.meta.json`、`.fabric/.cache/knowledge-test.index.json`、缺失的 `.fabric/events.jsonl` 和 stale hashes。知识条目的 demote/archive/default backfill 走 `fabric doctor --fix-knowledge` 或 `fabric-review`;语义冲突、未完成的初始化确认和本地客户端配置问题仍需要人工处理。
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ storeGitRemote
4
+ } from "./chunk-QPAW6IYT.js";
5
+ import {
6
+ loadProjectConfig
7
+ } from "./chunk-QFIVFZRH.js";
8
+ import {
9
+ loadGlobalConfig,
10
+ resolveGlobalRoot
11
+ } from "./chunk-FNHDQTPC.js";
12
+
13
+ // src/lib/unknown-flags.ts
14
+ function warnUnknownFlags(known) {
15
+ const knownSet = /* @__PURE__ */ new Set([...known, "help", "version"]);
16
+ const unknown = [];
17
+ for (const tok of process.argv.slice(2)) {
18
+ if (!tok.startsWith("--")) continue;
19
+ const name = tok.slice(2).split("=")[0].replace(/^no-/, "");
20
+ if (name.length === 0) continue;
21
+ if (!knownSet.has(name)) unknown.push(tok.split("=")[0]);
22
+ }
23
+ if (unknown.length > 0) {
24
+ process.stderr.write(`[fabric] ignored unknown flag(s): ${unknown.join(", ")}
25
+ `);
26
+ }
27
+ }
28
+
29
+ // src/store/info-ops.ts
30
+ function whoami(globalRoot = resolveGlobalRoot()) {
31
+ const config = loadGlobalConfig(globalRoot);
32
+ if (config === null) {
33
+ return null;
34
+ }
35
+ return {
36
+ uid: config.uid,
37
+ stores: config.stores.map((s) => ({
38
+ alias: s.alias,
39
+ mount_name: s.mount_name ?? null,
40
+ store_uuid: s.store_uuid,
41
+ // F4: parity with `fabric store list` — local-only reflects the store
42
+ // repo's TRUE git remote (what sync actually pushes to), not the registry
43
+ // metadata. A store with a physical `origin` but no registry `remote`
44
+ // (e.g. the personal store) was misreported as local-only by whoami while
45
+ // `store list` honestly showed its remote. Both now read the same source.
46
+ local_only: storeGitRemote(s.alias, globalRoot) === void 0
47
+ }))
48
+ };
49
+ }
50
+ function projectStatus(projectRoot, globalRoot = resolveGlobalRoot()) {
51
+ const global = loadGlobalConfig(globalRoot);
52
+ const project = loadProjectConfig(projectRoot);
53
+ return {
54
+ uid: global?.uid ?? null,
55
+ mounted: (global?.stores ?? []).map((s) => s.alias),
56
+ project_id: project?.project_id ?? null,
57
+ is_fabric_project: project !== null,
58
+ required: (project?.required_stores ?? []).map((r) => r.id),
59
+ active_write_store: project?.active_write_store ?? null,
60
+ default_write_store: project?.default_write_store ?? null,
61
+ write_routes: project?.write_routes ?? []
62
+ };
63
+ }
64
+
65
+ export {
66
+ warnUnknownFlags,
67
+ whoami,
68
+ projectStatus
69
+ };
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  resolveClients
4
- } from "./chunk-XC5RUHLK.js";
4
+ } from "./chunk-3IOLS5EK.js";
5
+ import {
6
+ loadGlobalConfig,
7
+ resolveGlobalRoot,
8
+ saveGlobalConfig
9
+ } from "./chunk-FNHDQTPC.js";
5
10
  import {
6
11
  t
7
- } from "./chunk-2CY4BMTH.js";
12
+ } from "./chunk-HORSMSZL.js";
8
13
 
9
14
  // src/commands/config.ts
10
15
  import { existsSync, statSync } from "fs";
@@ -18,8 +23,9 @@ import {
18
23
  } from "@fenglimg/fabric-shared";
19
24
  import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
20
25
  import { defineCommand } from "citty";
26
+ var LANGUAGE_FIELD_KEY = "fabric_language";
21
27
  async function loadFabricConfig(workspaceRoot) {
22
- const configPath = resolve(workspaceRoot, "fabric.config.json");
28
+ const configPath = resolve(workspaceRoot, ".fabric", "fabric-config.json");
23
29
  if (!existsSync(configPath)) {
24
30
  return {};
25
31
  }
@@ -178,8 +184,8 @@ var configCmd = defineCommand({
178
184
  "onboard-reset": onboardResetCmd
179
185
  },
180
186
  async run({ args }) {
181
- const argvSub = process.argv[3];
182
- if (argvSub === "dismiss-slot" || argvSub === "onboard-reset") {
187
+ const argvAfterConfig = process.argv.slice(3);
188
+ if (argvAfterConfig.includes("dismiss-slot") || argvAfterConfig.includes("onboard-reset")) {
183
189
  return;
184
190
  }
185
191
  const workspaceRoot = resolve(args.target ?? process.cwd());
@@ -201,6 +207,10 @@ var configCmd = defineCommand({
201
207
  let edited = false;
202
208
  while (true) {
203
209
  const current = await readPanelConfig(configPath);
210
+ const globalLanguage = loadGlobalConfig(resolveGlobalRoot())?.language;
211
+ if (globalLanguage !== void 0) {
212
+ current[LANGUAGE_FIELD_KEY] = globalLanguage;
213
+ }
204
214
  const fields = getPanelFields();
205
215
  const fieldChoice = await select({
206
216
  message: t("cli.config.menu.field-select"),
@@ -234,9 +244,22 @@ var configCmd = defineCommand({
234
244
  continue;
235
245
  }
236
246
  try {
237
- const refreshed = await readPanelConfig(configPath);
238
- const merged = { ...refreshed, [field.key]: newValue };
239
- await atomicWriteJson(configPath, merged);
247
+ if (field.key === LANGUAGE_FIELD_KEY) {
248
+ const globalRoot = resolveGlobalRoot();
249
+ const globalConfig = loadGlobalConfig(globalRoot);
250
+ if (globalConfig === null) {
251
+ log.error(t("cli.config.errors.uninit-workspace.message"));
252
+ continue;
253
+ }
254
+ saveGlobalConfig(
255
+ { ...globalConfig, language: newValue },
256
+ globalRoot
257
+ );
258
+ } else {
259
+ const refreshed = await readPanelConfig(configPath);
260
+ const merged = { ...refreshed, [field.key]: newValue };
261
+ await atomicWriteJson(configPath, merged);
262
+ }
240
263
  edited = true;
241
264
  log.success(
242
265
  t("cli.config.write.success", {
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ regenerateBindingsSnapshot
4
+ } from "./chunk-PTGQAZEW.js";
5
+ import {
6
+ storeBind,
7
+ storeProjectCreate,
8
+ storeProjectList,
9
+ storeSetWriteRoute,
10
+ storeSwitchWrite
11
+ } from "./chunk-QPAW6IYT.js";
12
+ import {
13
+ loadProjectConfig,
14
+ saveProjectConfig
15
+ } from "./chunk-QFIVFZRH.js";
16
+
17
+ // src/install/migrate-root-config.ts
18
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
19
+ import { dirname, join } from "path";
20
+ var ROOT_AUTHORITATIVE_KEYS = /* @__PURE__ */ new Set([
21
+ "embed_enabled",
22
+ "embed_model",
23
+ "embed_weight",
24
+ "plan_context_top_k",
25
+ "recall_relevance_ratio",
26
+ "mcpPayloadLimits",
27
+ "selection_token_ttl_ms",
28
+ "orphan_demote_proven_days",
29
+ "orphan_demote_verified_days",
30
+ "orphan_demote_draft_days",
31
+ "clientPaths"
32
+ ]);
33
+ function readJsonObject(path) {
34
+ if (!existsSync(path)) return null;
35
+ try {
36
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
37
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
38
+ return null;
39
+ }
40
+ return parsed;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ function migrateRootConfig(projectRoot) {
46
+ const rootPath = join(projectRoot, "fabric.config.json");
47
+ const fabricPath = join(projectRoot, ".fabric", "fabric-config.json");
48
+ const result = {
49
+ migrated: false,
50
+ mergedKeys: [],
51
+ rootPath,
52
+ fabricPath
53
+ };
54
+ const rootConfig = readJsonObject(rootPath);
55
+ if (rootConfig === null) {
56
+ if (existsSync(rootPath)) {
57
+ rmSync(rootPath, { force: true });
58
+ result.migrated = true;
59
+ }
60
+ return result;
61
+ }
62
+ const fabricConfig = readJsonObject(fabricPath) ?? {};
63
+ const merged = { ...fabricConfig };
64
+ const mergedKeys = [];
65
+ for (const [key, value] of Object.entries(rootConfig)) {
66
+ const fabricHasKey = Object.prototype.hasOwnProperty.call(merged, key);
67
+ if (ROOT_AUTHORITATIVE_KEYS.has(key) || !fabricHasKey) {
68
+ if (!fabricHasKey || merged[key] !== value) {
69
+ mergedKeys.push(key);
70
+ }
71
+ merged[key] = value;
72
+ }
73
+ }
74
+ mkdirSync(dirname(fabricPath), { recursive: true });
75
+ writeFileSync(fabricPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
76
+ rmSync(rootPath, { force: true });
77
+ result.migrated = true;
78
+ result.mergedKeys = mergedKeys;
79
+ return result;
80
+ }
81
+
82
+ // src/install/store-project-onboarding.ts
83
+ import { execFileSync } from "child_process";
84
+ import { randomUUID } from "crypto";
85
+ import { basename } from "path";
86
+ function ensureProjectId(projectRoot, uuid = randomUUID()) {
87
+ const config = loadProjectConfig(projectRoot) ?? {};
88
+ if (typeof config.project_id === "string" && config.project_id.length > 0) {
89
+ return config.project_id;
90
+ }
91
+ saveProjectConfig({ ...config, project_id: uuid }, projectRoot);
92
+ return uuid;
93
+ }
94
+ function suggestStoreProjectId(projectRoot) {
95
+ return normalizeStoreProjectId(readGitRemoteName(projectRoot) ?? basename(projectRoot));
96
+ }
97
+ function normalizeStoreProjectId(value) {
98
+ const normalized = value.trim().replace(/\.git$/iu, "").toLowerCase().replace(/[^a-z0-9_-]+/gu, "-").replace(/-+/gu, "-").replace(/^[-_]+|[-_]+$/gu, "");
99
+ return normalized.length > 0 ? normalized : "project";
100
+ }
101
+ async function ensureStoreProjectBinding(projectRoot, storeAlias, options) {
102
+ const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
103
+ const project_id = ensureProjectId(projectRoot, options.uuid);
104
+ const currentConfig = loadProjectConfig(projectRoot);
105
+ const requested = options.requestedProjectId ?? currentConfig?.active_project ?? suggestStoreProjectId(projectRoot);
106
+ const active_project = normalizeStoreProjectId(requested);
107
+ const projects = await storeProjectList(storeAlias, options.globalRoot);
108
+ const project_created = !projects.some((project) => project.id === active_project);
109
+ if (project_created) {
110
+ await storeProjectCreate(storeAlias, active_project, now, {
111
+ name: active_project,
112
+ globalRoot: options.globalRoot
113
+ });
114
+ }
115
+ const entry = options.suggestedRemote === void 0 ? { id: storeAlias } : { id: storeAlias, suggested_remote: options.suggestedRemote };
116
+ await storeBind(projectRoot, entry, { project: active_project, globalRoot: options.globalRoot });
117
+ storeSwitchWrite(projectRoot, storeAlias, { globalRoot: options.globalRoot });
118
+ storeSetWriteRoute(projectRoot, `project:${active_project}`, storeAlias, {
119
+ globalRoot: options.globalRoot
120
+ });
121
+ regenerateBindingsSnapshot(projectRoot, {
122
+ now,
123
+ globalRoot: options.globalRoot
124
+ });
125
+ return { project_id, active_project, project_created };
126
+ }
127
+ function readGitRemoteName(projectRoot) {
128
+ try {
129
+ const remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
130
+ cwd: projectRoot,
131
+ encoding: "utf8",
132
+ stdio: ["ignore", "pipe", "ignore"]
133
+ }).trim();
134
+ if (remote.length === 0) {
135
+ return null;
136
+ }
137
+ const lastSegment = remote.split(/[/:\\]/u).filter(Boolean).at(-1);
138
+ return lastSegment === void 0 ? null : lastSegment.replace(/\.git$/iu, "");
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ export {
145
+ migrateRootConfig,
146
+ suggestStoreProjectId,
147
+ normalizeStoreProjectId,
148
+ ensureStoreProjectBinding
149
+ };
@@ -231,16 +231,6 @@ var ClaudeCodeCLIWriter = class extends JsonClientConfigWriter {
231
231
  return this.scope === "user" ? join(homedir(), ".claude.json") : join(workspaceRoot, ".mcp.json");
232
232
  }
233
233
  };
234
- var CursorWriter = class extends JsonClientConfigWriter {
235
- clientKind = "Cursor";
236
- constructor(configuredPath) {
237
- super(configuredPath);
238
- }
239
- defaultPath(workspaceRoot) {
240
- const cursorDir = join(workspaceRoot, ".cursor");
241
- return existsSync(cursorDir) ? join(cursorDir, "mcp.json") : null;
242
- }
243
- };
244
234
 
245
235
  // src/config/claude-code.ts
246
236
  function getClaudeDesktopConfigPath() {
@@ -342,16 +332,11 @@ function trimTrailingBlankLines(value) {
342
332
  function removeCodexServerBlock(rawConfig, serverName) {
343
333
  const normalized = rawConfig.replace(/\r\n/g, "\n");
344
334
  const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
345
- const legacyPattern = new RegExp(
346
- String.raw`\n?\[mcp\.servers\.${escaped}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
347
- "g"
348
- );
349
335
  const currentPattern = new RegExp(
350
336
  String.raw`\n?\[mcp_servers\.${escaped}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
351
337
  "g"
352
338
  );
353
- const withoutLegacy = normalized.replace(legacyPattern, "");
354
- const withoutCurrent = withoutLegacy.replace(currentPattern, "");
339
+ const withoutCurrent = normalized.replace(currentPattern, "");
355
340
  const changed = withoutCurrent !== normalized;
356
341
  const text = changed ? `${trimTrailingBlankLines(withoutCurrent)}
357
342
  ` : rawConfig;
@@ -360,7 +345,6 @@ function removeCodexServerBlock(rawConfig, serverName) {
360
345
  function upsertCodexServerBlock(rawConfig, serverName, serverEntry) {
361
346
  const normalized = rawConfig.replace(/\r\n/g, "\n");
362
347
  const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
363
- const legacyPattern = new RegExp(String.raw`\n?\[mcp\.servers\.${escaped}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`, "g");
364
348
  const currentPattern = new RegExp(
365
349
  String.raw`\n?\[mcp_servers\.${escaped}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
366
350
  "g"
@@ -368,8 +352,7 @@ function upsertCodexServerBlock(rawConfig, serverName, serverEntry) {
368
352
  const existingMatch = normalized.match(currentPattern);
369
353
  const preservedUserLines = existingMatch !== null && existingMatch.length > 0 ? extractCodexBlockUserLines(existingMatch[0]) : [];
370
354
  const block = serializeCodexServerBlock(serverName, serverEntry, preservedUserLines);
371
- const withoutLegacy = normalized.replace(legacyPattern, "");
372
- const withoutExisting = withoutLegacy.replace(currentPattern, "");
355
+ const withoutExisting = normalized.replace(currentPattern, "");
373
356
  const trimmed = trimTrailingBlankLines(withoutExisting);
374
357
  if (trimmed.length === 0) {
375
358
  return block;
@@ -474,12 +457,6 @@ function resolveClients(workspaceRoot, fabricConfig = {}, opts = {}) {
474
457
  (configuredPath) => new ClaudeCodeDesktopWriter(configuredPath),
475
458
  hasExplicitPath(clientPaths, "claudeCodeDesktop") ? clientPaths.claudeCodeDesktop : void 0
476
459
  );
477
- addIfDetected(
478
- writers,
479
- existsSync4(join4(workspaceRoot, ".cursor")),
480
- (configuredPath) => new CursorWriter(configuredPath),
481
- hasExplicitPath(clientPaths, "cursor") ? clientPaths.cursor : void 0
482
- );
483
460
  addIfDetected(
484
461
  writers,
485
462
  existsSync4(join4(homedir4(), ".codex")),
@@ -492,7 +469,6 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
492
469
  const clientPaths = fabricConfig.clientPaths;
493
470
  const claudeDetected = existsSync4(join4(homedir4(), ".claude")) || existsSync4(join4(workspaceRoot, ".claude"));
494
471
  const claudeDesktopDetected = existsSync4(getClaudeDesktopConfigPath());
495
- const cursorDetected = existsSync4(join4(workspaceRoot, ".cursor"));
496
472
  const codexDetected = existsSync4(join4(homedir4(), ".codex"));
497
473
  return [
498
474
  {
@@ -523,18 +499,6 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
523
499
  skill: false
524
500
  }
525
501
  },
526
- {
527
- clientKind: "Cursor",
528
- label: "Cursor",
529
- detected: cursorDetected || hasExplicitPath(clientPaths, "cursor"),
530
- configPath: ".cursor/mcp.json",
531
- capabilities: {
532
- bootstrap: true,
533
- mcp: true,
534
- hook: false,
535
- skill: false
536
- }
537
- },
538
502
  {
539
503
  clientKind: "CodexCLI",
540
504
  label: "Codex CLI",
@@ -554,6 +518,27 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
554
518
  // skills as uninstalled even right after installing them).
555
519
  skill: existsSync4(join4(workspaceRoot, ".codex", "skills"))
556
520
  }
521
+ },
522
+ {
523
+ clientKind: "CodexDesktop",
524
+ label: "Codex Desktop",
525
+ // Codex Desktop shares the same ~/.codex config as Codex CLI — there is no
526
+ // separate adapter work: installing the Codex CLI assets (MCP config /
527
+ // hooks / skills) makes Desktop ready too. Display-only row mirroring Codex
528
+ // CLI's detection + installed state; no dedicated writer (CodexTOMLConfigWriter
529
+ // already targets the shared config, so removing/adding it once covers both).
530
+ detected: codexDetected || hasExplicitPath(clientPaths, "codexCLI"),
531
+ configPath: "~/.codex/config.toml (shared with Codex CLI)",
532
+ capabilities: {
533
+ bootstrap: true,
534
+ mcp: true,
535
+ hook: true,
536
+ skill: true
537
+ },
538
+ installedCapabilities: {
539
+ hook: existsSync4(join4(workspaceRoot, ".codex", "hooks.json")),
540
+ skill: existsSync4(join4(workspaceRoot, ".codex", "skills"))
541
+ }
557
542
  }
558
543
  ];
559
544
  }
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  resolveDevMode
4
- } from "./chunk-COI5VDFU.js";
4
+ } from "./chunk-WA3DYGSY.js";
5
5
 
6
6
  // src/commands/plan-context-hint.ts
7
7
  import { defineCommand } from "citty";
8
- import { planContext } from "@fenglimg/fabric-server";
8
+ import {
9
+ buildAlwaysActiveBodies,
10
+ buildKnowledgeCensus,
11
+ planContext
12
+ } from "@fenglimg/fabric-server";
9
13
  var ALL_PATHS_SENTINEL = "**";
10
14
  var planContextHintCommand = defineCommand({
11
15
  meta: {
@@ -56,22 +60,49 @@ async function runPlanContextHint(opts) {
56
60
  const targetPaths = all ? [ALL_PATHS_SENTINEL] : explicitPaths.length > 0 ? explicitPaths : [ALL_PATHS_SENTINEL];
57
61
  const resolution = resolveDevMode(opts.target, process.cwd());
58
62
  const result = await planContext(resolution.target, {
59
- paths: targetPaths
63
+ paths: targetPaths,
64
+ // lifecycle-refactor W3-T2 (§7 图谱消费 / §5): default-enable graph二阶召回 for
65
+ // the hint path. planContext appends the one-hop `related` neighbours that
66
+ // ranked outside top_k of the surfaced set and reports them in
67
+ // `related_appended` (appended id → source id). Honest no-op when the
68
+ // surfaced set declares no in-corpus related edge.
69
+ include_related: true
60
70
  });
61
71
  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
- }));
72
+ const relatedAppended = result.related_appended ?? {};
73
+ const entries = candidates.map((item) => {
74
+ const relatedTo = relatedAppended[item.stable_id];
75
+ return {
76
+ id: item.stable_id,
77
+ type: item.description.knowledge_type ?? "",
78
+ maturity: item.description.maturity ?? "",
79
+ summary: item.description.summary,
80
+ relevance_scope: item.description.relevance_scope ?? "broad",
81
+ // W2-2 (KT-DEC-0027): forward the must_read_if trigger hook for the
82
+ // SessionStart REFERENCE rendering. Omitted when absent/empty.
83
+ ...typeof item.description.must_read_if === "string" && item.description.must_read_if.length > 0 ? { must_read_if: item.description.must_read_if } : {},
84
+ // Only set when this entry was pulled in via a graph edge — its presence
85
+ // is the honest signal, never synthesized for ordinarily-ranked entries.
86
+ ...typeof relatedTo === "string" ? { related_to: relatedTo } : {}
87
+ };
88
+ });
69
89
  let narrow_count = 0;
70
90
  let broad_only_count = 0;
71
91
  for (const e of entries) {
72
92
  if (e.relevance_scope === "narrow") narrow_count += 1;
73
93
  else broad_only_count += 1;
74
94
  }
95
+ const alwaysBodies = await buildAlwaysActiveBodies(resolution.target).catch(() => []);
96
+ const census = await buildKnowledgeCensus(resolution.target).catch(
97
+ () => ({
98
+ by_type: {},
99
+ by_layer: { team: 0, personal: 0, project: 0 },
100
+ broad_by_type: {},
101
+ narrow_total: 0,
102
+ dropped_other_project: 0,
103
+ total: 0
104
+ })
105
+ );
75
106
  const output = {
76
107
  version: 2,
77
108
  revision_hash: result.revision_hash,
@@ -81,7 +112,15 @@ async function runPlanContextHint(opts) {
81
112
  // semantics unchanged from rc.18 (total candidate count).
82
113
  broad_count: candidates.length,
83
114
  narrow_count,
84
- broad_only_count
115
+ broad_only_count,
116
+ always_bodies: alwaysBodies.map((b) => ({
117
+ id: b.stable_id,
118
+ type: b.type,
119
+ layer: b.layer,
120
+ summary: b.summary,
121
+ body: b.body
122
+ })),
123
+ census
85
124
  };
86
125
  if (result.auto_healed === true) {
87
126
  output.auto_healed = true;
@@ -97,8 +136,9 @@ function parsePathsArg(raw) {
97
136
  }
98
137
  return raw.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
99
138
  }
139
+
100
140
  export {
101
- plan_context_hint_default as default,
102
141
  planContextHintCommand,
142
+ plan_context_hint_default,
103
143
  runPlanContextHint
104
144
  };