eagle-mem 3.0.1 → 3.1.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.
package/lib/db.sh CHANGED
@@ -1,740 +1,17 @@
1
1
  #!/usr/bin/env bash
2
2
  # ═══════════════════════════════════════════════════════════
3
- # Eagle Mem — Database helpers
4
- # Source after common.sh: . "$(dirname "$0")/../lib/db.sh"
3
+ # Eagle Mem — Database helpers (backward-compat shim)
4
+ # Sources all domain files. Callers: . "$LIB_DIR/db.sh"
5
5
  # ═══════════════════════════════════════════════════════════
6
-
7
- EAGLE_DB_SETUP=".headers off
8
- .output /dev/null
9
- PRAGMA journal_mode=WAL;
10
- PRAGMA synchronous=NORMAL;
11
- PRAGMA busy_timeout=5000;
12
- PRAGMA foreign_keys=ON;
13
- PRAGMA trusted_schema=ON;
14
- .output stdout"
15
-
16
- eagle_db() {
17
- local _eagle_db_err
18
- _eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_err.$$")
19
- local _eagle_db_out
20
- _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
21
- local _eagle_db_rc=$?
22
- if [ -s "$_eagle_db_err" ]; then
23
- cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
24
- fi
25
- rm -f "$_eagle_db_err" 2>/dev/null
26
- [ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
27
- return $_eagle_db_rc
28
- }
29
-
30
- eagle_db_pipe() {
31
- local _eagle_db_err
32
- _eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_pipe_err.$$")
33
- local _eagle_db_out
34
- _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".bail on"; cat; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
35
- local _eagle_db_rc=$?
36
- if [ -s "$_eagle_db_err" ]; then
37
- cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
38
- fi
39
- rm -f "$_eagle_db_err" 2>/dev/null
40
- [ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
41
- return $_eagle_db_rc
42
- }
43
-
44
- eagle_db_json() {
45
- local _eagle_db_err
46
- _eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_json_err.$$")
47
- local _eagle_db_out
48
- _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".mode json"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
49
- local _eagle_db_rc=$?
50
- if [ -s "$_eagle_db_err" ]; then
51
- cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
52
- fi
53
- rm -f "$_eagle_db_err" 2>/dev/null
54
- [ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
55
- return $_eagle_db_rc
56
- }
57
-
58
- eagle_ensure_db() {
59
- if [ ! -f "$EAGLE_MEM_DB" ]; then
60
- local script_dir
61
- script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../db" && pwd)"
62
- "$script_dir/migrate.sh"
63
- fi
64
- }
65
-
66
- eagle_upsert_session() {
67
- local session_id; session_id=$(eagle_sql_escape "$1")
68
- local project; project=$(eagle_sql_escape "$2")
69
- local cwd; cwd=$(eagle_sql_escape "${3:-}")
70
- local model; model=$(eagle_sql_escape "${4:-}")
71
- local source; source=$(eagle_sql_escape "${5:-}")
72
-
73
- eagle_db "INSERT INTO sessions (id, project, cwd, model, source, last_activity_at)
74
- VALUES ('$session_id', '$project', '$cwd', '$model', '$source', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
75
- ON CONFLICT(id) DO UPDATE SET
76
- cwd = COALESCE(excluded.cwd, sessions.cwd),
77
- model = COALESCE(excluded.model, sessions.model),
78
- source = COALESCE(excluded.source, sessions.source),
79
- status = 'active',
80
- last_activity_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
81
- }
82
-
83
- eagle_end_session() {
84
- local session_id; session_id=$(eagle_sql_escape "$1")
85
- eagle_db "UPDATE sessions SET status = 'completed', ended_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = '$session_id';"
86
- }
87
-
88
- eagle_insert_observation() {
89
- local session_id; session_id=$(eagle_sql_escape "$1")
90
- local project; project=$(eagle_sql_escape "$2")
91
- local tool_name; tool_name=$(eagle_sql_escape "$3")
92
- local tool_input_summary; tool_input_summary=$(eagle_sql_escape "$4")
93
- local files_read; files_read=$(eagle_sql_escape "$5")
94
- local files_modified; files_modified=$(eagle_sql_escape "$6")
95
- local output_bytes="${7:-}"
96
- local output_lines="${8:-}"
97
- local command_category; command_category=$(eagle_sql_escape "${9:-}")
98
-
99
- local extra_cols=""
100
- local extra_vals=""
101
- if [ -n "$output_bytes" ]; then
102
- extra_cols=", output_bytes, output_lines, command_category"
103
- extra_vals=", $(eagle_sql_int "$output_bytes"), $(eagle_sql_int "$output_lines"), '$command_category'"
104
- fi
105
-
106
- eagle_db "INSERT INTO observations (session_id, project, tool_name, tool_input_summary, files_read, files_modified${extra_cols})
107
- SELECT '$session_id', '$project', '$tool_name', '$tool_input_summary', '$files_read', '$files_modified'${extra_vals}
108
- WHERE NOT EXISTS (
109
- SELECT 1 FROM observations
110
- WHERE session_id = '$session_id'
111
- AND tool_name = '$tool_name'
112
- AND tool_input_summary = '$tool_input_summary'
113
- AND created_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-5 seconds')
114
- );"
115
- }
116
-
117
- eagle_insert_summary() {
118
- local session_id; session_id=$(eagle_sql_escape "$1")
119
- local project; project=$(eagle_sql_escape "$2")
120
- local request; request=$(eagle_sql_escape "$3")
121
- local investigated; investigated=$(eagle_sql_escape "$4")
122
- local learned; learned=$(eagle_sql_escape "$5")
123
- local completed; completed=$(eagle_sql_escape "$6")
124
- local next_steps; next_steps=$(eagle_sql_escape "$7")
125
- local files_read; files_read=$(eagle_sql_escape "$8")
126
- local files_modified; files_modified=$(eagle_sql_escape "$9")
127
- local notes; notes=$(eagle_sql_escape "${10:-}")
128
- local decisions; decisions=$(eagle_sql_escape "${11:-}")
129
- local gotchas; gotchas=$(eagle_sql_escape "${12:-}")
130
- local key_files; key_files=$(eagle_sql_escape "${13:-}")
131
-
132
- eagle_db_pipe <<SQL
133
- INSERT INTO summaries (session_id, project, request, investigated, learned, completed, next_steps, files_read, files_modified, notes, decisions, gotchas, key_files)
134
- VALUES (
135
- '$session_id',
136
- '$project',
137
- '$request',
138
- '$investigated',
139
- '$learned',
140
- '$completed',
141
- '$next_steps',
142
- '$files_read',
143
- '$files_modified',
144
- '$notes',
145
- '$decisions',
146
- '$gotchas',
147
- '$key_files'
148
- )
149
- ON CONFLICT(session_id) DO UPDATE SET
150
- project = excluded.project,
151
- request = COALESCE(NULLIF(excluded.request, ''), summaries.request),
152
- investigated = COALESCE(NULLIF(excluded.investigated, ''), summaries.investigated),
153
- learned = COALESCE(NULLIF(excluded.learned, ''), summaries.learned),
154
- completed = COALESCE(NULLIF(excluded.completed, ''), summaries.completed),
155
- next_steps = COALESCE(NULLIF(excluded.next_steps, ''), summaries.next_steps),
156
- files_read = COALESCE(NULLIF(excluded.files_read, '[]'), summaries.files_read),
157
- files_modified = COALESCE(NULLIF(excluded.files_modified, '[]'), summaries.files_modified),
158
- notes = COALESCE(NULLIF(excluded.notes, ''), summaries.notes),
159
- decisions = COALESCE(NULLIF(excluded.decisions, ''), summaries.decisions),
160
- gotchas = COALESCE(NULLIF(excluded.gotchas, ''), summaries.gotchas),
161
- key_files = COALESCE(NULLIF(excluded.key_files, ''), summaries.key_files);
162
- SQL
163
- }
164
-
165
- eagle_get_recent_summaries() {
166
- local project; project=$(eagle_sql_escape "$1")
167
- local limit; limit=$(eagle_sql_int "${2:-5}")
168
-
169
- eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at, s.decisions, s.gotchas, s.key_files
170
- FROM summaries s
171
- WHERE s.project = '$project'
172
- AND s.request NOT LIKE '%<local-command-caveat>%'
173
- ORDER BY s.created_at DESC
174
- LIMIT $limit;"
175
- }
176
-
177
- eagle_search_summaries() {
178
- local query; query=$(eagle_fts_sanitize "$1")
179
- query=$(eagle_sql_escape "$query")
180
- local project="${2:-}"
181
- local limit; limit=$(eagle_sql_int "${3:-10}")
182
-
183
- local where_clause=""
184
- if [ -n "$project" ]; then
185
- project=$(eagle_sql_escape "$project")
186
- where_clause="AND s.project = '$project'"
187
- fi
188
-
189
- eagle_db "SELECT s.request, s.completed, s.learned, s.next_steps, s.created_at, s.project, s.decisions, s.gotchas, s.key_files
190
- FROM summaries s
191
- JOIN summaries_fts f ON f.rowid = s.id
192
- WHERE summaries_fts MATCH '$query'
193
- $where_clause
194
- ORDER BY rank
195
- LIMIT $limit;"
196
- }
197
-
198
- eagle_upsert_overview() {
199
- local project; project=$(eagle_sql_escape "$1")
200
- local raw_content="$2"
201
- local ov_source; ov_source=$(eagle_sql_escape "${3:-manual}")
202
-
203
- if [ ${#raw_content} -gt 16384 ]; then
204
- raw_content="${raw_content:0:16384}"
205
- eagle_log "WARN" "Overview for '$1' truncated to 16 KB (was ${#2} bytes)"
206
- fi
207
-
208
- local content; content=$(eagle_sql_escape "$raw_content")
209
-
210
- eagle_db "INSERT INTO overviews (project, content, source, updated_at)
211
- VALUES ('$project', '$content', '$ov_source', strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
212
- ON CONFLICT(project) DO UPDATE SET
213
- content = excluded.content,
214
- source = excluded.source,
215
- updated_at = excluded.updated_at;"
216
- }
217
-
218
- eagle_get_overview_source() {
219
- local project; project=$(eagle_sql_escape "$1")
220
- eagle_db "SELECT source FROM overviews WHERE project = '$project';"
221
- }
222
-
223
- eagle_get_overview() {
224
- local project; project=$(eagle_sql_escape "$1")
225
-
226
- eagle_db "SELECT content FROM overviews WHERE project = '$project';"
227
- }
228
-
229
- eagle_search_code_chunks() {
230
- local query; query=$(eagle_fts_sanitize "$1")
231
- query=$(eagle_sql_escape "$query")
232
- local project; project=$(eagle_sql_escape "$2")
233
- local limit; limit=$(eagle_sql_int "${3:-5}")
234
-
235
- eagle_db "SELECT c.file_path, c.start_line, c.end_line, c.language
236
- FROM code_chunks c
237
- JOIN code_chunks_fts f ON f.rowid = c.id
238
- WHERE code_chunks_fts MATCH '$query'
239
- AND c.project = '$project'
240
- ORDER BY rank
241
- LIMIT $limit;"
242
- }
243
-
244
- eagle_count_code_chunks() {
245
- local project; project=$(eagle_sql_escape "$1")
246
- eagle_db "SELECT COUNT(*) FROM code_chunks WHERE project = '$project' LIMIT 1;"
247
- }
248
-
249
- eagle_prune_observations() {
250
- local days; days=$(eagle_sql_int "${1:-90}")
251
- local project_filter=""
252
- if [ -n "${2:-}" ]; then
253
- local proj; proj=$(eagle_sql_escape "$2")
254
- project_filter="AND session_id IN (SELECT id FROM sessions WHERE project = '$proj')"
255
- fi
256
- eagle_db "DELETE FROM observations WHERE created_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-$days days') $project_filter;"
257
- }
258
-
259
- eagle_capture_claude_memory() {
260
- local file_path="$1"
261
- local session_id="${2:-}"
262
- local project="${3:-}"
263
-
264
- [ ! -f "$file_path" ] && return 0
265
-
266
- local chash
267
- chash=$(shasum -a 256 "$file_path" | awk '{print $1}')
268
-
269
- local fm body
270
- fm=$(awk '/^---$/{c++; next} c==1' "$file_path")
271
- body=$(awk '/^---$/{c++; next} c>=2' "$file_path")
272
-
273
- _fm_field() { printf '%s\n' "$fm" | awk -F': *' -v k="$1" '$1==k{sub(/^[^:]+: */,""); gsub(/^"|"$/,""); print; exit}'; }
274
-
275
- local mname mdesc mtype morigin
276
- mname=$(_fm_field "name")
277
- mdesc=$(_fm_field "description")
278
- mtype=$(_fm_field "type")
279
- morigin=$(_fm_field "originSessionId")
280
- [ -z "$morigin" ] && morigin="$session_id"
281
-
282
- local fp_sql proj_sql name_sql desc_sql type_sql content_sql hash_sql origin_sql
283
- fp_sql=$(eagle_sql_escape "$file_path")
284
- proj_sql=$(eagle_sql_escape "$project")
285
- name_sql=$(eagle_sql_escape "$mname")
286
- desc_sql=$(eagle_sql_escape "$mdesc")
287
- type_sql=$(eagle_sql_escape "$mtype")
288
- content_sql=$(eagle_sql_escape "$body")
289
- hash_sql=$(eagle_sql_escape "$chash")
290
- origin_sql=$(eagle_sql_escape "$morigin")
291
-
292
- eagle_db_pipe <<SQL
293
- INSERT INTO claude_memories (project, file_path, memory_name, description, memory_type, content, content_hash, origin_session_id)
294
- VALUES ('$proj_sql', '$fp_sql', '$name_sql', '$desc_sql', '$type_sql', '$content_sql', '$hash_sql', '$origin_sql')
295
- ON CONFLICT(file_path) DO UPDATE SET
296
- memory_name = excluded.memory_name,
297
- description = excluded.description,
298
- memory_type = excluded.memory_type,
299
- content = excluded.content,
300
- content_hash = excluded.content_hash,
301
- origin_session_id = excluded.origin_session_id,
302
- updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
303
- WHERE claude_memories.content_hash != excluded.content_hash;
304
- SQL
305
- }
306
-
307
- eagle_search_claude_memories() {
308
- local query; query=$(eagle_fts_sanitize "$1")
309
- query=$(eagle_sql_escape "$query")
310
- local project="${2:-}"
311
- local limit; limit=$(eagle_sql_int "${3:-10}")
312
-
313
- local where_clause=""
314
- if [ -n "$project" ]; then
315
- project=$(eagle_sql_escape "$project")
316
- where_clause="AND m.project = '$project'"
317
- fi
318
-
319
- eagle_db "SELECT m.memory_name, m.memory_type, m.description,
320
- replace(substr(m.content, 1, 200), char(10), ' '),
321
- m.file_path, m.updated_at
322
- FROM claude_memories m
323
- JOIN claude_memories_fts f ON f.rowid = m.id
324
- WHERE claude_memories_fts MATCH '$query'
325
- $where_clause
326
- ORDER BY rank
327
- LIMIT $limit;"
328
- }
329
-
330
- eagle_list_claude_memories() {
331
- local project="${1:-}"
332
- local limit; limit=$(eagle_sql_int "${2:-20}")
333
-
334
- local where_clause=""
335
- if [ -n "$project" ]; then
336
- project=$(eagle_sql_escape "$project")
337
- where_clause="WHERE project = '$project'"
338
- fi
339
-
340
- eagle_db "SELECT memory_name, memory_type, description, file_path, updated_at
341
- FROM claude_memories
342
- $where_clause
343
- ORDER BY updated_at DESC
344
- LIMIT $limit;"
345
- }
346
-
347
- eagle_capture_claude_plan() {
348
- local file_path="$1"
349
- local session_id="${2:-}"
350
- local project="${3:-}"
351
-
352
- [ ! -f "$file_path" ] && return 0
353
-
354
- local chash
355
- chash=$(shasum -a 256 "$file_path" | awk '{print $1}')
356
-
357
- local title content
358
- title=$(awk '/^# /{print; exit}' "$file_path" | sed 's/^# //')
359
- content=$(cat "$file_path")
360
-
361
- local fp_sql proj_sql title_sql content_sql hash_sql origin_sql
362
- fp_sql=$(eagle_sql_escape "$file_path")
363
- proj_sql=$(eagle_sql_escape "$project")
364
- title_sql=$(eagle_sql_escape "$title")
365
- content_sql=$(eagle_sql_escape "$content")
366
- hash_sql=$(eagle_sql_escape "$chash")
367
- origin_sql=$(eagle_sql_escape "$session_id")
368
-
369
- eagle_db_pipe <<SQL
370
- INSERT INTO claude_plans (project, file_path, title, content, content_hash, origin_session_id)
371
- VALUES ('$proj_sql', '$fp_sql', '$title_sql', '$content_sql', '$hash_sql', '$origin_sql')
372
- ON CONFLICT(file_path) DO UPDATE SET
373
- title = excluded.title,
374
- content = excluded.content,
375
- content_hash = excluded.content_hash,
376
- origin_session_id = COALESCE(NULLIF(excluded.origin_session_id, ''), claude_plans.origin_session_id),
377
- project = CASE WHEN excluded.project != '' THEN excluded.project ELSE claude_plans.project END,
378
- updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
379
- WHERE claude_plans.content_hash != excluded.content_hash;
380
- SQL
381
- }
382
-
383
- eagle_search_claude_plans() {
384
- local query; query=$(eagle_fts_sanitize "$1")
385
- query=$(eagle_sql_escape "$query")
386
- local project="${2:-}"
387
- local limit; limit=$(eagle_sql_int "${3:-10}")
388
-
389
- local where_clause=""
390
- if [ -n "$project" ]; then
391
- project=$(eagle_sql_escape "$project")
392
- where_clause="AND p.project = '$project'"
393
- fi
394
-
395
- eagle_db "SELECT p.title, p.project,
396
- replace(substr(p.content, 1, 200), char(10), ' '),
397
- p.file_path, p.updated_at
398
- FROM claude_plans p
399
- JOIN claude_plans_fts f ON f.rowid = p.id
400
- WHERE claude_plans_fts MATCH '$query'
401
- $where_clause
402
- ORDER BY rank
403
- LIMIT $limit;"
404
- }
405
-
406
- eagle_list_claude_plans() {
407
- local project="${1:-}"
408
- local limit; limit=$(eagle_sql_int "${2:-20}")
409
-
410
- local where_clause=""
411
- if [ -n "$project" ]; then
412
- project=$(eagle_sql_escape "$project")
413
- where_clause="WHERE project = '$project'"
414
- fi
415
-
416
- eagle_db "SELECT title, project, file_path, updated_at
417
- FROM claude_plans
418
- $where_clause
419
- ORDER BY updated_at DESC
420
- LIMIT $limit;"
421
- }
422
-
423
- eagle_capture_claude_task() {
424
- local file_path="$1"
425
- local session_id="${2:-}"
426
- local project="${3:-}"
427
-
428
- [ ! -f "$file_path" ] && return 0
429
-
430
- local chash
431
- chash=$(shasum -a 256 "$file_path" | awk '{print $1}')
432
-
433
- local task_json
434
- task_json=$(cat "$file_path")
435
-
436
- local task_id subject desc active_form status blocks blocked_by
437
- task_id=$(printf '%s' "$task_json" | jq -r '.id // empty')
438
- subject=$(printf '%s' "$task_json" | jq -r '.subject // empty')
439
- desc=$(printf '%s' "$task_json" | jq -r '.description // empty')
440
- active_form=$(printf '%s' "$task_json" | jq -r '.activeForm // empty')
441
- status=$(printf '%s' "$task_json" | jq -r '.status // "pending"')
442
- blocks=$(printf '%s' "$task_json" | jq -c '.blocks // []')
443
- blocked_by=$(printf '%s' "$task_json" | jq -c '.blockedBy // []')
444
-
445
- [ -z "$task_id" ] && return 0
446
-
447
- local fp_sql proj_sql sid_sql tid_sql subj_sql desc_sql af_sql status_sql blocks_sql bb_sql hash_sql
448
- fp_sql=$(eagle_sql_escape "$file_path")
449
- proj_sql=$(eagle_sql_escape "$project")
450
- sid_sql=$(eagle_sql_escape "$session_id")
451
- tid_sql=$(eagle_sql_escape "$task_id")
452
- subj_sql=$(eagle_sql_escape "$subject")
453
- desc_sql=$(eagle_sql_escape "$desc")
454
- af_sql=$(eagle_sql_escape "$active_form")
455
- status_sql=$(eagle_sql_escape "$status")
456
- blocks_sql=$(eagle_sql_escape "$blocks")
457
- bb_sql=$(eagle_sql_escape "$blocked_by")
458
- hash_sql=$(eagle_sql_escape "$chash")
459
-
460
- eagle_db_pipe <<SQL
461
- INSERT INTO claude_tasks (project, source_session_id, source_task_id, file_path, subject, description, active_form, status, blocks, blocked_by, content_hash)
462
- VALUES ('$proj_sql', '$sid_sql', '$tid_sql', '$fp_sql', '$subj_sql', '$desc_sql', '$af_sql', '$status_sql', '$blocks_sql', '$bb_sql', '$hash_sql')
463
- ON CONFLICT(file_path) DO UPDATE SET
464
- subject = excluded.subject,
465
- description = excluded.description,
466
- active_form = excluded.active_form,
467
- status = excluded.status,
468
- blocks = excluded.blocks,
469
- blocked_by = excluded.blocked_by,
470
- content_hash = excluded.content_hash,
471
- project = CASE WHEN excluded.project != '' THEN excluded.project ELSE claude_tasks.project END,
472
- updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
473
- WHERE claude_tasks.content_hash != excluded.content_hash;
474
- SQL
475
- }
476
-
477
- eagle_list_claude_tasks() {
478
- local project="${1:-}"
479
- local limit; limit=$(eagle_sql_int "${2:-20}")
480
-
481
- local where_clause=""
482
- if [ -n "$project" ]; then
483
- project=$(eagle_sql_escape "$project")
484
- where_clause="WHERE project = '$project'"
485
- fi
486
-
487
- eagle_db "SELECT subject, status, source_session_id, source_task_id, updated_at
488
- FROM claude_tasks
489
- $where_clause
490
- ORDER BY updated_at DESC
491
- LIMIT $limit;"
492
- }
493
-
494
- eagle_search_claude_tasks() {
495
- local query; query=$(eagle_fts_sanitize "$1")
496
- query=$(eagle_sql_escape "$query")
497
- local project="${2:-}"
498
- local limit; limit=$(eagle_sql_int "${3:-10}")
499
-
500
- local where_clause=""
501
- if [ -n "$project" ]; then
502
- project=$(eagle_sql_escape "$project")
503
- where_clause="AND t.project = '$project'"
504
- fi
505
-
506
- eagle_db "SELECT t.subject, t.status,
507
- replace(substr(t.description, 1, 200), char(10), ' '),
508
- t.source_session_id, t.source_task_id, t.updated_at
509
- FROM claude_tasks t
510
- JOIN claude_tasks_fts f ON f.rowid = t.id
511
- WHERE claude_tasks_fts MATCH '$query'
512
- $where_clause
513
- ORDER BY rank
514
- LIMIT $limit;"
515
- }
516
-
517
- eagle_build_session_project_map() {
518
- local claude_projects_dir="$HOME/.claude/projects"
519
- [ ! -d "$claude_projects_dir" ] && return 0
520
-
521
- for proj_dir in "$claude_projects_dir"/*/; do
522
- [ ! -d "$proj_dir" ] && continue
523
-
524
- local project=""
525
- local sample_jsonl
526
- sample_jsonl=$(ls "$proj_dir"*.jsonl 2>/dev/null | head -1)
527
- if [ -n "$sample_jsonl" ] && [ -f "$sample_jsonl" ]; then
528
- local cwd
529
- cwd=$(head -10 "$sample_jsonl" | jq -r 'select(.cwd != null) | .cwd' 2>/dev/null | head -1)
530
- if [ -n "$cwd" ]; then
531
- project=$(eagle_project_from_cwd "$cwd")
532
- fi
533
- fi
534
- [ -z "$project" ] && continue
535
-
536
- for jsonl in "$proj_dir"*.jsonl; do
537
- [ ! -f "$jsonl" ] && continue
538
- local sid
539
- sid=$(basename "$jsonl" .jsonl)
540
- echo "$sid|$project"
541
- done
542
- done
543
- }
544
-
545
- eagle_backfill_projects() {
546
- local updated=0
547
- local map
548
- map=$(eagle_build_session_project_map)
549
- [ -z "$map" ] && echo "0" && return 0
550
-
551
- while IFS='|' read -r sid project; do
552
- [ -z "$sid" ] || [ -z "$project" ] && continue
553
- local sid_sql proj_sql
554
- sid_sql=$(eagle_sql_escape "$sid")
555
- proj_sql=$(eagle_sql_escape "$project")
556
-
557
- # All six tables updated atomically per session to prevent
558
- # partial backfill if the process is interrupted.
559
- # Note: total_changes() includes FTS trigger changes, so the
560
- # reported count may be higher than actual rows updated.
561
- # This is cosmetic — the count is only used for status messages.
562
- local ch
563
- ch=$(eagle_db_pipe <<SQL
564
- BEGIN;
565
- UPDATE sessions SET project = '$proj_sql' WHERE id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
566
- UPDATE claude_tasks SET project = '$proj_sql' WHERE source_session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
567
- UPDATE claude_memories SET project = '$proj_sql' WHERE origin_session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
568
- UPDATE claude_plans SET project = '$proj_sql' WHERE origin_session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
569
- UPDATE summaries SET project = '$proj_sql' WHERE session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
570
- UPDATE observations SET project = '$proj_sql' WHERE session_id = '$sid_sql' AND (project = '' OR project != '$proj_sql');
571
- SELECT total_changes();
572
- COMMIT;
573
- SQL
574
- )
575
- [ "${ch:-0}" -gt 0 ] && updated=$((updated + ch))
576
- done <<< "$map"
577
-
578
- echo "$updated"
579
- }
580
-
581
- eagle_prune_orphan_chunks() {
582
- local project; project=$(eagle_sql_escape "$1")
583
- local target_dir="$2"
584
-
585
- # Get all indexed file paths for this project
586
- local paths
587
- paths=$(eagle_db "SELECT DISTINCT file_path FROM code_chunks WHERE project = '$project';")
588
-
589
- local removed=0
590
- local txn_sql="BEGIN;"
591
- while IFS= read -r fpath; do
592
- [ -z "$fpath" ] && continue
593
- if [ ! -f "$target_dir/$fpath" ]; then
594
- local fpath_sql; fpath_sql=$(eagle_sql_escape "$fpath")
595
- txn_sql+="
596
- DELETE FROM code_chunks WHERE project = '$project' AND file_path = '$fpath_sql';"
597
- removed=$((removed + 1))
598
- fi
599
- done <<< "$paths"
600
- txn_sql+="
601
- COMMIT;"
602
-
603
- if [ "$removed" -gt 0 ]; then
604
- eagle_db_pipe <<< "$txn_sql"
605
- fi
606
- echo "$removed"
607
- }
608
-
609
- # ─── Feature graph helpers ─────��───────────────────────────
610
-
611
- eagle_upsert_feature() {
612
- local project; project=$(eagle_sql_escape "$1")
613
- local name; name=$(eagle_sql_escape "$2")
614
- local description; description=$(eagle_sql_escape "${3:-}")
615
-
616
- eagle_db "INSERT INTO features (project, name, description)
617
- VALUES ('$project', '$name', '$description')
618
- ON CONFLICT(project, name) DO UPDATE SET
619
- description = COALESCE(NULLIF('$description', ''), features.description),
620
- updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now');"
621
- }
622
-
623
- eagle_add_feature_dependency() {
624
- local feature_id; feature_id=$(eagle_sql_int "$1")
625
- local kind; kind=$(eagle_sql_escape "$2")
626
- local target; target=$(eagle_sql_escape "$3")
627
- local name; name=$(eagle_sql_escape "$4")
628
- local notes; notes=$(eagle_sql_escape "${5:-}")
629
-
630
- eagle_db "INSERT OR IGNORE INTO feature_dependencies (feature_id, kind, target, name, notes)
631
- VALUES ($feature_id, '$kind', '$target', '$name', '$notes');"
632
- }
633
-
634
- eagle_add_feature_file() {
635
- local feature_id; feature_id=$(eagle_sql_int "$1")
636
- local file_path; file_path=$(eagle_sql_escape "$2")
637
- local role; role=$(eagle_sql_escape "${3:-}")
638
-
639
- eagle_db "INSERT OR IGNORE INTO feature_files (feature_id, file_path, role)
640
- VALUES ($feature_id, '$file_path', '$role');"
641
- }
642
-
643
- eagle_add_feature_smoke_test() {
644
- local feature_id; feature_id=$(eagle_sql_int "$1")
645
- local command; command=$(eagle_sql_escape "$2")
646
- local description; description=$(eagle_sql_escape "${3:-}")
647
-
648
- eagle_db "INSERT OR IGNORE INTO feature_smoke_tests (feature_id, command, description)
649
- VALUES ($feature_id, '$command', '$description');"
650
- }
651
-
652
- eagle_verify_feature() {
653
- local project; project=$(eagle_sql_escape "$1")
654
- local name; name=$(eagle_sql_escape "$2")
655
- local notes; notes=$(eagle_sql_escape "${3:-}")
656
-
657
- eagle_db "UPDATE features SET
658
- last_verified_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
659
- last_verified_notes = '$notes',
660
- updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
661
- WHERE project = '$project' AND name = '$name';"
662
- }
663
-
664
- eagle_get_feature_id() {
665
- local project; project=$(eagle_sql_escape "$1")
666
- local name; name=$(eagle_sql_escape "$2")
667
- eagle_db "SELECT id FROM features WHERE project = '$project' AND name = '$name';"
668
- }
669
-
670
- eagle_list_features() {
671
- local project; project=$(eagle_sql_escape "$1")
672
- local limit; limit=$(eagle_sql_int "${2:-20}")
673
-
674
- eagle_db "SELECT f.name, f.description, f.status, f.last_verified_at,
675
- (SELECT COUNT(*) FROM feature_dependencies WHERE feature_id = f.id) as dep_count,
676
- (SELECT COUNT(*) FROM feature_files WHERE feature_id = f.id) as file_count,
677
- (SELECT COUNT(*) FROM feature_smoke_tests WHERE feature_id = f.id) as test_count
678
- FROM features f
679
- WHERE f.project = '$project' AND f.status = 'active'
680
- ORDER BY f.updated_at DESC
681
- LIMIT $limit;"
682
- }
683
-
684
- eagle_show_feature() {
685
- local project; project=$(eagle_sql_escape "$1")
686
- local name; name=$(eagle_sql_escape "$2")
687
-
688
- local feature_id
689
- feature_id=$(eagle_get_feature_id "$1" "$2")
690
- [ -z "$feature_id" ] && return 1
691
-
692
- echo "=== Feature: $2 ==="
693
- eagle_db "SELECT name, description, status, last_verified_at, last_verified_notes
694
- FROM features WHERE id = $feature_id;"
695
-
696
- local deps
697
- deps=$(eagle_db "SELECT kind, target, name, notes FROM feature_dependencies WHERE feature_id = $feature_id;")
698
- if [ -n "$deps" ]; then
699
- echo "--- Dependencies ---"
700
- echo "$deps"
701
- fi
702
-
703
- local files
704
- files=$(eagle_db "SELECT file_path, role FROM feature_files WHERE feature_id = $feature_id;")
705
- if [ -n "$files" ]; then
706
- echo "--- Files ---"
707
- echo "$files"
708
- fi
709
-
710
- local tests
711
- tests=$(eagle_db "SELECT command, description FROM feature_smoke_tests WHERE feature_id = $feature_id;")
712
- if [ -n "$tests" ]; then
713
- echo "--- Smoke Tests ---"
714
- echo "$tests"
715
- fi
716
- }
717
-
718
- eagle_find_features_for_file() {
719
- local project; project=$(eagle_sql_escape "$1")
720
- local file_path="$2"
721
- local fname; fname=$(basename "$file_path")
722
- local fname_esc; fname_esc=$(eagle_sql_escape "$fname")
723
- local fname_like; fname_like=$(eagle_like_escape "$fname_esc")
724
-
725
- eagle_db "SELECT f.name, f.description, f.last_verified_at,
726
- ff.role,
727
- (SELECT GROUP_CONCAT(fd.target || ':' || fd.name, ', ')
728
- FROM feature_dependencies fd WHERE fd.feature_id = f.id) as deps,
729
- (SELECT GROUP_CONCAT(ff2.file_path, ', ')
730
- FROM feature_files ff2 WHERE ff2.feature_id = f.id AND ff2.file_path != ff.file_path) as other_files,
731
- (SELECT GROUP_CONCAT(fst.command, ', ')
732
- FROM feature_smoke_tests fst WHERE fst.feature_id = f.id) as smoke_tests
733
- FROM features f
734
- JOIN feature_files ff ON ff.feature_id = f.id
735
- WHERE f.project = '$project'
736
- AND f.status = 'active'
737
- AND (ff.file_path LIKE '%$fname_like' ESCAPE '\\' OR ff.file_path LIKE '%$fname_like%' ESCAPE '\\')
738
- ORDER BY f.updated_at DESC
739
- LIMIT 3;"
740
- }
6
+ [ -n "${_EAGLE_DB_LOADED:-}" ] && return 0
7
+ _EAGLE_DB_LOADED=1
8
+
9
+ _eagle_db_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+
11
+ . "$_eagle_db_dir/db-core.sh"
12
+ . "$_eagle_db_dir/db-sessions.sh"
13
+ . "$_eagle_db_dir/db-observations.sh"
14
+ . "$_eagle_db_dir/db-summaries.sh"
15
+ . "$_eagle_db_dir/db-mirrors.sh"
16
+ . "$_eagle_db_dir/db-features.sh"
17
+ . "$_eagle_db_dir/db-backfill.sh"