faf-mcp 4.1.1 → 4.1.2
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 +11 -1
- package/CLAUDE.md +2 -2
- package/README.md +7 -3
- package/dist/src/handlers/fileHandler.d.ts +6 -0
- package/dist/src/handlers/fileHandler.js +119 -11
- package/dist/src/handlers/fileHandler.js.map +1 -1
- package/dist/src/handlers/tools.js +59 -4
- package/dist/src/handlers/tools.js.map +1 -1
- package/dist/src/utils/git-history.d.ts +38 -0
- package/dist/src/utils/git-history.js +628 -0
- package/dist/src/utils/git-history.js.map +1 -0
- package/dist/src/utils/safe-path.d.ts +21 -0
- package/dist/src/utils/safe-path.js +99 -0
- package/dist/src/utils/safe-path.js.map +1 -1
- package/package.json +1 -1
- package/project.faf +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- faf: faf-mcp | TypeScript | mcp | The Interop MCP for Context — the Cursor / IDE Edition. Persistent context for Cursor, VS Code, and every MCP-compatible IDE. IANA-registered application/vnd.faf+yaml. Start with "Use FAF". -->
|
|
2
|
-
<!-- faf: doc=changelog | latest=v4.1.
|
|
2
|
+
<!-- faf: doc=changelog | latest=v4.1.2 | canonical=project.faf | family=FAF -->
|
|
3
3
|
|
|
4
4
|
# Changelog
|
|
5
5
|
|
|
@@ -8,6 +8,16 @@ All notable changes to faf-mcp will be documented in this file.
|
|
|
8
8
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
9
9
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
10
10
|
|
|
11
|
+
## [4.1.2] - 2026-11-05 — The Instructions Edition
|
|
12
|
+
|
|
13
|
+
**faf_write can no longer overwrite the birth certificate or your context files, and faf_dna never issues a second one.**
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **`faf_write` refuses `.faf-dna`** in every spelling and route (case, `..`, absolute paths, symlinks, symlinked folders, `.faf-dna/x`), with or without `overwrite: true` ([#110](https://github.com/Wolfe-Jam/faf-mcp/pull/110)).
|
|
17
|
+
- **Existing `project.faf`, `*.faf`, `CLAUDE.md` and `AGENTS.md` need `overwrite: true`.** New files write as before. Writes stay inside the project folder, never in `.git/`, checked on the resolved real path, and land atomically with file permissions kept.
|
|
18
|
+
- **`faf_dna` issues one birth certificate, once.** Concurrent calls create exactly one. If `.faf-dna` is missing but git history shows it existed (any branch, the reflog, a stash, a linked worktree, a `--shared` clone's source), it refuses and prints the restore steps. If history can't be checked, it refuses rather than guess ([#110](https://github.com/Wolfe-Jam/faf-mcp/pull/110), [#111](https://github.com/Wolfe-Jam/faf-mcp/pull/111)).
|
|
19
|
+
- **Restore steps** list the latest version first: `git log --all --reflog --diff-filter=AM --oneline -- .faf-dna`, then `git checkout <sha> -- .faf-dna`.
|
|
20
|
+
|
|
11
21
|
## [4.1.1] - 2026-10-28 — The Instructions Edition
|
|
12
22
|
|
|
13
23
|
**One rule for project questions: read project.faf first.**
|
package/CLAUDE.md
CHANGED
|
@@ -25,12 +25,12 @@ The Interop MCP for Context — the Cursor / IDE Edition. Persistent context for
|
|
|
25
25
|
- **What:** The Cursor / IDE Edition — persistent context for Cursor, VS Code, and every MCP-compatible IDE — 29 MCP tools (15 essential shown by default), interop with AGENTS.md, .cursorrules, GEMINI.md
|
|
26
26
|
- **Why:** Eliminate the 20-minute AI context tax — give AI instant project understanding in 30 seconds
|
|
27
27
|
- **Where:** npm · MCP Registry · any MCP-compatible IDE — people get it how they wish
|
|
28
|
-
- **When:** Production/Stable — v4.1.
|
|
28
|
+
- **When:** Production/Stable — v4.1.2 The Instructions Edition, WJTTC certified
|
|
29
29
|
- **How:** npx faf-mcp or npm install -g faf-mcp, then add to your MCP config
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
33
|
-
*STATUS: SYNC ACTIVE — 2026-
|
|
33
|
+
*STATUS: SYNC ACTIVE — 2026-11-05T14:10:38.504Z*
|
|
34
34
|
<!-- faf:end -->
|
|
35
35
|
|
|
36
36
|
## Working here
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
**.FAF Context** is the MCP server for the IDE side of FAF. One `project.faf` in your repo, and every AI tool's context file is authored from it — AGENTS.md, .cursorrules, GEMINI.md, CLAUDE.md — and scored, so you know exactly where to focus. It runs locally over stdio on the same faf-cli the terminal uses. The FAF ecosystem it belongs to has comfortably passed 100k downloads across npm and PyPI ([live count](https://faf.one/downloads)).
|
|
21
21
|
|
|
22
|
-
⭐
|
|
22
|
+
⭐ [Star it on GitHub](https://github.com/Wolfe-Jam/faf-mcp): bookmarks it for you, helps other devs find it too.
|
|
23
23
|
|
|
24
24
|
[](https://github.com/Wolfe-Jam/faf-mcp/actions/workflows/ci.yml)
|
|
25
25
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
-
## What's New in 4.1.
|
|
31
|
+
## What's New in 4.1.2 — The Instructions Edition
|
|
32
|
+
|
|
33
|
+
**faf_write can no longer overwrite the birth certificate or your context files, and faf_dna never issues a second one.** `.faf-dna` is refused in every spelling and route, existing context files need `overwrite: true`, and `faf_dna` refuses rather than reissue when git shows a certificate existed.
|
|
34
|
+
|
|
35
|
+
## 4.1.1
|
|
32
36
|
|
|
33
37
|
**One rule for project questions: read project.faf first.**
|
|
34
38
|
|
|
@@ -231,7 +235,7 @@ The 15 Core tools, shown by default:
|
|
|
231
235
|
| `faf_gemini` | Import GEMINI.md into `project.faf`, or write it from `project.faf` |
|
|
232
236
|
| `faf_git` | Author a `project.faf` from a public GitHub repo URL |
|
|
233
237
|
|
|
234
|
-
**+14 more with `FAF_TOOLS=all`:** `faf_status` · `faf_what` · `faf_guide` · `faf_debug` · `faf_clear` · `faf_list` · `faf_read` (read a file within the allowed roots: cwd, the OS temp dir, or `FAF_ALLOWED_ROOTS`) · `faf_write` (write a file within
|
|
238
|
+
**+14 more with `FAF_TOOLS=all`:** `faf_status` · `faf_what` · `faf_guide` · `faf_debug` · `faf_clear` · `faf_list` · `faf_read` (read a file within the allowed roots: cwd, the OS temp dir, or `FAF_ALLOWED_ROOTS`) · `faf_write` (write a file within cwd or `FAF_ALLOWED_ROOTS`; never `.faf-dna` or `.git/`; an existing `*.faf`, `CLAUDE.md` or `AGENTS.md` needs `overwrite: true`) · `faf_readme` · `faf_human_add` · `faf_quick` · `faf_formats` · `faf_dna` · `faf_conductor`
|
|
235
239
|
|
|
236
240
|
**Built on faf-cli.** Every tool composes the bundled [faf-cli](https://www.npmjs.com/package/faf-cli) in-process — the same scorer, the same renderers, the same block injector the CLI uses. Nothing shells out to a `faf` on your PATH.
|
|
237
241
|
|
|
@@ -26,6 +26,12 @@ export declare const fafWriteTool: Tool;
|
|
|
26
26
|
* Handle faf_read tool execution
|
|
27
27
|
*/
|
|
28
28
|
export declare function handleFafRead(args: any): Promise<CallToolResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Context files faf_write will not replace unless the caller passes
|
|
31
|
+
* `overwrite: true`: any `*.faf` (project.faf, .faf), CLAUDE.md and AGENTS.md.
|
|
32
|
+
* Case-insensitive, so a case-insensitive filesystem cannot slip past.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isProtectedContextFile(p: string): boolean;
|
|
29
35
|
/**
|
|
30
36
|
* Handle faf_write tool execution
|
|
31
37
|
*/
|
|
@@ -37,9 +37,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
38
|
exports.fileHandlers = exports.fafWriteTool = exports.fafReadTool = exports.PathValidator = void 0;
|
|
39
39
|
exports.handleFafRead = handleFafRead;
|
|
40
|
+
exports.isProtectedContextFile = isProtectedContextFile;
|
|
40
41
|
exports.handleFafWrite = handleFafWrite;
|
|
41
42
|
const fs = __importStar(require("fs/promises"));
|
|
42
43
|
const path = __importStar(require("path"));
|
|
44
|
+
const fsSync = __importStar(require("fs"));
|
|
45
|
+
const crypto = __importStar(require("crypto"));
|
|
43
46
|
const safe_path_1 = require("../utils/safe-path");
|
|
44
47
|
/**
|
|
45
48
|
* Security validator for file paths
|
|
@@ -103,7 +106,7 @@ exports.fafReadTool = {
|
|
|
103
106
|
*/
|
|
104
107
|
exports.fafWriteTool = {
|
|
105
108
|
name: 'faf_write',
|
|
106
|
-
description: 'Write
|
|
109
|
+
description: 'Write a file within the project root (cwd, or FAF_ALLOWED_ROOTS when set). Paths outside it, inside .git/, and .faf-dna (the birth certificate) are always refused. An existing *.faf, CLAUDE.md or AGENTS.md is not replaced unless overwrite is true. Writes via temp file + rename.',
|
|
107
110
|
inputSchema: {
|
|
108
111
|
type: 'object',
|
|
109
112
|
properties: {
|
|
@@ -111,6 +114,10 @@ exports.fafWriteTool = {
|
|
|
111
114
|
type: 'string',
|
|
112
115
|
description: 'Absolute or relative file path to write'
|
|
113
116
|
},
|
|
117
|
+
overwrite: {
|
|
118
|
+
type: 'boolean',
|
|
119
|
+
description: 'Set true to replace an existing *.faf, CLAUDE.md or AGENTS.md. Without it, faf_write refuses and leaves the file unchanged. Never applies to .faf-dna.'
|
|
120
|
+
},
|
|
114
121
|
content: {
|
|
115
122
|
type: 'string',
|
|
116
123
|
description: 'Content to write to the file'
|
|
@@ -182,18 +189,107 @@ async function handleFafRead(args) {
|
|
|
182
189
|
};
|
|
183
190
|
}
|
|
184
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Context files faf_write will not replace unless the caller passes
|
|
194
|
+
* `overwrite: true`: any `*.faf` (project.faf, .faf), CLAUDE.md and AGENTS.md.
|
|
195
|
+
* Case-insensitive, so a case-insensitive filesystem cannot slip past.
|
|
196
|
+
*/
|
|
197
|
+
function isProtectedContextFile(p) {
|
|
198
|
+
const base = path.basename(p).toLowerCase();
|
|
199
|
+
return base.endsWith('.faf') || base === 'claude.md' || base === 'agents.md';
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Write `content` to `target` via a temp file in the same directory, then
|
|
203
|
+
* rename (replace) or link (no-clobber) it into place. A reader never sees a
|
|
204
|
+
* half-written file. With `noClobber`, an existing target is never replaced,
|
|
205
|
+
* even if it appears between the caller's check and this write: link() fails
|
|
206
|
+
* with EEXIST atomically.
|
|
207
|
+
*/
|
|
208
|
+
async function atomicWrite(target, content, noClobber) {
|
|
209
|
+
const dir = path.dirname(target);
|
|
210
|
+
const tmp = path.join(dir, `.${path.basename(target)}.faf-tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
|
|
211
|
+
await fs.writeFile(tmp, content, { encoding: 'utf8', flag: 'wx' });
|
|
212
|
+
try {
|
|
213
|
+
if (!noClobber) {
|
|
214
|
+
// Keep the replaced file's permission bits (0755 stays 0755). The temp
|
|
215
|
+
// file was created with the default mode, so copy the old one across.
|
|
216
|
+
await keepMode(target, tmp);
|
|
217
|
+
await fs.rename(tmp, target);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await fs.link(tmp, target);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
if (errCode(err) === 'EEXIST')
|
|
225
|
+
throw err;
|
|
226
|
+
// Filesystem without hard links: exclusive copy is still no-clobber.
|
|
227
|
+
await fs.copyFile(tmp, target, fsSync.constants.COPYFILE_EXCL);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
await fs.rm(tmp, { force: true });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Copy `from`'s permission bits onto `to`. No-op when `from` does not exist. */
|
|
235
|
+
async function keepMode(from, to) {
|
|
236
|
+
let mode;
|
|
237
|
+
try {
|
|
238
|
+
mode = (await fs.stat(from)).mode & 0o7777;
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
if (errCode(err) === 'ENOENT')
|
|
242
|
+
return;
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
await fs.chmod(to, mode);
|
|
246
|
+
}
|
|
247
|
+
function errCode(err) {
|
|
248
|
+
return typeof err === 'object' && err !== null && 'code' in err ? String(err.code) : undefined;
|
|
249
|
+
}
|
|
250
|
+
function existsNoFollow(p) {
|
|
251
|
+
try {
|
|
252
|
+
fsSync.lstatSync(p);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function refuseExisting(filePath) {
|
|
260
|
+
return {
|
|
261
|
+
content: [{
|
|
262
|
+
type: 'text',
|
|
263
|
+
text: `❌ Refused: ${filePath} already exists and is a protected context file ` +
|
|
264
|
+
`(*.faf, CLAUDE.md, AGENTS.md). faf_write did not change it.\n` +
|
|
265
|
+
`To replace it, call faf_write again with overwrite: true. ` +
|
|
266
|
+
`To update project.faf or CLAUDE.md without losing hand-written content, ` +
|
|
267
|
+
`use the dedicated faf tools instead.`
|
|
268
|
+
}],
|
|
269
|
+
isError: true
|
|
270
|
+
};
|
|
271
|
+
}
|
|
185
272
|
/**
|
|
186
273
|
* Handle faf_write tool execution
|
|
187
274
|
*/
|
|
188
275
|
async function handleFafWrite(args) {
|
|
189
276
|
const startTime = Date.now();
|
|
190
277
|
try {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
278
|
+
const input = (args ?? {});
|
|
279
|
+
const rawPath = input.path;
|
|
280
|
+
const content = input.content;
|
|
281
|
+
const overwrite = input.overwrite === true;
|
|
282
|
+
if (typeof content !== 'string') {
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: 'text', text: '❌ content must be a string' }],
|
|
285
|
+
isError: true
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// Confine to the project root before any write: cwd only (or an explicit
|
|
289
|
+
// FAF_ALLOWED_ROOTS), never .git/, never .faf-dna, real path resolved.
|
|
194
290
|
let filePath;
|
|
195
291
|
try {
|
|
196
|
-
filePath = (0, safe_path_1.
|
|
292
|
+
filePath = (0, safe_path_1.confineWriteOp)(rawPath);
|
|
197
293
|
}
|
|
198
294
|
catch (err) {
|
|
199
295
|
if (err instanceof safe_path_1.PathConfinementError) {
|
|
@@ -215,24 +311,36 @@ async function handleFafWrite(args) {
|
|
|
215
311
|
isError: true
|
|
216
312
|
};
|
|
217
313
|
}
|
|
314
|
+
const protectedFile = isProtectedContextFile(filePath) || isProtectedContextFile(String(rawPath));
|
|
315
|
+
const noClobber = protectedFile && !overwrite;
|
|
316
|
+
if (noClobber && existsNoFollow(filePath)) {
|
|
317
|
+
return refuseExisting(filePath);
|
|
318
|
+
}
|
|
218
319
|
// Create directory if needed
|
|
219
320
|
const dir = path.dirname(filePath);
|
|
220
321
|
await fs.mkdir(dir, { recursive: true });
|
|
221
322
|
// Write file with timeout
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
323
|
+
try {
|
|
324
|
+
await Promise.race([
|
|
325
|
+
atomicWrite(filePath, content, noClobber),
|
|
326
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Write timeout (30s)')), 30000).unref?.())
|
|
327
|
+
]);
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
if (noClobber && errCode(err) === 'EEXIST')
|
|
331
|
+
return refuseExisting(filePath);
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
226
334
|
const duration = Date.now() - startTime;
|
|
227
335
|
return {
|
|
228
336
|
content: [{
|
|
229
337
|
type: 'text',
|
|
230
|
-
text: `✅ Successfully wrote ${contentSize} bytes to ${
|
|
338
|
+
text: `✅ Successfully wrote ${contentSize} bytes to ${filePath}`
|
|
231
339
|
}],
|
|
232
340
|
metadata: {
|
|
233
341
|
duration_ms: duration,
|
|
234
342
|
bytes_written: contentSize,
|
|
235
|
-
file_path:
|
|
343
|
+
file_path: filePath,
|
|
236
344
|
message: `✅ Write completed in ${duration}ms`
|
|
237
345
|
}
|
|
238
346
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileHandler.js","sourceRoot":"","sources":["../../../src/handlers/fileHandler.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,4DAA4D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"fileHandler.js","sourceRoot":"","sources":["../../../src/handlers/fileHandler.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC,4DAA4D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoG5D,sCAkEC;AAOD,wDAGC;AA+ED,wCA0FC;AAvVD,gDAAkC;AAClC,2CAA6B;AAE7B,2CAA6B;AAC7B,+CAAiC;AACjC,kDAAyF;AAEzF;;GAEG;AACH,MAAa,aAAa;IAChB,MAAM,CAAU,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;IAC5E,MAAM,CAAU,aAAa,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO;IAEjE,MAAM,CAAC,QAAQ,CAAC,QAAgB;QAC9B,6BAA6B;QAC7B,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC5C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAExC,2BAA2B;QAC3B,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC;QAC5D,CAAC;QAED,wBAAwB;QACxB,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAC7C,IAAI,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBACnC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,SAAS,eAAe,EAAE,CAAC;YACxE,CAAC;QACH,CAAC;QAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACzB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,QAAgB;QACzC,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACtC,IAAI,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;gBACpC,OAAO;oBACL,KAAK,EAAE,KAAK;oBACZ,KAAK,EAAE,mBAAmB,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB;iBAChF,CAAC;YACJ,CAAC;YACD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACzB,CAAC;QAAC,OAAO,MAAe,EAAE,CAAC;YACzB,gDAAgD;YAChD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACzB,CAAC;IACH,CAAC;;AAtCH,sCAuCC;AAED;;GAEG;AACU,QAAA,WAAW,GAAS;IAC/B,IAAI,EAAE,UAAU;IAChB,WAAW,EAAE,oDAAoD;IACjE,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,wCAAwC;aACtD;SACF;QACD,QAAQ,EAAE,CAAC,MAAM,CAAC;KACnB;CACF,CAAC;AAEF;;GAEG;AACU,QAAA,YAAY,GAAS;IAChC,IAAI,EAAE,WAAW;IACjB,WAAW,EAAE,wRAAwR;IACrS,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,yCAAyC;aACvD;YACD,SAAS,EAAE;gBACT,IAAI,EAAE,SAAS;gBACf,WAAW,EAAE,wJAAwJ;aACtK;YACD,OAAO,EAAE;gBACP,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,8BAA8B;aAC5C;SACF;QACD,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC;KAC9B;CACF,CAAC;AAEF;;GAEG;AACI,KAAK,UAAU,aAAa,CAAC,IAAS;IAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAE7B,IAAI,CAAC;QACH,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;QAE/B,mEAAmE;QACnE,+EAA+E;QAC/E,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,IAAA,yBAAa,EAAC,OAAO,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,gCAAoB,EAAE,CAAC;gBACxC,OAAO;oBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;oBACrE,OAAO,EAAE,IAAI;iBACd,CAAC;YACJ,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;QAED,kBAAkB;QAClB,MAAM,cAAc,GAAG,MAAM,aAAa,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACnE,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;YAC1B,OAAO;gBACL,OAAO,EAAE,CAAC;wBACR,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,KAAK,cAAc,CAAC,KAAK,EAAE;qBAClC,CAAC;gBACF,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,yBAAyB;QACzB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YACjC,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;YAC7B,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAC/B,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC,EAAE,KAAK,CAAC,CACjE;SACF,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAEtC,OAAO;YACL,OAAO,EAAE,CAAC;oBACR,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,OAAO;iBACd,CAAC;YACF,QAAQ,EAAE;gBACR,WAAW,EAAE,QAAQ;gBACrB,SAAS,EAAE,KAAK,CAAC,IAAI;gBACrB,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;gBACjC,OAAO,EAAE,UAAU,KAAK,CAAC,IAAI,aAAa,QAAQ,IAAI;aACvD;SACF,CAAC;IAEJ,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,OAAO;YACL,OAAO,EAAE,CAAC;oBACR,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,0BAA0B,KAAK,CAAC,OAAO,EAAE;iBAChD,CAAC;YACF,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAgB,sBAAsB,CAAC,CAAS;IAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5C,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW,CAAC;AAC/E,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,WAAW,CAAC,MAAc,EAAE,OAAe,EAAE,SAAkB;IAC5E,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CACnB,GAAG,EACH,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,YAAY,OAAO,CAAC,GAAG,IAAI,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAC5F,CAAC;IACF,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACnE,IAAI,CAAC;QACH,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,uEAAuE;YACvE,sEAAsE;YACtE,MAAM,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAC5B,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAC7B,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,QAAQ;gBAAE,MAAM,GAAG,CAAC;YACzC,qEAAqE;YACrE,MAAM,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,KAAK,UAAU,QAAQ,CAAC,IAAY,EAAE,EAAU;IAC9C,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,MAAM,CAAC;IAC7C,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,QAAQ;YAAE,OAAO;QACtC,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,OAAO,CAAC,GAAY;IAC3B,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,MAAM,CAAE,GAAyB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACxH,CAAC;AAED,SAAS,cAAc,CAAC,CAAS;IAC/B,IAAI,CAAC;QACH,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,QAAgB;IACtC,OAAO;QACL,OAAO,EAAE,CAAC;gBACR,IAAI,EAAE,MAAM;gBACZ,IAAI,EACF,cAAc,QAAQ,kDAAkD;oBACxE,+DAA+D;oBAC/D,4DAA4D;oBAC5D,0EAA0E;oBAC1E,sCAAsC;aACzC,CAAC;QACF,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC;AAED;;GAEG;AACI,KAAK,UAAU,cAAc,CAAC,IAAS;IAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAE7B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,CAAC,IAAI,IAAI,EAAE,CAA+D,CAAC;QACzF,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC;QAC3B,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC9B,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC;QAE3C,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,4BAA4B,EAAE,CAAC;gBAC/D,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,yEAAyE;QACzE,uEAAuE;QACvE,IAAI,QAAgB,CAAC;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,IAAA,0BAAc,EAAC,OAAO,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,gCAAoB,EAAE,CAAC;gBACxC,OAAO;oBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;oBACrE,OAAO,EAAE,IAAI;iBACd,CAAC;YACJ,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;QAED,qBAAqB;QACrB,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACvD,IAAI,WAAW,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;YACnC,OAAO;gBACL,OAAO,EAAE,CAAC;wBACR,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,wBAAwB,CAAC,WAAW,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB;qBACrF,CAAC;gBACF,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,MAAM,aAAa,GAAG,sBAAsB,CAAC,QAAQ,CAAC,IAAI,sBAAsB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QAClG,MAAM,SAAS,GAAG,aAAa,IAAI,CAAC,SAAS,CAAC;QAC9C,IAAI,SAAS,IAAI,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1C,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC;QAED,6BAA6B;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACnC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEzC,0BAA0B;QAC1B,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,IAAI,CAAC;gBACjB,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC;gBACzC,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAC/B,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAC5E;aACF,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,IAAI,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,QAAQ;gBAAE,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC;YAC5E,MAAM,GAAG,CAAC;QACZ,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;QAExC,OAAO;YACL,OAAO,EAAE,CAAC;oBACR,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,wBAAwB,WAAW,aAAa,QAAQ,EAAE;iBACjE,CAAC;YACF,QAAQ,EAAE;gBACR,WAAW,EAAE,QAAQ;gBACrB,aAAa,EAAE,WAAW;gBAC1B,SAAS,EAAE,QAAQ;gBACnB,OAAO,EAAE,wBAAwB,QAAQ,IAAI;aAC9C;SACF,CAAC;IAEJ,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,OAAO;YACL,OAAO,EAAE,CAAC;oBACR,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,2BAA2B,KAAK,CAAC,OAAO,EAAE;iBACjD,CAAC;YACF,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;AACH,CAAC;AAED,kBAAkB;AACL,QAAA,YAAY,GAAG;IAC1B,QAAQ,EAAE,aAAa;IACvB,SAAS,EAAE,cAAc;CAC1B,CAAC"}
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.FafToolHandler = void 0;
|
|
37
37
|
const fileHandler_1 = require("./fileHandler");
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
|
+
const git_history_1 = require("../utils/git-history");
|
|
39
40
|
const os = __importStar(require("os"));
|
|
40
41
|
const pathModule = __importStar(require("path"));
|
|
41
42
|
const faf_file_finder_js_1 = require("../utils/faf-file-finder.js");
|
|
@@ -295,7 +296,7 @@ class FafToolHandler {
|
|
|
295
296
|
},
|
|
296
297
|
{
|
|
297
298
|
name: 'faf_write',
|
|
298
|
-
description: 'Write a file within the
|
|
299
|
+
description: 'Write a file within the project root (cwd, or FAF_ALLOWED_ROOTS when set). Paths outside it, inside .git/, and .faf-dna (the birth certificate) are always refused. An existing *.faf, CLAUDE.md or AGENTS.md is not replaced unless overwrite is true. Writes via temp file + rename.',
|
|
299
300
|
annotations: {
|
|
300
301
|
title: 'Write Project File',
|
|
301
302
|
readOnlyHint: false,
|
|
@@ -309,6 +310,10 @@ class FafToolHandler {
|
|
|
309
310
|
type: 'string',
|
|
310
311
|
description: 'Absolute or relative file path to write'
|
|
311
312
|
},
|
|
313
|
+
overwrite: {
|
|
314
|
+
type: 'boolean',
|
|
315
|
+
description: 'Set true to replace an existing *.faf, CLAUDE.md or AGENTS.md. Without it, faf_write refuses and leaves the file unchanged. Never applies to .faf-dna.'
|
|
316
|
+
},
|
|
312
317
|
content: {
|
|
313
318
|
type: 'string',
|
|
314
319
|
description: 'Content to write to the file'
|
|
@@ -2053,6 +2058,34 @@ Call these MCP tools:
|
|
|
2053
2058
|
}]
|
|
2054
2059
|
};
|
|
2055
2060
|
}
|
|
2061
|
+
// A .faf-dna that was ever committed is a birth that already happened.
|
|
2062
|
+
// Reissuing would replace the certificate; restore it from git instead.
|
|
2063
|
+
const history = (0, git_history_1.gitEverTracked)(dnaPath);
|
|
2064
|
+
if (history.status === 'found') {
|
|
2065
|
+
const committed = history.where;
|
|
2066
|
+
return {
|
|
2067
|
+
content: [{
|
|
2068
|
+
type: 'text',
|
|
2069
|
+
text: `🧬 FAF DNA Journey\n\n❌ .faf-dna is missing, but git shows it existed (${committed}).\nThe birth certificate is issued once and is not reissued.\n\n💡 Restore it from git instead:\n git log --all --reflog --diff-filter=AM --oneline -- .faf-dna (the first line is the latest version, reflog included)\n git checkout <sha> -- .faf-dna (restore that version)`
|
|
2070
|
+
}],
|
|
2071
|
+
isError: true
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
// Fail closed: if git history could not be read in full, a deleted
|
|
2075
|
+
// certificate cannot be ruled out, so none is issued.
|
|
2076
|
+
if (history.status === 'unknown') {
|
|
2077
|
+
return {
|
|
2078
|
+
content: [{
|
|
2079
|
+
type: 'text',
|
|
2080
|
+
text: `🧬 FAF DNA Journey\n\n❌ .faf-dna is missing and git history could not be checked (${history.reason}).\nThe birth certificate is issued once and is not reissued, so faf_dna did not create one.\n\n💡 If .faf-dna existed before, restore it from git:\n git log --all --reflog --diff-filter=AM --oneline -- .faf-dna (the first line is the latest version, reflog included)\n git checkout <sha> -- .faf-dna (restore that version)\n💡 If this really is a brand-new project, create the first one with faf-cli:\n faf init`
|
|
2081
|
+
}],
|
|
2082
|
+
isError: true
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
// Outside git there is no history to check: say so in the reply.
|
|
2086
|
+
const noHistoryNote = history.status === 'not-a-repo'
|
|
2087
|
+
? `\n\n⚠️ Not a git repository: no history could be checked, so a lost .faf-dna cannot be told apart from a new project. If this project had a .faf-dna before, replace this one with the restored original.`
|
|
2088
|
+
: '';
|
|
2056
2089
|
// .faf exists but no DNA - create initial DNA. Real scorer — same
|
|
2057
2090
|
// one faf_score reads — never a local pseudo-score (v3.0 fix).
|
|
2058
2091
|
const { readFafRaw, scoreFafYaml } = await faf_cli_bridge_js_1.fafCli;
|
|
@@ -2075,11 +2108,28 @@ Call these MCP tools:
|
|
|
2075
2108
|
],
|
|
2076
2109
|
format: 'faf-dna-v1'
|
|
2077
2110
|
};
|
|
2078
|
-
|
|
2111
|
+
// Exclusive create (O_EXCL): the birth certificate is written once.
|
|
2112
|
+
// If another call created it in the meantime, that one stands and is
|
|
2113
|
+
// the one reported — never a second certificate.
|
|
2114
|
+
try {
|
|
2115
|
+
fs.writeFileSync(dnaPath, JSON.stringify(dna, null, 2), { flag: 'wx' });
|
|
2116
|
+
}
|
|
2117
|
+
catch (err) {
|
|
2118
|
+
if (err?.code !== 'EEXIST')
|
|
2119
|
+
throw err;
|
|
2120
|
+
const existing = JSON.parse(fs.readFileSync(dnaPath, 'utf-8'));
|
|
2121
|
+
const bc = existing.birthCertificate ?? {};
|
|
2122
|
+
return {
|
|
2123
|
+
content: [{
|
|
2124
|
+
type: 'text',
|
|
2125
|
+
text: `🧬 FAF DNA Journey\n\n🎫 Birth Certificate already exists — not reissued.\n\n📊 Birth DNA: ${bc.birthDNA ?? 0}%\n📅 Born: ${String(bc.born ?? '').split('T')[0]}\n🎫 Certificate: ${bc.certificate ?? 'unknown'}`
|
|
2126
|
+
}]
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2079
2129
|
return {
|
|
2080
2130
|
content: [{
|
|
2081
2131
|
type: 'text',
|
|
2082
|
-
text: `🧬 FAF DNA Journey\n\n🐣 Birth Certificate Created!\n\n📊 Birth DNA: ${currentScore}%\n📅 Born: ${new Date().toISOString().split('T')[0]}\n🎫 Certificate: ${dna.birthCertificate.certificate}\n\n💡 Your journey begins here! Run faf_auto or faf_go to grow
|
|
2132
|
+
text: `🧬 FAF DNA Journey\n\n🐣 Birth Certificate Created!\n\n📊 Birth DNA: ${currentScore}%\n📅 Born: ${new Date().toISOString().split('T')[0]}\n🎫 Certificate: ${dna.birthCertificate.certificate}\n\n💡 Your journey begins here! Run faf_auto or faf_go to grow.${noHistoryNote}`
|
|
2083
2133
|
}]
|
|
2084
2134
|
};
|
|
2085
2135
|
}
|
|
@@ -2102,7 +2152,12 @@ Call these MCP tools:
|
|
|
2102
2152
|
const now = new Date().toISOString();
|
|
2103
2153
|
dna.current = { ...(dna.current ?? {}), score: live, lastSync: now };
|
|
2104
2154
|
milestones.push({ type: 'sync', score: live, date: now, version: dna.current.version ?? 'v1.0.0' });
|
|
2105
|
-
|
|
2155
|
+
// birthCertificate is carried over unchanged; temp + rename so a
|
|
2156
|
+
// crash mid-write cannot truncate the certificate.
|
|
2157
|
+
const tmpDna = `${dnaPath}.tmp-${process.pid}-${Date.now()}`;
|
|
2158
|
+
fs.writeFileSync(tmpDna, JSON.stringify(dna, null, 2), { flag: 'wx' });
|
|
2159
|
+
fs.chmodSync(tmpDna, fs.statSync(dnaPath).mode & 0o7777); // keep the certificate's mode
|
|
2160
|
+
fs.renameSync(tmpDna, dnaPath);
|
|
2106
2161
|
currentScore = live;
|
|
2107
2162
|
}
|
|
2108
2163
|
}
|