@fenglimg/fabric-cli 2.0.0-rc.10 → 2.0.0-rc.13
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 +3 -3
- package/dist/{chunk-MT3R57VG.js → chunk-FDRLV5PL.js} +14 -8
- package/dist/{chunk-HQLEHH4O.js → chunk-OHWQNSLH.js} +1 -1
- package/dist/{chunk-WPTA74BY.js → chunk-Q72D24BG.js} +6 -4
- package/dist/{chunk-AW3G7ZH5.js → chunk-X7QPY5KH.js} +68 -41
- package/dist/{hooks-NX32PPEN.js → hooks-HIWYI3VG.js} +2 -2
- package/dist/index.js +6 -6
- package/dist/{init-SAVH4SKE.js → install-SLS5W27W.js} +157 -114
- package/dist/{scan-ELSNCSKS.js → scan-VHKZPT2W.js} +3 -1
- package/dist/{uninstall-DBAR2JBS.js → uninstall-JHUSFENL.js} +27 -18
- package/package.json +3 -3
- package/templates/hooks/configs/README.md +1 -1
- package/templates/hooks/knowledge-hint-broad.cjs +28 -107
- package/templates/skills/fabric-archive/SKILL.md +184 -30
- package/templates/skills/fabric-import/SKILL.md +324 -34
- package/templates/skills/fabric-review/SKILL.md +358 -23
|
@@ -3,11 +3,12 @@ import {
|
|
|
3
3
|
__testing__,
|
|
4
4
|
createScanReport,
|
|
5
5
|
deriveTagsFromForensic,
|
|
6
|
+
detectExistingLanguage,
|
|
6
7
|
formatKnowledgeId,
|
|
7
8
|
runInitScan,
|
|
8
9
|
scanCommand,
|
|
9
10
|
scan_default
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FDRLV5PL.js";
|
|
11
12
|
import "./chunk-WWNXR34K.js";
|
|
12
13
|
import "./chunk-OBQU6NHO.js";
|
|
13
14
|
import "./chunk-6ICJICVU.js";
|
|
@@ -16,6 +17,7 @@ export {
|
|
|
16
17
|
createScanReport,
|
|
17
18
|
scan_default as default,
|
|
18
19
|
deriveTagsFromForensic,
|
|
20
|
+
detectExistingLanguage,
|
|
19
21
|
formatKnowledgeId,
|
|
20
22
|
runInitScan,
|
|
21
23
|
scanCommand
|
|
@@ -2,18 +2,16 @@
|
|
|
2
2
|
import {
|
|
3
3
|
detectClientSupports,
|
|
4
4
|
resolveClients
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OHWQNSLH.js";
|
|
6
6
|
import {
|
|
7
7
|
FABRIC_HOOK_COMMAND_PATHS,
|
|
8
|
+
FABRIC_SECTION_REGEX,
|
|
8
9
|
HOOK_CONFIG_ARRAY_PATHS,
|
|
9
10
|
HOOK_CONFIG_TARGETS,
|
|
10
11
|
HOOK_SCRIPT_DESTINATIONS,
|
|
11
|
-
|
|
12
|
-
POINTER_LINE,
|
|
13
|
-
POINTER_TARGETS,
|
|
14
|
-
REVIEW_POINTER_LINE,
|
|
12
|
+
SECTION_TARGETS,
|
|
15
13
|
SKILL_DESTINATIONS
|
|
16
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-X7QPY5KH.js";
|
|
17
15
|
import {
|
|
18
16
|
paint
|
|
19
17
|
} from "./chunk-WWNXR34K.js";
|
|
@@ -115,12 +113,12 @@ async function unmergeCursorHookConfig(projectRoot, opts = {}) {
|
|
|
115
113
|
cleanEmpties: opts.cleanEmpties === true
|
|
116
114
|
});
|
|
117
115
|
}
|
|
118
|
-
async function
|
|
116
|
+
async function stripFabricKnowledgeBaseSection(projectRoot) {
|
|
119
117
|
const results = [];
|
|
120
|
-
for (const rel of
|
|
118
|
+
for (const rel of SECTION_TARGETS) {
|
|
121
119
|
const target = join(projectRoot, rel);
|
|
122
120
|
if (!existsSync(target)) {
|
|
123
|
-
results.push({ step: "
|
|
121
|
+
results.push({ step: "section", path: target, status: "skipped", message: "absent" });
|
|
124
122
|
continue;
|
|
125
123
|
}
|
|
126
124
|
let existing;
|
|
@@ -128,30 +126,41 @@ async function stripArchiveSkillPointers(projectRoot) {
|
|
|
128
126
|
existing = await readFile(target, "utf8");
|
|
129
127
|
} catch (error) {
|
|
130
128
|
results.push({
|
|
131
|
-
step: "
|
|
129
|
+
step: "section",
|
|
132
130
|
path: target,
|
|
133
131
|
status: "error",
|
|
134
132
|
message: error instanceof Error ? error.message : String(error)
|
|
135
133
|
});
|
|
136
134
|
continue;
|
|
137
135
|
}
|
|
138
|
-
const
|
|
139
|
-
|
|
136
|
+
const match = existing.match(FABRIC_SECTION_REGEX);
|
|
137
|
+
if (match === null) {
|
|
138
|
+
results.push({
|
|
139
|
+
step: "section",
|
|
140
|
+
path: target,
|
|
141
|
+
status: "skipped",
|
|
142
|
+
message: "no-fabric-section"
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const before = existing.slice(0, match.index ?? 0);
|
|
147
|
+
const after = existing.slice((match.index ?? 0) + match[0].length);
|
|
148
|
+
const filtered = `${before}${after.replace(/^\r?\n/, "")}`;
|
|
140
149
|
if (filtered === existing) {
|
|
141
150
|
results.push({
|
|
142
|
-
step: "
|
|
151
|
+
step: "section",
|
|
143
152
|
path: target,
|
|
144
153
|
status: "skipped",
|
|
145
|
-
message: "no-fabric-
|
|
154
|
+
message: "no-fabric-section"
|
|
146
155
|
});
|
|
147
156
|
continue;
|
|
148
157
|
}
|
|
149
158
|
try {
|
|
150
159
|
await atomicWriteText(target, filtered);
|
|
151
|
-
results.push({ step: "
|
|
160
|
+
results.push({ step: "section", path: target, status: "removed" });
|
|
152
161
|
} catch (error) {
|
|
153
162
|
results.push({
|
|
154
|
-
step: "
|
|
163
|
+
step: "section",
|
|
155
164
|
path: target,
|
|
156
165
|
status: "error",
|
|
157
166
|
message: error instanceof Error ? error.message : String(error)
|
|
@@ -164,9 +173,9 @@ async function uninstallBootstrapStage(projectRoot, opts = {}) {
|
|
|
164
173
|
const results = [];
|
|
165
174
|
await runAndCollect(
|
|
166
175
|
results,
|
|
167
|
-
"
|
|
176
|
+
"section",
|
|
168
177
|
projectRoot,
|
|
169
|
-
() =>
|
|
178
|
+
() => stripFabricKnowledgeBaseSection(projectRoot)
|
|
170
179
|
);
|
|
171
180
|
await runAndCollectOne(
|
|
172
181
|
results,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fenglimg/fabric-cli",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fab": "dist/index.js",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"tree-sitter-javascript": "^0.25.0",
|
|
21
21
|
"tree-sitter-typescript": "^0.23.2",
|
|
22
22
|
"web-tree-sitter": "^0.26.8",
|
|
23
|
-
"@fenglimg/fabric-shared": "2.0.0-rc.
|
|
24
|
-
"@fenglimg/fabric-server": "2.0.0-rc.
|
|
23
|
+
"@fenglimg/fabric-shared": "2.0.0-rc.13",
|
|
24
|
+
"@fenglimg/fabric-server": "2.0.0-rc.13"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^22.15.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Client hook config templates
|
|
2
2
|
|
|
3
|
-
These JSON files are **fragment templates** consumed by `fabric
|
|
3
|
+
These JSON files are **fragment templates** consumed by `fabric install` and
|
|
4
4
|
`fabric hooks install`. They are not standalone client config files.
|
|
5
5
|
|
|
6
6
|
The supported clients are pinned by `packages/shared/src/schemas/fabric-config.ts`
|
|
@@ -49,41 +49,22 @@
|
|
|
49
49
|
const { spawnSync } = require("node:child_process");
|
|
50
50
|
const {
|
|
51
51
|
existsSync,
|
|
52
|
-
mkdirSync,
|
|
53
52
|
readdirSync,
|
|
54
53
|
readFileSync,
|
|
55
|
-
writeFileSync,
|
|
56
54
|
} = require("node:fs");
|
|
57
|
-
const {
|
|
55
|
+
const { join } = require("node:path");
|
|
58
56
|
|
|
59
57
|
// -----------------------------------------------------------------------------
|
|
60
|
-
// rc.
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
// update sidecar.
|
|
68
|
-
//
|
|
69
|
-
// The revision_hash is supplied by `fabric plan-context-hint --all`'s JSON
|
|
70
|
-
// payload (carried in payload.revision_hash since rc.5). Reusing the existing
|
|
71
|
-
// hash primitive keeps the gating predicate exactly aligned with the "is the
|
|
72
|
-
// knowledge graph different from last time?" question — no second hashing
|
|
73
|
-
// scheme to maintain. computeRevisionHash() is not needed at this layer; we
|
|
74
|
-
// compare the strings the CLI hands us.
|
|
75
|
-
//
|
|
76
|
-
// rc.8 underseed self-check: the retired `.fabric/.import-requested` sentinel
|
|
77
|
-
// mechanism is replaced by a deterministic three-condition probe in
|
|
78
|
-
// shouldRecommendImport(). When the probe says "recommend", a one-line
|
|
79
|
-
// `/fabric-import` banner is appended to the broad-injection output and
|
|
80
|
-
// the revision_hash gate is bypassed FOR THE BANNER ONLY (the broad-summary
|
|
81
|
-
// body itself remains hash-gated). See shouldRecommendImport() below for
|
|
82
|
-
// the full truth table.
|
|
58
|
+
// rc.12: SessionStart broad-menu is now unconditionally emitted on every
|
|
59
|
+
// SessionStart fire (matching Skill-style progressive disclosure). Prior
|
|
60
|
+
// versions (rc.5-rc.11) wrote `.fabric/.cache/sessionstart-last-hash` as a
|
|
61
|
+
// revision_hash cooldown sidecar to suppress re-emission on unchanged
|
|
62
|
+
// knowledge graphs; that gate was removed in rc.12. Orphaned sidecar files
|
|
63
|
+
// on existing dogfood repos are harmless dead state and are intentionally
|
|
64
|
+
// NOT cleaned up (zero-user clean-slate — no migration logic needed).
|
|
83
65
|
// -----------------------------------------------------------------------------
|
|
84
66
|
|
|
85
67
|
const FABRIC_DIR_REL = ".fabric";
|
|
86
|
-
const SESSIONSTART_HASH_CACHE_FILE = join(".fabric", ".cache", "sessionstart-last-hash");
|
|
87
68
|
|
|
88
69
|
// rc.8 underseed self-check constants (mirror fabric-hint.cjs ~line 76 / 83).
|
|
89
70
|
// Intentionally duplicated inline — hooks are independent .cjs files and
|
|
@@ -102,41 +83,6 @@ const KNOWLEDGE_CANONICAL_TYPES = [
|
|
|
102
83
|
];
|
|
103
84
|
const DEFAULT_UNDERSEED_NODE_THRESHOLD = 10;
|
|
104
85
|
|
|
105
|
-
/**
|
|
106
|
-
* Read the previously-emitted revision_hash from
|
|
107
|
-
* `.fabric/.cache/sessionstart-last-hash`. Missing file / read failure /
|
|
108
|
-
* empty file → null (treat as "no prior emit", forces re-emit).
|
|
109
|
-
*
|
|
110
|
-
* NEVER throws — best-effort read.
|
|
111
|
-
*/
|
|
112
|
-
function readSessionStartLastHash(projectRoot) {
|
|
113
|
-
try {
|
|
114
|
-
const p = join(projectRoot, SESSIONSTART_HASH_CACHE_FILE);
|
|
115
|
-
if (!existsSync(p)) return null;
|
|
116
|
-
const raw = readFileSync(p, "utf8").trim();
|
|
117
|
-
return raw.length > 0 ? raw : null;
|
|
118
|
-
} catch {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Write `hash` to `.fabric/.cache/sessionstart-last-hash` so subsequent
|
|
125
|
-
* SessionStart fires can compare. Creates the directory if missing.
|
|
126
|
-
* Best-effort: any write failure is swallowed so a read-only .fabric/
|
|
127
|
-
* never blocks session start.
|
|
128
|
-
*/
|
|
129
|
-
function writeSessionStartLastHash(projectRoot, hash) {
|
|
130
|
-
try {
|
|
131
|
-
if (typeof hash !== "string" || hash.length === 0) return;
|
|
132
|
-
const p = join(projectRoot, SESSIONSTART_HASH_CACHE_FILE);
|
|
133
|
-
mkdirSync(dirname(p), { recursive: true });
|
|
134
|
-
writeFileSync(p, hash, "utf8");
|
|
135
|
-
} catch {
|
|
136
|
-
// Silent — sidecar failure must never block session start.
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
86
|
// -----------------------------------------------------------------------------
|
|
141
87
|
// rc.8 underseed self-check helpers.
|
|
142
88
|
//
|
|
@@ -488,15 +434,21 @@ function renderTruncated(narrow) {
|
|
|
488
434
|
* (empty narrow set) so callers know to stay silent.
|
|
489
435
|
*/
|
|
490
436
|
function renderSummary(payload) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
437
|
+
// Local rebind: `payload.narrow` in `--all` mode degenerates to the full
|
|
438
|
+
// shared index (every broad-scoped entry), so the field name `narrow` is
|
|
439
|
+
// misleading at this rendering layer. We rename the local variable to
|
|
440
|
+
// `entries` to avoid name confusion when reading renderSummary in isolation.
|
|
441
|
+
// The CLI protocol field name (`payload.narrow`) is unchanged — a wire-shape
|
|
442
|
+
// rename is a deferred independent task.
|
|
443
|
+
const entries = Array.isArray(payload && payload.narrow) ? payload.narrow : [];
|
|
444
|
+
if (entries.length === 0) return [];
|
|
445
|
+
|
|
446
|
+
const truncated = entries.length > TRUNCATION_THRESHOLD;
|
|
495
447
|
const banner = truncated
|
|
496
|
-
? `[fabric] Session start — ${
|
|
497
|
-
: `[fabric] Session start — ${
|
|
448
|
+
? `[fabric] Session start — ${entries.length} broad-scoped knowledge entries available (truncated):`
|
|
449
|
+
: `[fabric] Session start — ${entries.length} broad-scoped knowledge entries available:`;
|
|
498
450
|
|
|
499
|
-
const body = truncated ? renderTruncated(
|
|
451
|
+
const body = truncated ? renderTruncated(entries) : renderFull(entries);
|
|
500
452
|
|
|
501
453
|
const lines = [banner, ...body];
|
|
502
454
|
const revHash = typeof payload.revision_hash === "string" ? payload.revision_hash : null;
|
|
@@ -524,32 +476,15 @@ function main(env, stdio) {
|
|
|
524
476
|
if (payload === null || payload === undefined) return; // silent
|
|
525
477
|
|
|
526
478
|
// rc.8 underseed self-check: decide whether to surface the one-line
|
|
527
|
-
// `/fabric-import` recommendation
|
|
528
|
-
// revision_hash gate so the banner can bypass it (an unchanged
|
|
529
|
-
// knowledge graph would otherwise hide the recommendation forever).
|
|
530
|
-
// The broad-summary BODY itself remains hash-gated below — only the
|
|
531
|
-
// banner line is unconditionally emitted when the probe says so.
|
|
479
|
+
// `/fabric-import` recommendation banner alongside the broad summary.
|
|
532
480
|
const recommendImport = shouldRecommendImport(cwd);
|
|
533
481
|
|
|
534
|
-
// rc.
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
const
|
|
540
|
-
typeof payload.revision_hash === "string" ? payload.revision_hash : "";
|
|
541
|
-
let bodySuppressed = false;
|
|
542
|
-
if (currentHash.length > 0) {
|
|
543
|
-
const lastHash = readSessionStartLastHash(cwd);
|
|
544
|
-
if (lastHash !== null && lastHash === currentHash) {
|
|
545
|
-
bodySuppressed = true;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Build emitted lines. When the body is hash-suppressed we skip the
|
|
550
|
-
// broad summary entirely; only the import banner (if applicable) goes
|
|
551
|
-
// to stderr in that case.
|
|
552
|
-
const lines = bodySuppressed ? [] : renderSummary(payload);
|
|
482
|
+
// rc.12: broad-summary body is unconditionally rendered on every
|
|
483
|
+
// SessionStart fire (Skill-style progressive disclosure). The prior
|
|
484
|
+
// revision_hash cooldown gate (rc.7 T8 — rc.11) was removed because
|
|
485
|
+
// compact/clear-triggered SessionStart re-fires must re-inject the menu
|
|
486
|
+
// for the agent's working memory.
|
|
487
|
+
const lines = renderSummary(payload);
|
|
553
488
|
|
|
554
489
|
if (recommendImport) {
|
|
555
490
|
lines.push(IMPORT_RECOMMENDATION_BANNER);
|
|
@@ -560,16 +495,6 @@ function main(env, stdio) {
|
|
|
560
495
|
for (const line of lines) {
|
|
561
496
|
err.write(`${line}\n`);
|
|
562
497
|
}
|
|
563
|
-
|
|
564
|
-
// Update sidecar AFTER successful emit. We only persist the hash when
|
|
565
|
-
// the broad-summary body actually went out (i.e. the gate let the body
|
|
566
|
-
// through). If the body was suppressed but the banner emitted on its
|
|
567
|
-
// own, we deliberately do NOT bump the sidecar — the next session
|
|
568
|
-
// should still get to compare against the prior canonical-graph hash
|
|
569
|
-
// and re-emit the body when the graph actually changes.
|
|
570
|
-
if (!bodySuppressed && currentHash.length > 0) {
|
|
571
|
-
writeSessionStartLastHash(cwd, currentHash);
|
|
572
|
-
}
|
|
573
498
|
} catch {
|
|
574
499
|
// Silent — never block session start on hook failure.
|
|
575
500
|
}
|
|
@@ -583,9 +508,6 @@ module.exports = {
|
|
|
583
508
|
renderTruncated,
|
|
584
509
|
renderSummary,
|
|
585
510
|
truncateSummary,
|
|
586
|
-
// rc.7 T8: revision_hash gating sidecar helpers (exported for unit testing).
|
|
587
|
-
readSessionStartLastHash,
|
|
588
|
-
writeSessionStartLastHash,
|
|
589
511
|
// rc.8 underseed self-check helpers (exported for unit testing).
|
|
590
512
|
countCanonicalNodes,
|
|
591
513
|
readUnderseedThreshold,
|
|
@@ -599,7 +521,6 @@ module.exports = {
|
|
|
599
521
|
MATURITY_PROVEN,
|
|
600
522
|
MATURITY_VERIFIED,
|
|
601
523
|
MATURITY_DRAFT,
|
|
602
|
-
SESSIONSTART_HASH_CACHE_FILE,
|
|
603
524
|
DEFAULT_UNDERSEED_NODE_THRESHOLD,
|
|
604
525
|
KNOWLEDGE_CANONICAL_TYPES,
|
|
605
526
|
IMPORT_RECOMMENDATION_BANNER,
|