Skip to content

Commit 04554fb

Browse files
committed
feat: add project-memory skill with OpenCode hook integration
1 parent c08809b commit 04554fb

26 files changed

+798
-4
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ public/assets/material-icons/
3232

3333
# Nix
3434
result
35+
36+
# Project memory (agent context)
37+
.memory/

.memory/SUMMARY.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@
4747
- Agent/model selection from REST API
4848
- Incremental SSE streaming
4949

50-
---
51-
52-
*Last updated: Session initialization*
50+
## Quick Stats
51+
52+
- Total memories: 0
53+
- Last updated: 2025-02-24
54+
- Sessions since review: 0
55+
- Session tools available: true
56+
- Last bootstrap: 2025-02-24
57+
- Bootstrapped sessions: []
58+
- Project phase: foundation
59+
- Project phase epoch: 1
60+
- Close state: clean
61+
- Index dirty: false
62+
- Active session id: 2025-02-24-1
63+
- Last closed session id: ""

.opencode/opencode.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "https://opencode.ai/config.json",
3+
"plugin": ["./plugin/memory-hook.js"]
4+
}

.opencode/package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.opencode/plugin/memory-hook.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import * as fs from "node:fs";
2+
import * as path from "node:path";
3+
4+
const SKILL_LOCATIONS = [
5+
(dir) => path.join(dir, ".opencode", "skill", "project-memory", "SKILL.md"),
6+
() =>
7+
path.join(
8+
process.env.HOME || process.env.USERPROFILE || "",
9+
".config",
10+
"opencode",
11+
"skill",
12+
"project-memory",
13+
"SKILL.md"
14+
),
15+
() =>
16+
path.join(
17+
process.env.HOME || process.env.USERPROFILE || "",
18+
".claude",
19+
"skills",
20+
"project-memory",
21+
"SKILL.md"
22+
),
23+
];
24+
25+
function hasMemorySkill(projectDir) {
26+
return SKILL_LOCATIONS.some((loc) => {
27+
try {
28+
return fs.existsSync(loc(projectDir));
29+
} catch {
30+
return false;
31+
}
32+
});
33+
}
34+
35+
export const MemoryHook = async (ctx) => {
36+
const loadedSessions = new Set();
37+
const subagentSessions = new Set();
38+
39+
async function isSubagent(sessionID) {
40+
if (subagentSessions.has(sessionID)) return true;
41+
try {
42+
const session = await ctx.client.session.get({ path: { id: sessionID } });
43+
if (session.data?.parentID) {
44+
subagentSessions.add(sessionID);
45+
return true;
46+
}
47+
} catch {
48+
return false;
49+
}
50+
return false;
51+
}
52+
53+
return {
54+
"chat.message": async (input, output) => {
55+
if (!hasMemorySkill(ctx.directory)) return;
56+
if (loadedSessions.has(input.sessionID)) return;
57+
loadedSessions.add(input.sessionID);
58+
if (await isSubagent(input.sessionID)) return;
59+
60+
const textPart = output.parts.find((p) => p.type === "text");
61+
if (textPart?.text) {
62+
textPart.text = `<system-reminder>Load project memory: read .memory/SUMMARY.md before starting work. If .memory/ doesn't exist, initialize it per the project-memory skill.</system-reminder>\n\n${textPart.text}`;
63+
}
64+
},
65+
66+
"tool.execute.after": async (input, output) => {
67+
if (ctx.client.app?.log) {
68+
await ctx.client.app.log({
69+
body: {
70+
service: "memory-hook",
71+
level: "debug",
72+
message: `tool.execute.after fired: tool="${input.tool}"`,
73+
},
74+
query: { directory: ctx.directory },
75+
});
76+
}
77+
78+
const toolLower = (input.tool || "").toLowerCase().replace("mcp_", "");
79+
if (toolLower !== "todowrite") return;
80+
if (await isSubagent(input.sessionID)) return;
81+
82+
const todos = input.args?.todos;
83+
if (!Array.isArray(todos)) return;
84+
if (!todos.some((t) => t.status === "completed")) return;
85+
86+
output.output += `\n\n<system-reminder>Task completed. If this work produced knowledge worth remembering (re-discovering would cost meaningful time), capture it to .memory/ following the project-memory skill. Only save decisions, patterns, bug root causes, or preferences — skip routine changes.</system-reminder>`;
87+
},
88+
89+
"event": async ({ event }) => {
90+
if (
91+
event.type === "session.deleted" ||
92+
event.type === "session.compacted"
93+
) {
94+
const sid =
95+
event.properties?.info?.id || event.properties?.sessionID;
96+
if (sid) {
97+
loadedSessions.delete(sid);
98+
subagentSessions.delete(sid);
99+
}
100+
}
101+
},
102+
};
103+
};

0 commit comments

Comments
 (0)