Docs/Skills and code/skill video-script-production

.claude/skills/video-script-production/scripts/session-log.js

Code51 lines4.0 KBSkill: video-script-production

What the file says about itself

Writes the research record of a Claude Code session as markdown: every web search, every page fetched (and what it was asked), every subagent spawned (with its prompt, if asked), and the user's messages.

Mentioned on: page 06, page 09, page 10, page 12, page in

#!/usr/bin/env node
// Writes the research record of a Claude Code session as markdown: every web search, every page fetched (and what it was asked),
// every subagent spawned (with its prompt, if asked), and the user's messages. Deterministic: it reads the session transcript, so nothing depends on memory.
//   node session-log.js [--jsonl <session.jsonl>] [--since <ISO>] [--until <ISO>] [--prompts] [--user] [--out <file.md>]
// Default transcript: the newest .jsonl under ~/.claude/projects/<this project>/. Times are UTC as stored.
// Use it at the end of a script run (SKILL.md Step 7) so the research behind each version is recorded in the repo, not just in the chat.

const fs = require('fs');
const os = require('os');
const path = require('path');

const args = {};
for (let i = 2; i < process.argv.length; i++) {
  const a = process.argv[i]; if (!a.startsWith('--')) continue;
  const k = a.slice(2), nxt = process.argv[i + 1];
  if (nxt && !nxt.startsWith('--')) { args[k] = nxt; i++; } else args[k] = true;
}
let file = args.jsonl;
if (!file) {
  const base = path.join(os.homedir(), '.claude', 'projects', process.cwd().replace(/[^A-Za-z0-9]/g, '-'));
  if (!fs.existsSync(base)) { console.error('no transcript folder at ' + base + ' (pass --jsonl)'); process.exit(2); }
  const all = fs.readdirSync(base).filter((f) => f.endsWith('.jsonl')).map((f) => ({ f, t: fs.statSync(path.join(base, f)).mtimeMs })).sort((a, b) => b.t - a.t);
  if (!all.length) { console.error('no .jsonl in ' + base); process.exit(2); }
  file = path.join(base, all[0].f);
}
const since = args.since ? Date.parse(args.since) : -Infinity, until = args.until ? Date.parse(args.until) : Infinity;
const inRange = (t) => { const x = Date.parse(t); return Number.isNaN(x) || (x >= since && x <= until); };

const searches = [], fetches = [], agents = [], users = [];
for (const l of fs.readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
  let o; try { o = JSON.parse(l); } catch { continue; }
  const m = o.message; if (!m || !inRange(o.timestamp)) continue;
  const text = typeof m.content === 'string' ? m.content : Array.isArray(m.content) ? m.content.filter((x) => x.type === 'text').map((x) => x.text).join('\n') : '';
  if (m.role === 'user' && args.user && text && !/^<system-reminder>|^<task-notification|^Tool loaded|This session is being continued|Another Claude session sent/.test(text.trim())) users.push({ t: o.timestamp, text });
  if (m.role !== 'assistant' || !Array.isArray(m.content)) continue;
  for (const b of m.content) {
    if (b.type !== 'tool_use') continue;
    if (b.name === 'WebSearch') searches.push({ t: o.timestamp, q: b.input.query });
    else if (b.name === 'WebFetch') fetches.push({ t: o.timestamp, url: b.input.url, ask: (b.input.prompt || '').replace(/\s+/g, ' ').slice(0, 200) });
    else if (b.name === 'Agent') agents.push({ t: o.timestamp, d: b.input.description, model: b.input.model || 'default', type: b.input.subagent_type || 'general-purpose', prompt: b.input.prompt || '' });
  }
}
const hhmm = (t) => (t || '').replace('T', ' ').slice(0, 16);
const md = [`# Session research log`, '', `Source: \`${path.basename(file)}\`${args.since ? `, from ${args.since}` : ''}${args.until ? `, to ${args.until}` : ''}. Times are UTC. ${searches.length} searches, ${fetches.length} page fetches, ${agents.length} subagents.`, '',
  '## Web searches', '', ...searches.map((s) => `- ${hhmm(s.t)}  ${s.q}`), '',
  '## Pages fetched', '', ...fetches.map((f) => `- ${hhmm(f.t)}  ${f.url}\n  - asked: ${f.ask}`), '',
  '## Subagents', '', ...(agents.length ? agents.map((a) => `- ${hhmm(a.t)}  **${a.d}** (${a.type}, model ${a.model}, prompt ${a.prompt.length} chars)${args.prompts ? '\n\n```\n' + a.prompt + '\n```\n' : ''}`) : ['(none)']),
  ...(args.user ? ['', '## User messages', '', ...users.map((u) => `- ${hhmm(u.t)}  ${u.text.replace(/\s+/g, ' ').slice(0, 700)}`)] : [])].join('\n');
if (args.out) { fs.writeFileSync(args.out, md + '\n', 'utf8'); console.log(`wrote ${args.out}: ${searches.length} searches, ${fetches.length} fetches, ${agents.length} subagents`); }
else console.log(md);

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.