feat: diary VPS cloud sync — generate-post fetches from VPS, memory-push.sh HOME fix
This commit is contained in:
parent
0540e24e67
commit
3ecdb23a29
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 40dace3dddf681af4f7f3bb54717a6900441e7e4
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
# Write ツール使用後に memory/ ファイルへの変更を自動コミット&プッシュする
|
# Write ツール使用後に memory/ ファイルへの変更を自動コミット&プッシュする
|
||||||
# 環境変数 CLAUDE_TOOL_INPUT (JSON) を stdin 経由で受け取る
|
# 環境変数 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)
|
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 ディレクトリ以外は何もしない
|
# memory ディレクトリ以外は何もしない
|
||||||
if ! echo "$FILE_PATH" | grep -qi "memory"; then
|
if ! echo "$FILE_PATH" | grep -qi "memory"; then
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
.env
|
||||||
|
node_modules/
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}`);
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
@ -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}`);
|
||||||
Loading…
Reference in New Issue