eagle-mem 4.10.13 → 4.12.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 (74) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +22 -22
  3. package/architecture.html +26 -14
  4. package/bin/eagle-mem +4 -0
  5. package/db/039_recall_events.sql +27 -0
  6. package/db/040_graph_decision_nodes.sql +21 -0
  7. package/db/041_graph_semantic_edge_types.sql +21 -0
  8. package/db/042_orchestration_auto_events.sql +23 -0
  9. package/db/043_eagle_events.sql +22 -0
  10. package/db/044_summary_capture_source.sql +12 -0
  11. package/docs/agent-compatibility/README.md +38 -0
  12. package/docs/agent-compatibility/claude-code.md +58 -0
  13. package/docs/agent-compatibility/codex.md +57 -0
  14. package/docs/agent-compatibility/opencode.md +72 -0
  15. package/hooks/post-tool-use.sh +8 -0
  16. package/hooks/pre-tool-use.sh +10 -1
  17. package/hooks/session-end.sh +3 -0
  18. package/hooks/session-start.sh +15 -17
  19. package/hooks/stop.sh +34 -5
  20. package/hooks/user-prompt-submit.sh +85 -10
  21. package/integrations/opencode_eagle_mem_plugin.js +387 -0
  22. package/lib/codex-hooks.sh +13 -6
  23. package/lib/common.sh +77 -7
  24. package/lib/db-events.sh +89 -0
  25. package/lib/db-graph.sh +154 -0
  26. package/lib/db-observations.sh +34 -0
  27. package/lib/db-orchestration.sh +149 -0
  28. package/lib/db-summaries.sh +70 -3
  29. package/lib/db.sh +2 -0
  30. package/lib/hooks.sh +41 -7
  31. package/lib/opencode-hooks.sh +105 -0
  32. package/lib/provider.sh +2 -2
  33. package/package.json +5 -2
  34. package/scripts/compaction.sh +109 -9
  35. package/scripts/dashboard.sh +372 -0
  36. package/scripts/doctor.sh +30 -3
  37. package/scripts/enrich-summary.sh +8 -2
  38. package/scripts/health.sh +40 -2
  39. package/scripts/help.sh +10 -2
  40. package/scripts/inspect.sh +285 -0
  41. package/scripts/install.sh +36 -7
  42. package/scripts/memories.sh +13 -0
  43. package/scripts/repair.sh +187 -0
  44. package/scripts/replay.sh +248 -0
  45. package/scripts/search.sh +44 -3
  46. package/scripts/session.sh +155 -18
  47. package/scripts/statusline-em.sh +34 -7
  48. package/scripts/tasks.sh +34 -0
  49. package/scripts/test.sh +13 -0
  50. package/scripts/uninstall.sh +9 -0
  51. package/scripts/update.sh +21 -2
  52. package/tests/fixtures/agent-hooks/claude-statusline.json +32 -0
  53. package/tests/fixtures/agent-hooks/claude-user-prompt-submit.json +9 -0
  54. package/tests/fixtures/agent-hooks/codex-pre-tool-use.json +10 -0
  55. package/tests/fixtures/agent-hooks/codex-user-prompt-submit.json +7 -0
  56. package/tests/fixtures/agent-hooks/opencode-chat-message.json +36 -0
  57. package/tests/fixtures/agent-hooks/opencode-session-compacting.json +9 -0
  58. package/tests/fixtures/agent-hooks/opencode-todo-updated.json +13 -0
  59. package/tests/fixtures/agent-hooks/opencode-tool-execute-after.json +15 -0
  60. package/tests/fixtures/agent-hooks/opencode-tool-execute-before.json +12 -0
  61. package/tests/test_agent_compatibility_docs_gate.sh +123 -0
  62. package/tests/test_auto_orchestration_detection.sh +109 -0
  63. package/tests/test_claude_stop_hook_registration.sh +56 -0
  64. package/tests/test_clean_session_capture.sh +105 -0
  65. package/tests/test_codex_hooks_config.sh +73 -0
  66. package/tests/test_compaction_survival_matrix.sh +237 -0
  67. package/tests/test_dashboard.sh +96 -0
  68. package/tests/test_eagle_events.sh +96 -0
  69. package/tests/test_opencode_hooks_config.sh +56 -0
  70. package/tests/test_opencode_plugin_adapter.sh +202 -0
  71. package/tests/test_recall_observability.sh +144 -0
  72. package/tests/test_repair.sh +63 -0
  73. package/tests/test_rust_migration_plan.sh +75 -0
  74. package/tests/test_trust_surfaces.sh +123 -0
