claude-faf-mcp 5.7.2 → 5.9.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
- <!-- faf: claude-faf-mcp | TypeScript | mcp-server | FAF MCP server for Claude Desktop — persistent project context, 32 tools -->
2
- <!-- faf: doc=changelog | latest=v5.7.2 | canonical=project.faf | family=FAF -->
1
+ <!-- faf: claude-faf-mcp | TypeScript | mcp-server | FAF MCP server for Claude Desktop — persistent project context, 35 tools -->
2
+ <!-- faf: doc=changelog | latest=v5.9.0 | canonical=project.faf | family=FAF -->
3
3
 
4
4
  # Changelog
5
5
 
@@ -11,11 +11,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
- **Interop now enhances your files.** The same solid, structured `.faf` data is prefixed to the top of your context files for rapid AI consumption upfront — and your Markdown stays in the instruction lane.
14
+ ## [5.9.0] - 2026-06-12 The Sourced Edition
15
+
16
+ **Every answer comes from one source.** claude-faf-mcp now composes faf-cli's single-source engines — the 6Ws interview and Turbo-Cat detection — instead of carrying its own copies that drift. Fills come from real evidence or stay honestly empty; nothing is guessed. FAF don't lie, by construction.
15
17
 
16
18
  ### Changed
17
19
 
