diff --git a/posimai-log b/posimai-log new file mode 160000 index 00000000..40dace3d --- /dev/null +++ b/posimai-log @@ -0,0 +1 @@ +Subproject commit 40dace3dddf681af4f7f3bb54717a6900441e7e4 diff --git a/scripts/memory-push.sh b/scripts/memory-push.sh index cb30ee63..96d37ab0 100644 --- a/scripts/memory-push.sh +++ b/scripts/memory-push.sh @@ -3,11 +3,11 @@ # Write ツール使用後に memory/ ファイルへの変更を自動コミット&プッシュする # 環境変数 CLAUDE_TOOL_INPUT (JSON) を stdin 経由で受け取る -MEMORY_DIR="C:/Users/maita/.claude/projects/c--Users-maita-posimai-project/memory" +MEMORY_DIR="$HOME/.claude/projects/c--Users-maita-posimai-project/memory" -# stdin から JSON を読み取り、file_path を取得 +# stdin から JSON を読み取り、tool_input.file_path を取得 INPUT=$(cat) -FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('file_path',''))" 2>/dev/null || echo "") +FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "") # memory ディレクトリ以外は何もしない if ! echo "$FILE_PATH" | grep -qi "memory"; then diff --git a/tools/posimai-scribe/.gitignore b/tools/posimai-scribe/.gitignore new file mode 100644 index 00000000..2d7ec5ce --- /dev/null +++ b/tools/posimai-scribe/.gitignore @@ -0,0 +1,2 @@ +.env +node_modules/ diff --git a/tools/posimai-scribe/package.json b/tools/posimai-scribe/package.json new file mode 100644 index 00000000..4806964c --- /dev/null +++ b/tools/posimai-scribe/package.json @@ -0,0 +1,14 @@ +{ + "name": "posimai-scribe", + "version": "2.0.0", + "description": "Auto-blogging engine for Posimai — Memory-First + Hook Driven", + "type": "module", + "scripts": { + "generate": "node src/generate-post.js", + "serve": "node src/diary-server.js" + }, + "dependencies": { + "@google/generative-ai": "^0.24.1", + "dotenv": "^16.4.5" + } +} diff --git a/tools/posimai-scribe/src/diary-server.js b/tools/posimai-scribe/src/diary-server.js new file mode 100644 index 00000000..69d3153b --- /dev/null +++ b/tools/posimai-scribe/src/diary-server.js @@ -0,0 +1,63 @@ +/** + * diary-server.js + * localhost:2626 で常時待機する軽量デーモン。 + * posimai-log のダイアリーパネルから diary.md を読み書きする。 + * Usage: npm run serve + */ +import http from 'http'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DIARY_FILE = path.join(__dirname, '../diary.md'); +const PORT = 2626; + +const server = http.createServer((req, res) => { + // localhost 専用サーバーなので CORS はワイルドカードで問題なし + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + return; + } + + // GET /diary → diary.md の内容を返す + if (req.url === '/diary' && req.method === 'GET') { + const content = fs.existsSync(DIARY_FILE) + ? fs.readFileSync(DIARY_FILE, 'utf8') + : ''; + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ content })); + return; + } + + // POST /diary → diary.md に上書き保存 + if (req.url === '/diary' && req.method === 'POST') { + let body = ''; + req.on('data', chunk => { body += chunk; }); + req.on('end', () => { + try { + const { content } = JSON.parse(body); + fs.writeFileSync(DIARY_FILE, content ?? '', 'utf8'); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true })); + } catch { + res.writeHead(400); + res.end(); + } + }); + return; + } + + res.writeHead(404); + res.end(); +}); + +server.listen(PORT, '127.0.0.1', () => { + console.log(`[diary-server] http://localhost:${PORT} ready`); + console.log(`[diary-server] diary -> ${DIARY_FILE}`); +}); diff --git a/tools/posimai-scribe/src/event-collector.js b/tools/posimai-scribe/src/event-collector.js new file mode 100644 index 00000000..58a31bea --- /dev/null +++ b/tools/posimai-scribe/src/event-collector.js @@ -0,0 +1,45 @@ +/** + * event-collector.js + * Called automatically by Claude Code PostToolUse hook. + * Reads tool payload from stdin, appends a lightweight JSONL entry to logs/events.jsonl. + * Never throws — a crash here would interrupt Claude Code sessions. + */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const LOG_FILE = path.join(__dirname, '../logs/events.jsonl'); + +// Only track tools that indicate real work +const TRACKED_TOOLS = new Set(['Edit', 'Write', 'Bash', 'NotebookEdit']); + +let raw = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', chunk => { raw += chunk; }); +process.stdin.on('end', () => { + try { + const payload = JSON.parse(raw); + const tool = payload.tool_name; + + if (!TRACKED_TOOLS.has(tool)) process.exit(0); + + const input = payload.tool_input || {}; + const entry = { + ts: new Date().toISOString(), + tool, + // file_path for Edit/Write, truncated command for Bash + target: input.file_path + ? path.basename(input.file_path) + : (input.command || '').slice(0, 80) || null, + ok: !(payload.tool_response?.is_error ?? false) + }; + + fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n', 'utf8'); + } catch { + // Silent fail — never interrupt Claude Code + } + process.exit(0); +}); diff --git a/tools/posimai-scribe/src/generate-post.js b/tools/posimai-scribe/src/generate-post.js new file mode 100644 index 00000000..03bad79a --- /dev/null +++ b/tools/posimai-scribe/src/generate-post.js @@ -0,0 +1,162 @@ +/** + * 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}`);