eagle-mem 4.10.13 → 4.11.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 (69) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +20 -20
  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/docs/agent-compatibility/README.md +38 -0
  11. package/docs/agent-compatibility/claude-code.md +50 -0
  12. package/docs/agent-compatibility/codex.md +51 -0
  13. package/docs/agent-compatibility/opencode.md +71 -0
  14. package/hooks/post-tool-use.sh +8 -0
  15. package/hooks/pre-tool-use.sh +10 -1
  16. package/hooks/session-end.sh +3 -0
  17. package/hooks/session-start.sh +7 -0
  18. package/hooks/stop.sh +10 -1
  19. package/hooks/user-prompt-submit.sh +79 -6
  20. package/integrations/opencode_eagle_mem_plugin.js +387 -0
  21. package/lib/codex-hooks.sh +13 -6
  22. package/lib/common.sh +63 -0
  23. package/lib/db-events.sh +89 -0
  24. package/lib/db-graph.sh +154 -0
  25. package/lib/db-observations.sh +34 -0
  26. package/lib/db-orchestration.sh +149 -0
  27. package/lib/db.sh +2 -0
  28. package/lib/hooks.sh +12 -7
  29. package/lib/opencode-hooks.sh +105 -0
  30. package/lib/provider.sh +2 -2
  31. package/package.json +5 -2
  32. package/scripts/compaction.sh +108 -8
  33. package/scripts/dashboard.sh +372 -0
  34. package/scripts/doctor.sh +30 -3
  35. package/scripts/health.sh +40 -2
  36. package/scripts/help.sh +10 -2
  37. package/scripts/inspect.sh +285 -0
  38. package/scripts/install.sh +31 -7
  39. package/scripts/memories.sh +13 -0
  40. package/scripts/repair.sh +187 -0
  41. package/scripts/replay.sh +248 -0
  42. package/scripts/search.sh +44 -3
  43. package/scripts/statusline-em.sh +34 -7
  44. package/scripts/tasks.sh +34 -0
  45. package/scripts/test.sh +13 -0
  46. package/scripts/uninstall.sh +9 -0
  47. package/scripts/update.sh +18 -2
  48. package/tests/fixtures/agent-hooks/claude-statusline.json +32 -0
  49. package/tests/fixtures/agent-hooks/claude-user-prompt-submit.json +9 -0
  50. package/tests/fixtures/agent-hooks/codex-pre-tool-use.json +10 -0
  51. package/tests/fixtures/agent-hooks/codex-user-prompt-submit.json +7 -0
  52. package/tests/fixtures/agent-hooks/opencode-chat-message.json +36 -0
  53. package/tests/fixtures/agent-hooks/opencode-session-compacting.json +9 -0
  54. package/tests/fixtures/agent-hooks/opencode-todo-updated.json +13 -0
  55. package/tests/fixtures/agent-hooks/opencode-tool-execute-after.json +15 -0
  56. package/tests/fixtures/agent-hooks/opencode-tool-execute-before.json +12 -0
  57. package/tests/test_agent_compatibility_docs_gate.sh +123 -0
  58. package/tests/test_auto_orchestration_detection.sh +109 -0
  59. package/tests/test_claude_stop_hook_registration.sh +56 -0
  60. package/tests/test_codex_hooks_config.sh +73 -0
  61. package/tests/test_compaction_survival_matrix.sh +237 -0
  62. package/tests/test_dashboard.sh +96 -0
  63. package/tests/test_eagle_events.sh +96 -0
  64. package/tests/test_opencode_hooks_config.sh +56 -0
  65. package/tests/test_opencode_plugin_adapter.sh +202 -0
  66. package/tests/test_recall_observability.sh +144 -0
  67. package/tests/test_repair.sh +63 -0
  68. package/tests/test_rust_migration_plan.sh +75 -0
  69. 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:-}"
@@ -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
+ }