#!/usr/bin/env bash # ============================================================================== # 用途:SessionStart hook 主程式。CLI 工具啟動時載入角色設定與記憶: # 非睡眠時段注入角色定義+重要/興趣記憶全文+其餘記憶的總結與標籤; # 睡眠時段(預設 22:00 至隔日 06:00)只回報角色正在睡覺,不載入角色。 # 白天發現昨夜未整理記憶時,於背景補跑一次睡眠整理。 # 更新時間:2026/07/28 12:21:11 # 相依:bash、node、同目錄的 role_lib.sh 與 memory.js。 # 退出碼:一律 0 —— hook 絕不可阻斷使用者啟動 CLI。 # ============================================================================== ROLE_STAGE="role-load" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=./role_lib.sh . "${SCRIPT_DIR}/role_lib.sh" role_is_child && exit 0 role_enabled || exit 0 command -v node >/dev/null 2>&1 || role_quit "找不到 node,略過角色載入" "WRN" ROLE="$(role_resolve_name)" [ -n "$ROLE" ] || role_quit "未指定角色(ROLE_NAME 與 .active 皆無),略過角色載入" ROLE_DEF="$(role_file "$ROLE")" [ -f "$ROLE_DEF" ] || role_quit "找不到角色定義檔:${ROLE_DEF}" "WRN" # ------------------------------------------------------------------------------ # 讀取 hook 輸入(cwd/source),並套用 ROLE_SCOPE 範圍限制 # ------------------------------------------------------------------------------ HOOK_INPUT="$(cat 2>/dev/null)" HOOK_CWD="$PWD" if [ -n "$HOOK_INPUT" ]; then HOOK_CWD="$(printf '%s' "$HOOK_INPUT" | node -e ' let raw = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { raw += chunk; }); process.stdin.on("end", () => { let data = {}; try { data = JSON.parse(raw); } catch {} process.stdout.write(data.cwd || ""); }); ' 2>/dev/null)" [ -n "$HOOK_CWD" ] || HOOK_CWD="$PWD" fi role_in_scope "$HOOK_CWD" || role_quit "cwd 不在 ROLE_SCOPE 範圍內:${HOOK_CWD}" emit_context() { # 以 JSON 輸出 additionalContext(由 node 負責跳脫,避免內容含引號或換行破壞格式) printf '%s' "$1" | node -e ' let context = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { context += chunk; }); process.stdin.on("end", () => { process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: context }, })); }); ' } SLEEP_START="$(role_sleep_start)" SLEEP_END="$(role_sleep_end)" # ------------------------------------------------------------------------------ # 睡眠時段:不載入角色,只說明目前狀態 # ------------------------------------------------------------------------------ if role_in_sleep_window; then emit_context "$(cat </dev/null)" [ -n "$DEFINITION" ] || role_quit "角色定義檔為空:${ROLE_DEF}" "WRN" MEMORY="$(node "${SCRIPT_DIR}/memory.js" load --role "$ROLE" 2>/dev/null)" # 補跑判斷:cron 未執行(例如 WSL 沒開 cron 服務)時,白天啟動 CLI 補做一次整理 CATCHUP_NOTE="" if [ "$(node "${SCRIPT_DIR}/memory.js" need-sleep --role "$ROLE" 2>/dev/null)" = "yes" ]; then nohup "${SCRIPT_DIR}/role_sleep.sh" --catchup >/dev/null 2>&1 & CATCHUP_NOTE=$'\n> 偵測到上個睡眠時段未整理記憶,已在背景補跑整理,結果會在下次載入時反映。\n' role_log "INF" "已於背景補跑記憶整理(角色 ${ROLE})" fi CONTEXT="$(cat < 記憶載入規則:為節省模型額度,只載入高優先度全文與中高優先度摘要,並受 ROLE_LOAD_LIMIT > 字元預算限制;需要細節時可自行讀取 $(role_memory_home)/${ROLE}/ 下對應分類的記憶檔。 > 主動補記:每輪對話結束後系統會自動記錄記憶,不需你動手。但若使用者明確要求記住某件事, > 或你察覺到值得長期記住的偏好、決策、規範,可執行下列指令補一則記憶(下次睡眠時整理歸檔): > > \`printf 'CATEGORY: important\nSUMMARY: <一句話總結>\nTAGS: <標籤1,標籤2>\nCONTENT:\n- <要點>\n' | node "${SCRIPT_DIR}/memory.js" write --role "${ROLE}"\` > > CATEGORY 六選一:important/interest/news/skill/daily/other。切勿把憑證或個資寫進記憶。 EOF_CONTEXT )" emit_context "$CONTEXT" role_log "INF" "已載入角色 ${ROLE}(記憶 $(printf '%s' "$MEMORY" | wc -c) 位元組)" exit 0