faf-mcp 4.1.1 → 4.1.3
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 +20 -1
- package/CLAUDE.md +2 -2
- package/README.md +11 -3
- package/dist/src/faf-core/commands/agents.js +1 -1
- package/dist/src/faf-core/commands/agents.js.map +1 -1
- package/dist/src/faf-core/commands/claude.js +31 -7
- package/dist/src/faf-core/commands/claude.js.map +1 -1
- package/dist/src/faf-core/commands/cursor.js +1 -1
- package/dist/src/faf-core/commands/cursor.js.map +1 -1
- package/dist/src/faf-core/commands/gemini.js +1 -1
- package/dist/src/faf-core/commands/gemini.js.map +1 -1
- package/dist/src/faf-core/parsers/agents-parser.js +7 -3
- package/dist/src/faf-core/parsers/agents-parser.js.map +1 -1
- package/dist/src/faf-core/parsers/cursorrules-parser.js +5 -3
- package/dist/src/faf-core/parsers/cursorrules-parser.js.map +1 -1
- package/dist/src/faf-core/parsers/gemini-parser.js +4 -3
- package/dist/src/faf-core/parsers/gemini-parser.js.map +1 -1
- 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 +64 -5
- 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
|
@@ -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":"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"}
|
|
@@ -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'
|
|
@@ -1963,7 +1968,7 @@ Call these MCP tools:
|
|
|
1963
1968
|
const path = await import('path');
|
|
1964
1969
|
try {
|
|
1965
1970
|
const steps = [];
|
|
1966
|
-
const { findFafFile: findFaf, readFaf, readFafRaw, scoreFafYaml, assembleFreshFaf, updateExistingFaf, writeFaf, renderClaudeMd, writeClaudeMd, } = await faf_cli_bridge_js_1.fafCli;
|
|
1971
|
+
const { findFafFile: findFaf, readFaf, readFafRaw, scoreFafYaml, assembleFreshFaf, updateExistingFaf, writeFaf, renderClaudeMd, writeClaudeMd, legacyStampNoteAt, } = await faf_cli_bridge_js_1.fafCli;
|
|
1967
1972
|
// Step 1: locate any existing .faf and its real "before" score.
|
|
1968
1973
|
const existingPath = findFaf(cwd);
|
|
1969
1974
|
const beforeScore = existingPath ? scoreFafYaml(readFafRaw(existingPath)).score : 0;
|
|
@@ -1987,8 +1992,12 @@ Call these MCP tools:
|
|
|
1987
1992
|
// Step 3: CLAUDE.md — faf-cli's render of the file just written (the
|
|
1988
1993
|
// same bytes `faf sync` writes), injected non-destructively.
|
|
1989
1994
|
const claudeExisted = fs.existsSync(path.join(cwd, 'CLAUDE.md'));
|
|
1995
|
+
const stampNote = claudeExisted ? legacyStampNoteAt(path.join(cwd, 'CLAUDE.md'), 'CLAUDE.md') : null;
|
|
1990
1996
|
writeClaudeMd(cwd, renderClaudeMd(readFaf(fafPath)));
|
|
1991
1997
|
steps.push(claudeExisted ? '✅ Updated CLAUDE.md (faf-managed block)' : '✅ Created CLAUDE.md');
|
|
1998
|
+
if (stampNote) {
|
|
1999
|
+
steps.push(stampNote);
|
|
2000
|
+
}
|
|
1992
2001
|
// Step 4: final score — same real scorer faf_score reads, on the file
|
|
1993
2002
|
// just written. By construction this can never disagree with faf_score.
|
|
1994
2003
|
const newScore = scoreFafYaml(readFafRaw(fafPath)).score;
|
|
@@ -2053,6 +2062,34 @@ Call these MCP tools:
|
|
|
2053
2062
|
}]
|
|
2054
2063
|
};
|
|
2055
2064
|
}
|
|
2065
|
+
// A .faf-dna that was ever committed is a birth that already happened.
|
|
2066
|
+
// Reissuing would replace the certificate; restore it from git instead.
|
|
2067
|
+
const history = (0, git_history_1.gitEverTracked)(dnaPath);
|
|
2068
|
+
if (history.status === 'found') {
|
|
2069
|
+
const committed = history.where;
|
|
2070
|
+
return {
|
|
2071
|
+
content: [{
|
|
2072
|
+
type: 'text',
|
|
2073
|
+
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)`
|
|
2074
|
+
}],
|
|
2075
|
+
isError: true
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
// Fail closed: if git history could not be read in full, a deleted
|
|
2079
|
+
// certificate cannot be ruled out, so none is issued.
|
|
2080
|
+
if (history.status === 'unknown') {
|
|
2081
|
+
return {
|
|
2082
|
+
content: [{
|
|
2083
|
+
type: 'text',
|
|
2084
|
+
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`
|
|
2085
|
+
}],
|
|
2086
|
+
isError: true
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
// Outside git there is no history to check: say so in the reply.
|
|
2090
|
+
const noHistoryNote = history.status === 'not-a-repo'
|
|
2091
|
+
? `\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.`
|
|
2092
|
+
: '';
|
|
2056
2093
|
// .faf exists but no DNA - create initial DNA. Real scorer — same
|
|
2057
2094
|
// one faf_score reads — never a local pseudo-score (v3.0 fix).
|
|
2058
2095
|
const { readFafRaw, scoreFafYaml } = await faf_cli_bridge_js_1.fafCli;
|
|
@@ -2075,11 +2112,28 @@ Call these MCP tools:
|
|
|
2075
2112
|
],
|
|
2076
2113
|
format: 'faf-dna-v1'
|
|
2077
2114
|
};
|
|
2078
|
-
|
|
2115
|
+
// Exclusive create (O_EXCL): the birth certificate is written once.
|
|
2116
|
+
// If another call created it in the meantime, that one stands and is
|
|
2117
|
+
// the one reported — never a second certificate.
|
|
2118
|
+
try {
|
|
2119
|
+
fs.writeFileSync(dnaPath, JSON.stringify(dna, null, 2), { flag: 'wx' });
|
|
2120
|
+
}
|
|
2121
|
+
catch (err) {
|
|
2122
|
+
if (err?.code !== 'EEXIST')
|
|
2123
|
+
throw err;
|
|
2124
|
+
const existing = JSON.parse(fs.readFileSync(dnaPath, 'utf-8'));
|
|
2125
|
+
const bc = existing.birthCertificate ?? {};
|
|
2126
|
+
return {
|
|
2127
|
+
content: [{
|
|
2128
|
+
type: 'text',
|
|
2129
|
+
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'}`
|
|
2130
|
+
}]
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2079
2133
|
return {
|
|
2080
2134
|
content: [{
|
|
2081
2135
|
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
|
|
2136
|
+
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
2137
|
}]
|
|
2084
2138
|
};
|
|
2085
2139
|
}
|
|
@@ -2102,7 +2156,12 @@ Call these MCP tools:
|
|
|
2102
2156
|
const now = new Date().toISOString();
|
|
2103
2157
|
dna.current = { ...(dna.current ?? {}), score: live, lastSync: now };
|
|
2104
2158
|
milestones.push({ type: 'sync', score: live, date: now, version: dna.current.version ?? 'v1.0.0' });
|
|
2105
|
-
|
|
2159
|
+
// birthCertificate is carried over unchanged; temp + rename so a
|
|
2160
|
+
// crash mid-write cannot truncate the certificate.
|
|
2161
|
+
const tmpDna = `${dnaPath}.tmp-${process.pid}-${Date.now()}`;
|
|
2162
|
+
fs.writeFileSync(tmpDna, JSON.stringify(dna, null, 2), { flag: 'wx' });
|
|
2163
|
+
fs.chmodSync(tmpDna, fs.statSync(dnaPath).mode & 0o7777); // keep the certificate's mode
|
|
2164
|
+
fs.renameSync(tmpDna, dnaPath);
|
|
2106
2165
|
currentScore = live;
|
|
2107
2166
|
}
|
|
2108
2167
|
}
|