#!/usr/bin/env node
// extract-vision: pull REAL user prompt-box messages + AskUserQuestion answers out of
// Claude Code transcripts for one project, into a chronological corpus for distillation.
//
// Usage: node extract_prompts.mjs <project-path> <output-file> [--include-worktrees]
//   project-path       e.g. /home/user/Projects/multideal
//   output-file        corpus markdown path
//   --include-worktrees  also scan worktree-derived transcript dirs (agent-heavy; tagged)
//
// What counts as a user message (all must hold):
//   type=user, isSidechain=false, userType=external, message.content is string or
//   text-items array, and text is not a harness artifact (<command-*, <local-command,
//   <system-reminder, <task-notification, <bash-*, tool_result blocks, caveat wrappers).
// AskUserQuestion answers are captured separately by pairing assistant tool_use with the
// user tool_result carrying the same tool_use_id (these carry key decisions and do not
// surface as plain user messages).
// Resumed sessions replay history into new files -> global dedup by text hash, earliest
// timestamp wins.
// Headless/SDK dispatches share userType=external; each entry is tagged with entrypoint +
// one-user-message-session marker so the distiller can drop dispatch-shaped prompts.

import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';

const [projectPath, outFile] = process.argv.slice(2).filter(a => !a.startsWith('--'));
const includeWorktrees = process.argv.includes('--include-worktrees');
if (!projectPath || !outFile) {
  console.error('usage: extract_prompts.mjs <project-path> <output-file> [--include-worktrees]');
  process.exit(1);
}

const PROJECTS_ROOT = path.join(process.env.HOME, '.claude', 'projects');
const enc = projectPath.replace(/[/.]/g, '-');
const dirs = fs.readdirSync(PROJECTS_ROOT).filter(d => {
  if (d === enc) return true;
  if (!d.startsWith(enc + '-')) return false;
  return includeWorktrees;
});
if (dirs.length === 0) { console.error(`no transcript dirs for ${enc}`); process.exit(1); }

const ARTIFACT_PREFIXES = ['<command-name>', '<command-message>', '<local-command', '<bash-input', '<bash-stdout', '<bash-stderr', '<system-reminder', '<task-notification', 'Caveat: The messages below', 'Base directory for this skill:', '[Request interrupted'];
const isArtifact = t => {
  const s = t.trimStart();
  return ARTIFACT_PREFIXES.some(p => s.startsWith(p)) || s.length === 0;
};
const textOf = c => {
  if (typeof c === 'string') return c;
  if (Array.isArray(c)) {
    const items = c.filter(i => i && i.type === 'text').map(i => i.text);
    const hasToolResult = c.some(i => i && i.type === 'tool_result');
    if (hasToolResult && items.length === 0) return null;
    return items.join('\n') || null;
  }
  return null;
};

const entries = new Map(); // hash -> entry
let sessions = 0, files = 0;

for (const d of dirs) {
  const dir = path.join(PROJECTS_ROOT, d);
  const isWorktree = d !== enc;
  for (const f of fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'))) {
    files++;
    let lines;
    try { lines = fs.readFileSync(path.join(dir, f), 'utf8').split('\n').filter(Boolean); } catch { continue; }
    sessions++;
    const pendingQuestions = new Map(); // tool_use_id -> questions JSON
    const sessionUserTexts = [];
    for (const l of lines) {
      let o; try { o = JSON.parse(l); } catch { continue; }
      if (o.type === 'assistant' && Array.isArray(o.message?.content)) {
        for (const item of o.message.content) {
          if (item?.type === 'tool_use' && item.name === 'AskUserQuestion') {
            pendingQuestions.set(item.id, item.input?.questions ?? null);
          }
        }
      }
      if (o.type !== 'user' || o.isSidechain || o.userType !== 'external') continue;
      // entrypoint 'sdk-*' = headless/harness dispatch, not the human prompt box
      if (o.entrypoint && o.entrypoint.startsWith('sdk')) continue;
      const content = o.message?.content;
      // AskUserQuestion answers ride in tool_result blocks
      if (Array.isArray(content)) {
        for (const item of content) {
          if (item?.type === 'tool_result' && pendingQuestions.has(item.tool_use_id)) {
            const qs = pendingQuestions.get(item.tool_use_id);
            const answerText = typeof item.content === 'string' ? item.content
              : Array.isArray(item.content) ? item.content.filter(i => i.type === 'text').map(i => i.text).join('\n') : '';
            const qSummary = Array.isArray(qs) ? qs.map(q => q.question).join(' | ') : '';
            const body = `QUESTIONS: ${qSummary}\nANSWERS: ${answerText}`;
            const h = crypto.createHash('sha1').update(body).digest('hex');
            if (!entries.has(h) || (o.timestamp && o.timestamp < entries.get(h).ts)) {
              entries.set(h, { ts: o.timestamp || '', kind: 'question-tool', body, session: f.slice(0, 8), entrypoint: o.entrypoint || '?', branch: o.gitBranch || '', worktree: isWorktree });
            }
          }
        }
      }
      const t = textOf(content);
      if (!t || isArtifact(t)) continue;
      sessionUserTexts.push(t);
      const h = crypto.createHash('sha1').update(t).digest('hex');
      if (!entries.has(h) || (o.timestamp && o.timestamp < entries.get(h).ts)) {
        entries.set(h, { ts: o.timestamp || '', kind: 'prompt', body: t, session: f.slice(0, 8), entrypoint: o.entrypoint || '?', branch: o.gitBranch || '', worktree: isWorktree, _sessionKey: d + '/' + f });
      }
    }
    // mark single-user-message sessions (dispatch-shaped: likely headless/harness)
    if (sessionUserTexts.length === 1) {
      const h = crypto.createHash('sha1').update(sessionUserTexts[0]).digest('hex');
      const e = entries.get(h);
      if (e) e.singleMsgSession = true;
    }
  }
}

const sorted = [...entries.values()].sort((a, b) => a.ts.localeCompare(b.ts));
const out = [`# User-prompt corpus — ${path.basename(projectPath)}`,
  `Extracted ${sorted.length} unique entries from ${sessions} sessions (${files} files, ${dirs.length} transcript dirs).`,
  `Tags: [Q] = QuestionTool answer (key decisions). [1msg] = only user message in its session (dispatch-shaped, likely headless — distiller: drop if it reads as an agent task spec). [wt] = worktree session.`, ''];
for (const e of sorted) {
  const tags = [e.kind === 'question-tool' ? 'Q' : null, e.singleMsgSession ? '1msg' : null, e.worktree ? 'wt' : null].filter(Boolean).map(t => `[${t}]`).join('');
  out.push(`### ${e.ts.slice(0, 16) || '????'} ${tags} (session ${e.session}, ${e.entrypoint}${e.branch ? ', ' + e.branch : ''})`);
  out.push(e.body, '');
}
fs.writeFileSync(outFile, out.join('\n'));
const kb = Math.round(fs.statSync(outFile).size / 1024);
const byKind = sorted.reduce((m, e) => (m[e.kind] = (m[e.kind] || 0) + 1, m), {});
const byEp = sorted.reduce((m, e) => (m[e.entrypoint] = (m[e.entrypoint] || 0) + 1, m), {});
console.log(JSON.stringify({ out: outFile, entries: sorted.length, sessions, files, kb, byKind, byEntrypoint: byEp, singleMsg: sorted.filter(e => e.singleMsgSession).length }));
