opencode-fractal-memory 0.2.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/LICENSE +21 -0
- package/README.md +493 -0
- package/agent/memory-hints.md +98 -0
- package/agent/memory-researcher.md +56 -0
- package/commands/memory-auto-test.md +10 -0
- package/commands/memory-cache-status.md +13 -0
- package/commands/memory-check-context.md +4 -0
- package/commands/memory-compress.md +13 -0
- package/commands/memory-dashboard.md +23 -0
- package/commands/memory-delete.md +24 -0
- package/commands/memory-detect-topics.md +28 -0
- package/commands/memory-distill.md +35 -0
- package/commands/memory-drilldown-query.md +28 -0
- package/commands/memory-drilldown.md +11 -0
- package/commands/memory-extract-patterns.md +4 -0
- package/commands/memory-generate-embeddings.md +26 -0
- package/commands/memory-get.md +26 -0
- package/commands/memory-help.md +55 -0
- package/commands/memory-injection-feedback.md +26 -0
- package/commands/memory-injection-stats.md +11 -0
- package/commands/memory-list.md +4 -0
- package/commands/memory-llm-compress.md +34 -0
- package/commands/memory-mcp.md +20 -0
- package/commands/memory-prune.md +4 -0
- package/commands/memory-rate.md +48 -0
- package/commands/memory-reflect.md +37 -0
- package/commands/memory-replace.md +26 -0
- package/commands/memory-retrieve.md +34 -0
- package/commands/memory-search.md +28 -0
- package/commands/memory-session-stats.md +4 -0
- package/commands/memory-set.md +31 -0
- package/commands/memory-stats.md +11 -0
- package/commands/memory-summarize.md +29 -0
- package/commands/memory-tool-stats.md +4 -0
- package/commands/memory-total-tokens.md +10 -0
- package/commands/memory-verify.md +4 -0
- package/commands/memory-version.md +9 -0
- package/dist/cache.js +39 -0
- package/dist/config.js +120 -0
- package/dist/embeddings.js +125 -0
- package/dist/ensure-models.js +70 -0
- package/dist/file-summary.js +143 -0
- package/dist/frontmatter.js +28 -0
- package/dist/hnsw-index.js +138 -0
- package/dist/hooks/auto-discover.js +4 -0
- package/dist/hooks/auto-distill.js +120 -0
- package/dist/hooks/auto-retrieve/content.js +47 -0
- package/dist/hooks/auto-retrieve/detection.js +50 -0
- package/dist/hooks/auto-retrieve/formatting.js +19 -0
- package/dist/hooks/auto-retrieve/index.js +163 -0
- package/dist/hooks/auto-retrieve/scoring.js +56 -0
- package/dist/hooks/auto-retrieve.js +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/predictive-rating.js +87 -0
- package/dist/journal.js +279 -0
- package/dist/logging.js +147 -0
- package/dist/management/helpers.js +227 -0
- package/dist/management/router.js +48 -0
- package/dist/management/routes.js +197 -0
- package/dist/management-server.js +4 -0
- package/dist/management-standalone.js +31 -0
- package/dist/mcp/logging.js +57 -0
- package/dist/mcp/server.js +251 -0
- package/dist/mcp/transform.js +48 -0
- package/dist/mcp-server.js +18 -0
- package/dist/memory.js +2 -0
- package/dist/ollama.js +74 -0
- package/dist/plugin/hooks.js +168 -0
- package/dist/plugin/index.js +28 -0
- package/dist/plugin/init.js +109 -0
- package/dist/plugin/state.js +75 -0
- package/dist/plugin/tools.js +45 -0
- package/dist/plugin.js +2 -0
- package/dist/procedural/store.js +1 -0
- package/dist/procedural/types.js +1 -0
- package/dist/seed-nodes.js +804 -0
- package/dist/storage/compress-ops.js +129 -0
- package/dist/storage/compression/formatters.js +243 -0
- package/dist/storage/compression/index.js +107 -0
- package/dist/storage/compression/patterns.js +138 -0
- package/dist/storage/expiration.js +66 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/injection-events.js +82 -0
- package/dist/storage/lifecycle.js +65 -0
- package/dist/storage/maintenance.js +60 -0
- package/dist/storage/migrations/definitions.js +374 -0
- package/dist/storage/migrations/index.js +21 -0
- package/dist/storage/navigation.js +98 -0
- package/dist/storage/queries/base.js +44 -0
- package/dist/storage/queries/links.js +32 -0
- package/dist/storage/queries/nodes.js +189 -0
- package/dist/storage/queries/search-helpers.js +239 -0
- package/dist/storage/scoring.js +36 -0
- package/dist/storage/search.js +233 -0
- package/dist/storage/session-tracking.js +180 -0
- package/dist/storage/sqlite.js +329 -0
- package/dist/storage/tool-usage.js +56 -0
- package/dist/storage/types.js +1 -0
- package/dist/storage/utils.js +94 -0
- package/dist/tools/auto-test.js +24 -0
- package/dist/tools/cache-status.js +36 -0
- package/dist/tools/compress.js +186 -0
- package/dist/tools/core.js +307 -0
- package/dist/tools/dashboard.js +97 -0
- package/dist/tools/help.js +59 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/inject.js +91 -0
- package/dist/tools/injection-debug.js +48 -0
- package/dist/tools/journal.js +105 -0
- package/dist/tools/llm-compress.js +41 -0
- package/dist/tools/middle-term.js +68 -0
- package/dist/tools/playbook.js +64 -0
- package/dist/tools/reflect.js +291 -0
- package/dist/tools/search.js +188 -0
- package/dist/tools/session.js +189 -0
- package/dist/tools/shared.js +74 -0
- package/dist/tools/skill.js +37 -0
- package/dist/tools/stats.js +256 -0
- package/dist/tools/version.js +13 -0
- package/dist/tools.js +18 -0
- package/dist/utils/hybridScore.js +67 -0
- package/management/public/app.js +1529 -0
- package/management/public/index.html +486 -0
- package/management/public/three.min.js +6 -0
- package/package.json +65 -0
- package/scripts/download-models.ts +16 -0
- package/scripts/postinstall.cjs +30 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createSqliteMemoryStore } from "../storage/sqlite";
|
|
4
|
+
import { withMcpLogging, mcpLog } from "./logging";
|
|
5
|
+
import { nodeToPlain, ensureScope, resourceStats } from "./transform";
|
|
6
|
+
let store;
|
|
7
|
+
export async function createMemoryMcpServer(projectDir, globalDbPath) {
|
|
8
|
+
store = createSqliteMemoryStore(projectDir, globalDbPath);
|
|
9
|
+
const server = new McpServer({ name: "opencode-fractal-memory", version: "0.1.0" }, { capabilities: { tools: {}, resources: {} } });
|
|
10
|
+
server.registerTool("memory_search", {
|
|
11
|
+
description: "Search memory nodes by text, embedding similarity, or BM25 score",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
query: z.string().describe("Search query"),
|
|
14
|
+
scope: z.enum(["global", "project"]).optional().default("project").describe("Memory scope"),
|
|
15
|
+
mode: z.enum(["text", "embedding", "bm25"]).optional().default("text").describe("Search mode"),
|
|
16
|
+
limit: z.number().int().positive().optional().default(50).describe("Max results"),
|
|
17
|
+
},
|
|
18
|
+
}, withMcpLogging("memory_search", async (args) => {
|
|
19
|
+
const q = args.query.trim();
|
|
20
|
+
if (!q)
|
|
21
|
+
return { content: [{ type: "text", text: "[]" }] };
|
|
22
|
+
const scope = args.scope;
|
|
23
|
+
const limit = args.limit;
|
|
24
|
+
try {
|
|
25
|
+
if (args.mode === "text") {
|
|
26
|
+
const nodes = await store.listNodes(scope);
|
|
27
|
+
const lower = q.toLowerCase();
|
|
28
|
+
const results = nodes
|
|
29
|
+
.filter(n => (n.label?.toLowerCase().includes(lower)) || n.content.toLowerCase().includes(lower))
|
|
30
|
+
.sort((a, b) => b.importance - a.importance)
|
|
31
|
+
.slice(0, limit)
|
|
32
|
+
.map(n => ({ ...nodeToPlain(n), score: n.importance }));
|
|
33
|
+
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
34
|
+
}
|
|
35
|
+
if (args.mode === "embedding") {
|
|
36
|
+
const { generateEmbedding } = await import("../embeddings");
|
|
37
|
+
const queryEmbedding = await generateEmbedding(q);
|
|
38
|
+
const nodes = await store.searchByEmbedding(queryEmbedding, limit, { queryText: q });
|
|
39
|
+
return { content: [{ type: "text", text: JSON.stringify(nodes.map(n => nodeToPlain(n)), null, 2) }] };
|
|
40
|
+
}
|
|
41
|
+
if (args.mode === "bm25") {
|
|
42
|
+
const nodes = await store.listNodes(scope);
|
|
43
|
+
const terms = q.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter(t => t.length >= 2);
|
|
44
|
+
if (terms.length === 0)
|
|
45
|
+
return { content: [{ type: "text", text: "[]" }] };
|
|
46
|
+
const scored = nodes.map(n => {
|
|
47
|
+
const text = `${n.label ?? ""} ${n.content}`.toLowerCase();
|
|
48
|
+
let score = 0;
|
|
49
|
+
for (const term of terms) {
|
|
50
|
+
const regex = new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "gi");
|
|
51
|
+
const matches = text.match(regex);
|
|
52
|
+
score += matches ? matches.length : 0;
|
|
53
|
+
}
|
|
54
|
+
return { ...nodeToPlain(n), score };
|
|
55
|
+
});
|
|
56
|
+
scored.sort((a, b) => b.score - a.score);
|
|
57
|
+
return { content: [{ type: "text", text: JSON.stringify(scored.slice(0, limit), null, 2) }] };
|
|
58
|
+
}
|
|
59
|
+
return { content: [{ type: "text", text: "[]" }] };
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}));
|
|
68
|
+
server.registerTool("memory_get", {
|
|
69
|
+
description: "Get a single memory node by ID",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
id: z.string().describe("Node ID"),
|
|
72
|
+
scope: z.enum(["global", "project"]).optional().default("project").describe("Memory scope"),
|
|
73
|
+
},
|
|
74
|
+
}, withMcpLogging("memory_get", async (args) => {
|
|
75
|
+
try {
|
|
76
|
+
const node = await store.getNode(args.id);
|
|
77
|
+
return { content: [{ type: "text", text: JSON.stringify(nodeToPlain(node), null, 2) }] };
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
81
|
+
if (msg.includes("not found")) {
|
|
82
|
+
return { content: [{ type: "text", text: "Node not found" }], isError: true };
|
|
83
|
+
}
|
|
84
|
+
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
|
|
85
|
+
}
|
|
86
|
+
}));
|
|
87
|
+
server.registerTool("memory_fetch", {
|
|
88
|
+
description: "Fetch a memory node by exact label",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
label: z.string().describe("Node label"),
|
|
91
|
+
scope: z.enum(["global", "project"]).optional().default("project").describe("Memory scope"),
|
|
92
|
+
},
|
|
93
|
+
}, withMcpLogging("memory_fetch", async (args) => {
|
|
94
|
+
try {
|
|
95
|
+
const node = await store.getNodeByLabel(ensureScope(args.scope), args.label);
|
|
96
|
+
return { content: [{ type: "text", text: JSON.stringify(nodeToPlain(node), null, 2) }] };
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
100
|
+
if (msg.includes("not found")) {
|
|
101
|
+
return { content: [{ type: "text", text: "Node not found" }], isError: true };
|
|
102
|
+
}
|
|
103
|
+
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
|
|
104
|
+
}
|
|
105
|
+
}));
|
|
106
|
+
server.registerTool("memory_list", {
|
|
107
|
+
description: "List memory nodes with optional filters",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
scope: z.enum(["global", "project"]).optional().default("project").describe("Memory scope"),
|
|
110
|
+
level: z.number().int().min(0).max(4).optional().describe("Filter by compression level"),
|
|
111
|
+
type: z.string().optional().describe("Filter by node type (note, event, concept, summary, etc.)"),
|
|
112
|
+
limit: z.number().int().positive().optional().default(100).describe("Max results"),
|
|
113
|
+
offset: z.number().int().nonnegative().optional().default(0).describe("Pagination offset"),
|
|
114
|
+
},
|
|
115
|
+
}, withMcpLogging("memory_list", async (args) => {
|
|
116
|
+
try {
|
|
117
|
+
let nodes = await store.listNodes(args.scope, args.level);
|
|
118
|
+
if (args.type) {
|
|
119
|
+
nodes = nodes.filter(n => n.type === args.type);
|
|
120
|
+
}
|
|
121
|
+
const total = nodes.length;
|
|
122
|
+
if (args.offset > 0)
|
|
123
|
+
nodes = nodes.slice(args.offset);
|
|
124
|
+
if (args.limit)
|
|
125
|
+
nodes = nodes.slice(0, args.limit);
|
|
126
|
+
return {
|
|
127
|
+
content: [{ type: "text", text: JSON.stringify({ total, nodes: nodes.map(n => nodeToPlain(n)) }, null, 2) }],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
133
|
+
isError: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}));
|
|
137
|
+
server.registerTool("memory_stats", {
|
|
138
|
+
description: "Get memory statistics for a scope",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
scope: z.enum(["global", "project"]).optional().default("project").describe("Memory scope"),
|
|
141
|
+
},
|
|
142
|
+
}, withMcpLogging("memory_stats", async (args) => {
|
|
143
|
+
try {
|
|
144
|
+
const [stats, nodes] = await Promise.all([
|
|
145
|
+
store.getFractalStats(args.scope),
|
|
146
|
+
store.listNodes(args.scope),
|
|
147
|
+
]);
|
|
148
|
+
const total = stats.totalNodes;
|
|
149
|
+
const avgImportance = total > 0 ? nodes.reduce((s, n) => s + n.importance, 0) / total : 0;
|
|
150
|
+
const avgUsefulness = total > 0 ? nodes.reduce((s, n) => s + (n.usefulnessScore ?? 0), 0) / total : 0;
|
|
151
|
+
const stickyCount = nodes.filter(n => n.sticky).length;
|
|
152
|
+
const types = {};
|
|
153
|
+
for (const n of nodes) {
|
|
154
|
+
const t = n.type ?? "unknown";
|
|
155
|
+
types[t] = (types[t] ?? 0) + 1;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
content: [{
|
|
159
|
+
type: "text",
|
|
160
|
+
text: JSON.stringify({
|
|
161
|
+
scope: args.scope,
|
|
162
|
+
totalNodes: total,
|
|
163
|
+
nodesPerLevel: stats.nodesPerLevel,
|
|
164
|
+
nodesPerType: types,
|
|
165
|
+
avgImportance: Math.round(avgImportance * 100) / 100,
|
|
166
|
+
avgUsefulness: Math.round(avgUsefulness * 100) / 100,
|
|
167
|
+
totalAccessCount: nodes.reduce((s, n) => s + n.accessCount, 0),
|
|
168
|
+
stickyCount,
|
|
169
|
+
}, null, 2),
|
|
170
|
+
}],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
176
|
+
isError: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}));
|
|
180
|
+
server.registerTool("memory_set", {
|
|
181
|
+
description: "Create or update a memory node",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
label: z.string().describe("Node label (if updating an existing node, provide existing label)"),
|
|
184
|
+
content: z.string().describe("Node content"),
|
|
185
|
+
scope: z.enum(["global", "project"]).optional().default("project").describe("Memory scope"),
|
|
186
|
+
level: z.number().int().min(0).max(4).optional().default(0).describe("Compression level"),
|
|
187
|
+
type: z.string().optional().describe("Node type (note, event, concept, summary, etc.)"),
|
|
188
|
+
importance: z.number().min(0).max(1).optional().default(0.5).describe("Importance score"),
|
|
189
|
+
},
|
|
190
|
+
}, withMcpLogging("memory_set", async (args) => {
|
|
191
|
+
const scope = ensureScope(args.scope);
|
|
192
|
+
try {
|
|
193
|
+
const existing = await store.getNodeByLabel(scope, args.label).catch(() => null);
|
|
194
|
+
if (existing) {
|
|
195
|
+
await store.updateNode(existing.id, {
|
|
196
|
+
content: args.content,
|
|
197
|
+
level: args.level,
|
|
198
|
+
type: args.type ?? null,
|
|
199
|
+
importance: args.importance,
|
|
200
|
+
});
|
|
201
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, id: existing.id, action: "updated" }) }] };
|
|
202
|
+
}
|
|
203
|
+
const node = await store.createNode({
|
|
204
|
+
scope,
|
|
205
|
+
label: args.label,
|
|
206
|
+
content: args.content,
|
|
207
|
+
level: args.level,
|
|
208
|
+
type: args.type ?? null,
|
|
209
|
+
importance: args.importance,
|
|
210
|
+
});
|
|
211
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, id: node.id, action: "created" }) }] };
|
|
212
|
+
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
return {
|
|
215
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
216
|
+
isError: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}));
|
|
220
|
+
server.registerTool("memory_delete", {
|
|
221
|
+
description: "Delete a memory node by ID",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
id: z.string().describe("Node ID to delete"),
|
|
224
|
+
scope: z.enum(["global", "project"]).optional().default("project").describe("Memory scope"),
|
|
225
|
+
},
|
|
226
|
+
}, withMcpLogging("memory_delete", async (args) => {
|
|
227
|
+
try {
|
|
228
|
+
const node = await store.getNode(args.id).catch(() => null);
|
|
229
|
+
if (!node) {
|
|
230
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: "Node not found" }) }], isError: true };
|
|
231
|
+
}
|
|
232
|
+
await store.deleteNode(args.id);
|
|
233
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, id: args.id, action: "deleted" }) }] };
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
return {
|
|
237
|
+
content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
|
|
238
|
+
isError: true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}));
|
|
242
|
+
server.registerResource("Memory Stats (project)", "memory://stats/project", { description: "Memory statistics for project scope" }, async (uri) => {
|
|
243
|
+
mcpLog("info", "resource:read", { uri: uri.href });
|
|
244
|
+
return { contents: [{ uri: uri.href, text: await resourceStats("project", store), mimeType: "application/json" }] };
|
|
245
|
+
});
|
|
246
|
+
server.registerResource("Memory Stats (global)", "memory://stats/global", { description: "Memory statistics for global scope" }, async (uri) => {
|
|
247
|
+
mcpLog("info", "resource:read", { uri: uri.href });
|
|
248
|
+
return { contents: [{ uri: uri.href, text: await resourceStats("global", store), mimeType: "application/json" }] };
|
|
249
|
+
});
|
|
250
|
+
return server;
|
|
251
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function nodeToPlain(n) {
|
|
2
|
+
return {
|
|
3
|
+
id: n.id,
|
|
4
|
+
label: n.label ?? "",
|
|
5
|
+
content: n.content,
|
|
6
|
+
summary: n.summary,
|
|
7
|
+
level: n.level,
|
|
8
|
+
type: n.type,
|
|
9
|
+
importance: n.importance,
|
|
10
|
+
usefulnessScore: n.usefulnessScore,
|
|
11
|
+
timesUsed: n.timesUsed,
|
|
12
|
+
timesHelpful: n.timesHelpful,
|
|
13
|
+
accessCount: n.accessCount,
|
|
14
|
+
sticky: n.sticky,
|
|
15
|
+
confidence: n.confidence,
|
|
16
|
+
createdAt: n.createdAt instanceof Date ? n.createdAt.getTime() : n.createdAt,
|
|
17
|
+
updatedAt: n.updatedAt instanceof Date ? n.updatedAt.getTime() : n.updatedAt,
|
|
18
|
+
parentIds: n.parentIds,
|
|
19
|
+
contentLength: n.content.length,
|
|
20
|
+
metadata: n.metadata,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function ensureScope(s, defaultScope = "project") {
|
|
24
|
+
if (s === "global" || s === "project")
|
|
25
|
+
return s;
|
|
26
|
+
return defaultScope;
|
|
27
|
+
}
|
|
28
|
+
export async function resourceStats(scope, store) {
|
|
29
|
+
try {
|
|
30
|
+
const [stats, nodes] = await Promise.all([
|
|
31
|
+
store.getFractalStats(scope),
|
|
32
|
+
store.listNodes(scope),
|
|
33
|
+
]);
|
|
34
|
+
const total = stats.totalNodes;
|
|
35
|
+
const avgImportance = total > 0 ? nodes.reduce((s, n) => s + n.importance, 0) / total : 0;
|
|
36
|
+
const avgUsefulness = total > 0 ? nodes.reduce((s, n) => s + (n.usefulnessScore ?? 0), 0) / total : 0;
|
|
37
|
+
return JSON.stringify({
|
|
38
|
+
scope,
|
|
39
|
+
totalNodes: total,
|
|
40
|
+
nodesPerLevel: stats.nodesPerLevel,
|
|
41
|
+
avgImportance: Math.round(avgImportance * 100) / 100,
|
|
42
|
+
avgUsefulness: Math.round(avgUsefulness * 100) / 100,
|
|
43
|
+
}, null, 2);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return JSON.stringify({ scope, error: "Database not found" });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { createMemoryMcpServer } from "./mcp/server";
|
|
6
|
+
import { mcpLog } from "./mcp/logging";
|
|
7
|
+
const projectDir = process.env.MGMT_PROJECT_DIR || process.cwd();
|
|
8
|
+
const globalDbPath = path.join(os.homedir(), ".config", "opencode", "memory.db");
|
|
9
|
+
async function main() {
|
|
10
|
+
mcpLog("info", "MCP server starting", { name: "opencode-fractal-memory", version: "0.1.0" });
|
|
11
|
+
const server = await createMemoryMcpServer(projectDir, globalDbPath);
|
|
12
|
+
const transport = new StdioServerTransport();
|
|
13
|
+
await server.connect(transport);
|
|
14
|
+
mcpLog("info", "MCP server connected");
|
|
15
|
+
}
|
|
16
|
+
if (import.meta.main) {
|
|
17
|
+
main().catch(console.error);
|
|
18
|
+
}
|
package/dist/memory.js
ADDED
package/dist/ollama.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Ollama client for chat and reranking
|
|
2
|
+
const OLLAMA_BASE = "http://localhost:11434";
|
|
3
|
+
import { memLog } from "./logging";
|
|
4
|
+
export async function chat(messages, opts = {}) {
|
|
5
|
+
const baseUrl = opts.baseUrl ?? OLLAMA_BASE;
|
|
6
|
+
const model = opts.model ?? "qwen2.5-coder:1.5b";
|
|
7
|
+
const res = await fetch(`${baseUrl}/api/chat`, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: { "Content-Type": "application/json" },
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
model,
|
|
12
|
+
messages,
|
|
13
|
+
stream: false,
|
|
14
|
+
temperature: opts.temperature ?? 0.7,
|
|
15
|
+
seed: opts.seed,
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
throw new Error(`Ollama error: ${res.status} ${res.statusText}`);
|
|
20
|
+
}
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
return data.message?.content ?? "";
|
|
23
|
+
}
|
|
24
|
+
export async function rerankDocuments(query, candidates, opts = {}) {
|
|
25
|
+
const baseUrl = opts.baseUrl ?? OLLAMA_BASE;
|
|
26
|
+
const model = opts.model ?? "qwen2.5-coder:1.5b";
|
|
27
|
+
const topK = opts.topK ?? 5;
|
|
28
|
+
memLog("debug", "ollama", "[ollama] rerankDocuments called:", { model, baseUrl, candidateCount: candidates.length, topK });
|
|
29
|
+
const docsList = candidates.map((doc, i) => {
|
|
30
|
+
const content = doc.content.slice(0, 500);
|
|
31
|
+
return `${i + 1}. [${doc.label}]: ${content}`;
|
|
32
|
+
}).join("\n\n");
|
|
33
|
+
const prompt = `You are a relevance judge. Rate each document as 1 (relevant) or 0 (not relevant) for the query.
|
|
34
|
+
|
|
35
|
+
IMPORTANT: You MUST return EXACTLY ${candidates.length} scores - one per document, in order.
|
|
36
|
+
Example for 3 docs: [1, 0, 1]
|
|
37
|
+
Your response must be ONLY a JSON array of ${candidates.length} numbers.
|
|
38
|
+
|
|
39
|
+
Query: ${query}
|
|
40
|
+
|
|
41
|
+
Documents:
|
|
42
|
+
${docsList}
|
|
43
|
+
|
|
44
|
+
Response (EXACTLY ${candidates.length} scores as JSON array):`;
|
|
45
|
+
try {
|
|
46
|
+
const response = await chat([{ role: "user", content: prompt }], { baseUrl, model, temperature: 0.0 });
|
|
47
|
+
const jsonMatch = response.match(/\[.*?\]/s);
|
|
48
|
+
if (!jsonMatch) {
|
|
49
|
+
memLog("error", "ollama", "[ollama] No JSON array found in response:", { response: response.slice(0, 200) });
|
|
50
|
+
return candidates.slice(0, topK).map(d => ({ id: d.id, score: 1 }));
|
|
51
|
+
}
|
|
52
|
+
const scores = JSON.parse(jsonMatch[0]);
|
|
53
|
+
if (!Array.isArray(scores) || scores.length === 0) {
|
|
54
|
+
memLog("error", "ollama", "[ollama] No valid scores in response:", { response: response.slice(0, 100) });
|
|
55
|
+
return candidates.slice(0, topK).map(d => ({ id: d.id, score: 1 }));
|
|
56
|
+
}
|
|
57
|
+
// Handle mismatch gracefully: pad with 1s or trim excess
|
|
58
|
+
const normalizedScores = [...scores];
|
|
59
|
+
while (normalizedScores.length < candidates.length) {
|
|
60
|
+
normalizedScores.push(1); // Pad with 1s (relevant)
|
|
61
|
+
}
|
|
62
|
+
const finalScores = normalizedScores.slice(0, candidates.length);
|
|
63
|
+
const results = candidates.map((d, i) => ({
|
|
64
|
+
id: d.id,
|
|
65
|
+
score: Number(finalScores[i]) === 1 ? 1 : 0,
|
|
66
|
+
}));
|
|
67
|
+
results.sort((a, b) => b.score - a.score);
|
|
68
|
+
return results.slice(0, topK);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
memLog("error", "ollama", "Ollama rerank error:", { error: err instanceof Error ? err.message : err });
|
|
72
|
+
return candidates.slice(0, topK).map(d => ({ id: d.id, score: 1 }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { memLog, memLogSimple, setSessionId } from "../logging";
|
|
2
|
+
import { generateFileSummary, generateFileLabel, SOURCE_FILE_EXTENSIONS } from "../file-summary";
|
|
3
|
+
import { distillRules, predictiveRateToolCall, applyScoreDecay } from "../hooks";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
export function createHookHandlers(store, client, memConfig, ruleCache, ruleCacheDirty, sessionInjectionLock, latestUserMessage) {
|
|
6
|
+
return {
|
|
7
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
8
|
+
const sessionId = input.sessionID ?? `session-${Date.now()}`;
|
|
9
|
+
setSessionId(sessionId);
|
|
10
|
+
if (sessionInjectionLock.get(sessionId))
|
|
11
|
+
return;
|
|
12
|
+
sessionInjectionLock.set(sessionId, true);
|
|
13
|
+
const reminders = [];
|
|
14
|
+
try {
|
|
15
|
+
if (ruleCacheDirty.value || ruleCache.size === 0) {
|
|
16
|
+
const ruleLabels = [
|
|
17
|
+
{ label: "rule:mandatory:memory", type: "mandatory" },
|
|
18
|
+
{ label: "rule:mandatory:core", type: "mandatory" },
|
|
19
|
+
{ label: "rule:mandatory:agent-pull", type: "mandatory" },
|
|
20
|
+
{ label: "rule:mandatory:tools", type: "mandatory" },
|
|
21
|
+
{ label: "rule:standard", type: "standard" },
|
|
22
|
+
{ label: "rule:suggestion", type: "suggestion" },
|
|
23
|
+
];
|
|
24
|
+
for (const { label, type } of ruleLabels) {
|
|
25
|
+
let node = null;
|
|
26
|
+
for (const scope of ["global", "project"]) {
|
|
27
|
+
try {
|
|
28
|
+
node = await store.getNodeByLabel(scope, label);
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore */ }
|
|
32
|
+
}
|
|
33
|
+
if (node) {
|
|
34
|
+
let content = node.content || "";
|
|
35
|
+
content = content.replace(/^## .*$/gm, "").replace(/^tag:.*$/gm, "").trim();
|
|
36
|
+
if (content)
|
|
37
|
+
ruleCache.set(label, { content, type });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
ruleCacheDirty.value = false;
|
|
41
|
+
}
|
|
42
|
+
for (const [label, cached] of ruleCache) {
|
|
43
|
+
if (cached.type === "mandatory" || cached.type === "standard") {
|
|
44
|
+
reminders.push(`<system_reminder type="${cached.type}">\n${cached.content}\n</system_reminder>`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (reminders.length > 0) {
|
|
48
|
+
const insertAt = output.system.length > 0 ? 1 : 0;
|
|
49
|
+
output.system.splice(insertAt, 0, reminders.join("\n\n"));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
memLog("error", "injection", "Rule injection failed", { error: String(err) });
|
|
54
|
+
}
|
|
55
|
+
output.system = output.system.map((part) => part.replace(/<available_skills>[\s\S]*?<\/available_skills>/g, ""));
|
|
56
|
+
sessionInjectionLock.delete(sessionId);
|
|
57
|
+
},
|
|
58
|
+
"tool.execute.before": async (input, output) => {
|
|
59
|
+
const fileSummarization = memConfig?.autoFileSummarization;
|
|
60
|
+
if (!fileSummarization?.enabled)
|
|
61
|
+
return;
|
|
62
|
+
if (input.tool !== "read")
|
|
63
|
+
return;
|
|
64
|
+
const toolInput = input;
|
|
65
|
+
if (!toolInput.args?.filePath)
|
|
66
|
+
return;
|
|
67
|
+
const filePath = toolInput.args.filePath;
|
|
68
|
+
const fileExt = filePath.split('.').pop() ?? "";
|
|
69
|
+
if (!SOURCE_FILE_EXTENSIONS.includes(fileExt))
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
const shortLabel = generateFileLabel(filePath);
|
|
73
|
+
const nodes = await store.listNodes("project");
|
|
74
|
+
const cached = nodes.find(n => n.label === shortLabel);
|
|
75
|
+
if (cached) {
|
|
76
|
+
output.output = cached.content;
|
|
77
|
+
output.metadata = {
|
|
78
|
+
...(output.metadata ?? {}),
|
|
79
|
+
cached: true,
|
|
80
|
+
};
|
|
81
|
+
memLogSimple(`FILE-CACHE-HIT: ${filePath} (${cached.content.length} chars)`, {
|
|
82
|
+
label: shortLabel,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
memLogSimple(`FILE-CACHE-MISS: ${filePath}`, { label: shortLabel });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
memLog("warn", "file-summary", "Cache lookup failed", { error: String(err) });
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"tool.execute.after": async (input, output) => {
|
|
94
|
+
if (input.tool?.startsWith("memory_")) {
|
|
95
|
+
await store.recordMemoryToolCall(input.sessionID ?? "unknown", input.tool, input.args);
|
|
96
|
+
}
|
|
97
|
+
const success = output.metadata?.error ? false : true;
|
|
98
|
+
if (memConfig?.predictiveRating?.enabled && input.tool?.startsWith("memory_")) {
|
|
99
|
+
predictiveRateToolCall(store, input, output, memConfig.predictiveRating).catch(err => memLog("warn", "predictive-rating", "Rating failed", { error: String(err) }));
|
|
100
|
+
}
|
|
101
|
+
if (!success)
|
|
102
|
+
return;
|
|
103
|
+
const fileSummarization = memConfig?.autoFileSummarization;
|
|
104
|
+
if (!fileSummarization?.enabled)
|
|
105
|
+
return;
|
|
106
|
+
if (input.tool !== "read")
|
|
107
|
+
return;
|
|
108
|
+
const toolInput = input;
|
|
109
|
+
if (!toolInput.args?.filePath)
|
|
110
|
+
return;
|
|
111
|
+
const isCached = output.metadata?.cached === true;
|
|
112
|
+
if (isCached)
|
|
113
|
+
return;
|
|
114
|
+
const filePath = toolInput.args.filePath;
|
|
115
|
+
const fileName = filePath.split('/').pop() ?? filePath;
|
|
116
|
+
const fileExt = fileName.split('.').pop() ?? "";
|
|
117
|
+
if (!SOURCE_FILE_EXTENSIONS.includes(fileExt))
|
|
118
|
+
return;
|
|
119
|
+
try {
|
|
120
|
+
const shortLabel = generateFileLabel(filePath);
|
|
121
|
+
const nodes = await store.listNodes("project");
|
|
122
|
+
const exists = nodes.some(n => n.label === shortLabel);
|
|
123
|
+
if (exists) {
|
|
124
|
+
memLog("debug", "file-summary", "File memory already exists, skipping", { shortLabel });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
let fullContent = "";
|
|
128
|
+
try {
|
|
129
|
+
fullContent = fs.readFileSync(filePath, "utf-8");
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
memLog("warn", "file-summary", "Could not read full file, using tool output", { filePath, error: String(err) });
|
|
133
|
+
fullContent = String(output.output ?? "");
|
|
134
|
+
}
|
|
135
|
+
const content = generateFileSummary(fileName, filePath, fullContent, fileExt);
|
|
136
|
+
await store.createNode({
|
|
137
|
+
scope: "project",
|
|
138
|
+
label: shortLabel,
|
|
139
|
+
content,
|
|
140
|
+
type: "note",
|
|
141
|
+
level: 0,
|
|
142
|
+
parentIds: null,
|
|
143
|
+
embedding: null,
|
|
144
|
+
importance: 0.7,
|
|
145
|
+
});
|
|
146
|
+
memLog("info", "file-summary", "Stored file memory", { label: shortLabel, fileName });
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
memLog("warn", "file-summary", "Failed to store file memory", { error: String(err) });
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"session.created": async (event) => {
|
|
153
|
+
const sessionId = event.properties?.info?.id ?? "unknown";
|
|
154
|
+
await store.createSessionMetrics(sessionId);
|
|
155
|
+
setSessionId(sessionId);
|
|
156
|
+
},
|
|
157
|
+
"session.idle": async (event) => {
|
|
158
|
+
const sessionId = event.properties?.sessionID ?? "unknown";
|
|
159
|
+
await store.updateSessionMetrics(sessionId, { endedAt: Date.now(), status: "completed" });
|
|
160
|
+
if (memConfig?.autoDistill?.enabled) {
|
|
161
|
+
distillRules(store, memConfig.autoDistill, sessionId, client).then(msg => memLog("info", "auto-distill", msg)).catch(err => memLog("error", "auto-distill", "Failed", { error: String(err) }));
|
|
162
|
+
}
|
|
163
|
+
if (memConfig?.predictiveRating?.enabled) {
|
|
164
|
+
applyScoreDecay(store, memConfig.predictiveRating).then(msg => memLog("info", "predictive-rating", msg)).catch(err => memLog("error", "predictive-rating", "Decay failed", { error: String(err) }));
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { initStorage, loadPluginConfig, seedRuleNodes, backfillData, scheduleBackgroundEmbeddings, setupJournal, startManagementIfEnabled, createAutoRetrieveIfEnabled } from "./init";
|
|
2
|
+
import { createHookHandlers } from "./hooks";
|
|
3
|
+
import { createToolMap } from "./tools";
|
|
4
|
+
export const MemoryPlugin = async (ctx) => {
|
|
5
|
+
const { directory, client } = ctx;
|
|
6
|
+
const store = await initStorage(directory);
|
|
7
|
+
const memConfig = await loadPluginConfig(directory);
|
|
8
|
+
await seedRuleNodes(store);
|
|
9
|
+
await backfillData(store);
|
|
10
|
+
scheduleBackgroundEmbeddings(store);
|
|
11
|
+
const journalTools = await setupJournal(directory);
|
|
12
|
+
startManagementIfEnabled(store, directory);
|
|
13
|
+
const ruleCache = new Map();
|
|
14
|
+
const ruleCacheDirty = { value: true };
|
|
15
|
+
const sessionInjectionLock = new Map();
|
|
16
|
+
const latestUserMessage = { value: "" };
|
|
17
|
+
const autoRetrieveHook = createAutoRetrieveIfEnabled(store, memConfig);
|
|
18
|
+
const handlers = createHookHandlers(store, client, memConfig, ruleCache, ruleCacheDirty, sessionInjectionLock, latestUserMessage);
|
|
19
|
+
const toolMap = createToolMap(store, journalTools, client);
|
|
20
|
+
return {
|
|
21
|
+
...handlers,
|
|
22
|
+
...(autoRetrieveHook || {}),
|
|
23
|
+
tool: toolMap,
|
|
24
|
+
cleanup: async () => {
|
|
25
|
+
await store.close();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
};
|