@hir4ta/mneme 0.17.0

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 (43) hide show
  1. package/.claude-plugin/plugin.json +29 -0
  2. package/.mcp.json +18 -0
  3. package/README.ja.md +400 -0
  4. package/README.md +410 -0
  5. package/bin/mneme.js +203 -0
  6. package/dist/lib/db.js +340 -0
  7. package/dist/lib/fuzzy-search.js +214 -0
  8. package/dist/lib/github.js +121 -0
  9. package/dist/lib/similarity.js +193 -0
  10. package/dist/lib/utils.js +62 -0
  11. package/dist/public/apple-touch-icon.png +0 -0
  12. package/dist/public/assets/index-BgqCALAg.css +1 -0
  13. package/dist/public/assets/index-EMvn4VEa.js +330 -0
  14. package/dist/public/assets/react-force-graph-2d-DWoBaKmT.js +46 -0
  15. package/dist/public/favicon-128-max.png +0 -0
  16. package/dist/public/favicon-256-max.png +0 -0
  17. package/dist/public/favicon-32-max.png +0 -0
  18. package/dist/public/favicon-512-max.png +0 -0
  19. package/dist/public/favicon-64-max.png +0 -0
  20. package/dist/public/index.html +15 -0
  21. package/dist/server.js +4791 -0
  22. package/dist/servers/db-server.js +30558 -0
  23. package/dist/servers/search-server.js +30366 -0
  24. package/hooks/default-tags.json +1055 -0
  25. package/hooks/hooks.json +61 -0
  26. package/hooks/post-tool-use.sh +96 -0
  27. package/hooks/pre-compact.sh +187 -0
  28. package/hooks/session-end.sh +567 -0
  29. package/hooks/session-start.sh +380 -0
  30. package/hooks/user-prompt-submit.sh +253 -0
  31. package/package.json +77 -0
  32. package/servers/db-server.ts +993 -0
  33. package/servers/search-server.ts +675 -0
  34. package/skills/AGENTS.override.md +5 -0
  35. package/skills/harvest/skill.md +295 -0
  36. package/skills/init-mneme/skill.md +101 -0
  37. package/skills/plan/skill.md +422 -0
  38. package/skills/report/skill.md +74 -0
  39. package/skills/resume/skill.md +278 -0
  40. package/skills/review/skill.md +419 -0
  41. package/skills/save/skill.md +482 -0
  42. package/skills/search/skill.md +175 -0
  43. package/skills/using-mneme/skill.md +185 -0
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # session-start.sh - SessionStart hook for mneme plugin
4
+ #
5
+ # Purpose: Initialize session JSON and inject context via additionalContext
6
+ #
7
+ # Input (stdin): JSON with session_id, cwd, trigger (startup|resume|clear|compact)
8
+ # Output (stdout): JSON with hookSpecificOutput.additionalContext
9
+ # Exit codes: 0 = success (continue session)
10
+ #
11
+ # Dependencies: jq
12
+ #
13
+
14
+ set -euo pipefail
15
+
16
+ # Read input from stdin
17
+ input_json=$(cat)
18
+
19
+ # Check for jq (required dependency)
20
+ if ! command -v jq &> /dev/null; then
21
+ echo "[mneme] Warning: jq not found. Install with: brew install jq" >&2
22
+ echo "[mneme] Session tracking disabled for this session." >&2
23
+ exit 0 # Non-blocking - allow session to continue without mneme
24
+ fi
25
+
26
+ cwd=$(echo "$input_json" | jq -r '.cwd // empty' 2>/dev/null || echo "")
27
+ session_id=$(echo "$input_json" | jq -r '.session_id // empty' 2>/dev/null || echo "")
28
+
29
+ # If no cwd, use PWD
30
+ if [ -z "$cwd" ]; then
31
+ cwd="${PWD}"
32
+ fi
33
+
34
+ # Resolve cwd to absolute path
35
+ cwd=$(cd "$cwd" 2>/dev/null && pwd || echo "$cwd")
36
+
37
+ # Determine plugin root directory
38
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
39
+
40
+ # Find .mneme directory
41
+ mneme_dir="${cwd}/.mneme"
42
+ sessions_dir="${mneme_dir}/sessions"
43
+ rules_dir="${mneme_dir}/rules"
44
+ patterns_dir="${mneme_dir}/patterns"
45
+ session_links_dir="${mneme_dir}/session-links"
46
+
47
+ # Check if mneme is initialized
48
+ if [ ! -d "$mneme_dir" ]; then
49
+ echo "[mneme] Not initialized in this project. Run: npx @hir4ta/mneme --init" >&2
50
+ exit 0
51
+ fi
52
+
53
+ # Current timestamp and date parts
54
+ now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
55
+ date_part=$(echo "$now" | cut -d'T' -f1)
56
+ year_part=$(echo "$date_part" | cut -d'-' -f1)
57
+ month_part=$(echo "$date_part" | cut -d'-' -f2)
58
+
59
+ # Generate session ID based on session_id (not date)
60
+ if [ -n "$session_id" ]; then
61
+ session_short_id="${session_id:0:8}"
62
+ else
63
+ session_short_id=$(uuidgen 2>/dev/null | tr '[:upper:]' '[:lower:]' | cut -c1-8 || date +%s | md5sum | cut -c1-8)
64
+ fi
65
+
66
+ # file_id is now just the session_short_id (no date prefix)
67
+ file_id="${session_short_id}"
68
+
69
+ # Get git info
70
+ current_branch=""
71
+ git_user_name="unknown"
72
+ git_user_email=""
73
+
74
+ if git -C "$cwd" rev-parse --git-dir &> /dev/null 2>&1; then
75
+ current_branch=$(git -C "$cwd" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
76
+ git_user_name=$(git -C "$cwd" config user.name 2>/dev/null || echo "unknown")
77
+ git_user_email=$(git -C "$cwd" config user.email 2>/dev/null || echo "")
78
+ fi
79
+
80
+ # Get project name from directory
81
+ project_name=$(basename "$cwd")
82
+
83
+ # Get repository name from git remote origin
84
+ repository=""
85
+ if git -C "$cwd" rev-parse --git-dir &> /dev/null 2>&1; then
86
+ git_remote_url=$(git -C "$cwd" remote get-url origin 2>/dev/null || echo "")
87
+ if [ -n "$git_remote_url" ]; then
88
+ # Extract user/repo from SSH or HTTPS URL
89
+ # git@github.com:user/repo.git → user/repo
90
+ # https://github.com/user/repo.git → user/repo
91
+ # Extract user/repo from SSH or HTTPS URL (BSD sed compatible)
92
+ repository=$(echo "$git_remote_url" | sed -E 's|.*[:/]([^/]+/[^/]+)(\.git)?$|\1|' | sed 's/\.git$//')
93
+ fi
94
+ fi
95
+
96
+ # ============================================
97
+ # Check session-links for master session
98
+ # ============================================
99
+ master_session_id=""
100
+ master_session_path=""
101
+ session_link_file="${session_links_dir}/${file_id}.json"
102
+
103
+ if [ -f "$session_link_file" ]; then
104
+ master_session_id=$(jq -r '.masterSessionId // empty' "$session_link_file" 2>/dev/null || echo "")
105
+ if [ -n "$master_session_id" ]; then
106
+ # Find master session file
107
+ master_session_path=$(find "$sessions_dir" -name "${master_session_id}.json" -type f 2>/dev/null | head -1)
108
+ echo "[mneme] Session linked to master: ${master_session_id}" >&2
109
+ fi
110
+ fi
111
+
112
+ # ============================================
113
+ # Find existing session file or create new one
114
+ # ============================================
115
+ session_path=""
116
+ is_resumed=false
117
+
118
+ # Search for existing session file across all year/month directories
119
+ if [ -d "$sessions_dir" ]; then
120
+ existing_file=$(find "$sessions_dir" -name "${file_id}.json" -type f 2>/dev/null | head -1)
121
+ if [ -n "$existing_file" ] && [ -f "$existing_file" ]; then
122
+ session_path="$existing_file"
123
+ is_resumed=true
124
+ fi
125
+ fi
126
+
127
+ # If no existing file, create in current year/month directory
128
+ if [ -z "$session_path" ]; then
129
+ session_year_month_dir="${sessions_dir}/${year_part}/${month_part}"
130
+ mkdir -p "$session_year_month_dir"
131
+ session_path="${session_year_month_dir}/${file_id}.json"
132
+ fi
133
+
134
+ # ============================================
135
+ # Find recent sessions for auto-suggestion (latest 3)
136
+ # ============================================
137
+ recent_sessions_info=""
138
+ if [ -d "$sessions_dir" ] && [ "$is_resumed" = false ]; then
139
+ # Get all sessions sorted by createdAt descending, excluding current
140
+ recent_sessions=$(find "$sessions_dir" -name "*.json" -type f 2>/dev/null | while read -r f; do
141
+ if [ -f "$f" ]; then
142
+ session_data=$(jq -r '[.id, .createdAt, .title // "", .context.branch // ""] | @tsv' "$f" 2>/dev/null || echo "")
143
+ if [ -n "$session_data" ]; then
144
+ echo "$session_data"
145
+ fi
146
+ fi
147
+ done | sort -t$'\t' -k2 -r | head -4) # Get 4 to filter out current
148
+
149
+ # Build recent sessions list (excluding current session)
150
+ count=0
151
+ while IFS=$'\t' read -r sid screated stitle sbranch; do
152
+ if [ -n "$sid" ] && [ "$sid" != "$file_id" ] && [ $count -lt 3 ]; then
153
+ count=$((count + 1))
154
+ # Format: [id] title (date, branch)
155
+ date_part_session=$(echo "$screated" | cut -d'T' -f1 2>/dev/null || echo "")
156
+ title_display="${stitle:-no title}"
157
+ branch_display="${sbranch:-no branch}"
158
+ recent_sessions_info="${recent_sessions_info} ${count}. [${sid}] ${title_display} (${date_part_session}, ${branch_display})\n"
159
+ fi
160
+ done <<< "$recent_sessions"
161
+ fi
162
+
163
+ # ============================================
164
+ # Initialize or update session JSON
165
+ # ============================================
166
+ if [ "$is_resumed" = true ]; then
167
+ # Resume: reset status to null for re-processing at SessionEnd
168
+ jq --arg resumedAt "$now" '.status = null | .resumedAt = $resumedAt' "$session_path" > "${session_path}.tmp" \
169
+ && mv "${session_path}.tmp" "$session_path"
170
+ echo "[mneme] Session resumed (status reset): ${session_path}" >&2
171
+ else
172
+ # New session: create initial JSON (log-focused schema)
173
+ # Note: summary, discussions, errors, handoff are set by /mneme:save
174
+ session_json=$(jq -n \
175
+ --arg id "$file_id" \
176
+ --arg sessionId "${session_id:-$session_short_id}" \
177
+ --arg createdAt "$now" \
178
+ --arg branch "$current_branch" \
179
+ --arg projectDir "$cwd" \
180
+ --arg projectName "$project_name" \
181
+ --arg repository "$repository" \
182
+ --arg userName "$git_user_name" \
183
+ --arg userEmail "$git_user_email" \
184
+ '{
185
+ id: $id,
186
+ sessionId: $sessionId,
187
+ createdAt: $createdAt,
188
+ title: "",
189
+ tags: [],
190
+ context: {
191
+ branch: (if $branch == "" then null else $branch end),
192
+ projectDir: $projectDir,
193
+ projectName: $projectName,
194
+ repository: (if $repository == "" then null else $repository end),
195
+ user: {
196
+ name: $userName,
197
+ email: (if $userEmail == "" then null else $userEmail end)
198
+ } | with_entries(select(.value != null))
199
+ } | with_entries(select(.value != null)),
200
+ metrics: {
201
+ userMessages: 0,
202
+ assistantResponses: 0,
203
+ thinkingBlocks: 0,
204
+ toolUsage: []
205
+ },
206
+ files: [],
207
+ status: null
208
+ }')
209
+
210
+ echo "$session_json" > "$session_path"
211
+ echo "[mneme] Session initialized: ${session_path}" >&2
212
+ fi
213
+
214
+ # ============================================
215
+ # Update master session workPeriods (if linked)
216
+ # ============================================
217
+ if [ -n "$master_session_id" ] && [ -n "$master_session_path" ] && [ -f "$master_session_path" ]; then
218
+ # Use full session_id for consistency with session-end.sh
219
+ claude_session_id="${session_id:-$session_short_id}"
220
+
221
+ # Check if workPeriod already exists for this claudeSessionId (prevent duplicates on clear/compact)
222
+ existing_period=$(jq --arg cid "$claude_session_id" '.workPeriods // [] | map(select(.claudeSessionId == $cid and .endedAt == null)) | length' "$master_session_path" 2>/dev/null || echo "0")
223
+
224
+ if [ "$existing_period" = "0" ]; then
225
+ # Add new workPeriod entry to master session
226
+ jq --arg claudeSessionId "$claude_session_id" \
227
+ --arg startedAt "$now" '
228
+ .workPeriods = ((.workPeriods // []) + [{
229
+ claudeSessionId: $claudeSessionId,
230
+ startedAt: $startedAt,
231
+ endedAt: null
232
+ }]) |
233
+ .updatedAt = $startedAt
234
+ ' "$master_session_path" > "${master_session_path}.tmp" \
235
+ && mv "${master_session_path}.tmp" "$master_session_path"
236
+ echo "[mneme] Master session workPeriods updated: ${master_session_path}" >&2
237
+ else
238
+ echo "[mneme] Master session workPeriod already exists for this Claude session" >&2
239
+ fi
240
+ fi
241
+
242
+ # Get relative path for additionalContext
243
+ # Extract year/month from session_path
244
+ session_relative_path="${session_path#$cwd/}"
245
+
246
+ # ============================================
247
+ # Initialize tags.json if not exists
248
+ # ============================================
249
+ tags_path="${mneme_dir}/tags.json"
250
+ default_tags_path="${SCRIPT_DIR}/default-tags.json"
251
+
252
+ if [ ! -f "$tags_path" ]; then
253
+ if [ -f "$default_tags_path" ]; then
254
+ cp "$default_tags_path" "$tags_path"
255
+ echo "[mneme] Tags master file created: ${tags_path}" >&2
256
+ fi
257
+ fi
258
+
259
+ # ============================================
260
+ # Initialize local database
261
+ # ============================================
262
+ local_db_path="${mneme_dir}/local.db"
263
+ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
264
+ schema_path="${PLUGIN_ROOT}/lib/schema.sql"
265
+
266
+ # Initialize local database if not exists
267
+ if [ ! -f "$local_db_path" ]; then
268
+ if [ -f "$schema_path" ]; then
269
+ sqlite3 "$local_db_path" < "$schema_path"
270
+ echo "[mneme] Local database initialized: ${local_db_path}" >&2
271
+ fi
272
+ fi
273
+ # Configure pragmas
274
+ sqlite3 "$local_db_path" "PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;" 2>/dev/null || true
275
+
276
+ # ============================================
277
+ # Ensure rules templates exist
278
+ # ============================================
279
+ rules_timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
280
+
281
+ init_rules_file() {
282
+ local path="$1"
283
+ if [ ! -f "$path" ]; then
284
+ cat <<RULEEOF > "$path"
285
+ {
286
+ "schemaVersion": 1,
287
+ "createdAt": "${rules_timestamp}",
288
+ "updatedAt": "${rules_timestamp}",
289
+ "items": []
290
+ }
291
+ RULEEOF
292
+ fi
293
+ }
294
+
295
+ init_rules_file "${rules_dir}/review-guidelines.json"
296
+ init_rules_file "${rules_dir}/dev-rules.json"
297
+
298
+ # ============================================
299
+ # Build additionalContext (superpowers style)
300
+ # ============================================
301
+ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
302
+
303
+ # Read using-mneme skill content
304
+ using_mneme_content=$(cat "${PLUGIN_ROOT}/skills/using-mneme/skill.md" 2>/dev/null || echo "")
305
+
306
+ # Escape for JSON using pure bash (superpowers style)
307
+ escape_for_json() {
308
+ local input="$1"
309
+ local output=""
310
+ local i char
311
+ for (( i=0; i<${#input}; i++ )); do
312
+ char="${input:$i:1}"
313
+ case "$char" in
314
+ $'\\') output+='\\' ;;
315
+ '"') output+='\"' ;;
316
+ $'\n') output+='\n' ;;
317
+ $'\r') output+='\r' ;;
318
+ $'\t') output+='\t' ;;
319
+ *) output+="$char" ;;
320
+ esac
321
+ done
322
+ printf '%s' "$output"
323
+ }
324
+
325
+ using_mneme_escaped=$(escape_for_json "$using_mneme_content")
326
+
327
+ resume_note=""
328
+ needs_summary=false
329
+ if [ "$is_resumed" = true ]; then
330
+ resume_note=" (Resumed)"
331
+ # Check if title is empty
332
+ session_title=$(jq -r '.title // ""' "$session_path" 2>/dev/null || echo "")
333
+ if [ -z "$session_title" ]; then
334
+ needs_summary=true
335
+ fi
336
+ fi
337
+
338
+ # Build the session info (no auto-save instruction)
339
+ session_info="**Session:** ${file_id}${resume_note}
340
+ **Path:** ${session_relative_path}
341
+
342
+ Sessions are saved:
343
+ - **Automatically** before Auto-Compact (context 95% full)
344
+ - **Manually** via \`/mneme:save\` or asking \"save the session\""
345
+
346
+ # Add recent sessions suggestion for new sessions
347
+ if [ "$is_resumed" = false ] && [ -n "$recent_sessions_info" ]; then
348
+ session_info="${session_info}
349
+
350
+ ---
351
+ **Recent sessions:**
352
+ $(echo -e "$recent_sessions_info")
353
+ Continue from a previous session? Use \`/mneme:resume <id>\` or \`/mneme:resume\` to see more."
354
+ fi
355
+
356
+ # Add summary creation prompt if needed (for resumed sessions)
357
+ if [ "$needs_summary" = true ]; then
358
+ session_info="${session_info}
359
+
360
+ ---
361
+ **Note:** This session was resumed but has no summary yet.
362
+ When you have enough context, consider creating a summary with \`/mneme:save\` to capture:
363
+ - What was accomplished in the previous session
364
+ - Key decisions made
365
+ - Any ongoing work or next steps"
366
+ fi
367
+
368
+ session_info_escaped=$(escape_for_json "$session_info")
369
+
370
+ # Output context injection as JSON (superpowers style)
371
+ cat <<EOF
372
+ {
373
+ "hookSpecificOutput": {
374
+ "hookEventName": "SessionStart",
375
+ "additionalContext": "${session_info_escaped}\n\n${using_mneme_escaped}"
376
+ }
377
+ }
378
+ EOF
379
+
380
+ exit 0
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # user-prompt-submit.sh - UserPromptSubmit hook for mneme plugin
4
+ #
5
+ # Purpose: Search mneme for relevant context and inject as additionalContext
6
+ #
7
+ # Input (stdin): JSON with prompt, cwd
8
+ # Output (stdout): JSON with hookSpecificOutput.additionalContext (if matches found)
9
+ # Exit codes: 0 = success (continue with optional context)
10
+ #
11
+ # Dependencies: jq
12
+ #
13
+
14
+ set -euo pipefail
15
+
16
+ # Read input from stdin
17
+ input_json=$(cat)
18
+
19
+ # Check for jq (required dependency)
20
+ if ! command -v jq &> /dev/null; then
21
+ echo "[mneme] Warning: jq not found, memory search skipped." >&2
22
+ exit 0 # Non-blocking - continue without memory search
23
+ fi
24
+
25
+ # Extract prompt and cwd
26
+ prompt=$(echo "$input_json" | jq -r '.prompt // empty' 2>/dev/null || echo "")
27
+ cwd=$(echo "$input_json" | jq -r '.cwd // empty' 2>/dev/null || echo "")
28
+
29
+ # Exit if no prompt or short prompt (less than 10 chars)
30
+ if [ -z "$prompt" ] || [ ${#prompt} -lt 10 ]; then
31
+ exit 0
32
+ fi
33
+
34
+ # Use PWD if cwd is empty
35
+ if [ -z "$cwd" ]; then
36
+ cwd="${PWD}"
37
+ fi
38
+
39
+ # Define mneme directory
40
+ mneme_dir="${cwd}/.mneme"
41
+
42
+ # Local database path
43
+ local_db_path="${mneme_dir}/local.db"
44
+
45
+ # Exit if no .mneme directory
46
+ if [ ! -d "$mneme_dir" ]; then
47
+ exit 0
48
+ fi
49
+
50
+ # Skip if prompt starts with /mneme (user is explicitly using mneme)
51
+ if [[ "$prompt" == /mneme* ]]; then
52
+ exit 0
53
+ fi
54
+
55
+ # Extract keywords from prompt (first 5 significant words, excluding common words)
56
+ # Simple extraction: take words longer than 3 chars, limit to 5
57
+ raw_keywords=$(echo "$prompt" | tr '[:upper:]' '[:lower:]' | \
58
+ tr -cs '[:alnum:]' '\n' | \
59
+ awk 'length > 3' | \
60
+ grep -vE '^(this|that|with|from|have|will|would|could|should|what|when|where|which|there|their|them|they|been|being|were|does|done|make|just|only|also|into|over|such|than|then|some|these|those|very|after|before|about|through)$' | \
61
+ head -5)
62
+
63
+ # Exit if no keywords extracted
64
+ if [ -z "$raw_keywords" ]; then
65
+ exit 0
66
+ fi
67
+
68
+ # Expand keywords using tag aliases from tags.json (fuzzy-search style)
69
+ tags_path="${mneme_dir}/tags.json"
70
+ expanded_keywords="$raw_keywords"
71
+
72
+ if [ -f "$tags_path" ]; then
73
+ # For each keyword, check if it matches any tag id, label, or alias
74
+ # If matched, expand to include all related terms
75
+ while IFS= read -r kw; do
76
+ [ -z "$kw" ] && continue
77
+ # Search for matching tag and get all related terms
78
+ related_terms=$(jq -r --arg kw "$kw" '
79
+ .tags[]? |
80
+ select(
81
+ (.id | ascii_downcase) == $kw or
82
+ (.label | ascii_downcase) == $kw or
83
+ (.aliases[]? | ascii_downcase) == $kw
84
+ ) |
85
+ [.id, .label] + .aliases | .[] | ascii_downcase
86
+ ' "$tags_path" 2>/dev/null | sort -u)
87
+
88
+ if [ -n "$related_terms" ]; then
89
+ # Add related terms to expanded keywords
90
+ expanded_keywords="${expanded_keywords}
91
+ ${related_terms}"
92
+ fi
93
+ done <<< "$raw_keywords"
94
+ fi
95
+
96
+ # Remove duplicates and build grep pattern
97
+ keywords=$(echo "$expanded_keywords" | sort -u | tr '\n' '|' | sed 's/|$//')
98
+
99
+ # Exit if no keywords
100
+ if [ -z "$keywords" ]; then
101
+ exit 0
102
+ fi
103
+
104
+ # Search local SQLite database for interactions
105
+ search_local_db() {
106
+ local pattern="$1"
107
+ local results=""
108
+
109
+ # Check if sqlite3 is available and local DB exists
110
+ if ! command -v sqlite3 &> /dev/null || [ ! -f "$local_db_path" ]; then
111
+ echo ""
112
+ return
113
+ fi
114
+
115
+ # Convert pattern to SQLite FTS5 or LIKE-compatible format
116
+ # Replace | with OR for FTS5, escape special chars
117
+ local fts_pattern=$(echo "$pattern" | sed 's/|/ OR /g')
118
+ local like_pattern=$(echo "$pattern" | sed 's/|/%/g')
119
+
120
+ # Try FTS5 first, fallback to LIKE
121
+ local db_matches=""
122
+
123
+ # Search interactions (project-local database)
124
+ # Limit to 3 most recent matches
125
+ db_matches=$(sqlite3 -separator '|' "$local_db_path" "
126
+ SELECT DISTINCT session_id, substr(content, 1, 100) as snippet
127
+ FROM interactions
128
+ WHERE content LIKE '%${like_pattern}%' OR thinking LIKE '%${like_pattern}%'
129
+ ORDER BY timestamp DESC
130
+ LIMIT 3;
131
+ " 2>/dev/null || echo "")
132
+
133
+ if [ -n "$db_matches" ]; then
134
+ while IFS='|' read -r session_id snippet; do
135
+ [ -z "$session_id" ] && continue
136
+ # Truncate snippet and clean up
137
+ snippet=$(echo "$snippet" | tr '\n' ' ' | sed 's/ */ /g' | head -c 80)
138
+ results="${results}[interaction:${session_id:0:8}] ${snippet}...\n"
139
+ done <<< "$db_matches"
140
+ fi
141
+
142
+ echo -e "$results"
143
+ }
144
+
145
+ # Search function - simple grep-based search
146
+ search_mneme() {
147
+ local pattern="$1"
148
+ local results=""
149
+
150
+ # Search sessions (limit to recent 10 files)
151
+ if [ -d "${mneme_dir}/sessions" ]; then
152
+ local session_matches=$(find "${mneme_dir}/sessions" -name "*.json" -type f 2>/dev/null | \
153
+ xargs -I{} sh -c "grep -l -i -E '$pattern' '{}' 2>/dev/null || true" | \
154
+ head -3)
155
+
156
+ for file in $session_matches; do
157
+ if [ -f "$file" ]; then
158
+ local title=$(jq -r '.title // .summary.title // ""' "$file" 2>/dev/null | head -1)
159
+ local id=$(jq -r '.id // ""' "$file" 2>/dev/null)
160
+ if [ -n "$title" ] && [ -n "$id" ]; then
161
+ results="${results}[session:${id}] ${title}\n"
162
+ fi
163
+ fi
164
+ done
165
+ fi
166
+
167
+ # Search decisions
168
+ if [ -d "${mneme_dir}/decisions" ]; then
169
+ local decision_matches=$(find "${mneme_dir}/decisions" -name "*.json" -type f 2>/dev/null | \
170
+ xargs -I{} sh -c "grep -l -i -E '$pattern' '{}' 2>/dev/null || true" | \
171
+ head -3)
172
+
173
+ for file in $decision_matches; do
174
+ if [ -f "$file" ]; then
175
+ local title=$(jq -r '.title // ""' "$file" 2>/dev/null | head -1)
176
+ local decision=$(jq -r '.decision // ""' "$file" 2>/dev/null | head -1)
177
+ if [ -n "$title" ]; then
178
+ results="${results}[decision] ${title}: ${decision}\n"
179
+ fi
180
+ fi
181
+ done
182
+ fi
183
+
184
+ # Search patterns
185
+ if [ -d "${mneme_dir}/patterns" ]; then
186
+ for file in "${mneme_dir}/patterns"/*.json; do
187
+ if [ -f "$file" ]; then
188
+ local pattern_matches=$(jq -r --arg p "$pattern" \
189
+ '.patterns[]? | select(.errorPattern | test($p; "i")) | "[pattern] \(.errorPattern | .[0:50])... → \(.solution | .[0:50])..."' \
190
+ "$file" 2>/dev/null | head -2)
191
+ if [ -n "$pattern_matches" ]; then
192
+ results="${results}${pattern_matches}\n"
193
+ fi
194
+ fi
195
+ done
196
+ fi
197
+
198
+ # Search local SQLite database
199
+ local db_results=$(search_local_db "$pattern")
200
+ if [ -n "$db_results" ]; then
201
+ results="${results}${db_results}"
202
+ fi
203
+
204
+ echo -e "$results"
205
+ }
206
+
207
+ # Perform search
208
+ search_results=$(search_mneme "$keywords")
209
+
210
+ # Exit if no results
211
+ if [ -z "$search_results" ] || [ "$search_results" = "\n" ]; then
212
+ exit 0
213
+ fi
214
+
215
+ # Escape for JSON
216
+ escape_for_json() {
217
+ local input="$1"
218
+ local output=""
219
+ local i char
220
+ for (( i=0; i<${#input}; i++ )); do
221
+ char="${input:$i:1}"
222
+ case "$char" in
223
+ $'\\') output+='\\' ;;
224
+ '"') output+='\"' ;;
225
+ $'\n') output+='\n' ;;
226
+ $'\r') output+='\r' ;;
227
+ $'\t') output+='\t' ;;
228
+ *) output+="$char" ;;
229
+ esac
230
+ done
231
+ printf '%s' "$output"
232
+ }
233
+
234
+ # Build context message
235
+ context_message="<mneme-context>
236
+ Related memories found:
237
+ $(echo -e "$search_results")
238
+ Use /mneme:search for more details.
239
+ </mneme-context>"
240
+
241
+ context_escaped=$(escape_for_json "$context_message")
242
+
243
+ # Output JSON with additionalContext
244
+ cat <<EOF
245
+ {
246
+ "hookSpecificOutput": {
247
+ "hookEventName": "UserPromptSubmit",
248
+ "additionalContext": "${context_escaped}"
249
+ }
250
+ }
251
+ EOF
252
+
253
+ exit 0