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 +38 -4
- package/CLAUDE.md +3 -1
- package/README.md +9 -7
- package/dist/src/faf-core/commands/bi-sync.d.ts +7 -1
- package/dist/src/faf-core/commands/bi-sync.js +47 -40
- package/dist/src/faf-core/commands/session-refresh.d.ts +13 -0
- package/dist/src/faf-core/commands/session-refresh.js +116 -0
- package/dist/src/faf-core/commands/setup-hook.d.ts +32 -0
- package/dist/src/faf-core/commands/setup-hook.js +168 -0
- package/dist/src/faf-core/extract/sourced-readme-context.d.ts +31 -0
- package/dist/src/faf-core/extract/sourced-readme-context.js +56 -0
- package/dist/src/faf-core/extract/turbocat-bridge.d.ts +34 -0
- package/dist/src/faf-core/extract/turbocat-bridge.js +84 -0
- package/dist/src/faf-core/generators/faf-generator-championship.js +4 -31
- package/dist/src/handlers/prompts.js +20 -25
- package/dist/src/handlers/tools.d.ts +6 -5
- package/dist/src/handlers/tools.js +483 -307
- package/dist/src/index.js +14 -0
- package/dist/src/server.js +1 -1
- package/dist/src/trust/parity.d.ts +68 -0
- package/dist/src/trust/parity.js +77 -0
- package/dist/src/trust/receipt.d.ts +50 -0
- package/dist/src/trust/receipt.js +79 -0
- package/dist/src/types/tool-visibility.js +7 -0
- package/dist/src/utils/sanitize-output.d.ts +17 -2
- package/dist/src/utils/sanitize-output.js +68 -3
- package/manifest.json +14 -2
- package/package.json +3 -3
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,
|
|
2
|
-
<!-- faf: doc=changelog | latest=v5.
|
|
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
|
-
|
|
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
|
-
-
|
|
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-
|
|
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,
|
|
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
|
|
4
|
+
# claude-faf-mcp — The Sourced Edition
|
|
5
5
|
|
|
6
6
|
[](https://www.npmjs.com/package/claude-faf-mcp)
|
|
7
7
|
[](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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
//
|
|
68
|
-
|
|
69
|
-
if (fafData.project) {
|
|
70
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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;
|