Initial release: Claude Code × Obsidian Wiki framework
This commit is contained in:
commit
f8d084eae4
23 changed files with 833 additions and 0 deletions
40
vault-template/scripts/git-pull-sync.sh
Executable file
40
vault-template/scripts/git-pull-sync.sh
Executable 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
|
||||
48
vault-template/scripts/on-file-change.sh
Executable file
48
vault-template/scripts/on-file-change.sh
Executable 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からJSON(tool_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
|
||||
63
vault-template/scripts/on-session-end.sh
Executable file
63
vault-template/scripts/on-session-end.sh
Executable 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
|
||||
167
vault-template/scripts/sync-openclaw-to-vault.sh
Executable file
167
vault-template/scripts/sync-openclaw-to-vault.sh
Executable 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
|
||||
72
vault-template/scripts/weekly-sync.sh
Executable file
72
vault-template/scripts/weekly-sync.sh
Executable 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue