@fenglimg/fabric-cli 2.1.0-rc.2 → 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.
- package/README.md +8 -5
- package/dist/chunk-27HK6H5Y.js +69 -0
- package/dist/{chunk-BATF4PEJ.js → chunk-2KBCTMID.js} +31 -8
- package/dist/chunk-3D7B2UAZ.js +149 -0
- package/dist/{chunk-MF3OTILQ.js → chunk-3IOLS5EK.js} +48 -42
- package/dist/{plan-context-hint-FC6P3WFE.js → chunk-722JU5BP.js} +52 -12
- package/dist/{chunk-F46ORPOA.js → chunk-7ZDXBOOU.js} +271 -166
- package/dist/{doctor-QVNPHLJK.js → chunk-E7HJUU34.js} +248 -72
- package/dist/chunk-EOT63RDH.js +36 -0
- package/dist/chunk-FNHDQTPC.js +16 -0
- package/dist/chunk-HORSMSZL.js +26 -0
- package/dist/chunk-NLNH64A3.js +43 -0
- package/dist/{chunk-WU6GAPKH.js → chunk-PTGQAZEW.js} +12 -4
- package/dist/chunk-QFIVFZRH.js +13 -0
- package/dist/chunk-QPAW6IYT.js +387 -0
- package/dist/{chunk-COI5VDFU.js → chunk-WA3DYGSY.js} +1 -2
- package/dist/{config-XJIPZNUP.js → config-A3LTECAY.js} +4 -3
- package/dist/context-UJCGYOT6.js +117 -0
- package/dist/doctor-MDTZWKBK.js +24 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +167 -16
- package/dist/info-7FKBTMVO.js +139 -0
- package/dist/install-v2-RINEA24K.js +3279 -0
- package/dist/{metrics-ACEQFPDU.js → metrics-HMFH4YHK.js} +22 -9
- package/dist/{onboard-coverage-MFCAEBDO.js → onboard-coverage-XSG77LL3.js} +48 -27
- package/dist/plan-context-hint-5TNGH3R4.js +12 -0
- package/dist/scope-explain-HLJZ2M33.js +48 -0
- package/dist/status-4R3TM4FJ.js +37 -0
- package/dist/store-HOCORVL3.js +563 -0
- package/dist/sync-DT5UJMMR.js +418 -0
- package/dist/{uninstall-TAXSUSKH.js → uninstall-IFN2KYBK.js} +128 -140
- package/dist/whoami-ITGEFWH4.js +49 -0
- package/package.json +7 -5
- package/templates/hooks/cite-policy-evict.cjs +412 -160
- package/templates/hooks/configs/README.md +14 -27
- package/templates/hooks/configs/claude-code.json +17 -2
- package/templates/hooks/configs/codex-hooks.json +15 -3
- package/templates/hooks/fabric-hint.cjs +573 -180
- package/templates/hooks/knowledge-hint-broad.cjs +648 -190
- package/templates/hooks/knowledge-hint-narrow.cjs +123 -77
- package/templates/hooks/lib/banner-i18n.cjs +31 -0
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +118 -7
- package/templates/hooks/lib/cite-line-parser.cjs +12 -20
- package/templates/hooks/lib/client-adapter.cjs +66 -7
- package/templates/hooks/lib/injection-log.cjs +91 -0
- package/templates/hooks/lib/nudge-policy.cjs +117 -0
- package/templates/hooks/lib/state-store.cjs +90 -11
- package/templates/hooks/post-tooluse-mutation.cjs +386 -0
- package/templates/hooks/session-end-marker.cjs +140 -0
- package/templates/skills/fabric/SKILL.md +100 -0
- package/templates/skills/fabric-archive/SKILL.md +35 -24
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
- package/templates/skills/fabric-archive/ref/i18n-policy.md +2 -3
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +2 -3
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-6-related-edges.md +18 -0
- package/templates/skills/fabric-archive/ref/phase-3-7-semantic-scope.md +47 -0
- package/templates/skills/fabric-audit/SKILL.md +63 -0
- package/templates/skills/fabric-connect/SKILL.md +48 -0
- package/templates/skills/fabric-import/SKILL.md +7 -7
- package/templates/skills/fabric-import/ref/i18n-policy.md +2 -3
- package/templates/skills/fabric-import/ref/state-recovery.md +1 -2
- package/templates/skills/fabric-review/SKILL.md +16 -5
- package/templates/skills/fabric-review/ref/cite-contract.md +56 -0
- package/templates/skills/fabric-review/ref/i18n-policy.md +2 -3
- package/templates/skills/fabric-review/ref/output-contract.md +1 -1
- package/templates/skills/fabric-review/ref/per-mode-flows.md +2 -2
- package/templates/skills/fabric-review/ref/worked-examples.md +1 -1
- package/templates/skills/fabric-store/SKILL.md +44 -0
- package/templates/skills/fabric-sync/SKILL.md +1 -1
- package/templates/skills/lib/shared-policy.md +2 -2
- package/dist/chunk-HFQVXY6P.js +0 -86
- package/dist/chunk-L4Q55UC4.js +0 -52
- package/dist/chunk-LFIKMVY7.js +0 -27
- package/dist/chunk-PWLW3B57.js +0 -18
- package/dist/chunk-RYAFBNES.js +0 -33
- package/dist/chunk-T5RPGCCM.js +0 -40
- package/dist/chunk-WWNXR34K.js +0 -49
- package/dist/install-2HDO5FTQ.js +0 -2683
- package/dist/scope-explain-2F2R5URO.js +0 -33
- package/dist/status-GLQWLWH6.js +0 -23
- package/dist/store-XTSE5TY6.js +0 -105
- package/dist/sync-BJCWDPNC.js +0 -245
- package/dist/whoami-B6AEMSEV.js +0 -31
- package/templates/hooks/configs/cursor-hooks.json +0 -18
- package/templates/hooks/lib/cite-contract-reminder.cjs +0 -179
- 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.
|
|
10
|
+
4. 重启 Claude Code / Codex CLI,在客户端里验证 `fab_recall`。
|
|
11
11
|
|
|
12
|
-
`fabric install` 会自动准备 bootstrap、MCP 配置和 git hooks
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
182
|
-
if (
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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() {
|
|
@@ -307,7 +297,7 @@ function serializeTomlInlineTable(values) {
|
|
|
307
297
|
const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key} = ${escapeTomlString(value)}`);
|
|
308
298
|
return `{ ${entries.join(", ")} }`;
|
|
309
299
|
}
|
|
310
|
-
function serializeCodexServerBlock(serverName, serverEntry) {
|
|
300
|
+
function serializeCodexServerBlock(serverName, serverEntry, preservedUserLines = []) {
|
|
311
301
|
const lines = [
|
|
312
302
|
`[mcp_servers.${serverName}]`,
|
|
313
303
|
`command = ${escapeTomlString(serverEntry.command)}`,
|
|
@@ -316,40 +306,53 @@ function serializeCodexServerBlock(serverName, serverEntry) {
|
|
|
316
306
|
if (serverEntry.env !== void 0 && Object.keys(serverEntry.env).length > 0) {
|
|
317
307
|
lines.push(`env = ${serializeTomlInlineTable(serverEntry.env)}`);
|
|
318
308
|
}
|
|
309
|
+
lines.push(...preservedUserLines);
|
|
319
310
|
return `${lines.join("\n")}
|
|
320
311
|
`;
|
|
321
312
|
}
|
|
313
|
+
var CODEX_MANAGED_BLOCK_KEYS = /* @__PURE__ */ new Set(["command", "args", "env"]);
|
|
314
|
+
function extractCodexBlockUserLines(blockText) {
|
|
315
|
+
const out = [];
|
|
316
|
+
const lines = blockText.split("\n");
|
|
317
|
+
for (const line of lines) {
|
|
318
|
+
const trimmed = line.trim();
|
|
319
|
+
if (trimmed.length === 0) continue;
|
|
320
|
+
if (/^\[/.test(trimmed)) continue;
|
|
321
|
+
const keyMatch = /^([A-Za-z0-9_-]+)\s*=/.exec(trimmed);
|
|
322
|
+
if (keyMatch !== null && CODEX_MANAGED_BLOCK_KEYS.has(keyMatch[1])) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
out.push(trimmed);
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
322
329
|
function trimTrailingBlankLines(value) {
|
|
323
330
|
return value.replace(/\s+$/u, "");
|
|
324
331
|
}
|
|
325
332
|
function removeCodexServerBlock(rawConfig, serverName) {
|
|
326
333
|
const normalized = rawConfig.replace(/\r\n/g, "\n");
|
|
327
334
|
const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
328
|
-
const legacyPattern = new RegExp(
|
|
329
|
-
String.raw`\n?\[mcp\.servers\.${escaped}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
|
|
330
|
-
"g"
|
|
331
|
-
);
|
|
332
335
|
const currentPattern = new RegExp(
|
|
333
336
|
String.raw`\n?\[mcp_servers\.${escaped}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
|
|
334
337
|
"g"
|
|
335
338
|
);
|
|
336
|
-
const
|
|
337
|
-
const withoutCurrent = withoutLegacy.replace(currentPattern, "");
|
|
339
|
+
const withoutCurrent = normalized.replace(currentPattern, "");
|
|
338
340
|
const changed = withoutCurrent !== normalized;
|
|
339
341
|
const text = changed ? `${trimTrailingBlankLines(withoutCurrent)}
|
|
340
342
|
` : rawConfig;
|
|
341
343
|
return { text, changed };
|
|
342
344
|
}
|
|
343
345
|
function upsertCodexServerBlock(rawConfig, serverName, serverEntry) {
|
|
344
|
-
const block = serializeCodexServerBlock(serverName, serverEntry);
|
|
345
346
|
const normalized = rawConfig.replace(/\r\n/g, "\n");
|
|
346
|
-
const
|
|
347
|
+
const escaped = serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
347
348
|
const currentPattern = new RegExp(
|
|
348
|
-
String.raw`\n?\[mcp_servers\.${
|
|
349
|
+
String.raw`\n?\[mcp_servers\.${escaped}\]\n[\s\S]*?(?=\n\[[^\n]+\]\n|$)`,
|
|
349
350
|
"g"
|
|
350
351
|
);
|
|
351
|
-
const
|
|
352
|
-
const
|
|
352
|
+
const existingMatch = normalized.match(currentPattern);
|
|
353
|
+
const preservedUserLines = existingMatch !== null && existingMatch.length > 0 ? extractCodexBlockUserLines(existingMatch[0]) : [];
|
|
354
|
+
const block = serializeCodexServerBlock(serverName, serverEntry, preservedUserLines);
|
|
355
|
+
const withoutExisting = normalized.replace(currentPattern, "");
|
|
353
356
|
const trimmed = trimTrailingBlankLines(withoutExisting);
|
|
354
357
|
if (trimmed.length === 0) {
|
|
355
358
|
return block;
|
|
@@ -454,12 +457,6 @@ function resolveClients(workspaceRoot, fabricConfig = {}, opts = {}) {
|
|
|
454
457
|
(configuredPath) => new ClaudeCodeDesktopWriter(configuredPath),
|
|
455
458
|
hasExplicitPath(clientPaths, "claudeCodeDesktop") ? clientPaths.claudeCodeDesktop : void 0
|
|
456
459
|
);
|
|
457
|
-
addIfDetected(
|
|
458
|
-
writers,
|
|
459
|
-
existsSync4(join4(workspaceRoot, ".cursor")),
|
|
460
|
-
(configuredPath) => new CursorWriter(configuredPath),
|
|
461
|
-
hasExplicitPath(clientPaths, "cursor") ? clientPaths.cursor : void 0
|
|
462
|
-
);
|
|
463
460
|
addIfDetected(
|
|
464
461
|
writers,
|
|
465
462
|
existsSync4(join4(homedir4(), ".codex")),
|
|
@@ -472,7 +469,6 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
|
|
|
472
469
|
const clientPaths = fabricConfig.clientPaths;
|
|
473
470
|
const claudeDetected = existsSync4(join4(homedir4(), ".claude")) || existsSync4(join4(workspaceRoot, ".claude"));
|
|
474
471
|
const claudeDesktopDetected = existsSync4(getClaudeDesktopConfigPath());
|
|
475
|
-
const cursorDetected = existsSync4(join4(workspaceRoot, ".cursor"));
|
|
476
472
|
const codexDetected = existsSync4(join4(homedir4(), ".codex"));
|
|
477
473
|
return [
|
|
478
474
|
{
|
|
@@ -504,22 +500,35 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
|
|
|
504
500
|
}
|
|
505
501
|
},
|
|
506
502
|
{
|
|
507
|
-
clientKind: "
|
|
508
|
-
label: "
|
|
509
|
-
detected:
|
|
510
|
-
configPath: "
|
|
503
|
+
clientKind: "CodexCLI",
|
|
504
|
+
label: "Codex CLI",
|
|
505
|
+
detected: codexDetected || hasExplicitPath(clientPaths, "codexCLI"),
|
|
506
|
+
configPath: "~/.codex/config.toml",
|
|
511
507
|
capabilities: {
|
|
512
508
|
bootstrap: true,
|
|
513
509
|
mcp: true,
|
|
514
|
-
hook:
|
|
515
|
-
skill:
|
|
510
|
+
hook: true,
|
|
511
|
+
skill: true
|
|
512
|
+
},
|
|
513
|
+
installedCapabilities: {
|
|
514
|
+
hook: existsSync4(join4(workspaceRoot, ".codex", "hooks.json")),
|
|
515
|
+
// F6: the v2 skills (fabric-archive/review/import/…) DO install to
|
|
516
|
+
// `.codex/skills/` now, so probe that directory instead of the stale
|
|
517
|
+
// hardcoded `false` (which made `fabric install` always re-report Codex
|
|
518
|
+
// skills as uninstalled even right after installing them).
|
|
519
|
+
skill: existsSync4(join4(workspaceRoot, ".codex", "skills"))
|
|
516
520
|
}
|
|
517
521
|
},
|
|
518
522
|
{
|
|
519
|
-
clientKind: "
|
|
520
|
-
label: "Codex
|
|
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).
|
|
521
530
|
detected: codexDetected || hasExplicitPath(clientPaths, "codexCLI"),
|
|
522
|
-
configPath: "~/.codex/config.toml",
|
|
531
|
+
configPath: "~/.codex/config.toml (shared with Codex CLI)",
|
|
523
532
|
capabilities: {
|
|
524
533
|
bootstrap: true,
|
|
525
534
|
mcp: true,
|
|
@@ -528,10 +537,7 @@ function detectClientSupports(workspaceRoot, fabricConfig = {}) {
|
|
|
528
537
|
},
|
|
529
538
|
installedCapabilities: {
|
|
530
539
|
hook: existsSync4(join4(workspaceRoot, ".codex", "hooks.json")),
|
|
531
|
-
|
|
532
|
-
// will return once rc.2/3/4 introduce the v2 skills (fabric-archive,
|
|
533
|
-
// fabric-review, fabric-import). Until then there is nothing to probe.
|
|
534
|
-
skill: false
|
|
540
|
+
skill: existsSync4(join4(workspaceRoot, ".codex", "skills"))
|
|
535
541
|
}
|
|
536
542
|
}
|
|
537
543
|
];
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
resolveDevMode
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WA3DYGSY.js";
|
|
5
5
|
|
|
6
6
|
// src/commands/plan-context-hint.ts
|
|
7
7
|
import { defineCommand } from "citty";
|
|
8
|
-
import {
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
};
|