@@ -0,0 +1,387 @@
1
+ // EAGLE_MEM_OPENCODE_PLUGIN
2
+ // OpenCode local plugin bridge for Eagle Mem.
3
+ import { spawn } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import { existsSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { basename, join } from "node:path";
8
+
9
+ const HOOK_TIMEOUT_MS = 30000;
10
+ const STOP_TIMEOUT_MS = 60000;
11
+
12
+ function eagleHome() {
13
+ return process.env.EAGLE_MEM_DIR || join(homedir(), ".eagle-mem");
14
+ }
15
+
16
+ function hookPath(name) {
17
+ return join(eagleHome(), "hooks", `${name}.sh`);
18
+ }
19
+
20
+ function safeString(value) {
21
+ return typeof value === "string" ? value : "";
22
+ }
23
+
24
+ function projectName(input, directory) {
25
+ return safeString(input?.project?.name) || basename(directory || process.cwd()) || "unknown";
26
+ }
27
+
28
+ function normalizeToolName(tool) {
29
+ switch (safeString(tool).toLowerCase()) {
30
+ case "bash":
31
+ case "shell":
32
+ return "Bash";
33
+ case "read":
34
+ return "Read";
35
+ case "write":
36
+ return "Write";
37
+ case "edit":
38
+ return "Edit";
39
+ case "patch":
40
+ case "apply_patch":
41
+ return "apply_patch";
42
+ default:
43
+ return safeString(tool);
44
+ }
45
+ }
46
+
47
+ function toEagleToolInput(toolName, args = {}) {
48
+ const input = { ...args };
49
+ if (args.filePath !== undefined && input.file_path === undefined) {
50
+ input.file_path = args.filePath;
51
+ }
52
+ if (args.path !== undefined && input.file_path === undefined) {
53
+ input.file_path = args.path;
54
+ }
55
+ if (args.command !== undefined && input.command === undefined) {
56
+ input.command = args.command;
57
+ }
58
+ if (toolName === "apply_patch" && args.patch !== undefined && input.command === undefined) {
59
+ input.command = args.patch;
60
+ }
61
+ return input;
62
+ }
63
+
64
+ function fromEagleToolInput(updatedInput, originalArgs = {}) {
65
+ const next = { ...originalArgs };
66
+ for (const [key, value] of Object.entries(updatedInput || {})) {
67
+ if (key === "file_path") {
68
+ next.filePath = value;
69
+ continue;
70
+ }
71
+ next[key] = value;
72
+ }
73
+ return next;
74
+ }
75
+
76
+ function textFromParts(parts = []) {
77
+ return parts
78
+ .filter((part) => part && part.type === "text" && typeof part.text === "string")
79
+ .map((part) => part.text)
80
+ .join("\n")
81
+ .trim();
82
+ }
83
+
84
+ function parseHookSpecificOutput(stdout) {
85
+ const lines = safeString(stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
86
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
87
+ try {
88
+ const parsed = JSON.parse(lines[i]);
89
+ return parsed.hookSpecificOutput || parsed;
90
+ } catch {
91
+ // Keep scanning for the last JSON line.
92
+ }
93
+ }
94
+ return {};
95
+ }
96
+
97
+ function appendTextPart(output, text) {
98
+ const context = safeString(text).trim();
99
+ if (!context) return;
100
+
101
+ const message = output?.message || {};
102
+ if (!Array.isArray(output.parts)) output.parts = [];
103
+ output.parts.push({
104
+ id: `eagle-mem-${Date.now()}`,
105
+ sessionID: safeString(message.sessionID),
106
+ messageID: safeString(message.id),
107
+ type: "text",
108
+ text: `\n\n${context}`,
109
+ synthetic: true,
110
+ time: { start: Date.now() },
111
+ metadata: { source: "eagle-mem" },
112
+ });
113
+ }
114
+
115
+ function appendToolContext(output, text) {
116
+ const context = safeString(text).trim();
117
+ if (!context) return;
118
+
119
+ output.metadata = {
120
+ ...(output.metadata || {}),
121
+ eagleMemContext: context,
122
+ };
123
+ output.output = `${safeString(output.output)}\n\n${context}`.trim();
124
+ }
125
+
126
+ function runHook(name, payload, timeoutMs = HOOK_TIMEOUT_MS) {
127
+ const script = hookPath(name);
128
+ if (!existsSync(script)) {
129
+ return Promise.resolve({ stdout: "", stderr: "", code: 0, skipped: true });
130
+ }
131
+
132
+ return new Promise((resolve) => {
133
+ const child = spawn("bash", [script], {
134
+ env: {
135
+ ...process.env,
136
+ EAGLE_AGENT_SOURCE: "opencode",
137
+ },
138
+ stdio: ["pipe", "pipe", "pipe"],
139
+ });
140
+
141
+ let stdout = "";
142
+ let stderr = "";
143
+ let finished = false;
144
+ const timer = setTimeout(() => {
145
+ if (finished) return;
146
+ finished = true;
147
+ child.kill("SIGTERM");
148
+ resolve({ stdout, stderr: `${stderr}\nHook timed out: ${name}`.trim(), code: 124 });
149
+ }, timeoutMs);
150
+
151
+ child.stdout.on("data", (chunk) => {
152
+ stdout += chunk.toString();
153
+ });
154
+ child.stderr.on("data", (chunk) => {
155
+ stderr += chunk.toString();
156
+ });
157
+ child.on("close", (code) => {
158
+ if (finished) return;
159
+ finished = true;
160
+ clearTimeout(timer);
161
+ resolve({ stdout, stderr, code: code ?? 0 });
162
+ });
163
+ child.on("error", (error) => {
164
+ if (finished) return;
165
+ finished = true;
166
+ clearTimeout(timer);
167
+ resolve({ stdout, stderr: `${stderr}\n${error.message}`.trim(), code: 1 });
168
+ });
169
+
170
+ child.stdin.end(JSON.stringify(payload));
171
+ });
172
+ }
173
+
174
+ function baseHookPayload(ctx, sessionID, extra = {}) {
175
+ return {
176
+ session_id: sessionID,
177
+ cwd: ctx.directory,
178
+ workspace: {
179
+ current_dir: ctx.directory,
180
+ project_dir: ctx.directory,
181
+ },
182
+ project: projectName(ctx, ctx.directory),
183
+ agent: "opencode",
184
+ ...extra,
185
+ };
186
+ }
187
+
188
+ function taskID(sessionID, todo) {
189
+ return `opencode-${createHash("sha256")
190
+ .update(`${sessionID}:${todo?.content || ""}`)
191
+ .digest("hex")
192
+ .slice(0, 16)}`;
193
+ }
194
+
195
+ function normalizeTodoStatus(status) {
196
+ switch (safeString(status)) {
197
+ case "completed":
198
+ case "cancelled":
199
+ case "in_progress":
200
+ case "pending":
201
+ return status;
202
+ default:
203
+ return "pending";
204
+ }
205
+ }
206
+
207
+ export const EagleMemPlugin = async (ctx) => {
208
+ const startedSessions = new Set();
209
+ const messageRoles = new Map();
210
+ const textPartsByMessage = new Map();
211
+ const latestAssistantMessageBySession = new Map();
212
+
213
+ async function ensureSessionStarted(sessionID, source = "startup") {
214
+ if (!sessionID) return "";
215
+ const key = `${sessionID}:${source}`;
216
+ if (source === "startup" && startedSessions.has(key)) return "";
217
+ startedSessions.add(key);
218
+
219
+ const result = await runHook("session-start", baseHookPayload(ctx, sessionID, {
220
+ hook_event_name: "SessionStart",
221
+ source,
222
+ model: "opencode",
223
+ }));
224
+ const hookOutput = parseHookSpecificOutput(result.stdout);
225
+ return safeString(hookOutput.additionalContext);
226
+ }
227
+
228
+ async function runUserPromptSubmit(sessionID, prompt) {
229
+ if (!sessionID || !prompt) return "";
230
+ const result = await runHook("user-prompt-submit", baseHookPayload(ctx, sessionID, {
231
+ hook_event_name: "UserPromptSubmit",
232
+ prompt,
233
+ }));
234
+ const hookOutput = parseHookSpecificOutput(result.stdout);
235
+ return safeString(hookOutput.additionalContext);
236
+ }
237
+
238
+ async function mirrorTodos(sessionID, todos = []) {
239
+ for (const todo of todos) {
240
+ const sourceTaskID = taskID(sessionID, todo);
241
+ const subject = safeString(todo?.content).trim();
242
+ if (!subject) continue;
243
+
244
+ const status = normalizeTodoStatus(todo?.status);
245
+ const description = JSON.stringify({
246
+ priority: safeString(todo?.priority) || "medium",
247
+ status,
248
+ source: "opencode.todo.updated",
249
+ });
250
+
251
+ await runHook("post-tool-use", baseHookPayload(ctx, sessionID, {
252
+ hook_event_name: status === "completed" ? "TaskCompleted" : "TaskCreated",
253
+ task_id: sourceTaskID,
254
+ task_subject: subject,
255
+ task_description: description,
256
+ }));
257
+
258
+ await runHook("post-tool-use", baseHookPayload(ctx, sessionID, {
259
+ hook_event_name: "PostToolUse",
260
+ tool_name: "TaskUpdate",
261
+ tool_input: {
262
+ taskId: sourceTaskID,
263
+ status,
264
+ },
265
+ }));
266
+ }
267
+ }
268
+
269
+ function rememberMessage(message) {
270
+ if (!message?.id) return;
271
+ messageRoles.set(message.id, message.role);
272
+ const text = safeString(textPartsByMessage.get(message.id));
273
+ if (message.role === "assistant" && text) {
274
+ latestAssistantMessageBySession.set(message.sessionID, text);
275
+ }
276
+ }
277
+
278
+ function rememberPart(part) {
279
+ if (!part?.messageID || part.type !== "text") return;
280
+ textPartsByMessage.set(part.messageID, safeString(part.text));
281
+ if (messageRoles.get(part.messageID) === "assistant") {
282
+ latestAssistantMessageBySession.set(part.sessionID, safeString(part.text));
283
+ }
284
+ }
285
+
286
+ return {
287
+ event: async ({ event }) => {
288
+ if (!event?.type) return;
289
+ const props = event.properties || {};
290
+ const sessionID = props.sessionID || props.info?.id;
291
+
292
+ if (event.type === "session.created") {
293
+ await ensureSessionStarted(sessionID, "startup");
294
+ }
295
+ if (event.type === "message.updated") {
296
+ rememberMessage(props.info);
297
+ }
298
+ if (event.type === "message.part.updated") {
299
+ rememberPart(props.part);
300
+ }
301
+ if (event.type === "todo.updated") {
302
+ await mirrorTodos(sessionID, props.todos || []);
303
+ }
304
+ if (event.type === "session.compacted") {
305
+ await ensureSessionStarted(sessionID, "compact");
306
+ }
307
+ if (event.type === "session.idle") {
308
+ await runHook("stop", baseHookPayload(ctx, sessionID, {
309
+ hook_event_name: "Stop",
310
+ agent_type: "main",
311
+ last_assistant_message: safeString(latestAssistantMessageBySession.get(sessionID)),
312
+ }), STOP_TIMEOUT_MS);
313
+ }
314
+ if (event.type === "session.deleted") {
315
+ await runHook("session-end", baseHookPayload(ctx, sessionID, {
316
+ hook_event_name: "SessionEnd",
317
+ }));
318
+ }
319
+ },
320
+
321
+ "chat.message": async (input, output) => {
322
+ const sessionID = input?.sessionID || output?.message?.sessionID;
323
+ const prompt = textFromParts(output?.parts || []);
324
+ const startupContext = await ensureSessionStarted(sessionID, "startup");
325
+ appendTextPart(output, startupContext);
326
+
327
+ const recallContext = await runUserPromptSubmit(sessionID, prompt);
328
+ appendTextPart(output, recallContext);
329
+ },
330
+
331
+ "tool.execute.before": async (input, output) => {
332
+ const sessionID = input?.sessionID;
333
+ const toolName = normalizeToolName(input?.tool);
334
+ await ensureSessionStarted(sessionID, "startup");
335
+
336
+ const payload = baseHookPayload(ctx, sessionID, {
337
+ hook_event_name: "PreToolUse",
338
+ tool_name: toolName,
339
+ tool_input: toEagleToolInput(toolName, output?.args || {}),
340
+ });
341
+ const result = await runHook("pre-tool-use", payload);
342
+ const hookOutput = parseHookSpecificOutput(result.stdout);
343
+ if (hookOutput.permissionDecision === "deny") {
344
+ throw new Error(hookOutput.permissionDecisionReason || "Eagle Mem denied this tool call");
345
+ }
346
+ if (hookOutput.updatedInput && output) {
347
+ output.args = fromEagleToolInput(hookOutput.updatedInput, output.args || {});
348
+ }
349
+ },
350
+
351
+ "tool.execute.after": async (input, output) => {
352
+ const sessionID = input?.sessionID;
353
+ const toolName = normalizeToolName(input?.tool);
354
+ const result = await runHook("post-tool-use", baseHookPayload(ctx, sessionID, {
355
+ hook_event_name: "PostToolUse",
356
+ tool_name: toolName,
357
+ tool_input: toEagleToolInput(toolName, input?.args || {}),
358
+ tool_response: {
359
+ stdout: safeString(output?.output),
360
+ metadata: output?.metadata || {},
361
+ title: safeString(output?.title),
362
+ },
363
+ }));
364
+ const hookOutput = parseHookSpecificOutput(result.stdout);
365
+ appendToolContext(output, hookOutput.additionalContext);
366
+ },
367
+
368
+ "shell.env": async (_input, output) => {
369
+ output.env = {
370
+ ...(output.env || {}),
371
+ EAGLE_AGENT_SOURCE: "opencode",
372
+ EAGLE_MEM_DIR: eagleHome(),
373
+ };
374
+ },
375
+
376
+ "experimental.session.compacting": async (input, output) => {
377
+ const sessionID = input?.sessionID;
378
+ const compactContext = await ensureSessionStarted(sessionID, "compact");
379
+ if (compactContext) {
380
+ if (!Array.isArray(output.context)) output.context = [];
381
+ output.context.push(`## Eagle Mem Compaction Context\n\n${compactContext}`);
382
+ }
383
+ },
384
+ };
385
+ };
386
+
387
+ export default EagleMemPlugin;
@@ -27,7 +27,7 @@ eagle_enable_codex_hooks() {
27
27
  if [ ! -f "$config" ]; then
28
28
  cat > "$config" << 'TOML'
29
29
  [features]
30
- codex_hooks = true
30
+ hooks = true
31
31
  TOML
32
32
  chmod 600 "$config" 2>/dev/null || true
33
33
  return 0
@@ -45,26 +45,33 @@ TOML
45
45
  }
46
46
  /^[[:space:]]*\[/ && in_features {
47
47
  if (!saw_flag && !inserted) {
48
- print "codex_hooks = true"
48
+ print "hooks = true"
49
49
  inserted=1
50
50
  }
51
51
  in_features=0
52
52
  }
53
- in_features && /^[[:space:]]*codex_hooks[[:space:]]*=/ {
54
- print "codex_hooks = true"
53
+ in_features && /^[[:space:]]*hooks[[:space:]]*=/ {
54
+ print "hooks = true"
55
55
  saw_flag=1
56
56
  next
57
57
  }
58
+ in_features && /^[[:space:]]*codex_hooks[[:space:]]*=/ {
59
+ if (!saw_flag && !inserted) {
60
+ print "hooks = true"
61
+ saw_flag=1
62
+ }
63
+ next
64
+ }
58
65
  { print }
59
66
  END {
60
67
  if (in_features && !saw_flag && !inserted) {
61
- print "codex_hooks = true"
68
+ print "hooks = true"
62
69
  inserted=1
63
70
  }
64
71
  if (!saw_features) {
65
72
  print ""
66
73
  print "[features]"
67
- print "codex_hooks = true"
74
+ print "hooks = true"
68
75
  }
69
76
  }
70
77
  ' "$config" > "$tmp" && mv "$tmp" "$config"
package/lib/common.sh CHANGED
@@ -21,6 +21,11 @@ EAGLE_CODEX_SKILLS_DIR="${EAGLE_CODEX_SKILLS_DIR:-$EAGLE_CODEX_DIR/skills}"
21
21
  EAGLE_CODEX_MEMORIES_DIR="${EAGLE_CODEX_MEMORIES_DIR:-$EAGLE_CODEX_DIR/memories}"
22
22
  EAGLE_GROK_DIR="${EAGLE_GROK_DIR:-$HOME/.grok}"
23
23
  EAGLE_GROK_SKILLS_DIR="${EAGLE_GROK_SKILLS_DIR:-$HOME/.grok/skills}"
24
+ EAGLE_OPENCODE_DIR="${EAGLE_OPENCODE_DIR:-$HOME/.config/opencode}"
25
+ EAGLE_OPENCODE_CONFIG="${EAGLE_OPENCODE_CONFIG:-$EAGLE_OPENCODE_DIR/opencode.json}"
26
+ EAGLE_OPENCODE_PLUGINS_DIR="${EAGLE_OPENCODE_PLUGINS_DIR:-$EAGLE_OPENCODE_DIR/plugins}"
27
+ EAGLE_OPENCODE_PLUGIN="${EAGLE_OPENCODE_PLUGIN:-$EAGLE_OPENCODE_PLUGINS_DIR/eagle-mem.js}"
28
+ EAGLE_OPENCODE_SKILLS_DIR="${EAGLE_OPENCODE_SKILLS_DIR:-$EAGLE_OPENCODE_DIR/skills}"
24
29
  EAGLE_RAW_BASH_UNLOCK="${EAGLE_RAW_BASH_UNLOCK:-/tmp/eagle-mem-raw-bash-unlock}"
25
30
 
26
31
  _eagle_sqlite_candidate_paths() {
@@ -103,6 +108,50 @@ eagle_require_sqlite_fts5() {
103
108
  return 1
104
109
  }
105
110
 
111
+ eagle_squash_ws() {
112
+ tr '\n' ' ' | sed 's/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//'
113
+ }
114
+
115
+ eagle_db_integrity_status() {
116
+ local db_path="${1:-$EAGLE_MEM_DB}"
117
+ local sqlite_bin output rc detail first_line
118
+
119
+ if [ ! -f "$db_path" ]; then
120
+ printf 'missing|database file not found\n'
121
+ return 1
122
+ fi
123
+
124
+ sqlite_bin=$(eagle_sqlite_path)
125
+ if [ -z "$sqlite_bin" ]; then
126
+ printf 'unavailable|sqlite3 with FTS5 not found\n'
127
+ return 1
128
+ fi
129
+
130
+ output=$({
131
+ echo ".output /dev/null"
132
+ echo "PRAGMA busy_timeout=5000;"
133
+ echo ".output stdout"
134
+ echo "PRAGMA quick_check;"
135
+ } | "$sqlite_bin" "$db_path" 2>&1)
136
+ rc=$?
137
+ detail=$(printf '%s' "$output" | eagle_squash_ws)
138
+ [ -n "$detail" ] || detail="no integrity output"
139
+
140
+ if [ "$rc" -ne 0 ]; then
141
+ printf 'error|%s\n' "$detail"
142
+ return "$rc"
143
+ fi
144
+
145
+ first_line=$(printf '%s\n' "$output" | awk 'NF { print; exit }')
146
+ if [ "$first_line" = "ok" ]; then
147
+ printf 'ok|ok\n'
148
+ return 0
149
+ fi
150
+
151
+ printf 'failed|%s\n' "$detail"
152
+ return 1
153
+ }
154
+
106
155
  eagle_log() {
107
156
  local level="$1"
108
157
  shift
@@ -895,6 +944,7 @@ eagle_agent_source_from_json() {
895
944
 
896
945
  case "$agent_field" in
897
946
  antigravity*) echo "antigravity"; return ;;
947
+ opencode*) echo "opencode"; return ;;
898
948
  esac
899
949
 
900
950
  case "$transcript_path" in
@@ -911,6 +961,7 @@ eagle_agent_label() {
911
961
  case "${1:-$(eagle_agent_source)}" in
912
962
  codex) echo "Codex" ;;
913
963
  antigravity*) echo "Antigravity" ;;
964
+ opencode) echo "OpenCode" ;;
914
965
  *) echo "Claude Code" ;;
915
966
  esac
916
967
  }
