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

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-WLEJ5XHT.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 +742 -259
  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 +50 -1
  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 +47 -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
@@ -1,27 +1,27 @@
1
1
  /**
2
2
  * v2.0.0-rc.37 NEW-30: shared client-protocol adapter for hook scripts.
3
3
  *
4
- * The three host clients (Claude Code / Codex CLI / Cursor) differ in how a
4
+ * The two host clients (Claude Code / Codex CLI) differ in how a
5
5
  * hook surfaces context back to the model:
6
6
  * - Claude Code reads a stdout JSON envelope
7
7
  * ({ hookSpecificOutput: { hookEventName, additionalContext } }).
8
- * - Codex CLI and Cursor read plain stderr text.
8
+ * - Codex CLI reads plain stderr text.
9
9
  * Each hook had its own copy of the detect-client + read-stdin + pick-channel
10
10
  * logic (fabric-hint.detectClient, cite-policy-evict.isClaudeCode + readStdinJson,
11
11
  * knowledge-hint-broad inline CLAUDE_PROJECT_DIR check). This module is the
12
12
  * single canonical implementation so the protocol choice lives in one place.
13
13
  *
14
14
  * Provides:
15
- * - detectClient(dirnameHint?) → 'cc' | 'codex' | 'cursor' | undefined
15
+ * - detectClient(dirnameHint?) → 'cc' | 'codex' | undefined
16
16
  * 3-tier: FABRIC_HINT_CLIENT env override → CLAUDE_PROJECT_DIR (cc) →
17
- * __dirname path heuristic (.claude / .codex / .cursor). dirnameHint
17
+ * __dirname path heuristic (.claude / .codex). dirnameHint
18
18
  * defaults to this lib's own dir (which still lives under the client
19
19
  * dir, e.g. .claude/hooks/lib), so the heuristic stays accurate.
20
20
  * - isClaudeCode() → boolean (CLAUDE_PROJECT_DIR present)
21
21
  * - readStdinJson({ timeoutMs }) → Promise<object | null>
22
22
  * Async stdin JSON reader; null on parse error / closed stdin / timeout.
23
23
  * - emitContext(text, { client, eventName, streams, forceStderr }) → void
24
- * Standardised output: Claude Code → stdout JSON envelope; Codex/Cursor
24
+ * Standardised output: Claude Code → stdout JSON envelope; Codex
25
25
  * → plain stderr. forceStderr pins stderr even on Claude Code (used for
26
26
  * SessionStart one-shot reminders). Best-effort — never throws.
27
27
  *
@@ -40,7 +40,7 @@ function detectClient(dirnameHint) {
40
40
  const envClient = process.env.FABRIC_HINT_CLIENT;
41
41
  if (typeof envClient === "string" && envClient.length > 0) {
42
42
  const normalised = envClient.trim().toLowerCase();
43
- if (normalised === "cc" || normalised === "codex" || normalised === "cursor") {
43
+ if (normalised === "cc" || normalised === "codex") {
44
44
  return normalised;
45
45
  }
46
46
  }
@@ -51,7 +51,6 @@ function detectClient(dirnameHint) {
51
51
  try {
52
52
  if (dir.includes(".claude/") || dir.includes(".claude\\")) return "cc";
53
53
  if (dir.includes(".codex/") || dir.includes(".codex\\")) return "codex";
54
- if (dir.includes(".cursor/") || dir.includes(".cursor\\")) return "cursor";
55
54
  } catch {
56
55
  // fall through
57
56
  }
@@ -98,9 +97,69 @@ function emitContext(text, opts) {
98
97
  }
99
98
  }
100
99
 
100
+ // v2.2 dual-sink (Goal A / D7): two-channel emit. Unlike emitContext (which
101
+ // picks ONE channel), emitDualSink surfaces a knowledge breadcrumb to BOTH the
102
+ // human and the AI in one render, split into two fields, with the protocol
103
+ // shaped per client:
104
+ //
105
+ // cc / codex (symmetric, D7): a single stdout JSON envelope carrying
106
+ // { systemMessage: <human>, // the human sink
107
+ // hookSpecificOutput: { hookEventName, additionalContext: <ai> } } // AI sink
108
+ // camelCase + nested. `systemMessage` is the universal human-facing field
109
+ // (verified against official hook docs in the mode④ design session); it is
110
+ // what fixes the "stderr human channel is dead on CC" gap — CC suppresses
111
+ // hook stderr at exit 0, so the human never saw the old breadcrumb.
112
+ //
113
+ // unknown client (detection failed, not CC): fall back to a plain stderr
114
+ // breadcrumb (human preferred, else ai) — no known JSON contract to target.
115
+ //
116
+ // Either field may be null/empty: pass { human, ai } and only the present
117
+ // channels are written (e.g. a PreToolUse miss passes human:null → AI-only;
118
+ // nudge_mode silent passes human:null too). The AI field is ALWAYS the caller's
119
+ // to decide independently — this fn never derives one channel from the other,
120
+ // preserving the flow ⊥ observation invariant (D5).
121
+ //
122
+ // Never-throw contract (KT-DEC-0007): every path degrades silently.
123
+ function emitDualSink(payload, opts) {
124
+ const { human = null, ai = null } = payload || {};
125
+ const { client, eventName = "SessionStart", streams = {} } = opts || {};
126
+ const stdout = streams.stdout || process.stdout;
127
+ const stderr = streams.stderr || process.stderr;
128
+ const hasHuman = typeof human === "string" && human.length > 0;
129
+ const hasAi = typeof ai === "string" && ai.length > 0;
130
+ const resolved = client || detectClient();
131
+ try {
132
+ const useEnvelope =
133
+ resolved === "cc" ||
134
+ resolved === "codex" ||
135
+ (resolved === undefined && isClaudeCode());
136
+ if (useEnvelope) {
137
+ const envelope = {};
138
+ if (hasHuman) envelope.systemMessage = human;
139
+ if (hasAi) {
140
+ envelope.hookSpecificOutput = {
141
+ hookEventName: eventName,
142
+ additionalContext: ai,
143
+ };
144
+ }
145
+ if (Object.keys(envelope).length > 0) {
146
+ stdout.write(`${JSON.stringify(envelope)}\n`);
147
+ }
148
+ return;
149
+ }
150
+ // Unknown client: no JSON contract — surface the human breadcrumb (or ai)
151
+ // on stderr as a last resort so something is visible.
152
+ const fallback = hasHuman ? human : hasAi ? ai : null;
153
+ if (fallback !== null) stderr.write(`${fallback}\n`);
154
+ } catch {
155
+ // best-effort — never throw
156
+ }
157
+ }
158
+
101
159
  module.exports = {
102
160
  isClaudeCode,
103
161
  detectClient,
104
162
  readStdinJson,
105
163
  emitContext,
164
+ emitDualSink,
106
165
  };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * v2.2 dual-sink (Goal A / D4): nudge_mode + observe.* resolver for hook scripts.
3
+ *
4
+ * This lib answers ONE question for the human-facing sink: given the configured
5
+ * nudge_mode preset, the per-event observe.* overrides, and the event's
6
+ * structural gate (PreToolUse hit / Stop value), should this lifecycle event
7
+ * emit a human-facing `systemMessage`, and at what verbosity?
8
+ *
9
+ * CORE INVARIANT (D5 / KT-DEC-0007): this resolver governs ONLY the human sink.
10
+ * It has NO say over the AI sink (`hookSpecificOutput.additionalContext`). The
11
+ * model receives the same knowledge regardless of how quiet the human channel
12
+ * is — flow ⊥ observation. There is deliberately no `emitAi()` here; callers
13
+ * always compute and emit the AI payload unconditionally, then ask this resolver
14
+ * whether to additionally surface a human breadcrumb. The dedicated invariant
15
+ * test asserts that no nudge_mode / observe combination changes the AI branch.
16
+ *
17
+ * Resolution order for `resolveHumanSink(projectRoot, event, gate)`:
18
+ * 1. observe.<event> === false → suppress (explicit per-event mute wins)
19
+ * 2. structural gate fails → suppress (PreToolUse miss / Stop low-value:
20
+ * nothing meaningful to show the human, mode-independent per C5/D2/D6)
21
+ * 3. observe.<event> === true → emit (explicit per-event opt-in)
22
+ * 4. nudge_mode === "silent" → suppress (global human-channel mute)
23
+ * 5. otherwise → emit at the preset's verbosity
24
+ *
25
+ * `verbosity` (minimal | normal | verbose) is forwarded to the renderer so it
26
+ * can scale the human breadcrumb's detail; it never affects the AI payload.
27
+ *
28
+ * Never-throw contract: any read/parse failure degrades to the "normal" preset
29
+ * with the structural gate respected — a malfunctioning config must not silence
30
+ * the human channel by surprise (it falls back to the historical visible
31
+ * behavior), nor block the hook.
32
+ */
33
+
34
+ const { readConfig } = require("./config-cache.cjs");
35
+
36
+ const NUDGE_MODES = ["silent", "minimal", "normal", "verbose"];
37
+ const DEFAULT_NUDGE_MODE = "normal";
38
+ // The three observe.* event keys, mirroring observeConfigSchema in
39
+ // packages/shared/src/schemas/fabric-config.ts. Hooks pass the matching key.
40
+ const OBSERVE_EVENTS = ["session_start", "pre_tool_use", "stop"];
41
+
42
+ /**
43
+ * Resolve the configured nudge_mode preset. Unknown / absent → "normal".
44
+ */
45
+ function readNudgeMode(projectRoot) {
46
+ try {
47
+ const v = readConfig(projectRoot).nudge_mode;
48
+ return typeof v === "string" && NUDGE_MODES.includes(v) ? v : DEFAULT_NUDGE_MODE;
49
+ } catch {
50
+ return DEFAULT_NUDGE_MODE;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Resolve the per-event observe.* override for one event. Returns a strict
56
+ * boolean when explicitly set, otherwise undefined (preset decides). Tolerant of
57
+ * a malformed observe value (non-object → undefined).
58
+ */
59
+ function readObserveOverride(projectRoot, event) {
60
+ try {
61
+ const observe = readConfig(projectRoot).observe;
62
+ if (!observe || typeof observe !== "object") return undefined;
63
+ const v = observe[event];
64
+ return typeof v === "boolean" ? v : undefined;
65
+ } catch {
66
+ return undefined;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Decide whether `event` should emit a human-facing systemMessage, and at what
72
+ * verbosity. `gate` carries the event's structural signal:
73
+ * - { hit: boolean } for pre_tool_use (false = narrow miss → suppress)
74
+ * - { highValue: boolean } for stop (false = no high-value archive
75
+ * candidate → suppress, D6 value-gate)
76
+ * - {} for session_start (no structural gate)
77
+ * Omitting a gate field means "gate passes" (e.g. session_start never gates).
78
+ *
79
+ * Returns { emitHuman: boolean, verbosity: "minimal"|"normal"|"verbose", mode }.
80
+ */
81
+ function resolveHumanSink(projectRoot, event, gate) {
82
+ const mode = readNudgeMode(projectRoot);
83
+ const verbosity = mode === "silent" ? "minimal" : mode;
84
+ const override = OBSERVE_EVENTS.includes(event)
85
+ ? readObserveOverride(projectRoot, event)
86
+ : undefined;
87
+
88
+ // 1. explicit per-event mute wins over everything (even a hit).
89
+ if (override === false) return { emitHuman: false, verbosity, mode };
90
+
91
+ // 2. structural gate (mode-independent, C5/D2/D6): nothing to show → mute.
92
+ const g = gate || {};
93
+ if (event === "pre_tool_use" && g.hit === false) {
94
+ return { emitHuman: false, verbosity, mode };
95
+ }
96
+ if (event === "stop" && g.highValue === false) {
97
+ return { emitHuman: false, verbosity, mode };
98
+ }
99
+
100
+ // 3. explicit per-event opt-in (gate already passed above).
101
+ if (override === true) return { emitHuman: true, verbosity, mode };
102
+
103
+ // 4. global human-channel mute.
104
+ if (mode === "silent") return { emitHuman: false, verbosity, mode };
105
+
106
+ // 5. preset default — emit at the preset's verbosity.
107
+ return { emitHuman: true, verbosity, mode };
108
+ }
109
+
110
+ module.exports = {
111
+ readNudgeMode,
112
+ readObserveOverride,
113
+ resolveHumanSink,
114
+ NUDGE_MODES,
115
+ DEFAULT_NUDGE_MODE,
116
+ OBSERVE_EVENTS,
117
+ };
@@ -24,6 +24,7 @@
24
24
  // Namespace import (not destructured) so the atomic write goes through a single
25
25
  // mutable fs reference — also what the atomicity tests spy on (ISS-016).
26
26
  const fs = require("node:fs");
27
+ const fsp = require("node:fs/promises");
27
28
  const { dirname, join } = require("node:path");
28
29
 
29
30
  const CACHE_DIR_REL = join(".fabric", ".cache");
@@ -52,6 +53,22 @@ function atomicWrite(path, data) {
52
53
  }
53
54
  }
54
55
 
56
+ async function atomicWriteAsync(path, data) {
57
+ await fsp.mkdir(dirname(path), { recursive: true });
58
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
59
+ try {
60
+ await fsp.writeFile(tmp, data);
61
+ await fsp.rename(tmp, path);
62
+ } catch (err) {
63
+ try {
64
+ await fsp.rm(tmp, { force: true });
65
+ } catch {
66
+ // best-effort temp cleanup
67
+ }
68
+ throw err;
69
+ }
70
+ }
71
+
55
72
  function readJsonState(projectRoot, fileName, validate) {
56
73
  const path = cachePath(projectRoot, fileName);
57
74
  if (!fs.existsSync(path)) return null;
@@ -64,6 +81,17 @@ function readJsonState(projectRoot, fileName, validate) {
64
81
  }
65
82
  }
66
83
 
84
+ async function readJsonStateAsync(projectRoot, fileName, validate) {
85
+ const path = cachePath(projectRoot, fileName);
86
+ try {
87
+ const parsed = JSON.parse(await fsp.readFile(path, "utf8"));
88
+ if (typeof validate === "function" && !validate(parsed)) return null;
89
+ return parsed;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
67
95
  function writeJsonState(projectRoot, fileName, value) {
68
96
  try {
69
97
  atomicWrite(cachePath(projectRoot, fileName), JSON.stringify(value));
@@ -73,6 +101,15 @@ function writeJsonState(projectRoot, fileName, value) {
73
101
  }
74
102
  }
75
103
 
104
+ async function writeJsonStateAsync(projectRoot, fileName, value) {
105
+ try {
106
+ await atomicWriteAsync(cachePath(projectRoot, fileName), JSON.stringify(value));
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
76
113
  function readTextState(projectRoot, fileName) {
77
114
  const path = cachePath(projectRoot, fileName);
78
115
  if (!fs.existsSync(path)) return null;
@@ -83,6 +120,15 @@ function readTextState(projectRoot, fileName) {
83
120
  }
84
121
  }
85
122
 
123
+ async function readTextStateAsync(projectRoot, fileName) {
124
+ const path = cachePath(projectRoot, fileName);
125
+ try {
126
+ return (await fsp.readFile(path, "utf8")).trim();
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
86
132
  function writeTextState(projectRoot, fileName, text) {
87
133
  try {
88
134
  atomicWrite(cachePath(projectRoot, fileName), String(text));
@@ -92,12 +138,26 @@ function writeTextState(projectRoot, fileName, text) {
92
138
  }
93
139
  }
94
140
 
141
+ async function writeTextStateAsync(projectRoot, fileName, text) {
142
+ try {
143
+ await atomicWriteAsync(cachePath(projectRoot, fileName), String(text));
144
+ return true;
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+
95
150
  module.exports = {
96
151
  cachePath,
97
152
  readJsonState,
153
+ readJsonStateAsync,
98
154
  writeJsonState,
155
+ writeJsonStateAsync,
99
156
  readTextState,
157
+ readTextStateAsync,
100
158
  writeTextState,
159
+ writeTextStateAsync,
101
160
  atomicWrite,
161
+ atomicWriteAsync,
102
162
  CACHE_DIR_REL,
103
163
  };