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.
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -0
- package/dist/agent.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +55 -0
- package/dist/auth.js.map +1 -1
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +26 -2
- package/dist/dashboard.js.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/pty-manager.d.ts +19 -0
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +111 -0
- package/dist/pty-manager.js.map +1 -1
- package/dist/setup.d.ts +15 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +603 -0
- package/dist/setup.js.map +1 -0
- package/hooks/termify-response.js +151 -124
- package/hooks/termify-sync.js +165 -116
- package/mcp/memsearch-mcp-server.mjs +149 -0
- package/package.json +3 -2
- package/plugins/context7/.claude-plugin/plugin.json +7 -0
- package/plugins/context7/.mcp.json +6 -0
- package/plugins/memsearch/.claude-plugin/plugin.json +5 -0
- package/plugins/memsearch/README.md +762 -0
- package/plugins/memsearch/hooks/common.sh +151 -0
- package/plugins/memsearch/hooks/hooks.json +50 -0
- package/plugins/memsearch/hooks/parse-transcript.sh +117 -0
- package/plugins/memsearch/hooks/session-end.sh +9 -0
- package/plugins/memsearch/hooks/session-start.sh +119 -0
- package/plugins/memsearch/hooks/stop.sh +117 -0
- package/plugins/memsearch/hooks/user-prompt-submit.sh +21 -0
- package/plugins/memsearch/scripts/derive-collection.sh +50 -0
- package/plugins/memsearch/skills/memory-recall/SKILL.md +42 -0
- 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,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."
|