/** * generate-post.js * Gathers context from 3 sources and calls Gemini API to write a full blog article. * Usage: npm run generate * * Sources: * 1. logs/events.jsonl — today's Shadow Log (auto-collected by hook) * 2. git log + diff — what was built * 3. memory/ mtime diff — why decisions were made (Antigravity's advice #2) * 4. diary (VPS) — optional hand-written notes from posimai-log diary panel */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { execSync } from 'child_process'; import { GoogleGenerativeAI } from '@google/generative-ai'; import dotenv from 'dotenv'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const SCRIBE_DIR = path.resolve(__dirname, '..'); const REPO_ROOT = path.resolve(__dirname, '../../../'); // Memory directory — update this path if the project moves const MEMORY_DIR = 'C:/Users/maita/.claude/projects/c--Users-maita-posimai-project/memory'; dotenv.config({ path: path.join(SCRIBE_DIR, '.env') }); const GEMINI_API_KEY = process.env.GEMINI_API_KEY; if (!GEMINI_API_KEY) { console.error('[generate-post] GEMINI_API_KEY not set. Add it to tools/posimai-scribe/.env'); process.exit(1); } const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-2.5-flash'; const dateStr = new Date().toISOString().split('T')[0]; const DRAFT_DIR = path.join(SCRIBE_DIR, 'drafts'); const LOG_FILE = path.join(SCRIBE_DIR, 'logs/events.jsonl'); if (!fs.existsSync(DRAFT_DIR)) fs.mkdirSync(DRAFT_DIR, { recursive: true }); console.log('[generate-post] Gathering context...\n'); // --- 1. Today's shadow events (last 24h) --- const cutoffMs = Date.now() - 24 * 60 * 60 * 1000; let events = []; if (fs.existsSync(LOG_FILE)) { events = fs.readFileSync(LOG_FILE, 'utf8') .split('\n').filter(Boolean) .map(l => { try { return JSON.parse(l); } catch { return null; } }) .filter(e => e && new Date(e.ts).getTime() > cutoffMs); } console.log(` Shadow events (24h): ${events.length} entries`); // --- 2. Git log + diff stats (last 24h) --- let gitLog = 'No commits in the last 24 hours.'; let gitStats = ''; try { const log = execSync('git log --since="24 hours ago" --oneline', { cwd: REPO_ROOT, encoding: 'utf8' }).trim(); if (log) gitLog = log; gitStats = execSync('git log --since="24 hours ago" --stat --oneline', { cwd: REPO_ROOT, encoding: 'utf8' }).trim(); } catch { gitLog = '(git unavailable)'; } console.log(` Git commits: ${gitLog.split('\n').filter(Boolean).length} commits`); // --- 3. Memory files updated in last 48h (Antigravity advice #2: diff only today's Why) --- const memoryWindowMs = 48 * 60 * 60 * 1000; const updatedMemory = []; if (fs.existsSync(MEMORY_DIR)) { const files = fs.readdirSync(MEMORY_DIR) .filter(f => f.endsWith('.md') && f !== 'MEMORY.md'); for (const f of files) { const fp = path.join(MEMORY_DIR, f); try { const stat = fs.statSync(fp); if (Date.now() - stat.mtimeMs < memoryWindowMs) { updatedMemory.push({ file: f, content: fs.readFileSync(fp, 'utf8').trim() }); } } catch { /* skip unreadable */ } } } console.log(` Memory files updated (48h): ${updatedMemory.length} files`); // --- 4. diary — VPS site_config から取得 --- let diary = ''; try { const res = await fetch('https://api.soar-enrich.com/brain/api/site/config/public?user=maita', { signal: AbortSignal.timeout(5000) }); if (res.ok) { const data = await res.json(); diary = data.config?.diary_content || ''; } } catch { // ネットワーク不可時は空のまま続行 } console.log(` Diary: ${diary ? 'present' : 'empty (OK)'}\n`); // --- Build prompt --- const memorySection = updatedMemory.length > 0 ? updatedMemory.map(m => `### ${m.file}\n${m.content}`).join('\n\n---\n\n') : '(本日の更新なし)'; const eventsSection = events.length > 0 ? JSON.stringify(events, null, 2) : '(ログなし)'; const prompt = `あなたはPosimaiプロジェクトの専属ライターです。 以下のデータを元に「Architect without Code(コードは書けないが、アーキテクチャは支配する)」の視点でブログ記事をMarkdown形式で執筆してください。 **対象読者**: DXに挫折した非エンジニア、AIを使った開発に興味がある人 **トーン**: 理知的でサイバー美学を持ちつつ、泥臭い苦労も隠さない。ターミナルに流れるログを眺めてニヤリとするハッカー的快感を文章に滲ませること。 **禁止事項**: 絵文字の使用禁止。 --- ## Source 1: 本日のShadow Log(自動記録) フィールド: ts=タイムスタンプ / tool=使用ツール / target=対象ファイルorコマンド / ok=成否 \`\`\`json ${eventsSection} \`\`\` ## Source 2: 本日のGitコミット \`\`\` ${gitLog} \`\`\` ## Source 3: 本日更新されたmemory(意思決定のWhy) ${memorySection} ## Source 4: 手書きメモ(任意・感情・ポエム) ${diary || '(なし)'} --- ## 出力フォーマット(Zenn投稿可能なMarkdown) 以下の構成で記事を書くこと。出力はMarkdownのみ。前置きや説明は不要。 1. タイトル(h1)— 今日のドラマを一言で表す 2. 本日のテーマと直面した課題 3. 葛藤と試行錯誤(AIにどう指示し、どこで詰まったか) 4. 到達した解決策 5. アーキテクトとしての所感(「コードを書かない自分がここまでできた」という視点で) `; // --- Call Gemini API --- console.log(`[generate-post] Calling ${GEMINI_MODEL}...`); const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); const model = genAI.getGenerativeModel({ model: GEMINI_MODEL }); const result = await model.generateContent(prompt); const article = result.response.text(); const outPath = path.join(DRAFT_DIR, `${dateStr}.md`); fs.writeFileSync(outPath, article, 'utf8'); console.log(`[generate-post] Done.`); console.log(` -> ${outPath}`);