opencode-fractal-memory 0.4.0 → 0.4.2
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/README.md +13 -3
- package/dist/hooks/auto-retrieve/index.js +14 -1
- package/dist/logging.js +7 -2
- package/dist/mcp/logging.js +6 -1
- package/dist/mcp/server.js +2 -0
- package/dist/plugin/index.js +18 -0
- package/dist/plugin/init.js +17 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,11 +60,11 @@ Add the plugin name to `~/.config/opencode/opencode.json`:
|
|
|
60
60
|
|
|
61
61
|
```json
|
|
62
62
|
{
|
|
63
|
-
"
|
|
63
|
+
"plugin": ["opencode-fractal-memory"]
|
|
64
64
|
}
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
OpenCode installs it automatically at startup. Model files (~24 MB) download on first plugin load via `ensureModels()` — no manual steps needed.
|
|
67
|
+
OpenCode installs it automatically at startup from npm. Model files (~24 MB) download on first plugin load via `ensureModels()` — no manual steps needed.
|
|
68
68
|
|
|
69
69
|
### For development / manual install
|
|
70
70
|
|
|
@@ -317,6 +317,9 @@ Skills are specialized instruction sets stored as memory nodes. When a task matc
|
|
|
317
317
|
| `svelte-core-bestpractices` | svelte, component, runes |
|
|
318
318
|
| `svelte-code-writer` | svelte 5, sveltekit, component |
|
|
319
319
|
| `customize-opencode` | opencode config, agent, plugin |
|
|
320
|
+
| `context-engineering` | context, prompt, system message |
|
|
321
|
+
| `git-workflow-and-versioning` | git, commit, branch, version, publish |
|
|
322
|
+
| `incremental-implementation` | step by step, increment, gradual |
|
|
320
323
|
|
|
321
324
|
### Loading a skill
|
|
322
325
|
|
|
@@ -410,13 +413,20 @@ When OpenCode loads the plugin, `initStorage()` runs automatically:
|
|
|
410
413
|
6. **Background embeddings** — after 1s, generates embeddings for nodes that lack them
|
|
411
414
|
7. **Auto-retrieve hook** — if enabled in config, injects relevant context into prompts
|
|
412
415
|
|
|
416
|
+
Every initialization step is logged with timing in `logs/memory-plugin.log`, making it easy to diagnose startup issues.
|
|
417
|
+
|
|
413
418
|
All of this happens automatically — no manual intervention required.
|
|
414
419
|
|
|
415
420
|
## Logs
|
|
416
421
|
|
|
422
|
+
All plugin logs are consolidated under `~/.config/opencode/logs/`:
|
|
423
|
+
|
|
417
424
|
| Log | Path | Contents |
|
|
418
425
|
|-----|------|----------|
|
|
419
|
-
|
|
|
426
|
+
| Plugin | `logs/memory-plugin.log` | Plugin operations, init steps with timing, auto-retrieve, session events |
|
|
427
|
+
| MCP server | `logs/mcp-server.log` | MCP tool calls, resources, errors |
|
|
428
|
+
| Injection debug | `logs/memory-injection.log` | Full auto-retrieve injection payloads (rotated at 1 MB) |
|
|
429
|
+
| Context dump | `logs/context-dump.log` | Full context snapshots for debugging |
|
|
420
430
|
| OpenCode | `~/.local/share/opencode/log/` | Application lifecycle, tool calls |
|
|
421
431
|
|
|
422
432
|
## Development
|
|
@@ -7,7 +7,13 @@ import { scoreCandidates } from "./scoring";
|
|
|
7
7
|
import { detectRelevantSkills, detectRelevantPlaybooks } from "./detection";
|
|
8
8
|
import { formatPlaybooksAsAvailable, formatSkillsAsAvailable } from "./formatting";
|
|
9
9
|
import { formatNodeForInjection } from "./content";
|
|
10
|
-
const
|
|
10
|
+
const INJECTION_LOG_DIR = path.join(os.homedir(), ".config", "opencode", "logs");
|
|
11
|
+
const INJECTION_LOG_FILE = path.join(INJECTION_LOG_DIR, "memory-injection.log");
|
|
12
|
+
const INJECTION_LOG_MAX_SIZE = 1024 * 1024;
|
|
13
|
+
try {
|
|
14
|
+
fs.mkdirSync(INJECTION_LOG_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
11
17
|
export function createAutoRetrieveHook(deps) {
|
|
12
18
|
const { store, config, log } = deps;
|
|
13
19
|
const autoConfig = config.autoRetrieve;
|
|
@@ -136,6 +142,13 @@ ${JSON.stringify(memoriesJson, null, 2)}
|
|
|
136
142
|
return;
|
|
137
143
|
const debugLine = `[${new Date().toISOString()}] Query: ${userText.slice(0, 100)}...\n${fullBlock}\n\n`;
|
|
138
144
|
try {
|
|
145
|
+
try {
|
|
146
|
+
const stat = fs.statSync(INJECTION_LOG_FILE);
|
|
147
|
+
if (stat.size > INJECTION_LOG_MAX_SIZE) {
|
|
148
|
+
fs.renameSync(INJECTION_LOG_FILE, INJECTION_LOG_FILE + ".old");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch { }
|
|
139
152
|
fs.appendFileSync(INJECTION_LOG_FILE, debugLine);
|
|
140
153
|
}
|
|
141
154
|
catch { /* silent fail */ }
|
package/dist/logging.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const LOG_DIR = path.join(os.homedir(), ".config", "opencode", "logs");
|
|
5
|
+
const LOG_FILE = path.join(LOG_DIR, "memory-plugin.log");
|
|
6
|
+
const CONTEXT_DUMP_FILE = path.join(LOG_DIR, "context-dump.log");
|
|
6
7
|
const MAX_LOG_SIZE = 5 * 1024 * 1024;
|
|
8
|
+
try {
|
|
9
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
catch { }
|
|
7
12
|
let currentSessionId = null;
|
|
8
13
|
let logLevel = "info";
|
|
9
14
|
// Categories to always skip (OpenCode core noise)
|
package/dist/mcp/logging.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as os from "node:os";
|
|
4
|
-
const
|
|
4
|
+
const MCP_LOG_DIR = path.join(os.homedir(), ".config", "opencode", "logs");
|
|
5
|
+
const MCP_LOG_FILE = path.join(MCP_LOG_DIR, "mcp-server.log");
|
|
5
6
|
const MAX_LOG_SIZE = 1024 * 1024;
|
|
7
|
+
try {
|
|
8
|
+
fs.mkdirSync(MCP_LOG_DIR, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
catch { }
|
|
6
11
|
export function mcpLog(level, msg, data) {
|
|
7
12
|
try {
|
|
8
13
|
const ts = new Date().toISOString().slice(0, 19).replace("T", " ");
|
package/dist/mcp/server.js
CHANGED
|
@@ -11,7 +11,9 @@ const __dirname = path.dirname(__filename);
|
|
|
11
11
|
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../../package.json"), "utf-8"));
|
|
12
12
|
let store;
|
|
13
13
|
export async function createMemoryMcpServer(projectDir, globalDbPath) {
|
|
14
|
+
mcpLog("info", "Creating memory store", { projectDir });
|
|
14
15
|
store = createSqliteMemoryStore(projectDir, globalDbPath);
|
|
16
|
+
mcpLog("info", "Memory store created");
|
|
15
17
|
const server = new McpServer({ name: "opencode-fractal-memory", version: pkg.version }, { capabilities: { tools: {}, resources: {} } });
|
|
16
18
|
server.registerTool("memory_search", {
|
|
17
19
|
description: "Search memory nodes by text, embedding similarity, or BM25 score",
|
package/dist/plugin/index.js
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
import { initStorage, loadPluginConfig, seedRuleNodes, backfillData, scheduleBackgroundEmbeddings, setupJournal, startManagementIfEnabled, createAutoRetrieveIfEnabled } from "./init";
|
|
2
2
|
import { createHookHandlers } from "./hooks";
|
|
3
3
|
import { createToolMap } from "./tools";
|
|
4
|
+
import { memLog, perfNow } from "../logging";
|
|
4
5
|
export const MemoryPlugin = async (ctx) => {
|
|
5
6
|
const { directory, client } = ctx;
|
|
7
|
+
const t0 = perfNow();
|
|
8
|
+
memLog("info", "init", "Plugin initialization started", { directory });
|
|
9
|
+
let t = perfNow();
|
|
6
10
|
const store = await initStorage(directory);
|
|
11
|
+
memLog("info", "init", "Storage initialized", { durationMs: perfNow() - t });
|
|
12
|
+
t = perfNow();
|
|
7
13
|
const memConfig = await loadPluginConfig(directory);
|
|
14
|
+
memLog("info", "init", "Config loaded", { durationMs: perfNow() - t });
|
|
15
|
+
t = perfNow();
|
|
8
16
|
await seedRuleNodes(store);
|
|
17
|
+
memLog("info", "init", "Seed nodes completed", { durationMs: perfNow() - t });
|
|
18
|
+
t = perfNow();
|
|
9
19
|
await backfillData(store);
|
|
20
|
+
memLog("info", "init", "Backfill completed", { durationMs: perfNow() - t });
|
|
21
|
+
t = perfNow();
|
|
10
22
|
scheduleBackgroundEmbeddings(store);
|
|
23
|
+
memLog("info", "init", "Background embeddings scheduled", { durationMs: perfNow() - t });
|
|
24
|
+
t = perfNow();
|
|
11
25
|
const journalTools = await setupJournal(directory);
|
|
26
|
+
memLog("info", "init", "Journal setup completed", { durationMs: perfNow() - t });
|
|
27
|
+
t = perfNow();
|
|
12
28
|
startManagementIfEnabled(store, directory);
|
|
29
|
+
memLog("info", "init", "Management server check completed", { durationMs: perfNow() - t });
|
|
13
30
|
const ruleCache = new Map();
|
|
14
31
|
const ruleCacheDirty = { value: true };
|
|
15
32
|
const sessionInjectionLock = new Map();
|
|
@@ -17,6 +34,7 @@ export const MemoryPlugin = async (ctx) => {
|
|
|
17
34
|
const autoRetrieveHook = createAutoRetrieveIfEnabled(store, memConfig);
|
|
18
35
|
const handlers = createHookHandlers(store, client, memConfig, ruleCache, ruleCacheDirty, sessionInjectionLock, latestUserMessage);
|
|
19
36
|
const toolMap = createToolMap(store, journalTools, client);
|
|
37
|
+
memLog("info", "init", "Plugin initialization completed", { totalDurationMs: perfNow() - t0 });
|
|
20
38
|
return {
|
|
21
39
|
...handlers,
|
|
22
40
|
...(autoRetrieveHook || {}),
|
package/dist/plugin/init.js
CHANGED
|
@@ -12,10 +12,15 @@ import { memLog } from "../logging";
|
|
|
12
12
|
import { setCacheConfig } from "../cache";
|
|
13
13
|
import { setHighContextThreshold, setCriticalContextThreshold, setMaxInjectionTokens, setCoreInjectionTokens, setAutoCompressThreshold } from "./state";
|
|
14
14
|
export async function initStorage(directory) {
|
|
15
|
+
memLog("info", "init", "Creating memory store", { directory });
|
|
15
16
|
const store = createMemoryStore(directory);
|
|
17
|
+
memLog("info", "init", "Ensuring seed nodes");
|
|
16
18
|
await store.ensureSeed();
|
|
19
|
+
memLog("info", "init", "Ensuring models");
|
|
17
20
|
await ensureModels();
|
|
21
|
+
memLog("info", "init", "Ensuring agent files");
|
|
18
22
|
await ensureAgentFiles().catch(() => { });
|
|
23
|
+
memLog("info", "init", "Ensuring command files");
|
|
19
24
|
await ensureCommandFiles().catch(() => { });
|
|
20
25
|
return store;
|
|
21
26
|
}
|
|
@@ -31,6 +36,7 @@ export async function loadPluginConfig(directory) {
|
|
|
31
36
|
return memConfig;
|
|
32
37
|
}
|
|
33
38
|
export async function seedRuleNodes(store) {
|
|
39
|
+
let created = 0;
|
|
34
40
|
for (const seed of SEED_NODES) {
|
|
35
41
|
try {
|
|
36
42
|
await store.getNodeByLabel("global", seed.label);
|
|
@@ -48,15 +54,21 @@ export async function seedRuleNodes(store) {
|
|
|
48
54
|
importance: 1,
|
|
49
55
|
metadata: seed.metadata ?? null,
|
|
50
56
|
});
|
|
57
|
+
created++;
|
|
51
58
|
}
|
|
52
59
|
}
|
|
60
|
+
memLog("info", "init", "Seed nodes checked", { total: SEED_NODES.length, created });
|
|
53
61
|
}
|
|
54
62
|
export async function backfillData(store) {
|
|
55
63
|
for (const scope of ["global", "project"]) {
|
|
64
|
+
memLog("info", "init", `Backfilling links for ${scope}`);
|
|
56
65
|
await store.backfillLinks(scope);
|
|
66
|
+
memLog("info", "init", `Backfilling embeddings and BM25 for ${scope}`);
|
|
57
67
|
await store.backfillBinaryEmbeddingsAndBM25(scope);
|
|
58
68
|
}
|
|
69
|
+
memLog("info", "init", "Rebuilding HNSW index");
|
|
59
70
|
await store.rebuildHNSWIndex();
|
|
71
|
+
memLog("info", "init", "HNSW index rebuilt");
|
|
60
72
|
}
|
|
61
73
|
export function scheduleBackgroundEmbeddings(store) {
|
|
62
74
|
setTimeout(async () => {
|
|
@@ -94,9 +106,13 @@ export function startManagementIfEnabled(store, directory) {
|
|
|
94
106
|
loadConfig().then(c => {
|
|
95
107
|
const mgmtConfig = c.management;
|
|
96
108
|
if (mgmtConfig?.enabled === true) {
|
|
109
|
+
memLog("info", "init", "Starting management server", { port: mgmtConfig.port ?? 8787 });
|
|
97
110
|
startManagementServer(store, directory, { enabled: true, port: mgmtConfig?.port ?? 8787 });
|
|
98
111
|
}
|
|
99
|
-
|
|
112
|
+
else {
|
|
113
|
+
memLog("info", "init", "Management server disabled");
|
|
114
|
+
}
|
|
115
|
+
}).catch(() => { memLog("info", "init", "Management server config not available"); });
|
|
100
116
|
}
|
|
101
117
|
export function createAutoRetrieveIfEnabled(store, memConfig) {
|
|
102
118
|
return memConfig?.autoRetrieve?.enabled
|