Initial release: Claude Code × Obsidian Wiki framework

This commit is contained in:
Masahiro Chaen 2026-04-08 00:39:33 +09:00
commit f8d084eae4
23 changed files with 833 additions and 0 deletions

View file

@ -0,0 +1,40 @@
#!/bin/bash
# git-pull-sync.sh — GitHub から最新を取得して iCloud に反映
# launchd で毎時実行。Cloud Taskがpushした変更をローカルに取り込む。
set +e
VAULT_DIR="$HOME/vault"
LOG_FILE="$VAULT_DIR/.sync.log"
LOCKFILE="$VAULT_DIR/.git-sync.lock"
cd "$VAULT_DIR" || exit 0
# ロックファイルで同時実行を防止
if [ -f "$LOCKFILE" ] && kill -0 "$(cat "$LOCKFILE" 2>/dev/null)" 2>/dev/null; then
echo "[$(date '+%F %T')] git-pull: locked, skipping" >> "$LOG_FILE"; exit 0
fi
echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT
DIRTY=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
if [ "$DIRTY" -gt "0" ]; then
git stash 2>/dev/null
if ! git pull --rebase origin main 2>/dev/null; then
git rebase --abort 2>/dev/null
echo "[$(date '+%F %T')] git-pull: rebase failed, aborted" >> "$LOG_FILE"
fi
if ! git stash pop 2>/dev/null; then
echo "[$(date '+%F %T')] git-pull: stash pop FAILED — manual resolution needed" >> "$LOG_FILE"
git checkout -- . 2>/dev/null
git stash drop 2>/dev/null
fi
echo "[$(date '+%F %T')] git-pull: pulled with stash (dirty=$DIRTY)" >> "$LOG_FILE"
else
RESULT=$(git pull --rebase origin main 2>&1)
if ! echo "$RESULT" | grep -q "Already up to date"; then
echo "[$(date '+%F %T')] git-pull: $RESULT" >> "$LOG_FILE"
fi
fi
exit 0

View file

