eagle-mem 4.9.6 → 4.9.8

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/common.sh CHANGED
@@ -303,12 +303,14 @@ eagle_remember_session_project() {
303
303
  eagle_get_session_project_light() {
304
304
  local session_id="${1:-}"
305
305
  eagle_validate_session_id "$session_id" || return 1
306
- command -v sqlite3 >/dev/null 2>&1 || return 1
306
+ local sqlite_bin
307
+ sqlite_bin=$(eagle_sqlite_path)
308
+ [ -n "$sqlite_bin" ] || return 1
307
309
  [ -f "$EAGLE_MEM_DB" ] || return 1
308
310
 
309
311
  local sid_sql project
310
312
  sid_sql=$(eagle_sql_escape "$session_id")
311
- project=$(sqlite3 "$EAGLE_MEM_DB" "SELECT project FROM sessions WHERE id = '$sid_sql' AND project != '' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
313
+ project=$("$sqlite_bin" "$EAGLE_MEM_DB" "SELECT project FROM sessions WHERE id = '$sid_sql' AND project != '' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
312
314
  [ -n "$project" ] || return 1
313
315
  printf '%s\n' "$project"
314
316
  }
@@ -317,12 +319,14 @@ eagle_project_has_table_row() {
317
319
  local table="${1:-}"
318
320
  local project="${2:-}"
319
321
  [ -n "$table" ] && [ -n "$project" ] || return 1
320
- command -v sqlite3 >/dev/null 2>&1 || return 1
322
+ local sqlite_bin
323
+ sqlite_bin=$(eagle_sqlite_path)
324
+ [ -n "$sqlite_bin" ] || return 1
321
325
  [ -f "$EAGLE_MEM_DB" ] || return 1
322
326
 
323
327
  local project_sql found
324
328
  project_sql=$(eagle_sql_escape "$project")
325
- found=$(sqlite3 "$EAGLE_MEM_DB" "SELECT 1 FROM $table WHERE project = '$project_sql' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
329
+ found=$("$sqlite_bin" "$EAGLE_MEM_DB" "SELECT 1 FROM $table WHERE project = '$project_sql' LIMIT 1;" 2>/dev/null | awk 'NF { print; exit }')
326
330
  [ "$found" = "1" ]
327
331
  }
328
332
 
@@ -330,11 +334,16 @@ eagle_project_from_existing_ancestor() {
330
334
  local path="${1:-}"
331
335
  [ -n "$path" ] || return 1
332
336
 
333
- local current key
337
+ local start current key
334
338
  current=$(eagle_normalize_project_path "$path")
339
+ start="$current"
335
340
  eagle_is_ephemeral_project_path "$current" && return 1
336
341
 
337
342
  while [ -n "$current" ] && [ "$current" != "/" ]; do
343
+ if [ "$current" = "$HOME" ] && [ "$start" != "$HOME" ]; then
344
+ break
345
+ fi
346
+
338
347
  key=$(eagle_project_key_from_target_dir "$current")
339
348
  if [ -n "$key" ]; then
340
349
  # Prefer ancestors with durable memory/summary content. Session-only
@@ -353,6 +362,121 @@ eagle_project_from_existing_ancestor() {
353
362
  return 1
354
363
  }
355
364
 
365
+ eagle_project_has_recall_rows() {
366
+ local project="${1:-}"
367
+ [ -n "$project" ] || return 1
368
+
369
+ eagle_project_has_table_row "agent_memories" "$project" \
370
+ || eagle_project_has_table_row "agent_plans" "$project" \
371
+ || eagle_project_has_table_row "agent_tasks" "$project" \
372
+ || eagle_project_has_table_row "summaries" "$project"
373
+ }
374
+
375
+ eagle_recall_ancestor_project_from_cwd() {
376
+ local path="${1:-$(pwd)}"
377
+ local current_project="${2:-}"
378
+ local current key
379
+
380
+ current=$(eagle_normalize_project_path "$path")
381
+ eagle_is_ephemeral_project_path "$current" && return 1
382
+
383
+ [ -d "$current" ] || current=$(dirname "$current")
384
+ current=$(dirname "$current")
385
+
386
+ while [ -n "$current" ] && [ "$current" != "/" ]; do
387
+ if [ "$current" = "$HOME" ]; then
388
+ break
389
+ fi
390
+
391
+ key=$(eagle_project_key_from_target_dir "$current")
392
+ if [ -n "$key" ] && [ "$key" != "$current_project" ]; then
393
+ if eagle_project_has_recall_rows "$key"; then
394
+ printf '%s\n' "$key"
395
+ return 0
396
+ fi
397
+ fi
398
+
399
+ current=$(dirname "$current")
400
+ done
401
+
402
+ return 1
403
+ }
404
+
405
+ eagle_recall_project_scope_from_cwd() {
406
+ local path="${1:-$(pwd)}"
407
+ local project="${2:-}"
408
+ local ancestor
409
+
410
+ [ -z "$project" ] && project=$(eagle_project_from_cwd "$path")
411
+ if ancestor=$(eagle_recall_ancestor_project_from_cwd "$path" "$project"); then
412
+ if [ -n "$project" ] && [ "$ancestor" != "$project" ]; then
413
+ printf '%s|%s\n' "$project" "$ancestor"
414
+ return 0
415
+ fi
416
+ printf '%s\n' "$ancestor"
417
+ return 0
418
+ fi
419
+
420
+ printf '%s\n' "$project"
421
+ }
422
+
423
+ eagle_sql_project_scope_condition() {
424
+ local column="${1:-project}"
425
+ local scope="${2:-}"
426
+ local values="" count=0 item escaped
427
+
428
+ while IFS= read -r item; do
429
+ [ -n "$item" ] || continue
430
+ escaped=$(eagle_sql_escape "$item")
431
+ values="${values},'${escaped}'"
432
+ count=$((count + 1))
433
+ done <<EOF
434
+ $(printf '%s' "$scope" | tr '|' '\n')
435
+ EOF
436
+
437
+ if [ "$count" -eq 0 ]; then
438
+ printf '1 = 0\n'
439
+ elif [ "$count" -eq 1 ]; then
440
+ printf "%s = %s\n" "$column" "${values#,}"
441
+ else
442
+ printf "%s IN (%s)\n" "$column" "${values#,}"
443
+ fi
444
+ }
445
+
446
+ eagle_project_scope_label() {
447
+ local scope="${1:-}"
448
+ printf '%s\n' "$scope" | tr '|' ','
449
+ }
450
+
451
+ eagle_project_scope_contains() {
452
+ local scope="${1:-}"
453
+ local target="${2:-}"
454
+ local item
455
+ [ -n "$target" ] || return 1
456
+
457
+ while IFS= read -r item; do
458
+ [ -z "$item" ] && continue
459
+ [ "$item" = "$target" ] && return 0
460
+ done <<EOF
461
+ $(printf '%s' "$scope" | tr '|' '\n')
462
+ EOF
463
+
464
+ return 1
465
+ }
466
+
467
+ eagle_claude_project_dir_for_key() {
468
+ local project="${1:-}"
469
+ [ -n "$project" ] || return 1
470
+
471
+ local abs slug
472
+ case "$project" in
473
+ /*) abs="$project" ;;
474
+ *) abs="$HOME/$project" ;;
475
+ esac
476
+ slug=$(printf '%s' "$abs" | sed -E 's#[^[:alnum:].]+#-#g')
477
+ printf '%s/%s\n' "$EAGLE_CLAUDE_PROJECTS_DIR" "$slug"
478
+ }
479
+
356
480
  eagle_project_from_workspace_path() {
357
481
  if [ -n "${EAGLE_MEM_PROJECT:-}" ]; then
358
482
  printf '%s\n' "$EAGLE_MEM_PROJECT"
@@ -413,7 +537,7 @@ eagle_project_from_statusline_input() {
413
537
  local project_dir="${2:-}"
414
538
  local cwd="${3:-}"
415
539
  local session_id="${4:-}"
416
- local project workspace_project_dir transcript_path
540
+ local project session_project workspace_project workspace_project_dir transcript_path
417
541
 
418
542
  if [ -n "${EAGLE_MEM_PROJECT:-}" ]; then
419
543
  printf '%s\n' "$EAGLE_MEM_PROJECT"
@@ -430,27 +554,49 @@ eagle_project_from_statusline_input() {
430
554
  [ -z "$cwd" ] && cwd=$(printf '%s' "$input" | jq -r '.workspace.current_dir // .cwd // empty' 2>/dev/null)
431
555
  fi
432
556
 
433
- # Explicit workspace project and transcript start are stronger than older
434
- # cached session rows because they repair pre-fix sessions that were stored
435
- # under a nested folder key.
436
- if [ -n "$workspace_project_dir" ]; then
437
- project=$(eagle_project_from_workspace_path "$workspace_project_dir")
438
- [ -n "$project" ] && { printf '%s\n' "$project"; return; }
557
+ # Claude can report $HOME as workspace.project_dir for a child project before
558
+ # the project is fully initialized. Treat that as too broad for statusline
559
+ # stats; the current working directory is the more useful project boundary.
560
+ if [ -n "$workspace_project_dir" ] && [ -n "$cwd" ]; then
561
+ local workspace_resolved cwd_resolved
562
+ workspace_resolved=$(eagle_normalize_project_path "$workspace_project_dir")
563
+ cwd_resolved=$(eagle_normalize_project_path "$cwd")
564
+ if [ "$workspace_resolved" = "$HOME" ] && [ "$cwd_resolved" != "$HOME" ]; then
565
+ workspace_project_dir="$cwd_resolved"
566
+ fi
439
567
  fi
440
568
 
441
- if [ -n "$transcript_path" ]; then
442
- if project=$(eagle_project_from_transcript_start "$transcript_path" "${cwd:-$project_dir}"); then
443
- printf '%s\n' "$project"
444
- return
445
- fi
569
+ if [ -n "$workspace_project_dir" ]; then
570
+ workspace_project=$(eagle_project_from_workspace_path "$workspace_project_dir" || true)
446
571
  fi
447
572
 
448
573
  if [ -n "$session_id" ]; then
449
- if project=$(eagle_get_session_project_light "$session_id"); then
450
- printf '%s\n' "$project"
574
+ session_project=$(eagle_get_session_project_light "$session_id" || true)
575
+ if [ -n "$session_project" ]; then
576
+ # If an older row was captured under a nested folder key, a current
577
+ # workspace root should still be allowed to repair the display.
578
+ if [ -n "$workspace_project" ] && [ "$session_project" != "$workspace_project" ]; then
579
+ case "$session_project" in
580
+ "$workspace_project"/*)
581
+ printf '%s\n' "$workspace_project"
582
+ return
583
+ ;;
584
+ esac
585
+ fi
586
+ printf '%s\n' "$session_project"
451
587
  return
452
588
  fi
453
- if project=$(eagle_get_session_project_marker "$session_id"); then
589
+ fi
590
+
591
+ # Explicit workspace project and transcript start are stronger than generic
592
+ # path fallback because they repair pre-fix sessions that were stored under
593
+ # a nested folder key.
594
+ if [ -n "$workspace_project_dir" ]; then
595
+ [ -n "$workspace_project" ] && { printf '%s\n' "$workspace_project"; return; }
596
+ fi
597
+
598
+ if [ -n "$transcript_path" ]; then
599
+ if project=$(eagle_project_from_transcript_start "$transcript_path" "${cwd:-$project_dir}"); then
454
600
  printf '%s\n' "$project"
455
601
  return
456
602
  fi
@@ -697,20 +843,15 @@ eagle_emit_context_for_agent() {
697
843
 
698
844
  [ -z "$context" ] && return 0
699
845
 
700
- if [ "$agent" = "codex" ]; then
701
- jq -cn \
702
- --arg event "$hook_event" \
703
- --arg context "$context" \
704
- '{
705
- hookSpecificOutput: {
706
- hookEventName: $event,
707
- additionalContext: $context
708
- }
709
- }'
710
- return 0
711
- fi
712
-
713
- printf '%s\n' "$context"
846
+ jq -cn \
847
+ --arg event "$hook_event" \
848
+ --arg context "$context" \
849
+ '{
850
+ hookSpecificOutput: {
851
+ hookEventName: $event,
852
+ additionalContext: $context
853
+ }
854
+ }'
714
855
  }
715
856
 
716
857
  eagle_config_get_light() {
@@ -1146,7 +1287,7 @@ eagle_statusline_script_from_command() {
1146
1287
  eagle_statusline_script_uses_input() {
1147
1288
  local sl_file="${1:-}"
1148
1289
  [ -f "$sl_file" ] || return 1
1149
- grep -Eq 'eagle_project_from_statusline_input|eagle_mem_statusline.*(\$\{input:-\}|\$input)' "$sl_file"
1290
+ grep -Eq 'eagle_project_from_statusline_input|eagle_mem_statusline.*(\$\{input:-\}|\$input)|statusline-em\.sh.*--hud' "$sl_file"
1150
1291
  }
1151
1292
 
1152
1293
  eagle_patch_statusline_script() {
@@ -1178,6 +1319,14 @@ eagle_patch_statusline_script() {
1178
1319
  fi
1179
1320
 
1180
1321
  perl -0pi -e '
1322
+ s{# [^\n]*EAGLE MEM[^\n]*\n.*?\n# [^\n]*CLOCK[^\n]*\n}{q~# -- EAGLE MEM (second line - branded) --------------------------------------
1323
+ em_line=""
1324
+ if [ -n "$cwd" ] && [ -f "$HOME/.eagle-mem/scripts/statusline-em.sh" ]; then
1325
+ em_line=$(printf "%s" "$input" | bash "$HOME/.eagle-mem/scripts/statusline-em.sh" --hud 2>/dev/null)
1326
+ fi
1327
+
1328
+ # -- CLOCK ---------------------------------------------------------------------
1329
+ ~}ems;
1181
1330
  s/(project_dir=\$\(echo "\$input" \| jq -r \x27)\.workspace\.current_dir \/\/ \.cwd/$1.workspace.project_dir \/\/ .workspace.current_dir \/\/ .cwd/g;
1182
1331
  s/(project_dir=\$\(echo "\$input" \| jq -r ")\.workspace\.current_dir \/\/ \.cwd/$1.workspace.project_dir \/\/ .workspace.current_dir \/\/ .cwd/g;
1183
1332
  s/eagle_mem_statusline "\$project_dir" "\$session_id" "\$\{input\}"/eagle_mem_statusline "\$project_dir" "\$session_id" "\${input:-}"/g;
@@ -1212,6 +1361,307 @@ eagle_patch_statusline_script() {
1212
1361
  return 0
1213
1362
  }
1214
1363
 
1364
+ eagle_runtime_change_plan() {
1365
+ local action="${1:-install}"
1366
+ local package_dir="${2:-}"
1367
+ local claude_found="${3:-false}"
1368
+ local codex_found="${4:-false}"
1369
+
1370
+ echo ""
1371
+ echo -e " ${BOLD:-}What will change${RESET:-}"
1372
+ echo -e " ${DIM:-}Eagle Mem shows this before touching runtime files or agent configs.${RESET:-}"
1373
+ echo ""
1374
+ echo -e " ${CYAN:-}->${RESET:-} Copy runtime files"
1375
+ echo -e " ${DIM:-}from:${RESET:-} ${package_dir:-current package}"
1376
+ echo -e " ${DIM:-}to: ${RESET:-} $EAGLE_MEM_DIR/{hooks,lib,db,scripts}"
1377
+ echo -e " ${CYAN:-}->${RESET:-} Open database"
1378
+ echo -e " ${DIM:-}path:${RESET:-} $EAGLE_MEM_DB"
1379
+ echo -e " ${DIM:-}mode:${RESET:-} migrate only when needed; existing memories are preserved"
1380
+
1381
+ if [ "$claude_found" = true ]; then
1382
+ echo -e " ${CYAN:-}->${RESET:-} Update Claude Code"
1383
+ echo -e " ${DIM:-}settings:${RESET:-} $EAGLE_SETTINGS"
1384
+ echo -e " ${DIM:-}hooks: ${RESET:-} SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd"
1385
+ echo -e " ${DIM:-}status: ${RESET:-} patch Eagle Mem statusline block when auto-detectable; backup first"
1386
+ echo -e " ${DIM:-}skills: ${RESET:-} $EAGLE_SKILLS_DIR/eagle-mem-*"
1387
+ echo -e " ${DIM:-}guide: ${RESET:-} $HOME/.claude/CLAUDE.md Eagle Mem section"
1388
+ else
1389
+ echo -e " ${DIM:-}-> Claude Code not detected; Claude hooks/skills skipped${RESET:-}"
1390
+ fi
1391
+
1392
+ if [ "$codex_found" = true ]; then
1393
+ echo -e " ${CYAN:-}->${RESET:-} Update Codex"
1394
+ echo -e " ${DIM:-}config:${RESET:-} $EAGLE_CODEX_CONFIG"
1395
+ echo -e " ${DIM:-}hooks: ${RESET:-} $EAGLE_CODEX_HOOKS"
1396
+ echo -e " ${DIM:-}skills:${RESET:-} $EAGLE_CODEX_SKILLS_DIR/eagle-mem-*"
1397
+ echo -e " ${DIM:-}guide: ${RESET:-} $EAGLE_CODEX_AGENTS_MD Eagle Mem section"
1398
+ else
1399
+ echo -e " ${DIM:-}-> Codex not detected; Codex hooks/skills skipped${RESET:-}"
1400
+ fi
1401
+
1402
+ if [ "$action" = "update" ]; then
1403
+ echo -e " ${CYAN:-}->${RESET:-} Refresh installed version metadata"
1404
+ fi
1405
+ echo ""
1406
+ }
1407
+
1408
+ eagle_uninstall_change_plan() {
1409
+ echo ""
1410
+ echo -e " ${BOLD:-}What will change${RESET:-}"
1411
+ echo -e " ${DIM:-}Uninstall removes Eagle Mem-owned integration points. Runtime data is deleted only if you confirm later.${RESET:-}"
1412
+ echo ""
1413
+ echo -e " ${CYAN:-}->${RESET:-} Remove Claude hooks from $EAGLE_SETTINGS"
1414
+ echo -e " ${CYAN:-}->${RESET:-} Remove Codex hooks from $EAGLE_CODEX_HOOKS"
1415
+ echo -e " ${CYAN:-}->${RESET:-} Remove Eagle Mem skill links from:"
1416
+ echo -e " ${DIM:-}$EAGLE_SKILLS_DIR${RESET:-}"
1417
+ echo -e " ${DIM:-}$EAGLE_CODEX_SKILLS_DIR${RESET:-}"
1418
+ echo -e " ${CYAN:-}->${RESET:-} Remove Eagle Mem instruction blocks from:"
1419
+ echo -e " ${DIM:-}$HOME/.claude/CLAUDE.md${RESET:-}"
1420
+ echo -e " ${DIM:-}$EAGLE_CODEX_AGENTS_MD${RESET:-}"
1421
+ echo -e " ${CYAN:-}->${RESET:-} Remove Eagle Mem statusline integration when auto-detectable"
1422
+ echo -e " ${DIM:-}-> Backups are written next to edited user config files.${RESET:-}"
1423
+ echo ""
1424
+ }
1425
+
1426
+ eagle_backup_user_file() {
1427
+ local file="${1:-}"
1428
+ [ -f "$file" ] || return 1
1429
+ local backup
1430
+ backup="${file}.eagle-mem.uninstall-bak-$(date -u +%Y%m%dT%H%M%SZ)"
1431
+ cp "$file" "$backup" 2>/dev/null || return 1
1432
+ printf '%s\n' "$backup"
1433
+ }
1434
+
1435
+ eagle_remove_marked_markdown_section() {
1436
+ local file="${1:-}"
1437
+ local marker="${2:-## Eagle Mem — Persistent Memory}"
1438
+ [ -f "$file" ] || return 1
1439
+ grep -qF "$marker" "$file" 2>/dev/null || return 1
1440
+
1441
+ local tmp
1442
+ tmp=$(mktemp) || return 1
1443
+ awk -v marker="$marker" '
1444
+ BEGIN { skip=0; pending_sep="" }
1445
+ $0 == "---" && !skip {
1446
+ pending_sep=$0 ORS
1447
+ next
1448
+ }
1449
+ index($0, marker) {
1450
+ skip=1
1451
+ pending_sep=""
1452
+ next
1453
+ }
1454
+ skip {
1455
+ if ($0 == "---") {
1456
+ skip=0
1457
+ pending_sep=""
1458
+ next
1459
+ }
1460
+ if ($0 ~ /^## /) {
1461
+ skip=0
1462
+ if (pending_sep != "") {
1463
+ printf "%s", pending_sep
1464
+ pending_sep=""
1465
+ }
1466
+ print
1467
+ }
1468
+ next
1469
+ }
1470
+ {
1471
+ if (pending_sep != "") {
1472
+ printf "%s", pending_sep
1473
+ pending_sep=""
1474
+ }
1475
+ print
1476
+ }
1477
+ END {
1478
+ if (!skip && pending_sep != "") {
1479
+ printf "%s", pending_sep
1480
+ }
1481
+ }
1482
+ ' "$file" > "$tmp" || { rm -f "$tmp"; return 1; }
1483
+
1484
+ if cmp -s "$file" "$tmp"; then
1485
+ rm -f "$tmp"
1486
+ return 1
1487
+ fi
1488
+ mv "$tmp" "$file"
1489
+ }
1490
+
1491
+ eagle_remove_statusline_block() {
1492
+ local sl_file="${1:-}"
1493
+ [ -f "$sl_file" ] || return 1
1494
+ command -v perl >/dev/null 2>&1 || return 1
1495
+ grep -Eq 'EAGLE MEM|Eagle Mem|eagle_mem_statusline|statusline-em\.sh|agent_memories|claude_memories' "$sl_file" 2>/dev/null || return 1
1496
+ grep -Eq '\.eagle-mem|eagle_mem_statusline|statusline-em\.sh|agent_memories|claude_memories' "$sl_file" 2>/dev/null || return 1
1497
+
1498
+ local tmp backup mode
1499
+ tmp=$(mktemp) || return 1
1500
+ cp "$sl_file" "$tmp" || { rm -f "$tmp"; return 1; }
1501
+
1502
+ perl -0pi -e '
1503
+ s{\n?# [^\n]*(?:EAGLE MEM|Eagle Mem)[^\n]*\n.*?\n(# [^\n]*CLOCK[^\n]*\n)}{\n$1}is;
1504
+ ' "$tmp" || { rm -f "$tmp"; return 1; }
1505
+
1506
+ if cmp -s "$sl_file" "$tmp"; then
1507
+ rm -f "$tmp"
1508
+ return 1
1509
+ fi
1510
+
1511
+ backup=$(eagle_backup_user_file "$sl_file" 2>/dev/null || true)
1512
+ mode=$(stat -f %Lp "$sl_file" 2>/dev/null || stat -c %a "$sl_file" 2>/dev/null || echo "")
1513
+ mv "$tmp" "$sl_file"
1514
+ [ -n "$mode" ] && chmod "$mode" "$sl_file" 2>/dev/null || chmod +x "$sl_file" 2>/dev/null || true
1515
+ [ -n "$backup" ] && printf '%s\n' "$backup"
1516
+ }
1517
+
1518
+ eagle_remove_statusline_integration() {
1519
+ local settings="${1:-$EAGLE_SETTINGS}"
1520
+ [ -f "$settings" ] || return 1
1521
+ command -v jq >/dev/null 2>&1 || return 1
1522
+
1523
+ local command sl_file wrapper tmp changed=1
1524
+ command=$(jq -r '.statusLine.command // .statusline.command // empty' "$settings" 2>/dev/null)
1525
+ [ -n "$command" ] || return 1
1526
+ sl_file=$(eagle_statusline_script_from_command "$command" 2>/dev/null || true)
1527
+ wrapper="$EAGLE_MEM_DIR/scripts/statusline-wrapper.sh"
1528
+
1529
+ if printf '%s' "$command" | grep -q "$wrapper"; then
1530
+ eagle_backup_user_file "$settings" >/dev/null 2>&1 || true
1531
+ tmp=$(mktemp) || return 1
1532
+ jq 'del(.statusLine) | del(.statusline)' "$settings" > "$tmp" && mv "$tmp" "$settings"
1533
+ return 0
1534
+ fi
1535
+
1536
+ if [ -n "$sl_file" ] && [ -f "$sl_file" ]; then
1537
+ if eagle_remove_statusline_block "$sl_file" >/dev/null; then
1538
+ changed=0
1539
+ fi
1540
+ fi
1541
+ return "$changed"
1542
+ }
1543
+
1544
+ eagle_runtime_manifest_path() {
1545
+ printf '%s/install-manifest.json\n' "$EAGLE_MEM_DIR"
1546
+ }
1547
+
1548
+ eagle_file_sha256() {
1549
+ local file="${1:-}"
1550
+ [ -f "$file" ] || return 1
1551
+ if command -v shasum >/dev/null 2>&1; then
1552
+ shasum -a 256 "$file" 2>/dev/null | awk '{print $1}'
1553
+ elif command -v sha256sum >/dev/null 2>&1; then
1554
+ sha256sum "$file" 2>/dev/null | awk '{print $1}'
1555
+ else
1556
+ return 1
1557
+ fi
1558
+ }
1559
+
1560
+ eagle_runtime_manifest_write() {
1561
+ local package_dir="${1:-}"
1562
+ local action="${2:-update}"
1563
+ local manifest tmp_files tmp_json
1564
+ manifest=$(eagle_runtime_manifest_path)
1565
+ mkdir -p "$EAGLE_MEM_DIR"
1566
+ tmp_files=$(mktemp) || return 1
1567
+ tmp_json=$(mktemp) || { rm -f "$tmp_files"; return 1; }
1568
+
1569
+ local group rel file sha size mode package_version sqlite_bin sqlite_version files_json
1570
+ for group in hooks lib db scripts; do
1571
+ [ -d "$EAGLE_MEM_DIR/$group" ] || continue
1572
+ while IFS= read -r rel; do
1573
+ [ -n "$rel" ] || continue
1574
+ file="$EAGLE_MEM_DIR/$rel"
1575
+ [ -f "$file" ] || continue
1576
+ sha=$(eagle_file_sha256 "$file" 2>/dev/null || true)
1577
+ size=$(wc -c < "$file" 2>/dev/null | tr -d ' ')
1578
+ mode=$(stat -f %Lp "$file" 2>/dev/null || stat -c %a "$file" 2>/dev/null || echo "")
1579
+ printf '%s\t%s\t%s\t%s\n' "$rel" "${sha:-unknown}" "${size:-0}" "$mode"
1580
+ done < <(cd "$EAGLE_MEM_DIR" && find "$group" -type f | sort 2>/dev/null)
1581
+ done > "$tmp_files"
1582
+
1583
+ package_version=$(jq -r .version "$package_dir/package.json" 2>/dev/null || tr -d '[:space:]' < "$EAGLE_MEM_DIR/.version" 2>/dev/null || echo "unknown")
1584
+ sqlite_bin=$(eagle_sqlite_path)
1585
+ sqlite_version=$(eagle_sqlite_version)
1586
+ files_json=$(jq -R -s '
1587
+ split("\n")
1588
+ | map(select(length > 0)
1589
+ | split("\t")
1590
+ | {
1591
+ path: .[0],
1592
+ sha256: .[1],
1593
+ size: ((.[2] // "0") | tonumber),
1594
+ mode: (.[3] // "")
1595
+ })
1596
+ ' "$tmp_files")
1597
+
1598
+ jq -nc \
1599
+ --arg schema "1" \
1600
+ --arg action "$action" \
1601
+ --arg package_name "eagle-mem" \
1602
+ --arg package_version "$package_version" \
1603
+ --arg package_dir "$package_dir" \
1604
+ --arg runtime_dir "$EAGLE_MEM_DIR" \
1605
+ --arg db "$EAGLE_MEM_DB" \
1606
+ --arg sqlite_bin "${sqlite_bin:-}" \
1607
+ --arg sqlite_version "${sqlite_version:-}" \
1608
+ --arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
1609
+ --argjson files "$files_json" \
1610
+ '{schema:$schema,
1611
+ generated_at:$generated_at,
1612
+ action:$action,
1613
+ package:{name:$package_name, version:$package_version, dir:$package_dir},
1614
+ runtime:{dir:$runtime_dir, db:$db},
1615
+ sqlite:{path:$sqlite_bin, version:$sqlite_version},
1616
+ files:$files}' > "$tmp_json" || { rm -f "$tmp_files" "$tmp_json"; return 1; }
1617
+
1618
+ mv "$tmp_json" "$manifest"
1619
+ chmod 600 "$manifest" 2>/dev/null || true
1620
+ rm -f "$tmp_files"
1621
+ }
1622
+
1623
+ eagle_runtime_manifest_check() {
1624
+ local manifest
1625
+ manifest=$(eagle_runtime_manifest_path)
1626
+ if [ ! -f "$manifest" ]; then
1627
+ printf 'missing|0|0|0\n'
1628
+ return 0
1629
+ fi
1630
+ command -v jq >/dev/null 2>&1 || {
1631
+ printf 'unreadable|0|0|0\n'
1632
+ return 0
1633
+ }
1634
+
1635
+ local checked=0 missing=0 drift=0 rel expected file actual
1636
+ while IFS=$'\t' read -r rel expected; do
1637
+ [ -n "$rel" ] || continue
1638
+ checked=$((checked + 1))
1639
+ file="$EAGLE_MEM_DIR/$rel"
1640
+ if [ ! -f "$file" ]; then
1641
+ missing=$((missing + 1))
1642
+ continue
1643
+ fi
1644
+ actual=$(eagle_file_sha256 "$file" 2>/dev/null || true)
1645
+ if [ -z "$actual" ] || [ "$actual" != "$expected" ]; then
1646
+ drift=$((drift + 1))
1647
+ fi
1648
+ done < <(jq -r '.files[]? | [.path, .sha256] | @tsv' "$manifest" 2>/dev/null)
1649
+
1650
+ if [ "$missing" -eq 0 ] && [ "$drift" -eq 0 ]; then
1651
+ printf 'ok|%s|0|0\n' "$checked"
1652
+ else
1653
+ printf 'drift|%s|%s|%s\n' "$checked" "$missing" "$drift"
1654
+ fi
1655
+ }
1656
+
1657
+ eagle_runtime_manifest_field() {
1658
+ local field="${1:-}"
1659
+ local manifest
1660
+ manifest=$(eagle_runtime_manifest_path)
1661
+ [ -f "$manifest" ] || return 1
1662
+ jq -r "$field // empty" "$manifest" 2>/dev/null
1663
+ }
1664
+
1215
1665
  _eagle_claude_md_section() {
1216
1666
  cat << 'EAGLE_MD'
1217
1667