termify-agent 1.0.39 → 1.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/agent.d.ts.map +1 -1
  2. package/dist/agent.js +2 -0
  3. package/dist/agent.js.map +1 -1
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +55 -0
  6. package/dist/auth.js.map +1 -1
  7. package/dist/dashboard.d.ts.map +1 -1
  8. package/dist/dashboard.js +26 -2
  9. package/dist/dashboard.js.map +1 -1
  10. package/dist/index.js +5 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/pty-manager.d.ts +19 -0
  13. package/dist/pty-manager.d.ts.map +1 -1
  14. package/dist/pty-manager.js +111 -0
  15. package/dist/pty-manager.js.map +1 -1
  16. package/dist/setup.d.ts +15 -0
  17. package/dist/setup.d.ts.map +1 -0
  18. package/dist/setup.js +603 -0
  19. package/dist/setup.js.map +1 -0
  20. package/hooks/termify-response.js +151 -124
  21. package/hooks/termify-sync.js +165 -116
  22. package/mcp/memsearch-mcp-server.mjs +149 -0
  23. package/package.json +3 -2
  24. package/plugins/context7/.claude-plugin/plugin.json +7 -0
  25. package/plugins/context7/.mcp.json +6 -0
  26. package/plugins/memsearch/.claude-plugin/plugin.json +5 -0
  27. package/plugins/memsearch/README.md +762 -0
  28. package/plugins/memsearch/hooks/common.sh +151 -0
  29. package/plugins/memsearch/hooks/hooks.json +50 -0
  30. package/plugins/memsearch/hooks/parse-transcript.sh +117 -0
  31. package/plugins/memsearch/hooks/session-end.sh +9 -0
  32. package/plugins/memsearch/hooks/session-start.sh +119 -0
  33. package/plugins/memsearch/hooks/stop.sh +117 -0
  34. package/plugins/memsearch/hooks/user-prompt-submit.sh +21 -0
  35. package/plugins/memsearch/scripts/derive-collection.sh +50 -0
  36. package/plugins/memsearch/skills/memory-recall/SKILL.md +42 -0
  37. package/scripts/postinstall.js +21 -483
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bash
2
+ # Shared setup for memsearch command hooks.
3
+ # Sourced by all hook scripts — not executed directly.
4
+
5
+ set -euo pipefail
6
+
7
+ # Read stdin JSON into $INPUT
8
+ INPUT="$(cat)"
9
+
10
+ # Ensure common user bin paths are in PATH (hooks may run in a minimal env)
11
+ for p in "$HOME/.local/bin" "$HOME/.cargo/bin" "$HOME/bin" "/usr/local/bin"; do
12
+ [[ -d "$p" ]] && [[ ":$PATH:" != *":$p:"* ]] && export PATH="$p:$PATH"
13
+ done
14
+
15
+ # Memory directory and memsearch state directory are project-scoped
16
+ MEMSEARCH_DIR="${CLAUDE_PROJECT_DIR:-.}/.memsearch"
17
+ MEMORY_DIR="$MEMSEARCH_DIR/memory"
18
+
19
+ # Find memsearch binary: prefer PATH, fallback to uvx
20
+ _detect_memsearch() {
21
+ MEMSEARCH_CMD=""
22
+ if command -v memsearch &>/dev/null; then
23
+ MEMSEARCH_CMD="memsearch"
24
+ elif command -v uvx &>/dev/null; then
25
+ MEMSEARCH_CMD="uvx memsearch"
26
+ fi
27
+ }
28
+ _detect_memsearch
29
+
30
+ # Short command prefix for injected instructions (falls back to "memsearch" even if unavailable)
31
+ MEMSEARCH_CMD_PREFIX="${MEMSEARCH_CMD:-memsearch}"
32
+
33
+ # Derive per-project collection name from project directory
34
+ COLLECTION_NAME=$("$(dirname "${BASH_SOURCE[0]}")/../scripts/derive-collection.sh" "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || true)
35
+
36
+ # --- JSON helpers (jq preferred, python3 fallback) ---
37
+
38
+ # _json_val <json_string> <dotted_key> [default]
39
+ # Extract a value from JSON. Key supports dotted notation (e.g. "info.version").
40
+ # Returns the default (or empty string) if the key is missing or extraction fails.
41
+ _json_val() {
42
+ local json="$1" key="$2" default="${3:-}"
43
+ local result=""
44
+
45
+ if command -v jq &>/dev/null; then
46
+ # Build jq filter from dotted key: "info.version" → ".info.version"
47
+ result=$(printf '%s' "$json" | jq -r ".${key} // empty" 2>/dev/null) || true
48
+ else
49
+ result=$(python3 -c "
50
+ import json, sys
51
+ try:
52
+ obj = json.loads(sys.argv[1])
53
+ val = obj
54
+ for k in sys.argv[2].split('.'):
55
+ val = val[k]
56
+ if val is None:
57
+ print('')
58
+ elif isinstance(val, bool):
59
+ print(str(val).lower())
60
+ else:
61
+ print(val)
62
+ except Exception:
63
+ print('')
64
+ " "$json" "$key" 2>/dev/null) || true
65
+ fi
66
+
67
+ if [ -z "$result" ]; then
68
+ printf '%s' "$default"
69
+ else
70
+ printf '%s' "$result"
71
+ fi
72
+ return 0
73
+ }
74
+
75
+ # _json_encode_str <string>
76
+ # Encode a string as a JSON string (with surrounding quotes).
77
+ _json_encode_str() {
78
+ local str="$1"
79
+ if command -v jq &>/dev/null; then
80
+ printf '%s' "$str" | jq -Rs . 2>/dev/null && return 0
81
+ fi
82
+ printf '%s' "$str" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read()))" 2>/dev/null && return 0
83
+ # Last resort: simple quoting (no special char escaping)
84
+ printf '"%s"' "$str"
85
+ return 0
86
+ }
87
+
88
+ # Helper: ensure memory directory exists
89
+ ensure_memory_dir() {
90
+ mkdir -p "$MEMORY_DIR"
91
+ }
92
+
93
+ # Helper: run memsearch with arguments, silently fail if not available
94
+ run_memsearch() {
95
+ if [ -n "$MEMSEARCH_CMD" ] && [ -n "$COLLECTION_NAME" ]; then
96
+ $MEMSEARCH_CMD "$@" --collection "$COLLECTION_NAME" 2>/dev/null || true
97
+ elif [ -n "$MEMSEARCH_CMD" ]; then
98
+ $MEMSEARCH_CMD "$@" 2>/dev/null || true
99
+ fi
100
+ }
101
+
102
+ # --- Watch singleton management ---
103
+
104
+ WATCH_PIDFILE="$MEMSEARCH_DIR/.watch.pid"
105
+
106
+ # Kill a process and its entire process group to avoid orphans
107
+ _kill_tree() {
108
+ local pid="$1"
109
+ # Kill the process group (negative PID) to catch child processes
110
+ kill -- -"$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true
111
+ }
112
+
113
+ # Stop the watch process: pidfile first, then sweep for orphans
114
+ stop_watch() {
115
+ # 1. Kill the process recorded in pidfile
116
+ if [ -f "$WATCH_PIDFILE" ]; then
117
+ local pid
118
+ pid=$(cat "$WATCH_PIDFILE" 2>/dev/null)
119
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
120
+ _kill_tree "$pid"
121
+ fi
122
+ rm -f "$WATCH_PIDFILE"
123
+ fi
124
+
125
+ # 2. Sweep for orphaned watch processes targeting this MEMORY_DIR
126
+ local orphans
127
+ orphans=$(pgrep -f "memsearch watch $MEMORY_DIR" 2>/dev/null || true)
128
+ if [ -n "$orphans" ]; then
129
+ echo "$orphans" | while read -r opid; do
130
+ kill "$opid" 2>/dev/null || true
131
+ done
132
+ fi
133
+ }
134
+
135
+ # Start memsearch watch — always stop-then-start to pick up config changes
136
+ start_watch() {
137
+ if [ -z "$MEMSEARCH_CMD" ]; then
138
+ return 0
139
+ fi
140
+ ensure_memory_dir
141
+
142
+ # Always restart: ensures latest config (milvus_uri, etc.) is used
143
+ stop_watch
144
+
145
+ if [ -n "$COLLECTION_NAME" ]; then
146
+ nohup $MEMSEARCH_CMD watch "$MEMORY_DIR" --collection "$COLLECTION_NAME" &>/dev/null &
147
+ else
148
+ nohup $MEMSEARCH_CMD watch "$MEMORY_DIR" &>/dev/null &
149
+ fi
150
+ echo $! > "$WATCH_PIDFILE"
151
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "description": "memsearch memory hooks — automatic capture, search, and recall",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh",
10
+ "timeout": 10
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "UserPromptSubmit": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/user-prompt-submit.sh",
21
+ "timeout": 15
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "Stop": [
27
+ {
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/stop.sh",
32
+ "async": true,
33
+ "timeout": 120
34
+ }
35
+ ]
36
+ }
37
+ ],
38
+ "SessionEnd": [
39
+ {
40
+ "hooks": [
41
+ {
42
+ "type": "command",
43
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh",
44
+ "timeout": 10
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
50
+ }
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # Parse a Claude Code JSONL transcript into a concise text summary
3
+ # suitable for AI summarization.
4
+ #
5
+ # Usage: bash parse-transcript.sh <transcript_path>
6
+ #
7
+ # Truncation rules:
8
+ # - Only process the last MAX_LINES lines (default 200)
9
+ # - User/assistant text content > MAX_CHARS chars (default 500) is truncated to tail
10
+ # - Tool calls: only output tool name + truncated input summary
11
+ # - Tool results: only output a one-line truncated preview
12
+ # - Skip file-history-snapshot entries entirely
13
+
14
+ set -euo pipefail
15
+
16
+ # parse-transcript requires jq for JSON processing; gracefully degrade if missing
17
+ if ! command -v jq &>/dev/null; then
18
+ echo "(transcript parsing skipped — jq not installed)"
19
+ exit 0
20
+ fi
21
+
22
+ TRANSCRIPT_PATH="${1:-}"
23
+ MAX_LINES="${MEMSEARCH_MAX_LINES:-200}"
24
+ MAX_CHARS="${MEMSEARCH_MAX_CHARS:-500}"
25
+
26
+ if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
27
+ echo "ERROR: transcript not found: $TRANSCRIPT_PATH" >&2
28
+ exit 1
29
+ fi
30
+
31
+ # Count total lines for context
32
+ TOTAL_LINES=$(wc -l < "$TRANSCRIPT_PATH")
33
+
34
+ if [ "$TOTAL_LINES" -eq 0 ]; then
35
+ echo "(empty transcript)"
36
+ exit 0
37
+ fi
38
+
39
+ # Helper: truncate text to last N chars
40
+ truncate_tail() {
41
+ local text="$1"
42
+ local max="$2"
43
+ local len=${#text}
44
+ if [ "$len" -le "$max" ]; then
45
+ printf '%s' "$text"
46
+ else
47
+ printf '...%s' "${text: -$max}"
48
+ fi
49
+ }
50
+
51
+ # Print header
52
+ if [ "$TOTAL_LINES" -gt "$MAX_LINES" ]; then
53
+ echo "=== Transcript (last $MAX_LINES of $TOTAL_LINES lines) ==="
54
+ else
55
+ echo "=== Transcript ($TOTAL_LINES lines) ==="
56
+ fi
57
+ echo ""
58
+
59
+ # Process JSONL — take the last MAX_LINES, parse with jq line by line
60
+ tail -n "$MAX_LINES" "$TRANSCRIPT_PATH" | while IFS= read -r line; do
61
+ # Extract type
62
+ entry_type=$(printf '%s' "$line" | jq -r '.type // empty' 2>/dev/null) || continue
63
+
64
+ # Skip file snapshots
65
+ [ "$entry_type" = "file-history-snapshot" ] && continue
66
+
67
+ # Extract timestamp
68
+ ts=$(printf '%s' "$line" | jq -r '.timestamp // empty' 2>/dev/null)
69
+ ts_short=""
70
+ if [ -n "$ts" ]; then
71
+ # Extract HH:MM:SS from ISO timestamp
72
+ ts_short=$(printf '%s' "$ts" | sed -n 's/.*T\([0-9][0-9]:[0-9][0-9]:[0-9][0-9]\).*/\1/p' 2>/dev/null || echo "")
73
+ fi
74
+
75
+ if [ "$entry_type" = "user" ]; then
76
+ # Check if it's a tool_result or a normal user message
77
+ content_type=$(printf '%s' "$line" | jq -r '.message.content | if type == "array" then .[0].type // "text" else "text" end' 2>/dev/null) || content_type="text"
78
+
79
+ if [ "$content_type" = "tool_result" ]; then
80
+ # Tool result — one-line preview
81
+ result_text=$(printf '%s' "$line" | jq -r '.message.content[0].content // "" | if type == "array" then .[0].text // "" else . end' 2>/dev/null)
82
+ result_short=$(truncate_tail "$result_text" "$MAX_CHARS")
83
+ echo "[${ts_short}] TOOL RESULT: ${result_short}"
84
+ else
85
+ # Normal user message
86
+ user_text=$(printf '%s' "$line" | jq -r '.message.content // "" | if type == "array" then map(select(.type == "text") | .text) | join("\n") else . end' 2>/dev/null)
87
+ user_short=$(truncate_tail "$user_text" "$MAX_CHARS")
88
+ echo ""
89
+ echo "[${ts_short}] USER: ${user_short}"
90
+ fi
91
+
92
+ elif [ "$entry_type" = "assistant" ]; then
93
+ # Process each content block
94
+ num_blocks=$(printf '%s' "$line" | jq -r '.message.content | length' 2>/dev/null) || num_blocks=0
95
+
96
+ for (( i=0; i<num_blocks; i++ )); do
97
+ block_type=$(printf '%s' "$line" | jq -r ".message.content[$i].type // empty" 2>/dev/null)
98
+
99
+ if [ "$block_type" = "text" ]; then
100
+ text=$(printf '%s' "$line" | jq -r ".message.content[$i].text // empty" 2>/dev/null)
101
+ [ -z "$text" ] && continue
102
+ text_short=$(truncate_tail "$text" "$MAX_CHARS")
103
+ echo "[${ts_short}] ASSISTANT: ${text_short}"
104
+
105
+ elif [ "$block_type" = "tool_use" ]; then
106
+ tool_name=$(printf '%s' "$line" | jq -r ".message.content[$i].name // \"unknown\"" 2>/dev/null)
107
+ # One-line summary of tool input
108
+ tool_input_summary=$(printf '%s' "$line" | jq -r ".message.content[$i].input | to_entries | map(\"\(.key)=\(.value | tostring | .[0:80])\") | join(\", \")" 2>/dev/null || echo "")
109
+ tool_input_short=$(truncate_tail "$tool_input_summary" 200)
110
+ echo "[${ts_short}] TOOL USE: ${tool_name}(${tool_input_short})"
111
+ fi
112
+ done
113
+ fi
114
+ done
115
+
116
+ echo ""
117
+ echo "=== End of transcript ==="
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # SessionEnd hook: stop the memsearch watch singleton.
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/common.sh"
6
+
7
+ stop_watch
8
+
9
+ exit 0
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ # SessionStart hook: start watch singleton + inject recent memory context.
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/common.sh"
6
+
7
+ # Bootstrap: if memsearch not available, install uv and warm up uvx cache
8
+ if [ -z "$MEMSEARCH_CMD" ]; then
9
+ if ! command -v uvx &>/dev/null; then
10
+ curl -LsSf https://astral.sh/uv/install.sh | sh 2>/dev/null
11
+ export PATH="$HOME/.local/bin:$PATH"
12
+ fi
13
+ # Warm up uvx cache with --upgrade to pull latest version
14
+ # First run downloads packages (~2s); subsequent runs use cache (<0.3s)
15
+ uvx --upgrade memsearch --version &>/dev/null || true
16
+ _detect_memsearch
17
+ fi
18
+
19
+ # Read resolved config and version for status display
20
+ PROVIDER="openai"; MODEL=""; MILVUS_URI=""; VERSION=""
21
+ if [ -n "$MEMSEARCH_CMD" ]; then
22
+ PROVIDER=$($MEMSEARCH_CMD config get embedding.provider 2>/dev/null || echo "openai")
23
+ MODEL=$($MEMSEARCH_CMD config get embedding.model 2>/dev/null || echo "")
24
+ MILVUS_URI=$($MEMSEARCH_CMD config get milvus.uri 2>/dev/null || echo "")
25
+ # "memsearch, version 0.1.10" → "0.1.10"
26
+ VERSION=$($MEMSEARCH_CMD --version 2>/dev/null | sed 's/.*version //' || echo "")
27
+ fi
28
+
29
+ # Determine required API key for the configured provider
30
+ _required_env_var() {
31
+ case "$1" in
32
+ openai) echo "OPENAI_API_KEY" ;;
33
+ google) echo "GOOGLE_API_KEY" ;;
34
+ voyage) echo "VOYAGE_API_KEY" ;;
35
+ *) echo "" ;; # ollama, local — no API key needed
36
+ esac
37
+ }
38
+ REQUIRED_KEY=$(_required_env_var "$PROVIDER")
39
+
40
+ KEY_MISSING=false
41
+ if [ -n "$REQUIRED_KEY" ] && [ -z "${!REQUIRED_KEY:-}" ]; then
42
+ KEY_MISSING=true
43
+ fi
44
+
45
+ # Check PyPI for newer version (2s timeout, non-blocking on failure)
46
+ UPDATE_HINT=""
47
+ if [ -n "$VERSION" ]; then
48
+ _PYPI_JSON=$(curl -s --max-time 2 https://pypi.org/pypi/memsearch/json 2>/dev/null || true)
49
+ LATEST=$(_json_val "$_PYPI_JSON" "info.version" "")
50
+ if [ -n "$LATEST" ] && [ "$LATEST" != "$VERSION" ]; then
51
+ UPDATE_HINT=" | UPDATE: v${LATEST} available"
52
+ fi
53
+ fi
54
+
55
+ # Build status line: version | provider/model | milvus | optional update/error
56
+ VERSION_TAG="${VERSION:+ v${VERSION}}"
57
+ COLLECTION_HINT=""
58
+ if [ -n "$COLLECTION_NAME" ]; then
59
+ COLLECTION_HINT=" | collection: ${COLLECTION_NAME}"
60
+ fi
61
+ status="[memsearch${VERSION_TAG}] embedding: ${PROVIDER}/${MODEL:-unknown} | milvus: ${MILVUS_URI:-unknown}${COLLECTION_HINT}${UPDATE_HINT}"
62
+ if [ "$KEY_MISSING" = true ]; then
63
+ status+=" | ERROR: ${REQUIRED_KEY} not set — memory search disabled"
64
+ fi
65
+
66
+ # Write session heading to today's memory file
67
+ ensure_memory_dir
68
+ TODAY=$(date +%Y-%m-%d)
69
+ NOW=$(date +%H:%M)
70
+ MEMORY_FILE="$MEMORY_DIR/$TODAY.md"
71
+ echo -e "\n## Session $NOW\n" >> "$MEMORY_FILE"
72
+
73
+ # If API key is missing, show status and exit early (watch/search would fail)
74
+ if [ "$KEY_MISSING" = true ]; then
75
+ json_status=$(_json_encode_str "$status")
76
+ echo "{\"systemMessage\": $json_status}"
77
+ exit 0
78
+ fi
79
+
80
+ # Start memsearch watch as a singleton background process.
81
+ # This is the ONLY place indexing is managed — all other hooks just write .md files.
82
+ start_watch
83
+
84
+ # Always include status in systemMessage
85
+ json_status=$(_json_encode_str "$status")
86
+
87
+ # If memory dir has no .md files (other than the one we just created), nothing to inject
88
+ if [ ! -d "$MEMORY_DIR" ] || ! ls "$MEMORY_DIR"/*.md &>/dev/null; then
89
+ echo "{\"systemMessage\": $json_status}"
90
+ exit 0
91
+ fi
92
+
93
+ context=""
94
+
95
+ # Find the 2 most recent daily log files (sorted by filename descending)
96
+ recent_files=$(ls -1 "$MEMORY_DIR"/*.md 2>/dev/null | sort -r | head -2)
97
+
98
+ if [ -n "$recent_files" ]; then
99
+ context="# Recent Memory\n\n"
100
+ for f in $recent_files; do
101
+ basename_f=$(basename "$f")
102
+ # Read last ~30 lines from each file
103
+ content=$(tail -30 "$f" 2>/dev/null || true)
104
+ if [ -n "$content" ]; then
105
+ context+="## $basename_f\n$content\n\n"
106
+ fi
107
+ done
108
+ fi
109
+
110
+ # Note: Detailed memory search is handled by the memory-recall skill (pull-based).
111
+ # The cold-start context above gives Claude enough awareness of recent sessions
112
+ # to decide when to invoke the skill for deeper recall.
113
+
114
+ if [ -n "$context" ]; then
115
+ json_context=$(_json_encode_str "$context")
116
+ echo "{\"systemMessage\": $json_status, \"hookSpecificOutput\": {\"hookEventName\": \"SessionStart\", \"additionalContext\": $json_context}}"
117
+ else
118
+ echo "{\"systemMessage\": $json_status}"
119
+ fi
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook: parse transcript, summarize with claude -p, and save to memory.
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/common.sh"
6
+
7
+ # Prevent infinite loop: if this Stop was triggered by a previous Stop hook, bail out
8
+ STOP_HOOK_ACTIVE=$(_json_val "$INPUT" "stop_hook_active" "false")
9
+ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
10
+ echo '{}'
11
+ exit 0
12
+ fi
13
+
14
+ # Skip summarization when the required API key is missing — embedding/search
15
+ # would fail, and the session likely only contains the "key not set" warning.
16
+ _required_env_var() {
17
+ case "$1" in
18
+ openai) echo "OPENAI_API_KEY" ;;
19
+ google) echo "GOOGLE_API_KEY" ;;
20
+ voyage) echo "VOYAGE_API_KEY" ;;
21
+ *) echo "" ;; # ollama, local — no API key needed
22
+ esac
23
+ }
24
+ _PROVIDER=$($MEMSEARCH_CMD config get embedding.provider 2>/dev/null || echo "openai")
25
+ _REQ_KEY=$(_required_env_var "$_PROVIDER")
26
+ if [ -n "$_REQ_KEY" ] && [ -z "${!_REQ_KEY:-}" ]; then
27
+ echo '{}'
28
+ exit 0
29
+ fi
30
+
31
+ # Extract transcript path from hook input
32
+ TRANSCRIPT_PATH=$(_json_val "$INPUT" "transcript_path" "")
33
+
34
+ if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
35
+ echo '{}'
36
+ exit 0
37
+ fi
38
+
39
+ # Check if transcript is empty (< 3 lines = no real content)
40
+ LINE_COUNT=$(wc -l < "$TRANSCRIPT_PATH" 2>/dev/null || echo "0")
41
+ if [ "$LINE_COUNT" -lt 3 ]; then
42
+ echo '{}'
43
+ exit 0
44
+ fi
45
+
46
+ ensure_memory_dir
47
+
48
+ # Parse transcript into concise text
49
+ PARSED=$("$SCRIPT_DIR/parse-transcript.sh" "$TRANSCRIPT_PATH" 2>/dev/null || true)
50
+
51
+ if [ -z "$PARSED" ] || [ "$PARSED" = "(empty transcript)" ]; then
52
+ echo '{}'
53
+ exit 0
54
+ fi
55
+
56
+ # Determine today's date and current time
57
+ TODAY=$(date +%Y-%m-%d)
58
+ NOW=$(date +%H:%M)
59
+ MEMORY_FILE="$MEMORY_DIR/$TODAY.md"
60
+
61
+ # Extract session ID and last user turn UUID for progressive disclosure anchors
62
+ SESSION_ID=$(basename "$TRANSCRIPT_PATH" .jsonl)
63
+ LAST_USER_TURN_UUID=$(python3 -c "
64
+ import json, sys
65
+ uuid = ''
66
+ with open(sys.argv[1]) as f:
67
+ for line in f:
68
+ try:
69
+ obj = json.loads(line)
70
+ if obj.get('type') == 'user' and isinstance(obj.get('message', {}).get('content'), str):
71
+ uuid = obj.get('uuid', '')
72
+ except: pass
73
+ print(uuid)
74
+ " "$TRANSCRIPT_PATH" 2>/dev/null || true)
75
+
76
+ # Use claude -p to summarize the parsed transcript into concise bullet points.
77
+ # --model haiku: cheap and fast model for summarization
78
+ # --no-session-persistence: don't save this throwaway session to disk
79
+ # --no-chrome: skip browser integration
80
+ # --system-prompt: separate role instructions from data (transcript via stdin)
81
+ SUMMARY=""
82
+ if command -v claude &>/dev/null; then
83
+ SUMMARY=$(printf '%s' "$PARSED" | claude -p \
84
+ --model haiku \
85
+ --no-session-persistence \
86
+ --no-chrome \
87
+ --system-prompt "You are a session memory writer. Your ONLY job is to output bullet-point summaries. Output NOTHING else — no greetings, no questions, no offers to help, no preamble, no closing remarks.
88
+
89
+ Rules:
90
+ - Output 3-8 bullet points, each starting with '- '
91
+ - Focus on: decisions made, problems solved, code changes, key findings
92
+ - Be specific and factual — mention file names, function names, and concrete details
93
+ - Do NOT include timestamps, headers, or any formatting beyond bullet points
94
+ - Do NOT add any text before or after the bullet points" \
95
+ 2>/dev/null || true)
96
+ fi
97
+
98
+ # If claude is not available or returned empty, fall back to raw parsed output
99
+ if [ -z "$SUMMARY" ]; then
100
+ SUMMARY="$PARSED"
101
+ fi
102
+
103
+ # Append as a sub-heading under the session heading written by SessionStart
104
+ # Include HTML comment anchor for progressive disclosure (L3 transcript lookup)
105
+ {
106
+ echo "### $NOW"
107
+ if [ -n "$SESSION_ID" ]; then
108
+ echo "<!-- session:${SESSION_ID} turn:${LAST_USER_TURN_UUID} transcript:${TRANSCRIPT_PATH} -->"
109
+ fi
110
+ echo "$SUMMARY"
111
+ echo ""
112
+ } >> "$MEMORY_FILE"
113
+
114
+ # Index immediately — don't rely on watch (which may be killed by SessionEnd before debounce fires)
115
+ run_memsearch index "$MEMORY_DIR"
116
+
117
+ echo '{}'
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # UserPromptSubmit hook: lightweight hint reminding Claude about the memory-recall skill.
3
+ # The actual search + expand is handled by the memory-recall skill (pull-based, context: fork).
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ source "$SCRIPT_DIR/common.sh"
7
+
8
+ # Skip short prompts (greetings, single words, etc.)
9
+ PROMPT=$(_json_val "$INPUT" "prompt" "")
10
+ if [ -z "$PROMPT" ] || [ "${#PROMPT}" -lt 10 ]; then
11
+ echo '{}'
12
+ exit 0
13
+ fi
14
+
15
+ # Need memsearch available
16
+ if [ -z "$MEMSEARCH_CMD" ]; then
17
+ echo '{}'
18
+ exit 0
19
+ fi
20
+
21
+ echo '{"systemMessage": "[memsearch] Memory available"}'
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # Derive a unique Milvus collection name from a project directory path.
3
+ # Used by hooks (via common.sh) and skill (via SKILL.md ! syntax).
4
+ #
5
+ # Usage: derive-collection.sh [project_dir]
6
+ # If no argument given, uses pwd.
7
+ #
8
+ # Output: ms_<sanitized_basename>_<8char_sha256>
9
+ # e.g. /home/user/my-app → ms_my_app_a1b2c3d4
10
+
11
+ set -euo pipefail
12
+
13
+ PROJECT_DIR="${1:-$(pwd)}"
14
+
15
+ # Resolve to absolute path (realpath preferred, cd fallback, raw last resort)
16
+ if command -v realpath &>/dev/null; then
17
+ PROJECT_DIR="$(realpath -m "$PROJECT_DIR")"
18
+ elif [ -d "$PROJECT_DIR" ]; then
19
+ PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
20
+ else
21
+ # If directory doesn't exist and no realpath, ensure it starts with /
22
+ case "$PROJECT_DIR" in
23
+ /*) ;; # already absolute
24
+ *) PROJECT_DIR="$(pwd)/$PROJECT_DIR" ;;
25
+ esac
26
+ fi
27
+
28
+ # Extract basename and sanitize:
29
+ # - lowercase
30
+ # - replace non-alphanumeric with underscore
31
+ # - collapse consecutive underscores
32
+ # - trim leading/trailing underscores
33
+ # - truncate to 40 chars
34
+ sanitized=$(basename "$PROJECT_DIR" \
35
+ | tr '[:upper:]' '[:lower:]' \
36
+ | sed 's/[^a-z0-9]/_/g' \
37
+ | sed 's/__*/_/g' \
38
+ | sed 's/^_//;s/_$//' \
39
+ | cut -c1-40)
40
+
41
+ # Compute 8-char SHA-256 hash of the full absolute path
42
+ if command -v sha256sum &>/dev/null; then
43
+ hash=$(printf '%s' "$PROJECT_DIR" | sha256sum | cut -c1-8)
44
+ elif command -v shasum &>/dev/null; then
45
+ hash=$(printf '%s' "$PROJECT_DIR" | shasum -a 256 | cut -c1-8)
46
+ else
47
+ hash=$(python3 -c "import hashlib,sys; print(hashlib.sha256(sys.argv[1].encode()).hexdigest()[:8])" "$PROJECT_DIR")
48
+ fi
49
+
50
+ echo "ms_${sanitized}_${hash}"
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: memory-recall
3
+ description: "Search and recall relevant memories from past sessions. Use when the user's question could benefit from historical context, past decisions, debugging notes, previous conversations, or project knowledge. Also use when you see '[memsearch] Memory available' hints."
4
+ context: fork
5
+ allowed-tools: Bash
6
+ ---
7
+
8
+ You are a memory retrieval agent for memsearch. Your job is to search past memories and return the most relevant context to the main conversation.
9
+
10
+ ## Project Collection
11
+
12
+ Collection: !`bash ${CLAUDE_PLUGIN_ROOT}/scripts/derive-collection.sh`
13
+
14
+ ## Your Task
15
+
16
+ Search for memories relevant to: $ARGUMENTS
17
+
18
+ ## Steps
19
+
20
+ 1. **Search**: Run `memsearch search "<query>" --top-k 5 --json-output --collection <collection name above>` to find relevant chunks.
21
+ - If `memsearch` is not found, try `uvx memsearch` instead.
22
+ - Choose a search query that captures the core intent of the user's question.
23
+
24
+ 2. **Evaluate**: Look at the search results. Skip chunks that are clearly irrelevant or too generic.
25
+
26
+ 3. **Expand**: For each relevant result, run `memsearch expand <chunk_hash> --collection <collection name above>` to get the full markdown section with surrounding context.
27
+
28
+ 4. **Deep drill (optional)**: If an expanded chunk contains transcript anchors (JSONL path + turn UUID), and the original conversation seems critical, run:
29
+ ```
30
+ memsearch transcript <jsonl_path> --turn <uuid> --context 3
31
+ ```
32
+ to retrieve the original conversation turns.
33
+
34
+ 5. **Return results**: Output a curated summary of the most relevant memories. Be concise — only include information that is genuinely useful for the user's current question.
35
+
36
+ ## Output Format
37
+
38
+ Organize by relevance. For each memory include:
39
+ - The key information (decisions, patterns, solutions, context)
40
+ - Source reference (file name, date) for traceability
41
+
42
+ If nothing relevant is found, simply say "No relevant memories found."