@@ -1516,6 +1567,7 @@ eagle_runtime_change_plan() {
1516
1567
  local package_dir="${2:-}"
1517
1568
  local claude_found="${3:-false}"
1518
1569
  local codex_found="${4:-false}"
1570
+ local opencode_found="${5:-false}"
1519
1571
 
1520
1572
  echo ""
1521
1573
  echo -e " ${BOLD:-}What will change${RESET:-}"
@@ -1549,6 +1601,15 @@ eagle_runtime_change_plan() {
1549
1601
  echo -e " ${DIM:-}-> Codex not detected; Codex hooks/skills skipped${RESET:-}"
1550
1602
  fi
1551
1603
 
1604
+ if [ "$opencode_found" = true ]; then
1605
+ echo -e " ${CYAN:-}->${RESET:-} Update OpenCode"
1606
+ echo -e " ${DIM:-}plugin:${RESET:-} $EAGLE_OPENCODE_PLUGIN"
1607
+ echo -e " ${DIM:-}skills:${RESET:-} $EAGLE_OPENCODE_SKILLS_DIR/eagle-mem-*"
1608
+ echo -e " ${DIM:-}mode: ${RESET:-} global local plugin; OpenCode --pure disables external plugins"
1609
+ else
1610
+ echo -e " ${DIM:-}-> OpenCode not detected; OpenCode plugin/skills skipped${RESET:-}"
1611
+ fi
1612
+
1552
1613
  if [ "$action" = "update" ]; then
1553
1614
  echo -e " ${CYAN:-}->${RESET:-} Refresh installed version metadata"
1554
1615
  fi
@@ -1562,9 +1623,11 @@ eagle_uninstall_change_plan() {
1562
1623
  echo ""
1563
1624
  echo -e " ${CYAN:-}->${RESET:-} Remove Claude hooks from $EAGLE_SETTINGS"
1564
1625
  echo -e " ${CYAN:-}->${RESET:-} Remove Codex hooks from $EAGLE_CODEX_HOOKS"
1626
+ echo -e " ${CYAN:-}->${RESET:-} Remove Eagle Mem OpenCode plugin from $EAGLE_OPENCODE_PLUGIN"
1565
1627
  echo -e " ${CYAN:-}->${RESET:-} Remove Eagle Mem skill links from:"
1566
1628
  echo -e " ${DIM:-}$EAGLE_SKILLS_DIR${RESET:-}"
1567
1629
  echo -e " ${DIM:-}$EAGLE_CODEX_SKILLS_DIR${RESET:-}"
1630
+ echo -e " ${DIM:-}$EAGLE_OPENCODE_SKILLS_DIR${RESET:-}"
1568
1631
  echo -e " ${CYAN:-}->${RESET:-} Remove Eagle Mem instruction blocks from:"
1569
1632
  echo -e " ${DIM:-}$HOME/.claude/CLAUDE.md${RESET:-}"
1570
1633
  echo -e " ${DIM:-}$EAGLE_CODEX_AGENTS_MD${RESET:-}"
@@ -1830,16 +1893,19 @@ The person reading this transcript should experience natural, thoughtful, human
1830
1893
 
1831
1894
  The rich structured data still gets saved (so future sessions get excellent context and the anti-regression system keeps working), but it happens without making the transcript feel like you are reading a debug dump.
1832
1895
 
1833
- **Recommended capture approaches (in order of preference):**
1834
- - Best: Use `eagle-mem session save --summary "..."` or the equivalent capture skill/tool. The CLI writes the structured record cleanly; you only show the nice prose to the user.
1835
- - Strong alternative: Put any machine block inside a collapsed `<details>` element so it is available for debugging but invisible by default.
1836
- - Only as last resort: Emit the raw block at all and only when the user has explicitly asked for technical output.
1896
+ **How to capture (clean, no raw blocks):**
1897
+ - When you wrap up substantive work, run this once via the shell it is pre-approved and prints only a short confirmation. Fill the flags you have; omit the rest:
1898
+ `eagle-mem session save --session-id <id> --request "..." --completed "..." --learned "..." --decisions "choice why; choice why" --gotchas "..." --next-steps "..." --key-files "path — role" --files-modified "a.ts,b.ts" --affected-features "..." --verified-features "..." --regression-risks "..."`
1899
+ Your session id is injected by SessionStart; use it verbatim so the capture merges into the live session.
1900
+ - Then end with a short, human recap in prose, followed by one line: `**Eagle Mem** | Session captured — N decisions, M gotchas`.
1901
+ - NEVER print `<eagle-summary>` tags, XML, or raw field lists (`request:`/`completed:`/...) in the visible reply.
1902
+ - If you never run the CLI, the Stop hook still captures a summary automatically as a lower-fidelity fallback — so capture is never lost, it is just richer when you run the command.
1837
1903
 
1838
1904
  **Why this rule exists:** Every single session ends with a summary. If it looks technical, the entire conversation history slowly becomes unpleasant to read. The memory layer should make the experience of working with agents *more* human, not less.
1839
1905
 
1840
1906
  **How to apply:**
1841
1907
  - Write the visible recap in clear, narrative prose first.
1842
- - Persist the structured details via tool or collapsed element — never as raw tags in the main reply.
1908
+ - Persist the structured details via the `session save` command — never as raw tags in the main reply.
1843
1909
  - Keep durable task descriptions truthful and up to date; they often carry more value across compactions than any one summary.
1844
1910
  - When Eagle Mem context appears, attribute it naturally.
1845
1911
  - Protect secrets. Update the durable record if you change direction.
@@ -1853,8 +1919,11 @@ eagle_patch_claude_md() {
1853
1919
  mkdir -p "$HOME/.claude"
1854
1920
 
1855
1921
  if [ -f "$claude_md" ] && grep -qF "$marker" "$claude_md" 2>/dev/null; then
1856
- # Check if section has outdated pipe-separated format
1857
- if grep -qF 'request: \[what user asked\] | completed:' "$claude_md" 2>/dev/null; then
1922
+ # Rewrite when the section uses an outdated capture doctrine:
1923
+ # - the old pipe-separated <eagle-summary> template, or
1924
+ # - the superseded "collapsed <details>" recommendation (pre-CLI-first).
1925
+ if grep -qF 'request: \[what user asked\] | completed:' "$claude_md" 2>/dev/null \
1926
+ || grep -qF 'collapsed `<details>` element' "$claude_md" 2>/dev/null; then
1858
1927
  # Replace the outdated section: remove old, append new
1859
1928
  local tmp_md
1860
1929
  tmp_md=$(mktemp)
@@ -1902,6 +1971,7 @@ Eagle Mem hooks are active for Codex in this project. SessionStart and UserPromp
1902
1971
  - For broad multi-agent work, YOU run `eagle-mem orchestrate`; do not ask the user to run these commands
1903
1972
  - Codex does not currently expose a persistent custom statusline like Claude Code; if the user asks for Eagle Mem status, run `eagle-mem statusline`
1904
1973
  - For important decisions, preferences, gotchas, or durable project facts, include them briefly in normal prose. Eagle Mem will extract them from the transcript.
1974
+ - To persist a richer structured capture without printing anything raw, run once at wrap-up: `eagle-mem session save --agent codex --completed "..." --decisions "choice — why" --gotchas "..." --files-modified "a.ts,b.ts"` (fill what applies)
1905
1975
  - Do not revert Eagle Mem-surfaced decisions without asking the user
1906
1976
  - If Eagle Mem reports pending feature verification, verify or waive it before push/PR/publish
1907
1977
  - Never put raw secrets in summaries
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ # Eagle Mem event log helpers.
3
+
4
+ eagle_insert_event() {
5
+ local project="$1"
6
+ local session_id="$2"
7
+ local agent="$3"
8
+ local event_type="$4"
9
+ local command="${5:-}"
10
+ local hook_event_name="${6:-}"
11
+ local status="${7:-ok}"
12
+ local detail_json="${8:-}"
13
+ [ -n "$detail_json" ] || detail_json="{}"
14
+
15
+ [ -n "$event_type" ] || return 0
16
+ [ -f "$EAGLE_MEM_DB" ] || return 0
17
+
18
+ if [ -z "$(eagle_db "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'eagle_events' LIMIT 1;" 2>/dev/null || true)" ]; then
19
+ return 0
20
+ fi
21
+
22
+ detail_json=$(printf '%s' "$detail_json" | jq -c . 2>/dev/null || printf '{}')
23
+
24
+ local p_sql sid_sql agent_sql type_sql command_sql hook_sql status_sql detail_sql
25
+ p_sql=$(eagle_sql_escape "$project")
26
+ sid_sql=$(eagle_sql_escape "$session_id")
27
+ agent_sql=$(eagle_sql_escape "$agent")
28
+ type_sql=$(eagle_sql_escape "$event_type")
29
+ command_sql=$(eagle_sql_escape "$command")
30
+ hook_sql=$(eagle_sql_escape "$hook_event_name")
31
+ status_sql=$(eagle_sql_escape "$status")
32
+ detail_sql=$(eagle_sql_escape "$detail_json")
33
+
34
+ eagle_db "INSERT INTO eagle_events (
35
+ project, session_id, agent, event_type, command,
36
+ hook_event_name, status, detail_json
37
+ )
38
+ VALUES (
39
+ '$p_sql', '$sid_sql', '$agent_sql', '$type_sql', '$command_sql',
40
+ '$hook_sql', '$status_sql', '$detail_sql'
41
+ );" >/dev/null 2>&1 || true
42
+ }
43
+
44
+ eagle_hook_observability_begin() {
45
+ local input="$1"
46
+ local default_hook="$2"
47
+
48
+ EAGLE_EVENT_HOOK_NAME=$(printf '%s' "$input" | jq -r '.hook_event_name // empty' 2>/dev/null)
49
+ [ -n "$EAGLE_EVENT_HOOK_NAME" ] || EAGLE_EVENT_HOOK_NAME="$default_hook"
50
+ EAGLE_EVENT_SESSION_ID=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)
51
+ EAGLE_EVENT_CWD=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)
52
+ EAGLE_EVENT_TOOL_NAME=$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)
53
+ EAGLE_EVENT_AGENT=$(eagle_agent_source_from_json "$input")
54
+ EAGLE_EVENT_PROJECT=$(eagle_project_from_hook_input "$input")
55
+ EAGLE_EVENT_COMPLETION_DETAIL="{}"
56
+
57
+ [ -n "$EAGLE_EVENT_PROJECT" ] || return 0
58
+
59
+ local detail
60
+ detail=$(jq -nc \
61
+ --arg cwd "$EAGLE_EVENT_CWD" \
62
+ --arg tool "$EAGLE_EVENT_TOOL_NAME" \
63
+ '{cwd:$cwd, tool_name:$tool}')
64
+ eagle_insert_event "$EAGLE_EVENT_PROJECT" "$EAGLE_EVENT_SESSION_ID" "$EAGLE_EVENT_AGENT" "hook_started" "" "$EAGLE_EVENT_HOOK_NAME" "ok" "$detail"
65
+ }
66
+
67
+ eagle_hook_observability_set_detail() {
68
+ local detail_json="${1:-}"
69
+ [ -n "$detail_json" ] || detail_json="{}"
70
+ EAGLE_EVENT_COMPLETION_DETAIL=$(printf '%s' "$detail_json" | jq -c . 2>/dev/null || printf '{}')
71
+ }
72
+
73
+ eagle_hook_observability_complete() {
74
+ local rc="${1:-0}"
75
+ [ -n "${EAGLE_EVENT_PROJECT:-}" ] || return 0
76
+
77
+ local status="ok"
78
+ [ "$rc" -ne 0 ] 2>/dev/null && status="error"
79
+
80
+ eagle_insert_event \
81
+ "$EAGLE_EVENT_PROJECT" \
82
+ "${EAGLE_EVENT_SESSION_ID:-}" \
83
+ "${EAGLE_EVENT_AGENT:-}" \
84
+ "hook_completed" \
85
+ "" \
86
+ "${EAGLE_EVENT_HOOK_NAME:-}" \
87
+ "$status" \
88
+ "${EAGLE_EVENT_COMPLETION_DETAIL:-}"
89
+ }