18
- - **Non-destructive interop.** `faf_agents`, `faf_gemini`, `faf_cursor`, and `faf_sync` now inject a structured `.faf` block at the top of AGENTS.md, GEMINI.md, .cursorrules, and CLAUDE.md and preserve everything you've written below. Re-runs update the block in place (idempotent); existing faf-generated files upgrade cleanly in one pass.
20
+ - **`faf_go` composes the canonical interview.** The guided interview is now sourced from faf-cli's public `INTERVIEW` / `interviewForMissing` (shipped in faf-cli 6.9.0) the hand-maintained local question registry is deleted, so question text and options can never drift from the kernel. Only missing AND active slots are asked (`slotignored` is skipped), so the full canonical set never means more questions on screen.
21
+ - **Turbo-Cat detection composes faf-cli.** `faf_formats` and `faf_auto` now use faf-cli's single-source Turbo-Cat (`turboCatScan` / `turboCatSlots`, ~200 formats; faf-cli 6.10.1) — the local 25-entry copy (`discoverFormatsInternal`) is deleted. Detection that's correct and can't drift: a server with an `mcpb` `manifest.json` is detected as what it is, not mis-flagged as a Chrome extension.
22
+ - **The `/faf` prompt drives to a *verified* 100%, and maintains it.** It now runs `faf_trust` to attest the score with the `✪` parity receipt, offers `faf_setup` to keep 100% fresh every session, and reports the quiet `✪` seal. Its lead rule: fill only from sourced evidence — never guess; hand the human, plainly, only what only they can answer.
23
+
24
+ ### Removed
25
+
26
+ - **The v5 guessing extractor.** The legacy human-context extractor inferred slots from the project name / tech stack (writing `INFERRED`/`DEFAULT` placeholders into empty slots). Removed; replaced by a sourced-or-empty README extractor, verified by a test that proves it cannot fabricate. No-guess is now structural, not accidental.
27
+
28
+ 35 tools live. 567 tests, 0 fail. Composes faf-cli `^6.10.1`.
29
+
30
+ ## [5.8.0] - 2026-06-11 — The Trust Edition
31
+
32
+ **Claude Code-native context that just works.** Five pillars, one theme: the experience an agent can parse and a human can read (quiet + typed), the proof a third party can verify (parity hash + ✪ receipt), and the integration that earns "native" (the session hook). Plus enhanced interop — your files get enhanced, never replaced.
33
+
34
+ ### Added
35
+
36
+ - **Native SessionStart hook (`--session-refresh`).** One cheap, deterministic action at every Claude Code session start: refresh CLAUDE.md's faf-managed block from project.faf, then hand over the baton — a one-line heartbeat carrying the quiet-ladder seal and score (`faf: context ✪ 100% — fresh`). Freshness-gated (no write, no mtime churn when current), never throws, always exit 0; non-faf directories stay fully silent.
37
+ - **`faf_setup` (tool #35).** The explicit hook installer. Preview by default — shows the exact settings JSON and path, writes nothing; `confirm: true` merges non-destructively into the project's `.claude/settings.json` (every existing key and hook preserved, idempotent); `remove: true` takes out exactly the faf hook. Malformed settings are surfaced, never overwritten.
38
+ - **Determinism parity hash.** `faf_score` (and `faf_trust`) now carry an engine-agnostic `parity` receipt: a SHA-256 over the canonical score projection, bound to the input bytes — any conformant engine (faf-cli, the Rust kernel, this server) reproduces it, and a third party can verify `sha256(projection) === parityHash`. Falsifiable, not self-asserted.
39
+ - **The ✪ trust receipt.** `faf_trust` emits a render-identical, no-emoji, self-verifying score+parity artifact (text + structuredContent) — a receipt that means the same thing in a terminal, a CI log, and a report.
40
+ - **Typed structured output across the data tools.** `faf_context`, `faf_list`, `faf_formats`, `faf_dna`, `faf_doctor`, `faf_check` join `faf_score`/`faf_status` with `outputSchema` + `structuredContent` on every return path (text retained for back-compat).
41
+
42
+ ### Changed
43
+
44
+ - **Quiet output, everywhere, always.** Tool results and descriptions carry no emoji — plain, parseable, scriptable. Tier glyphs use the quiet ladder `♡ ○ ● ◇ ◆ ★ ✪` (geometric symbols, render-identical); ✪ is the Trophy seal. One predictable output format, no toggle.
45
+ - **The generated CLAUDE.md block is quiet too.** The native faf→CLAUDE.md renderer now emits the canonical format (`What This Is / Stack / Context`) — no emoji, no brand voice — matching faf-cli's export, since the session hook makes that block part of every session's context. `slotignored` slots stay invisible.
46
+
47
+ ### Fixed
48
+
49
+ - **Enhanced interop — your files are enhanced, never replaced.** The interop tools (`faf_agents`, `faf_gemini`, `faf_cursor`, `faf_sync`) overwrote AGENTS.md, GEMINI.md, .cursorrules, and CLAUDE.md wholesale — anything you'd written was lost. Now every write goes through a non-destructive injector: a structured `.faf` block owns the top of the file, everything you've written below is preserved, re-runs update the block in place (idempotent), and existing faf-generated files upgrade cleanly in one pass. A build-failing write-guard makes the old behavior impossible to reintroduce.
50
+ - **`faf_trust` works again.** It shelled out to a `faf trust` subcommand that no longer exists in faf-cli (every call errored); now a self-contained local attestation.
51
+
52
+ 35 tools live. 547 tests, 0 fail.
19
53
 
20
54
  ## [5.7.2] - 2026-06-11 — The Canonical Edition
21
55
 
package/CLAUDE.md CHANGED
@@ -1,3 +1,4 @@
1
+ <!-- faf:start -->
1
2
  <!-- faf: claude-faf-mcp | TypeScript | mcp | Persistent project context for Claude — define once, never re-explain -->
2
3
  <!-- faf: claim=project.faf | family=FAF -->
3
4
 
@@ -28,4 +29,5 @@ Persistent project context for Claude — define once, never re-explain
28
29
 
29
30
  ---
30
31
 
31
- *STATUS: BI-SYNC ACTIVE — 2026-06-10T01:27:09.822Z*
32
+ *STATUS: BI-SYNC ACTIVE — 2026-06-12T16:18:54.214Z*
33
+ <!-- faf:end -->
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
- <!-- faf: claude-faf-mcp | TypeScript | mcp-server | FAF MCP server for Claude — persistent project context, 32 tools -->
1
+ <!-- faf: claude-faf-mcp | TypeScript | mcp-server | FAF MCP server for Claude — persistent project context, 35 tools -->
2
2
  <!-- faf: doc=readme | canonical=project.faf | score=100 | family=FAF -->
3
3
 
4
- # claude-faf-mcp — The Canonical Edition
4
+ # claude-faf-mcp — The Sourced Edition
5
5
 
6
6
  [![npm version](https://img.shields.io/npm/v/claude-faf-mcp?color=00CCFF)](https://www.npmjs.com/package/claude-faf-mcp)
7
7
  [![FAF Trophy 100%](https://img.shields.io/badge/FAF-%F0%9F%8F%86%20100%25-000000?labelColor=FF6B35)](https://faf.one)
@@ -26,9 +26,11 @@
26
26
 
27
27
  > ⚡ **New: `/faf` prompt** — type `/faf` in Claude Desktop. It checks your project, scores it, drives it to 100%, and syncs. Relentlessly. One command.
28
28
 
29
- > 🏆 **v5.7.2 — The Canonical Edition.** Security patch caller-supplied `path` arguments are now confined: reads are restricted to `.faf` / `.fafm` context files and file ops to the project root (override with `FAF_ALLOWED_ROOTS`), closing an arbitrary local file read. Built on the Canonical foundation: typed structured output (`structuredContent` + output schemas), edge-direct remote, 32 must-have tools.
29
+ > 🏆 **v5.9.0 — The Sourced Edition.** Every answer comes from one source. `faf_go` and Turbo-Cat detection now **compose faf-cli's single-source engines** instead of carrying their own copies fills come from real evidence or stay honestly empty, nothing guessed. The legacy guessing extractor is gone; the `/faf` prompt drives to a *verified* 100% (`faf_trust` + `✪` parity receipt) and keeps it fresh. FAF don't lie, by construction.
30
30
 
31
- 32 MCP tools. IANA-registered formats (`application/vnd.faf+yaml` · `application/vnd.fafm+yaml`). 2,538 test executions per push.
31
+ > 🏆 **v5.8.0 — The Trust Edition.** Claude Code-native context that just works. A native SessionStart hook opens every session with fresh context and a one-line `✪` heartbeat (`faf: context ✪ 100% — fresh`); tool output is quiet (no emoji, parseable) and typed (`structuredContent` everywhere); every score carries a deterministic parity hash any engine reproduces, sealed in a self-verifying `✪` receipt. Installed explicitly via `faf_setup` — preview first, your settings preserved. Built on the Canonical foundation: path-confined file access, edge-direct remote, 35 tools.
32
+
33
+ 35 MCP tools. IANA-registered formats (`application/vnd.faf+yaml` · `application/vnd.fafm+yaml`). 1,701 test executions per push.
32
34
 
33
35
  ---
34
36
 
@@ -82,9 +84,9 @@ Same `.faf`, every surface — Claude, Gemini, Grok, Cursor. **[faf-cli on npm
82
84
 
83
85
  **Click** — one-click `.mcpb`
84
86
 
85
- [**⬇ Download `claude-faf-mcp-5.7.2.mcpb`**](https://github.com/Wolfe-Jam/claude-faf-mcp/releases/latest/download/claude-faf-mcp-5.7.2.mcpb)
87
+ [**⬇ Download `claude-faf-mcp-5.9.0.mcpb`**](https://github.com/Wolfe-Jam/claude-faf-mcp/releases/latest/download/claude-faf-mcp-5.9.0.mcpb)
86
88
 
87
- Double-click. **No terminal. No JSON config. 32 tools live in 10 seconds.**
89
+ Double-click. **No terminal. No JSON config. 35 tools live in 10 seconds.**
88
90
 
89
91
  **Copy** — paste-prompt to Claude
90
92
 
@@ -246,7 +248,7 @@ Same `project.faf`. Same scoring. Same result. Different execution layer.
246
248
 
247
249
  ## Quality
248
250
 
249
- 423 tests · 13 suites · 6 platforms (ubuntu/macos/windows × Node 18/20)
251
+ 567 tests · 26 suites · 3 platforms (bun on ubuntu/macos/windows)
250
252
 
251
253
  **[CI Dashboard →](https://github.com/Wolfe-Jam/claude-faf-mcp/actions/workflows/ci.yml)**
252
254
 
@@ -22,7 +22,13 @@ export interface BiSyncResult {
22
22
  message: string;
23
23
  }
24
24
  /**
25
- * 🔄 Convert project.faf YAML content to CLAUDE.md Markdown format
25
+ * Convert project.faf YAML content to CLAUDE.md Markdown — QUIET format
26
+ * (Trust Edition P1 applied to the generated artifact, not just tool output).
27
+ *
28
+ * The block this renders is injected into the file Claude READS every session,
29
+ * so it is plain, parseable context: no emoji, no brand voice (that lives in
30
+ * marketing), nothing the consuming model has to skip over. Mirrors the
31
+ * canonical faf-cli CLAUDE.md export shape: What This Is / Stack / Context.
26
32
  */
27
33
  export declare function fafToClaudeMd(fafContent: string): string;
28
34
  /**
@@ -48,56 +48,63 @@ const agents_js_1 = require("./agents.js");
48
48
  const inject_1 = require("../inject");
49
49
  const cursor_js_1 = require("./cursor.js");
50
50
  const gemini_js_1 = require("./gemini.js");
51
+ /** A slot value that carries real content (not empty, not slotignored). */
52
+ function filled(v) {
53
+ return typeof v === 'string' && v.trim() !== '' && v.trim() !== 'slotignored';
54
+ }
55
+ /** snake_case slot key → Title Case label (main_language → Main Language). */
56
+ function labelOf(key) {
57
+ return key.split('_').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
58
+ }
51
59
  /**
52
- * 🔄 Convert project.faf YAML content to CLAUDE.md Markdown format
60
+ * Convert project.faf YAML content to CLAUDE.md Markdown — QUIET format
61
+ * (Trust Edition P1 applied to the generated artifact, not just tool output).
62
+ *
63
+ * The block this renders is injected into the file Claude READS every session,
64
+ * so it is plain, parseable context: no emoji, no brand voice (that lives in
65
+ * marketing), nothing the consuming model has to skip over. Mirrors the
66
+ * canonical faf-cli CLAUDE.md export shape: What This Is / Stack / Context.
53
67
  */
54
68
  function fafToClaudeMd(fafContent) {
55
69
  try {
56
70
  const fafData = (0, yaml_1.parse)(fafContent);
57
- let claudeMd = `# 🏎️ CLAUDE.md - ${fafData.project?.name || 'Project'} Persistent Context & Intelligence\n\n`;
58
- // Project State
59
- if (fafData.project) {
60
- claudeMd += `## PROJECT STATE: ${fafData.context_quality?.overall_assessment || 'ACTIVE'} 🚀\n`;
61
- if (fafData.project.goal) {
62
- claudeMd += `**Current Position:** ${fafData.project.goal}\n`;
63
- }
64
- claudeMd += `**Tyre Compound:** ULTRASOFT C5 (Maximum Performance)\n\n`;
65
- claudeMd += `---\n\n`;
71
+ const name = fafData.project?.name || 'Project';
72
+ const lines = [`# CLAUDE.md — ${name}`, ''];
73
+ // What This Is — the one-line identity.
74
+ const what = fafData.project?.goal || fafData.instant_context?.what_building;
75
+ if (filled(what)) {
76
+ lines.push('## What This Is', '', what, '');
66
77
  }
67
- // Core Context
68
- claudeMd += `## 🎨 CORE CONTEXT\n\n`;
69
- if (fafData.project) {
70
- claudeMd += `### Project Identity\n`;
71
- claudeMd += `- **Name:** ${fafData.project.name || 'Unknown'}\n`;
72
- if (fafData.instant_context?.tech_stack) {
73
- claudeMd += `- **Stack:** ${fafData.instant_context.tech_stack}\n`;
74
- }
75
- claudeMd += `- **Quality:** F1-INSPIRED (Championship Performance)\n\n`;
78
+ // Stack — every filled slot, slotignored stays invisible (the What-Not).
79
+ const stackEntries = [];
80
+ if (filled(fafData.project?.main_language)) {
81
+ stackEntries.push(`- **Language:** ${fafData.project.main_language}`);
76
82
  }
77
- // Technical Context
78
- if (fafData.instant_context) {
79
- claudeMd += `### Technical Architecture\n`;
80
- if (fafData.instant_context.what_building) {
81
- claudeMd += `- **What Building:** ${fafData.instant_context.what_building}\n`;
82
- }
83
- if (fafData.instant_context.main_language) {
84
- claudeMd += `- **Main Language:** ${fafData.instant_context.main_language}\n`;
83
+ else if (filled(fafData.instant_context?.main_language)) {
84
+ stackEntries.push(`- **Language:** ${fafData.instant_context.main_language}`);
85
+ }
86
+ if (fafData.stack && typeof fafData.stack === 'object') {
87
+ for (const [key, value] of Object.entries(fafData.stack)) {
88
+ if (filled(value))
89
+ stackEntries.push(`- **${labelOf(key)}:** ${value}`);
85
90
  }
86
- claudeMd += `\n`;
87
91
  }
88
- // Context Quality
89
- if (fafData.context_quality) {
90
- claudeMd += `### 📊 Context Quality Status\n`;
91
- claudeMd += `- **Overall Assessment:** ${fafData.context_quality.overall_assessment || 'Good'}\n`;
92
- claudeMd += `- **Last Updated:** ${new Date().toISOString().split('T')[0]}\n\n`;
92
+ if (stackEntries.length === 0 && filled(fafData.instant_context?.tech_stack)) {
93
+ stackEntries.push(`- **Stack:** ${fafData.instant_context.tech_stack}`);
94
+ }
95
+ if (stackEntries.length > 0) {
96
+ lines.push('## Stack', '', ...stackEntries, '');
97
+ }
98
+ // Context — the 6Ws, exactly as authored.
99
+ const sixWs = ['who', 'what', 'why', 'where', 'when', 'how'];
100
+ const contextEntries = sixWs
101
+ .filter((w) => filled(fafData.human_context?.[w]))
102
+ .map((w) => `- **${labelOf(w)}:** ${fafData.human_context[w]}`);
103
+ if (contextEntries.length > 0) {
104
+ lines.push('## Context', '', ...contextEntries, '');
93
105
  }
94
- // Championship Footer
95
- claudeMd += `---\n\n`;
96
- claudeMd += `**STATUS: BI-SYNC ACTIVE 🔗 - Synchronized with .faf context!**\n\n`;
97
- claudeMd += `*Last Sync: ${new Date().toISOString()}*\n`;
98
- claudeMd += `*Sync Engine: F1-Inspired Software Engineering*\n`;
99
- claudeMd += `*🏎️⚡️_championship_sync*\n`;
100
- return claudeMd;
106
+ lines.push('---', '', `*STATUS: BI-SYNC ACTIVE — ${new Date().toISOString()}*`, '');
107
+ return lines.join('\n');
101
108
  }
102
109
  catch (error) {
103
110
  throw new Error(`Failed to convert .faf to CLAUDE.md: ${error instanceof Error ? error.message : String(error)}`);
@@ -0,0 +1,13 @@
1
+ export type SessionRefreshAction = 'fresh' | 'refreshed' | 'created' | 'no-faf' | 'error';
2
+ export interface SessionRefreshResult {
3
+ action: SessionRefreshAction;
4
+ /** The one-line heartbeat for SessionStart stdout (joins session context) — '' only for no-faf. */
5
+ message: string;
6
+ }
7
+ /**
8
+ * Refresh CLAUDE.md from project.faf in `projectDir` (defaults to cwd — Claude
9
+ * Code runs hooks with cwd at the project root). Never throws: a hook must not
10
+ * break a session. No deep search — the hook is fast and predictable or it is
11
+ * nothing.
12
+ */
13
+ export declare function sessionRefresh(projectDir?: string): Promise<SessionRefreshResult>;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.sessionRefresh = sessionRefresh;
37
+ /**
38
+ * Session refresh — Trust Edition Pillar 5 (the hook action).
39
+ *
40
+ * What the Claude Code SessionStart hook runs: ONE cheap, deterministic action —
41
+ * refresh CLAUDE.md's faf-managed block from project.faf, non-destructively —
42
+ * then hand over the baton: a ONE-LINE heartbeat carrying the ✪-ladder seal and
43
+ * score. The line is a relay — it proves the native integration is alive (a
44
+ * silent hook is indistinguishable from a broken one) and it grounds the model
45
+ * ("a current .faf exists, score N%, don't re-derive the project").
46
+ *
47
+ * Freshness gate: if CLAUDE.md already carries the faf markers and is at least
48
+ * as new as project.faf, nothing is WRITTEN — no write, no mtime churn — but
49
+ * the heartbeat still speaks. Only non-faf directories stay fully silent: the
50
+ * hook never talks where it has no business.
51
+ */
52
+ const path = __importStar(require("path"));
53
+ const fs_1 = require("fs");
54
+ const inject_1 = require("../inject");
55
+ const bi_sync_1 = require("./bi-sync");
56
+ const receipt_1 = require("../../trust/receipt");
57
+ /** Score the .faf via the truthful single-source kernel; '' when unscorable (heartbeat survives). */
58
+ async function scoreLine(fafContent) {
59
+ try {
60
+ const { scoreFafYaml } = await import('../../utils/faf-cli-bridge.js').then((m) => m.fafCli);
61
+ const result = scoreFafYaml(fafContent);
62
+ const score = Math.round(result.score);
63
+ return `${(0, receipt_1.sealForScore)(score)} ${score}%`;
64
+ }
65
+ catch {
66
+ return ''; // no kernel, no score — the heartbeat still beats
67
+ }
68
+ }
69
+ /**
70
+ * Refresh CLAUDE.md from project.faf in `projectDir` (defaults to cwd — Claude
71
+ * Code runs hooks with cwd at the project root). Never throws: a hook must not
72
+ * break a session. No deep search — the hook is fast and predictable or it is
73
+ * nothing.
74
+ */
75
+ async function sessionRefresh(projectDir = process.cwd()) {
76
+ try {
77
+ const fafPath = path.join(projectDir, 'project.faf');
78
+ let fafStat;
79
+ try {
80
+ fafStat = await fs_1.promises.stat(fafPath);
81
+ }
82
+ catch {
83
+ return { action: 'no-faf', message: '' }; // not a .faf project — silently not our session
84
+ }
85
+ const claudeMdPath = path.join(projectDir, 'CLAUDE.md');
86
+ let claudeStat = null;
87
+ let claudeContent = null;
88
+ try {
89
+ claudeStat = await fs_1.promises.stat(claudeMdPath);
90
+ claudeContent = await fs_1.promises.readFile(claudeMdPath, 'utf-8');
91
+ }
92
+ catch {
93
+ /* CLAUDE.md does not exist yet */
94
+ }
95
+ const fafContent = await fs_1.promises.readFile(fafPath, 'utf-8');
96
+ const seal = await scoreLine(fafContent);
97
+ // Freshness gate: markers present + CLAUDE.md at least as new as project.faf
98
+ // → no WRITE (no mtime churn), but the heartbeat still hands over the baton.
99
+ if (claudeStat && claudeContent !== null && claudeContent.includes(inject_1.FAF_START) && claudeStat.mtimeMs >= fafStat.mtimeMs) {
100
+ return { action: 'fresh', message: `faf: context ${seal ? `${seal} — ` : ''}fresh`.replace(' ', ' ') };
101
+ }
102
+ const block = (0, bi_sync_1.fafToClaudeMd)(fafContent);
103
+ await (0, inject_1.injectFafBlock)(claudeMdPath, block);
104
+ return claudeStat === null
105
+ ? { action: 'created', message: `faf: CLAUDE.md created${seal ? ` — ${seal}` : ''}` }
106
+ : { action: 'refreshed', message: `faf: context refreshed${seal ? ` — ${seal}` : ''}` };
107
+ }
108
+ catch (error) {
109
+ // Never break a session start. Quiet diagnostic on stderr is the caller's call.
110
+ return {
111
+ action: 'error',
112
+ message: `faf: session refresh skipped (${error instanceof Error ? error.message : String(error)})`,
113
+ };
114
+ }
115
+ }
116
+ //# sourceMappingURL=session-refresh.js.map
@@ -0,0 +1,32 @@
1
+ /** The fingerprint by which we recognise OUR hook (and only ours). */
2
+ export declare const HOOK_FINGERPRINT = "claude-faf-mcp --session-refresh";
3
+ /** The command the SessionStart hook runs. npx resolves local or global installs. */
4
+ export declare const HOOK_COMMAND = "npx -y claude-faf-mcp --session-refresh";
5
+ /** The exact entry merged into settings.hooks.SessionStart. */
6
+ export declare function buildHookEntry(): {
7
+ hooks: Array<{
8
+ type: 'command';
9
+ command: string;
10
+ }>;
11
+ };
12
+ export interface SetupHookResult {
13
+ action: 'preview' | 'installed' | 'already-installed' | 'removed' | 'not-installed' | 'error';
14
+ settingsPath: string;
15
+ /** The full settings object as it would be / has been written (preview + writes). */
16
+ settings?: Record<string, unknown>;
17
+ message: string;
18
+ }
19
+ /** Pure merge: existing settings + our hook appended (no mutation of input). */
20
+ export declare function mergeHookIntoSettings(existing: Record<string, unknown>): Record<string, unknown>;
21
+ /** Pure removal: strips exactly OUR hook entries; prunes only what emptied. */
22
+ export declare function removeHookFromSettings(existing: Record<string, unknown>): Record<string, unknown>;
23
+ /**
24
+ * The faf_setup flow.
25
+ * - no flags → PREVIEW: show the exact JSON, write NOTHING
26
+ * - confirm: true → non-destructive merge into .claude/settings.json
27
+ * - remove: true → take out exactly our hook
28
+ */
29
+ export declare function setupSessionHook(projectDir: string, opts?: {
30
+ confirm?: boolean;
31
+ remove?: boolean;
32
+ }): Promise<SetupHookResult>;
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.HOOK_COMMAND = exports.HOOK_FINGERPRINT = void 0;
37
+ exports.buildHookEntry = buildHookEntry;
38
+ exports.mergeHookIntoSettings = mergeHookIntoSettings;
39
+ exports.removeHookFromSettings = removeHookFromSettings;
40
+ exports.setupSessionHook = setupSessionHook;
41
+ /**
42
+ * faf_setup — Trust Edition Pillar 5 (the explicit installer).
43
+ *
44
+ * Installs the SessionStart refresh hook into the PROJECT's
45
+ * `.claude/settings.json` — and only with the caller's explicit confirm.
46
+ * The tool SHOWS the exact JSON it will write before it writes anything;
47
+ * the merge is non-destructive ("enhance, never replace" applied to
48
+ * settings): every existing key and every existing hook is preserved,
49
+ * ours is appended once, and re-runs are idempotent. `remove` takes out
50
+ * exactly ours and nothing else.
51
+ *
52
+ * JSON has no comment markers, so injectFafBlock cannot own a block here —
53
+ * the structured merge below is the JSON equivalent: parse, preserve, append.
54
+ */
55
+ const path = __importStar(require("path"));
56
+ const fs_1 = require("fs");
57
+ /** The fingerprint by which we recognise OUR hook (and only ours). */
58
+ exports.HOOK_FINGERPRINT = 'claude-faf-mcp --session-refresh';
59
+ /** The command the SessionStart hook runs. npx resolves local or global installs. */
60
+ exports.HOOK_COMMAND = `npx -y ${exports.HOOK_FINGERPRINT}`;
61
+ /** The exact entry merged into settings.hooks.SessionStart. */
62
+ function buildHookEntry() {
63
+ return { hooks: [{ type: 'command', command: exports.HOOK_COMMAND }] };
64
+ }
65
+ function hasOurHook(settings) {
66
+ const sessionStart = settings?.hooks?.SessionStart;
67
+ if (!Array.isArray(sessionStart))
68
+ return false;
69
+ return sessionStart.some((m) => Array.isArray(m?.hooks) && m.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(exports.HOOK_FINGERPRINT)));
70
+ }
71
+ async function readSettings(settingsPath) {
72
+ try {
73
+ const raw = await fs_1.promises.readFile(settingsPath, 'utf-8');
74
+ const parsed = JSON.parse(raw);
75
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
76
+ throw new Error('settings.json is not a JSON object');
77
+ }
78
+ return parsed;
79
+ }
80
+ catch (error) {
81
+ if (error?.code === 'ENOENT')
82
+ return {};
83
+ throw error; // malformed JSON etc. — surface it, never overwrite what we can't parse
84
+ }
85
+ }
86
+ /** Pure merge: existing settings + our hook appended (no mutation of input). */
87
+ function mergeHookIntoSettings(existing) {
88
+ const merged = { ...existing };
89
+ const hooks = { ...(existing.hooks ?? {}) };
90
+ const sessionStart = Array.isArray(hooks.SessionStart) ? [...hooks.SessionStart] : [];
91
+ sessionStart.push(buildHookEntry());
92
+ hooks.SessionStart = sessionStart;
93
+ merged.hooks = hooks;
94
+ return merged;
95
+ }
96
+ /** Pure removal: strips exactly OUR hook entries; prunes only what emptied. */
97
+ function removeHookFromSettings(existing) {
98
+ const merged = { ...existing };
99
+ const hooks = { ...(existing.hooks ?? {}) };
100
+ const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
101
+ const kept = sessionStart
102
+ .map((m) => {
103
+ if (!Array.isArray(m?.hooks))
104
+ return m;
105
+ const remaining = m.hooks.filter((h) => !(typeof h?.command === 'string' && h.command.includes(exports.HOOK_FINGERPRINT)));
106
+ return remaining.length === m.hooks.length ? m : { ...m, hooks: remaining };
107
+ })
108
+ .filter((m) => !Array.isArray(m?.hooks) || m.hooks.length > 0);
109
+ if (kept.length > 0)
110
+ hooks.SessionStart = kept;
111
+ else
112
+ delete hooks.SessionStart;
113
+ if (Object.keys(hooks).length > 0)
114
+ merged.hooks = hooks;
115
+ else
116
+ delete merged.hooks;
117
+ return merged;
118
+ }
119
+ /**
120
+ * The faf_setup flow.
121
+ * - no flags → PREVIEW: show the exact JSON, write NOTHING
122
+ * - confirm: true → non-destructive merge into .claude/settings.json
123
+ * - remove: true → take out exactly our hook
124
+ */
125
+ async function setupSessionHook(projectDir, opts = {}) {
126
+ const settingsPath = path.join(projectDir, '.claude', 'settings.json');
127
+ try {
128
+ const existing = await readSettings(settingsPath);
129
+ const installed = hasOurHook(existing);
130
+ if (opts.remove) {
131
+ if (!installed) {
132
+ return { action: 'not-installed', settingsPath, message: 'No faf SessionStart hook found — nothing to remove.' };
133
+ }
134
+ const next = removeHookFromSettings(existing);
135
+ await fs_1.promises.writeFile(settingsPath, JSON.stringify(next, null, 2) + '\n', 'utf-8');
136
+ return { action: 'removed', settingsPath, settings: next, message: 'faf SessionStart hook removed. Everything else in settings.json untouched.' };
137
+ }
138
+ if (installed) {
139
+ return { action: 'already-installed', settingsPath, settings: existing, message: 'faf SessionStart hook already installed — nothing to do (idempotent).' };
140
+ }
141
+ const next = mergeHookIntoSettings(existing);
142
+ if (!opts.confirm) {
143
+ return {
144
+ action: 'preview',
145
+ settingsPath,
146
+ settings: next,
147
+ message: `PREVIEW — nothing written. This is exactly what ${settingsPath} would become. ` +
148
+ `Run faf_setup again with confirm: true to install, or copy the hooks block in by hand.`,
149
+ };
150
+ }
151
+ await fs_1.promises.mkdir(path.dirname(settingsPath), { recursive: true });
152
+ await fs_1.promises.writeFile(settingsPath, JSON.stringify(next, null, 2) + '\n', 'utf-8');
153
+ return {
154
+ action: 'installed',
155
+ settingsPath,
156
+ settings: next,
157
+ message: 'faf SessionStart hook installed. Every Claude Code session in this project now starts with fresh context.',
158
+ };
159
+ }
160
+ catch (error) {
161
+ return {
162
+ action: 'error',
163
+ settingsPath,
164
+ message: `faf_setup failed: ${error instanceof Error ? error.message : String(error)}`,
165
+ };
166
+ }
167
+ }
168
+ //# sourceMappingURL=setup-hook.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Sourced README context — the honest human-context (6W) extractor.
3
+ *
4
+ * This REPLACES the retired v5 RelentlessContextExtractor, which inferred slots
5
+ * from the project name / tech stack (an `INFERRED`/`DEFAULT` tier) and so could
6
+ * write a guessed value into an empty slot. That violated the FAF doctrine:
7
+ * FAF don't lie. This extractor follows the same rule as Turbo-Cat (stack slots
8
+ * from real formats) and faf-cli's v6 `relentlessContext` (human slots, sourced):
9
+ *
10
+ * Fill a slot ONLY from evidence present in the README. No inference, no
11
+ * defaults, no placeholders. Every field starts null and is set only on a
12
+ * real pattern match. No match → the slot stays empty, to be handed to the
13
+ * human (e.g. via faf_go). An honest empty beats a guessed fill.
14
+ *
15
+ * Sourced-but-partial by design: the patterns are deliberately conservative.
16
+ * Missing a fillable slot (false negative) is acceptable; fabricating one
17
+ * (false positive) is not.
18
+ */
19
+ export interface SixWs {
20
+ who: string | null;
21
+ what: string | null;
22
+ why: string | null;
23
+ where: string | null;
24
+ when: string | null;
25
+ how: string | null;
26
+ }
27
+ /**
28
+ * Extract the 6 human-context slots from README text. Sourced-or-empty: a slot
29
+ * is filled only from a pattern match in `content`, never inferred.
30
+ */
31
+ export declare function extractSixWsFromReadme(content: string): SixWs;