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

163 lines
5.9 KiB
JavaScript
Raw Normal View History

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