@ -0,0 +1,48 @@
#!/usr/bin/env bash
# on-file-change.sh — Claude Code PostToolUse hook (Write|Edit)
# ファイル変更時にvaultの関連ページを更新する
# 呼び出し元: ~/.claude/settings.json → hooks.PostToolUse (async: true)
set +e
LOG="$HOME/vault/.sync.log"
# stdinからJSONtool_inputを読み取る
INPUT=$(cat 2>/dev/null || true)
[ -z "$INPUT" ] && exit 0
# 変更されたファイルパスを抽出
FP=$(echo "$INPUT" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
ti = d.get('tool_input', d)
print(ti.get('file_path', ti.get('path', '')))
except:
print('')
" 2>/dev/null || echo "")
[ -z "$FP" ] && exit 0
# パスに基づいて同期対象を判定case文でgrepを避ける
case "$FP" in
*/vault/*)
# vault内のファイル変更は無視再帰防止
;;
*/CLAUDE.md)
echo "[$(date '+%F %T')] CLAUDE.md changed: $FP" >> "$LOG"
;;
*/.claude/projects/*/memory/*|*/memory/*)
echo "[$(date '+%F %T')] Memory changed: $FP" >> "$LOG"
;;
*/.claude/skills/*|*/skills/*)
echo "[$(date '+%F %T')] Skill changed: $FP" >> "$LOG"
;;
*/clients/*/minutes/*)
echo "[$(date '+%F %T')] Minutes changed: $FP" >> "$LOG"
;;
*)
# 同期対象外
;;
esac
exit 0

View file

@ -0,0 +1,63 @@
#!/usr/bin/env bash
# on-session-end.sh — Claude Code Stop hook
# セッション終了時にdaily noteにサマリーを追記する
# 呼び出し元: ~/.claude/settings.json → hooks.Stop (async: true)
set +e
VAULT_DIR="$HOME/vault"
DAILY_DIR="$VAULT_DIR/daily"
LOG="$VAULT_DIR/.sync.log"
TODAY=$(date +%Y-%m-%d)
DAILY_FILE="$DAILY_DIR/$TODAY.md"
NOW=$(date +%H:%M)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] on-session-end" >> "$LOG"
# daily noteが存在しない場合は作成
if [ ! -f "$DAILY_FILE" ]; then
WEEKDAY=$(TODAY="$TODAY" python3 -c "
import os, datetime
d = datetime.date.fromisoformat(os.environ['TODAY'])
print(d.strftime('%A'))
")
printf -- "---\ndate: %s\nweekday: %s\n---\n\n## Schedule\n\n## Log\n\n## Thoughts\n\n## Links\n" "$TODAY" "$WEEKDAY" > "$DAILY_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Created daily note: $DAILY_FILE" >> "$LOG"
fi
# stdinからJSONを読み取るhookから渡される
INPUT=$(cat 2>/dev/null || true)
CWD=$(echo "${INPUT:-{}}" | python3 -c "import sys,json;print(json.load(sys.stdin).get('cwd',''))" 2>/dev/null || echo "")
LABEL=$([ -n "$CWD" ] && basename "$CWD" || echo "unknown")
ENTRY="- $NOW セッション終了 (cwd: $LABEL)"
# ## Log セクションに追記環境変数経由でPythonに渡す — シェル補間を避ける)
export DAILY_FILE ENTRY
python3 -c '
import os
f = os.environ["DAILY_FILE"]
e = os.environ["ENTRY"]
lines = open(f).readlines()
out = []
in_log = False
done = False
for l in lines:
if l.strip() == "## Log":
in_log = True
out.append(l)
continue
if in_log and not done and l.startswith("## "):
out.append(e + "\n\n")
done = True
out.append(l)
if in_log and not done:
out.append(e + "\n\n")
open(f, "w").writelines(out)
' 2>/dev/null || echo "$ENTRY" >> "$DAILY_FILE"
exit 0

View file

@ -0,0 +1,167 @@
#!/bin/bash
# sync-openclaw-to-vault.sh — OpenClawのJSON → vault/daily/ に追記
# Usage: bash sync-openclaw-to-vault.sh [morning|evening]
# OpenClaw cron: 07:30 (morning), 18:30 (evening)
# Cloud TaskがLayer 1でdaily noteを作成済み。このスクリプトはLayer 2でデータ追記。
set +e
MODE="${1:-morning}"
TODAY=$(date +%Y-%m-%d)
WEEKDAY=$(date +%A)
VAULT_DIR="$HOME/vault"
DAILY_FILE="$VAULT_DIR/daily/$TODAY.md"
JSON_FILE="$HOME/clawd/reports/data/daily/$TODAY.json"
LOG_FILE="$VAULT_DIR/.sync.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] sync-vault($MODE): $1" >> "$LOG_FILE"
}
# ロックファイルで同時実行を防止
LOCKFILE="$VAULT_DIR/.git-sync.lock"
if [ -f "$LOCKFILE" ] && kill -0 "$(cat "$LOCKFILE" 2>/dev/null)" 2>/dev/null; then
log "locked, skipping"; exit 0
fi
echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT
# まずGitHub最新を取得Cloud Taskがpush済みの可能性
cd "$VAULT_DIR" && git pull --rebase origin main 2>/dev/null
if [ ! -f "$JSON_FILE" ]; then
log "JSON not found: $JSON_FILE (OpenClaw may not have generated yet)"
exit 0
fi
log "Starting $MODE sync"
export DAILY_FILE JSON_FILE MODE TODAY WEEKDAY
python3 << 'PYEOF'
import os, json
mode = os.environ['MODE']
today = os.environ['TODAY']
weekday = os.environ['WEEKDAY']
daily_file = os.environ['DAILY_FILE']
json_file = os.environ['JSON_FILE']
with open(json_file) as f:
d = json.load(f)
score = d.get('score', 0)
meetings = d.get('meetings', {})
emails = d.get('emails', {})
slack = d.get('slack', {})
deals = d.get('deals', {})
ai = d.get('aiAnalysis', {})
tomorrow = d.get('tomorrow', {})
highlights = d.get('highlights', [])
# daily noteが存在しない場合Cloud Taskが未実行→ 作成
if not os.path.exists(daily_file):
lines = [
'---', f'date: {today}', f'weekday: {weekday}', 'type: daily',
f'score: {score}', '---', '',
]
# 最低限のセクション
for section in ['Schedule', 'Gmail', 'Slack Highlights', 'Salesforce',
'AI Analysis', 'Morning Reflection', 'Claude Code Session',
'Evening Update', 'Evening Reflection', 'Tomorrow',
'Thoughts', 'Links']:
lines.append(f'## {section}')
lines.append('')
with open(daily_file, 'w') as f:
f.write('\n'.join(lines))
content = open(daily_file).read()
# OpenClaw専用データを追記セクションとして構築
enrich_lines = []
# Salesforce
deal_items = deals.get('items', [])
if isinstance(deal_items, list) and deal_items:
sf_lines = []
for dl in deal_items:
if isinstance(dl, dict):
name = dl.get('name', dl.get('title', ''))
stage = dl.get('stage', dl.get('status', ''))
sf_lines.append(f'- {name}: {stage}')
active = deals.get('active', 0)
sf_lines.append(f'- アクティブ案件: {active}件')
sf_text = '\n'.join(sf_lines)
if '## Salesforce' in content and content.split('## Salesforce')[1].split('##')[0].strip() == '':
content = content.replace('## Salesforce\n', f'## Salesforce\n{sf_text}\n', 1)
# AI Analysis
if isinstance(ai, dict) and ai:
ai_lines = []
for key, label in [('productivity','生産性'), ('responsiveness','対応力'), ('salesProgress','営業')]:
info = ai.get(key, {})
if isinstance(info, dict) and info:
ai_lines.append(f'- {label}: {info.get("grade","")} — {info.get("comment","")}')
recs = ai.get('recommendations', [])
if recs:
ai_lines.append('')
for r in recs:
ai_lines.append(f'- {r}')
ai_text = '\n'.join(ai_lines)
if '## AI Analysis' in content and content.split('## AI Analysis')[1].split('##')[0].strip() == '':
content = content.replace('## AI Analysis\n', f'## AI Analysis\n{ai_text}\n', 1)
if mode == 'evening':
# Evening Update
completed = meetings.get('completed', 0)
total = meetings.get('total', 0)
sent = emails.get('sent', 0)
pending = emails.get('pending', 0)
ev_lines = [
f'- 会議: {completed}/{total} 完了',
f'- スコア: {score}',
f'- メール送信: {sent}件 / 未対応: {pending}件',
]
if highlights:
for h in highlights:
if isinstance(h, dict):
ev_lines.append(f'- {h.get("emoji","")} {h.get("text","")}')
ev_text = '\n'.join(ev_lines)
content = content.replace(
'<!-- sync-openclaw-to-vault.sh evening が自動追記 -->',
ev_text
)
# Tomorrow
t_lines = []
t_items = tomorrow.get('items', [])
if isinstance(t_items, list):
for t in t_items:
if isinstance(t, dict):
time = t.get('time', '')
title = t.get('title', '')
warn = ' !!!' if t.get('warning') else ''
t_lines.append(f'- {time} {title}{warn}')
t_count = tomorrow.get('meetings', 0)
t_lines.append(f'')
t_lines.append(f'明日の会議: {t_count}件')
advice = ai.get('tomorrowAdvice', '') if isinstance(ai, dict) else ''
if advice:
t_lines.append(f'')
t_lines.append(advice)
t_text = '\n'.join(t_lines)
content = content.replace(
'<!-- sync-openclaw-to-vault.sh evening が自動生成 -->',
t_text
)
with open(daily_file, 'w') as f:
f.write(content)
print(f'{mode} enrichment done: {daily_file}')
PYEOF
# git commit & push
cd "$VAULT_DIR" && git add "daily/$TODAY.md" && git commit -m "enrich: $TODAY $MODE (OpenClaw data)" 2>/dev/null && git push 2>/dev/null
log "Completed $MODE sync"
exit 0

View file

@ -0,0 +1,72 @@
#!/usr/bin/env bash
# weekly-sync.sh — 週次Lintスクリプト
# 毎週日曜 03:00 に実行cron or manual
# vault内の整合性チェック・壊れたリンク検出・orphanページ検出
set +e
VAULT="$HOME/vault"
LOG="$VAULT/.sync.log"
ISSUES=""
IC=0
add() {
ISSUES="${ISSUES}"$'\n'"- $1"
IC=$((IC + 1))
}
echo "[$(date '+%F %T')] weekly-sync start" >> "$LOG"
# 1. 壊れたwikilinksを検出grep -oE はmacOS互換
echo "--- Checking broken wikilinks ---"
while IFS= read -r -d '' f; do
# grep -oE で [[...]] リンクを抽出(-oP は macOS 非対応なので使わない)
while IFS= read -r link; do
[ -z "$link" ] && continue
[ ! -f "$VAULT/${link}.md" ] && add "Broken: [[${link}]] in $(basename "$f")"
done < <(grep -oE '\[\[[^]|]+' "$f" 2>/dev/null | sed 's/\[\[//')
done < <(find "$VAULT" -name "*.md" -not -path "*/.obsidian/*" -not -path "*/templates/*" -print0)
# 2. 過去7日のdaily noteが存在するかチェック
echo "--- Checking daily notes ---"
for i in $(seq 1 7); do
d=$(date -v-"${i}d" +%Y-%m-%d 2>/dev/null || continue)
[ ! -f "$VAULT/daily/${d}.md" ] && add "Missing daily: ${d}"
done
# 3. SYNCEDファイルのヘッダーチェック
echo "--- Checking SYNCED headers ---"
while IFS= read -r -d '' f; do
FIRST_LINE=$(head -1 "$f")
case "$FIRST_LINE" in
"<!-- SYNCED: DO NOT EDIT -->") ;;
*) add "Missing SYNCED header: $(basename "$f")" ;;
esac
done < <(find "$VAULT/system" -name "*.md" -print0 2>/dev/null)
# 4. frontmatterのないファイルを検出
echo "--- Checking frontmatter ---"
while IFS= read -r -d '' f; do
FIRST_LINE=$(head -1 "$f")
case "$FIRST_LINE" in
"---"|"<!-- SYNCED: DO NOT EDIT -->") ;;
*) add "Missing frontmatter: $(basename "$f")" ;;
esac
done < <(find "$VAULT" -name "*.md" -not -path "*/templates/*" -not -path "*/.obsidian/*" -print0 2>/dev/null)
# 5. CLAUDE.mdの総数をカウント
echo "--- Counting CLAUDE.md files ---"
CLAUDE_COUNT=0
while IFS= read -r -d '' _f; do
CLAUDE_COUNT=$((CLAUDE_COUNT + 1))
done < <(find "$HOME/work" "$HOME/dev" "$HOME/content" -name "CLAUDE.md" -print0 2>/dev/null)
echo "[$(date '+%F %T')] weekly-sync done: $IC issues, $CLAUDE_COUNT CLAUDE.md files" >> "$LOG"
# サマリー出力
if [ "$IC" -gt 0 ]; then
printf "Lint: %s issues%s\n" "$IC" "$ISSUES"
else
echo "Lint: All clear! ($CLAUDE_COUNT CLAUDE.md files)"
fi
exit 0