eagle-mem 4.10.12 → 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.
- package/CHANGELOG.md +25 -0
- package/README.md +20 -20
- 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/docs/agent-compatibility/README.md +38 -0
- package/docs/agent-compatibility/claude-code.md +50 -0
- package/docs/agent-compatibility/codex.md +51 -0
- package/docs/agent-compatibility/opencode.md +71 -0
- package/hooks/post-tool-use.sh +8 -0
- package/hooks/pre-tool-use.sh +11 -3
- package/hooks/session-end.sh +3 -0
- package/hooks/session-start.sh +7 -0
- package/hooks/stop.sh +10 -1
- package/hooks/user-prompt-submit.sh +79 -6
- package/integrations/opencode_eagle_mem_plugin.js +387 -0
- package/lib/codex-hooks.sh +13 -6
- package/lib/common.sh +71 -8
- package/lib/db-events.sh +89 -0
- package/lib/db-features.sh +26 -23
- package/lib/db-graph.sh +154 -0
- package/lib/db-observations.sh +34 -0
- package/lib/db-orchestration.sh +149 -0
- package/lib/db.sh +2 -0
- package/lib/hooks.sh +12 -7
- package/lib/opencode-hooks.sh +105 -0
- package/lib/provider.sh +2 -2
- package/package.json +5 -2
- package/scripts/compaction.sh +108 -8
- package/scripts/dashboard.sh +372 -0
- package/scripts/doctor.sh +30 -3
- package/scripts/health.sh +40 -2
- package/scripts/help.sh +10 -2
- package/scripts/inspect.sh +285 -0
- package/scripts/install.sh +31 -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/statusline-em.sh +34 -7
- package/scripts/tasks.sh +34 -0
- package/scripts/test.sh +14 -0
- package/scripts/uninstall.sh +9 -0
- package/scripts/update.sh +18 -2
- package/skills/eagle-mem-feature/SKILL.md +3 -3
- 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_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_feature_verification_gate.sh +230 -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_reliability_guards.sh +20 -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
|
}
|
|
@@ -1160,10 +1211,10 @@ eagle_is_release_boundary_command() {
|
|
|
1160
1211
|
function has_dry_run_flag(line) {
|
|
1161
1212
|
return line ~ /(^|[[:space:]])--dry-run([[:space:]]|$|=([Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss])([[:space:]]|$))/
|
|
1162
1213
|
}
|
|
1163
|
-
function
|
|
1164
|
-
return line ~ /(^|[[:space:]])([^[:space:]]*\/)?eagle-mem[[:space:]]+feature[[:space:]]+(verify|waive|pending|list)([[:space:]]|$)/
|
|
1214
|
+
function is_eagle_state_command(line) {
|
|
1215
|
+
return line ~ /(^|[[:space:]])([^[:space:]]*\/)?eagle-mem[[:space:]]+(feature[[:space:]]+(verify|waive|pending|list)|orchestrate|tasks)([[:space:]]|$)/
|
|
1165
1216
|
}
|
|
1166
|
-
|
|
1217
|
+
is_eagle_state_command($0) { next }
|
|
1167
1218
|
/(^|[[:space:]])gh[[:space:]]+pr[[:space:]]+create([[:space:]]|$)/ ||
|
|
1168
1219
|
/(^|[[:space:]])npm[[:space:]]+publish([[:space:]]|$)/ ||
|
|
1169
1220
|
/(^|[[:space:]])pnpm[[:space:]]+publish([[:space:]]|$)/ ||
|
|
@@ -1185,10 +1236,10 @@ eagle_is_release_boundary_command() {
|
|
|
1185
1236
|
function has_dry_run_flag(line) {
|
|
1186
1237
|
return line ~ /(^|[[:space:]])--dry-run([[:space:]]|$|=([Tt][Rr][Uu][Ee]|1|[Yy][Ee][Ss])([[:space:]]|$))/
|
|
1187
1238
|
}
|
|
1188
|
-
function
|
|
1189
|
-
return line ~ /(^|[[:space:]])([^[:space:]]*\/)?eagle-mem[[:space:]]+feature[[:space:]]+(verify|waive|pending|list)([[:space:]]|$)/
|
|
1239
|
+
function is_eagle_state_command(line) {
|
|
1240
|
+
return line ~ /(^|[[:space:]])([^[:space:]]*\/)?eagle-mem[[:space:]]+(feature[[:space:]]+(verify|waive|pending|list)|orchestrate|tasks)([[:space:]]|$)/
|
|
1190
1241
|
}
|
|
1191
|
-
|
|
1242
|
+
is_eagle_state_command($0) { next }
|
|
1192
1243
|
/(^|[[:space:]])git[[:space:]]+push([[:space:]]|$)/ {
|
|
1193
1244
|
if (!has_dry_run_flag($0)) found = 1
|
|
1194
1245
|
}
|
|
@@ -1301,10 +1352,10 @@ eagle_fts_sanitize() {
|
|
|
1301
1352
|
printf '%s' "$1" | sed 's/[^A-Za-z0-9_]/ /g' | sed 's/ */ /g; s/^ //; s/ $//'
|
|
1302
1353
|
}
|
|
1303
1354
|
|
|
1304
|
-
# Escape SQL LIKE wildcards
|
|
1355
|
+
# Escape SQL LIKE wildcards and the escape character so literal filenames match exactly.
|
|
1305
1356
|
# Apply AFTER eagle_sql_escape, since this only handles LIKE metacharacters.
|
|
1306
1357
|
eagle_like_escape() {
|
|
1307
|
-
printf '%s' "$1" | sed 's/%/\\%/g; s/_/\\_/g'
|
|
1358
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/%/\\%/g; s/_/\\_/g'
|
|
1308
1359
|
}
|
|
1309
1360
|
|
|
1310
1361
|
# Validate a session ID is safe for use in file paths (no traversal).
|
|
@@ -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:-}"
|
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
|
+
}
|