← back to The Ai Factory

src/stages/memory_write.js

63 lines

// Stage 9 — memory_write. Appends one line to Steve's MEMORY.md index after a
// successful activation. We do NOT write a separate memory file per artifact —
// that would pollute the memory dir; the index entry is enough for future
// sessions to learn the artifact exists.

const fs = require('node:fs/promises');
const path = require('node:path');
const os = require('node:os');

const MEMORY_INDEX = path.join(
  os.homedir(),
  '.claude',
  'projects',
  '-Users-stevestudio2',
  'memory',
  'MEMORY.md'
);

function todayStamp() {
  return new Date().toISOString().slice(0, 10); // YYYY-MM-DD
}

// SECURITY (P1 fix 2026-05-04): scope + triggers are LLM-generated. Without
// sanitization a poisoned response could inject newlines/markdown headers
// (e.g. `\n# OVERWRITE`) into MEMORY.md. Strip newlines, cap length.
function sanitize(s, max = 200) {
  return String(s ?? '').replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max);
}
function buildLine({ runId, artifactType, artifactName, spec, dest }) {
  const safeName = sanitize(artifactName, 60);
  const safeType = sanitize(artifactType, 20);
  const scope = sanitize(spec?.scope, 200);
  const triggerArr = Array.isArray(spec?.triggers)
    ? spec.triggers.slice(0, 3).map(t => sanitize(t, 60)).filter(Boolean)
    : [];
  const triggers = triggerArr.length ? ` triggers: ${triggerArr.join(', ')};` : '';
  const surface = safeType === 'subagent' ? '~/.claude/agents/' : '~/.claude/skills/';
  return `- AI Factory shipped: \`${safeName}\` (${safeType}) — ${scope}${triggers} [run #${runId}, ${todayStamp()}, lives in ${surface}]`;
}

async function appendMemoryIndex(line) {
  let existing = '';
  try { existing = await fs.readFile(MEMORY_INDEX, 'utf8'); }
  catch (e) {
    if (e.code !== 'ENOENT') throw e;
  }
  // Avoid double-entry if activate is called twice for the same artifact.
  if (existing.includes(line.split(' [run #')[0])) {
    return { skipped: true, reason: 'already present' };
  }
  const trailing = existing.endsWith('\n') ? '' : (existing ? '\n' : '');
  await fs.writeFile(MEMORY_INDEX, existing + trailing + line + '\n', 'utf8');
  return { skipped: false };
}

async function runMemoryWrite({ runId, artifactType, artifactName, spec, dest }) {
  const line = buildLine({ runId, artifactType, artifactName, spec, dest });
  const result = await appendMemoryIndex(line);
  return { line, ...result };
}

module.exports = { runMemoryWrite, MEMORY_INDEX };