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.
@@ -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 content to any file on the local filesystem',
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 { path: rawPath, content } = args;
192
- // Confine to the project root(s) before any write — closes arbitrary file
193
- // write outside the project (e.g. overwriting ~/.bashrc).
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.confineFileOp)(rawPath);
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
- await Promise.race([
223
- fs.writeFile(filePath, content, 'utf8'),
224
- new Promise((_, reject) => setTimeout(() => reject(new Error('Write timeout (30s)')), 30000))
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 ${path.resolve(filePath)}`
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: path.resolve(filePath),
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":"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"}
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 allowed roots (cwd, the OS temp dir, or FAF_ALLOWED_ROOTS). Paths outside them are refused.',
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
- fs.writeFileSync(dnaPath, JSON.stringify(dna, null, 2));
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
- fs.writeFileSync(dnaPath, JSON.stringify(dna, null, 2));
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
  }