@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.
@@ -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-MT3R57VG.js";
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-HQLEHH4O.js";
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
- IMPORT_POINTER_LINE,
12
- POINTER_LINE,
13
- POINTER_TARGETS,
14
- REVIEW_POINTER_LINE,
12
+ SECTION_TARGETS,
15
13
  SKILL_DESTINATIONS
16
- } from "./chunk-AW3G7ZH5.js";
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 stripArchiveSkillPointers(projectRoot) {
116
+ async function stripFabricKnowledgeBaseSection(projectRoot) {
119
117
  const results = [];
120
- for (const rel of POINTER_TARGETS) {
118
+ for (const rel of SECTION_TARGETS) {
121
119
  const target = join(projectRoot, rel);
122
120
  if (!existsSync(target)) {
123
- results.push({ step: "pointer", path: target, status: "skipped", message: "absent" });
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: "pointer",
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 pointerLiterals = [POINTER_LINE, REVIEW_POINTER_LINE, IMPORT_POINTER_LINE];
139
- const filtered = existing.split("\n").filter((line) => !pointerLiterals.some((literal) => line.includes(literal))).join("\n");
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: "pointer",
151
+ step: "section",
143
152
  path: target,
144
153
  status: "skipped",
145
- message: "no-fabric-pointers"
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: "pointer", path: target, status: "removed" });
160
+ results.push({ step: "section", path: target, status: "removed" });
152
161
  } catch (error) {
153
162
  results.push({
154
- step: "pointer",
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
- "pointer",
176
+ "section",
168
177
  projectRoot,
169
- () => stripArchiveSkillPointers(projectRoot)
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.10",
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.10",
24
- "@fenglimg/fabric-server": "2.0.0-rc.10"
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 init` and
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 { dirname, join } = require("node:path");
55
+ const { join } = require("node:path");
58
56
 
59
57
  // -----------------------------------------------------------------------------
60
- // rc.7 T8: SessionStart revision_hash gating.
61
- //
62
- // Q-14 problem: every SessionStart re-dumped the full broad knowledge list,
63
- // causing banner blindness. Solution: hash-of-canonical-graph gating — record
64
- // the last-emitted `payload.revision_hash` to a sidecar; on subsequent
65
- // SessionStart fires, compare. Match silent exit 0 (no re-dump). Mismatch
66
- // (canonical/ corpus changed planContext bumps revision_hash) emit AND
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
- const narrow = Array.isArray(payload && payload.narrow) ? payload.narrow : [];
492
- if (narrow.length === 0) return [];
493
-
494
- const truncated = narrow.length > TRUNCATION_THRESHOLD;
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 — ${narrow.length} broad-scoped knowledge entries available (truncated):`
497
- : `[fabric] Session start — ${narrow.length} broad-scoped knowledge entries available:`;
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(narrow) : renderFull(narrow);
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. The decision is taken BEFORE the
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.7 T8: revision_hash gate. If the CLI payload carries a stable
535
- // revision_hash and it matches the previously-emitted hash recorded in
536
- // the sidecar, the knowledge graph is unchanged since last session →
537
- // suppress the broad-summary body. The import-recommendation banner
538
- // (when applicable) is still emitted below regardless of this gate.
539
- const currentHash =
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,