posimai-root/tools/posimai-scribe/src/generate-post.js

163 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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}`);