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.
- package/CHANGELOG.md +25 -0
- package/README.md +22 -22
- package/architecture.html +26 -14
- package/bin/eagle-mem +4 -0
- package/db/039_recall_events.sql +27 -0
- package/db/040_graph_decision_nodes.sql +21 -0
- package/db/041_graph_semantic_edge_types.sql +21 -0
- package/db/042_orchestration_auto_events.sql +23 -0
- package/db/043_eagle_events.sql +22 -0
- package/db/044_summary_capture_source.sql +12 -0
- package/docs/agent-compatibility/README.md +38 -0
- package/docs/agent-compatibility/claude-code.md +58 -0
- package/docs/agent-compatibility/codex.md +57 -0
- package/docs/agent-compatibility/opencode.md +72 -0
- package/hooks/post-tool-use.sh +8 -0
- package/hooks/pre-tool-use.sh +10 -1
- package/hooks/session-end.sh +3 -0
- package/hooks/session-start.sh +15 -17
- package/hooks/stop.sh +34 -5
- package/hooks/user-prompt-submit.sh +85 -10
- package/integrations/opencode_eagle_mem_plugin.js +387 -0
- package/lib/codex-hooks.sh +13 -6
- package/lib/common.sh +77 -7
- package/lib/db-events.sh +89 -0
- package/lib/db-graph.sh +154 -0
- package/lib/db-observations.sh +34 -0
- package/lib/db-orchestration.sh +149 -0
- package/lib/db-summaries.sh +70 -3
- package/lib/db.sh +2 -0
- package/lib/hooks.sh +41 -7
- package/lib/opencode-hooks.sh +105 -0
- package/lib/provider.sh +2 -2
- package/package.json +5 -2
- package/scripts/compaction.sh +109 -9
- package/scripts/dashboard.sh +372 -0
- package/scripts/doctor.sh +30 -3
- package/scripts/enrich-summary.sh +8 -2
- package/scripts/health.sh +40 -2
- package/scripts/help.sh +10 -2
- package/scripts/inspect.sh +285 -0
- package/scripts/install.sh +36 -7
- package/scripts/memories.sh +13 -0
- package/scripts/repair.sh +187 -0
- package/scripts/replay.sh +248 -0
- package/scripts/search.sh +44 -3
- package/scripts/session.sh +155 -18
- package/scripts/statusline-em.sh +34 -7
- package/scripts/tasks.sh +34 -0
- package/scripts/test.sh +13 -0
- package/scripts/uninstall.sh +9 -0
- package/scripts/update.sh +21 -2
- package/tests/fixtures/agent-hooks/claude-statusline.json +32 -0
- package/tests/fixtures/agent-hooks/claude-user-prompt-submit.json +9 -0
- package/tests/fixtures/agent-hooks/codex-pre-tool-use.json +10 -0
- package/tests/fixtures/agent-hooks/codex-user-prompt-submit.json +7 -0
- package/tests/fixtures/agent-hooks/opencode-chat-message.json +36 -0
- package/tests/fixtures/agent-hooks/opencode-session-compacting.json +9 -0
- package/tests/fixtures/agent-hooks/opencode-todo-updated.json +13 -0
- package/tests/fixtures/agent-hooks/opencode-tool-execute-after.json +15 -0
- package/tests/fixtures/agent-hooks/opencode-tool-execute-before.json +12 -0
- package/tests/test_agent_compatibility_docs_gate.sh +123 -0
- package/tests/test_auto_orchestration_detection.sh +109 -0
- package/tests/test_claude_stop_hook_registration.sh +56 -0
- package/tests/test_clean_session_capture.sh +105 -0
- package/tests/test_codex_hooks_config.sh +73 -0
- package/tests/test_compaction_survival_matrix.sh +237 -0
- package/tests/test_dashboard.sh +96 -0
- package/tests/test_eagle_events.sh +96 -0
- package/tests/test_opencode_hooks_config.sh +56 -0
- package/tests/test_opencode_plugin_adapter.sh +202 -0
- package/tests/test_recall_observability.sh +144 -0
- package/tests/test_repair.sh +63 -0
- package/tests/test_rust_migration_plan.sh +75 -0
- 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;
|
package/lib/codex-hooks.sh
CHANGED
|
@@ -27,7 +27,7 @@ eagle_enable_codex_hooks() {
|
|
|
27
27
|
if [ ! -f "$config" ]; then
|
|
28
28
|
cat > "$config" << 'TOML'
|
|
29
29
|
[features]
|
|
30
|
-
|
|
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 "
|
|
48
|
+
print "hooks = true"
|
|
49
49
|
inserted=1
|
|
50
50
|
}
|
|
51
51
|
in_features=0
|
|
52
52
|
}
|
|
53
|
-
in_features && /^[[:space:]]*
|
|
54
|
-
print "
|
|
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 "
|
|
68
|
+
print "hooks = true"
|
|
62
69
|
inserted=1
|
|
63
70
|
}
|
|
64
71
|
if (!saw_features) {
|
|
65
72
|
print ""
|
|
66
73
|
print "[features]"
|
|
67
|
-
print "
|
|
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
|
-
**
|
|
1834
|
-
-
|
|
1835
|
-
-
|
|
1836
|
-
|
|
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
|
|
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
|
-
#
|
|
1857
|
-
|
|
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
|
package/lib/db-events.sh
ADDED
|
@@ -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
|
+
}
|