eagle-mem 3.0.0 → 3.0.2

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