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,186 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { generateEmbedding } from "../embeddings";
|
|
3
|
+
import { resolveNode, wrapWithTracking } from "./shared";
|
|
4
|
+
export function MemoryPrune(store) {
|
|
5
|
+
const t = tool({
|
|
6
|
+
description: "Find and optionally prune stale/unused memory nodes.",
|
|
7
|
+
args: {
|
|
8
|
+
scope: tool.schema.enum(["all", "global", "project"]).optional(),
|
|
9
|
+
minAccessCount: tool.schema.number().optional(),
|
|
10
|
+
maxAgeDays: tool.schema.number().optional(),
|
|
11
|
+
minImportance: tool.schema.number().optional(),
|
|
12
|
+
excludeSticky: tool.schema.boolean().optional(),
|
|
13
|
+
excludeCore: tool.schema.boolean().optional(),
|
|
14
|
+
dryRun: tool.schema.boolean().optional(),
|
|
15
|
+
},
|
|
16
|
+
async execute(args) {
|
|
17
|
+
const scope = (args.scope ?? "all");
|
|
18
|
+
const dryRun = args.dryRun ?? true;
|
|
19
|
+
const result = await store.pruneNodes(scope, {
|
|
20
|
+
minAccessCount: args.minAccessCount ?? 0,
|
|
21
|
+
maxAgeDays: args.maxAgeDays ?? 90,
|
|
22
|
+
minImportance: args.minImportance ?? 0,
|
|
23
|
+
excludeSticky: args.excludeSticky ?? true,
|
|
24
|
+
excludeCore: args.excludeCore ?? true,
|
|
25
|
+
dryRun,
|
|
26
|
+
});
|
|
27
|
+
if (result.prunable.length === 0) {
|
|
28
|
+
return "No nodes found matching pruning criteria.";
|
|
29
|
+
}
|
|
30
|
+
if (dryRun) {
|
|
31
|
+
const lines = result.prunable.map(n => `- ${n.id.slice(0, 8)}: "${n.label ?? "(no label)"}" (accesses: ${n.accessCount}, importance: ${n.importance})`);
|
|
32
|
+
return `Found ${result.prunable.length} prunable nodes (dry-run):\n${lines.join("\n")}\n\nRun with dryRun=false to delete these nodes.`;
|
|
33
|
+
}
|
|
34
|
+
return `Pruned ${result.pruned} nodes.`;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
return wrapWithTracking(t, store, "memory_prune");
|
|
38
|
+
}
|
|
39
|
+
export function MemoryVerify(store) {
|
|
40
|
+
const t = tool({
|
|
41
|
+
description: "Verify that a memory node's information is correct.",
|
|
42
|
+
args: {
|
|
43
|
+
id: tool.schema.string().optional(),
|
|
44
|
+
label: tool.schema.string().optional(),
|
|
45
|
+
scope: tool.schema.enum(["global", "project"]).optional(),
|
|
46
|
+
},
|
|
47
|
+
async execute(args) {
|
|
48
|
+
const node = await resolveNode(store, args);
|
|
49
|
+
const verified = await store.verifyNode(node.id);
|
|
50
|
+
return `Verified node ${node.id.slice(0, 8)}. Confidence: ${(verified.confidence * 100).toFixed(0)}%, last_verified: ${verified.lastVerified ? new Date(verified.lastVerified).toISOString() : "never"}.`;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
return wrapWithTracking(t, store, "memory_verify");
|
|
54
|
+
}
|
|
55
|
+
export function MemoryCompress(store) {
|
|
56
|
+
const t = tool({
|
|
57
|
+
description: "Compress old memory nodes into summaries at higher levels.",
|
|
58
|
+
args: {
|
|
59
|
+
scope: tool.schema.enum(["all", "global", "project"]).optional(),
|
|
60
|
+
level: tool.schema.number().int().nonnegative().optional(),
|
|
61
|
+
dry_run: tool.schema.boolean().optional(),
|
|
62
|
+
force: tool.schema.boolean().optional(),
|
|
63
|
+
},
|
|
64
|
+
async execute(args) {
|
|
65
|
+
const scope = (args.scope ?? "all");
|
|
66
|
+
const level = (args.level ?? 0);
|
|
67
|
+
if (args.dry_run) {
|
|
68
|
+
const candidates = await store.getCompressionCandidates(scope, level, undefined, args.force);
|
|
69
|
+
if (candidates.length === 0) {
|
|
70
|
+
return `No nodes need compression at level ${level}${args.force ? " (force)" : ""}.`;
|
|
71
|
+
}
|
|
72
|
+
return `Found ${candidates.length} nodes to compress:\n` +
|
|
73
|
+
candidates.map(c => `- ${c.id.slice(0, 8)}: ${c.content.slice(0, 50)}...`).join("\n");
|
|
74
|
+
}
|
|
75
|
+
const candidates = await store.getCompressionCandidates(scope, level, undefined, args.force);
|
|
76
|
+
if (candidates.length === 0) {
|
|
77
|
+
return `No nodes to compress at level ${level}${args.force ? " (force)" : ""}.`;
|
|
78
|
+
}
|
|
79
|
+
const result = await store.runCompression(scope, args.force);
|
|
80
|
+
return `Compression complete: ${result.compressed} nodes compressed, ${result.created} summary nodes created.`;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
return wrapWithTracking(t, store, "memory_compress");
|
|
84
|
+
}
|
|
85
|
+
export function MemoryExtractPatterns(store) {
|
|
86
|
+
const t = tool({
|
|
87
|
+
description: "Extract cross-layer patterns from L0 nodes and create pattern summary nodes at L1.",
|
|
88
|
+
args: {
|
|
89
|
+
scope: tool.schema.enum(["all", "global", "project"]).optional(),
|
|
90
|
+
dry_run: tool.schema.boolean().optional(),
|
|
91
|
+
},
|
|
92
|
+
async execute(args) {
|
|
93
|
+
const scope = (args.scope ?? "all");
|
|
94
|
+
if (args.dry_run) {
|
|
95
|
+
const nodes = await store.listNodes(scope, 0);
|
|
96
|
+
const eligible = nodes.filter(n => n.content.length > 50 && !n.sticky && n.type !== "summary");
|
|
97
|
+
if (eligible.length < 2) {
|
|
98
|
+
return `Not enough eligible nodes for pattern extraction (need at least 2, found ${eligible.length}).`;
|
|
99
|
+
}
|
|
100
|
+
return `Found ${eligible.length} eligible nodes. Pattern extraction would create 1 summary node referencing all sources.`;
|
|
101
|
+
}
|
|
102
|
+
const result = await store.runPatternExtraction(scope);
|
|
103
|
+
if (result.created === 0) {
|
|
104
|
+
return `No pattern extraction performed. Need at least 2 eligible nodes with patterns to extract.`;
|
|
105
|
+
}
|
|
106
|
+
return `Pattern extraction complete: Created 1 pattern summary node from ${result.sources} source nodes.`;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
return wrapWithTracking(t, store, "memory_extract_patterns");
|
|
110
|
+
}
|
|
111
|
+
export function MemorySummarize(store) {
|
|
112
|
+
const t = tool({
|
|
113
|
+
description: "Generate an LLM prompt to summarize a memory node.",
|
|
114
|
+
args: {
|
|
115
|
+
id: tool.schema.string().optional(),
|
|
116
|
+
label: tool.schema.string().optional(),
|
|
117
|
+
scope: tool.schema.enum(["global", "project"]).optional(),
|
|
118
|
+
},
|
|
119
|
+
async execute(args) {
|
|
120
|
+
let node;
|
|
121
|
+
if (args.id) {
|
|
122
|
+
node = await store.getNode(args.id);
|
|
123
|
+
}
|
|
124
|
+
else if (args.label) {
|
|
125
|
+
const scope = args.scope ?? "project";
|
|
126
|
+
node = await store.getNodeByLabel(scope, args.label);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
throw new Error("Must provide either id or label");
|
|
130
|
+
}
|
|
131
|
+
const prompt = `Create a concise summary of the following memory content. Focus on key points and essential information. Keep it under 200 words.
|
|
132
|
+
|
|
133
|
+
Memory (${node.scope}:${node.label ?? node.id.slice(0, 8)}):
|
|
134
|
+
---
|
|
135
|
+
${node.content}
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
Summary:`;
|
|
139
|
+
return `Here's a prompt you can use with an LLM to summarize this node:
|
|
140
|
+
|
|
141
|
+
${prompt}
|
|
142
|
+
|
|
143
|
+
After the LLM generates a summary, you can create a new summary node using:
|
|
144
|
+
memory_set(scope="${node.scope}", content="<summary from LLM>", type="summary", level=${node.level + 1}, parent_id="${node.id}")`;
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
return wrapWithTracking(t, store, "memory_summarize");
|
|
148
|
+
}
|
|
149
|
+
export function MemoryGenerateEmbeddings(store) {
|
|
150
|
+
const t = tool({
|
|
151
|
+
description: "Generate embeddings for existing memory nodes that don't have them.",
|
|
152
|
+
args: {
|
|
153
|
+
scope: tool.schema.enum(["all", "global", "project"]).optional(),
|
|
154
|
+
level: tool.schema.number().int().nonnegative().optional(),
|
|
155
|
+
dry_run: tool.schema.boolean().optional(),
|
|
156
|
+
},
|
|
157
|
+
async execute(args) {
|
|
158
|
+
const scope = (args.scope ?? "project");
|
|
159
|
+
const level = args.level;
|
|
160
|
+
const allNodes = await store.listNodes(scope, level);
|
|
161
|
+
const nodesWithoutEmbedding = allNodes.filter(n => !n.embedding);
|
|
162
|
+
if (args.dry_run) {
|
|
163
|
+
return `Found ${nodesWithoutEmbedding.length} nodes without embeddings${level !== undefined ? ` at level ${level}` : ""}.\n` +
|
|
164
|
+
nodesWithoutEmbedding.slice(0, 10).map(n => `- ${n.label ?? n.id.slice(0, 8)}`).join("\n") +
|
|
165
|
+
(nodesWithoutEmbedding.length > 10 ? `\n... and ${nodesWithoutEmbedding.length - 10} more` : "");
|
|
166
|
+
}
|
|
167
|
+
if (nodesWithoutEmbedding.length === 0) {
|
|
168
|
+
return "All nodes already have embeddings.";
|
|
169
|
+
}
|
|
170
|
+
let generated = 0;
|
|
171
|
+
let failed = 0;
|
|
172
|
+
for (const node of nodesWithoutEmbedding) {
|
|
173
|
+
try {
|
|
174
|
+
const embedding = await generateEmbedding(node.content);
|
|
175
|
+
await store.updateNode(node.id, { embedding });
|
|
176
|
+
generated++;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
failed++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return `Generated embeddings for ${generated} nodes${failed > 0 ? `, ${failed} failed` : ""}.`;
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
return wrapWithTracking(t, store, "memory_generate_embeddings");
|
|
186
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { generateEmbedding } from "../embeddings";
|
|
3
|
+
import { memLog } from "../logging";
|
|
4
|
+
import { resolveNode, wrapWithContextWarning, wrapWithTracking } from "./shared";
|
|
5
|
+
// Try to generate embedding, returning null on failure instead of throwing
|
|
6
|
+
async function tryGenerateEmbedding(content, logContext) {
|
|
7
|
+
try {
|
|
8
|
+
return await generateEmbedding(content);
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
if (logContext) {
|
|
12
|
+
memLog("warn", "embeddings", `${logContext}:`, { error: err instanceof Error ? err.message : err });
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Normalize content: convert literal escape sequences to actual characters
|
|
18
|
+
function normalizeContent(content) {
|
|
19
|
+
if (!content)
|
|
20
|
+
return content;
|
|
21
|
+
return content
|
|
22
|
+
.replace(/\\n/g, '\n')
|
|
23
|
+
.replace(/\\t/g, '\t')
|
|
24
|
+
.replace(/\\r/g, '\r')
|
|
25
|
+
.replace(/\\"/g, '"')
|
|
26
|
+
.replace(/\\\\/g, '\\');
|
|
27
|
+
}
|
|
28
|
+
export function MemoryList(store) {
|
|
29
|
+
const t = tool({
|
|
30
|
+
description: "List available memory nodes (content, level, importance).",
|
|
31
|
+
args: {
|
|
32
|
+
scope: tool.schema.enum(["all", "global", "project"]).optional(),
|
|
33
|
+
level: tool.schema.number().int().nonnegative().optional(),
|
|
34
|
+
},
|
|
35
|
+
async execute(args) {
|
|
36
|
+
const scope = (args.scope ?? "all");
|
|
37
|
+
const level = args.level;
|
|
38
|
+
const nodes = await store.listNodes(scope, level);
|
|
39
|
+
if (nodes.length === 0) {
|
|
40
|
+
return "No memory nodes found.";
|
|
41
|
+
}
|
|
42
|
+
const result = nodes
|
|
43
|
+
.map((n) => `${n.scope}:${n.id} ${n.label ? `(label: ${n.label})` : ""}\n level=${n.level} importance=${n.importance.toFixed(2)} access=${n.accessCount}\n type=${n.type ?? "none"}\n ${n.content.slice(0, 100)}${n.content.length > 100 ? "..." : ""}`)
|
|
44
|
+
.join("\n\n");
|
|
45
|
+
return wrapWithContextWarning(result);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return wrapWithTracking(t, store, "memory_list");
|
|
49
|
+
}
|
|
50
|
+
export function MemorySet(store) {
|
|
51
|
+
const t = tool({
|
|
52
|
+
description: "Create or update a memory node. If label is provided and a node with that label exists, updates it instead of creating new. Embeddings are auto-generated for semantic search (use no_embedding=true to disable). Use sticky=true to prevent a node from being compressed.",
|
|
53
|
+
args: {
|
|
54
|
+
scope: tool.schema.enum(["global", "project"]).optional(),
|
|
55
|
+
label: tool.schema.string().optional(),
|
|
56
|
+
content: tool.schema.string(),
|
|
57
|
+
summary: tool.schema.string().optional(),
|
|
58
|
+
level: tool.schema.number().int().nonnegative().optional(),
|
|
59
|
+
parent_ids: tool.schema.string().optional(),
|
|
60
|
+
importance: tool.schema.number().optional(),
|
|
61
|
+
type: tool.schema.string().optional(),
|
|
62
|
+
ttl_days: tool.schema.number().int().min(0).optional(),
|
|
63
|
+
no_embedding: tool.schema.boolean().optional(),
|
|
64
|
+
sticky: tool.schema.boolean().optional(),
|
|
65
|
+
usefulness_score: tool.schema.number().min(0).max(5).optional(),
|
|
66
|
+
},
|
|
67
|
+
async execute(args) {
|
|
68
|
+
const scope = (args.scope ?? "project");
|
|
69
|
+
const sticky = args.sticky ?? false;
|
|
70
|
+
const ttlDays = args.ttl_days ?? undefined;
|
|
71
|
+
if (args.label) {
|
|
72
|
+
try {
|
|
73
|
+
const existing = await store.getNodeByLabel(scope, args.label);
|
|
74
|
+
const embedding = args.no_embedding ? null : await tryGenerateEmbedding(args.content, "Failed to regenerate embedding on update");
|
|
75
|
+
const updates = {
|
|
76
|
+
content: normalizeContent(args.content),
|
|
77
|
+
summary: args.summary ?? undefined,
|
|
78
|
+
level: args.level,
|
|
79
|
+
type: args.type,
|
|
80
|
+
sticky,
|
|
81
|
+
embedding,
|
|
82
|
+
};
|
|
83
|
+
if (args.usefulness_score !== undefined) {
|
|
84
|
+
updates.usefulnessScore = args.usefulness_score;
|
|
85
|
+
}
|
|
86
|
+
if (ttlDays !== undefined) {
|
|
87
|
+
updates.ttlDays = ttlDays;
|
|
88
|
+
}
|
|
89
|
+
await store.updateNode(existing.id, updates);
|
|
90
|
+
return `Updated memory node ${existing.id.slice(0, 8)} (${scope}:${args.label})${sticky ? " [sticky]" : ""}${args.usefulness_score !== undefined ? ` usefulness: ${args.usefulness_score}/5` : ""}${embedding ? " (embedding refreshed)" : ""}.`;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Node doesn't exist, will create new
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const parentIds = args.parent_ids ? [args.parent_ids] : null;
|
|
97
|
+
const embedding = args.no_embedding ? null : await tryGenerateEmbedding(args.content, "Failed to generate embedding");
|
|
98
|
+
const node = await store.createNode({
|
|
99
|
+
scope,
|
|
100
|
+
label: args.label,
|
|
101
|
+
content: normalizeContent(args.content),
|
|
102
|
+
summary: args.summary ?? null,
|
|
103
|
+
level: (args.level ?? 0),
|
|
104
|
+
parentIds,
|
|
105
|
+
embedding,
|
|
106
|
+
importance: args.importance ?? 0.5,
|
|
107
|
+
type: args.type,
|
|
108
|
+
metadata: null,
|
|
109
|
+
sticky,
|
|
110
|
+
ttlDays: ttlDays ?? null,
|
|
111
|
+
});
|
|
112
|
+
const ttlSuffix = ttlDays ? ` [TTL: ${ttlDays}d]` : "";
|
|
113
|
+
return `Created memory node ${node.id.slice(0, 8)} at level ${node.level}${sticky ? " [sticky]" : ""}${ttlSuffix}${embedding ? " (with embedding)" : " (no embedding)"}.`;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
return wrapWithTracking(t, store, "memory_set");
|
|
117
|
+
}
|
|
118
|
+
export function MemoryRate(store) {
|
|
119
|
+
const t = tool({
|
|
120
|
+
description: "Mark a memory node as helpful (or not) and optionally adjust its usefulness score.",
|
|
121
|
+
args: {
|
|
122
|
+
id: tool.schema.string().optional(),
|
|
123
|
+
label: tool.schema.string().optional(),
|
|
124
|
+
scope: tool.schema.enum(["global", "project"]).optional(),
|
|
125
|
+
helpful: tool.schema.boolean().optional(),
|
|
126
|
+
usefulness_score: tool.schema.number().min(0).max(5).optional(),
|
|
127
|
+
},
|
|
128
|
+
async execute(args) {
|
|
129
|
+
const scope = (args.scope ?? "project");
|
|
130
|
+
let nodeId;
|
|
131
|
+
if (args.id) {
|
|
132
|
+
nodeId = args.id;
|
|
133
|
+
}
|
|
134
|
+
else if (args.label) {
|
|
135
|
+
const node = await store.getNodeByLabel(scope, args.label);
|
|
136
|
+
nodeId = node.id;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
throw new Error("Must provide either id or label");
|
|
140
|
+
}
|
|
141
|
+
const updates = {};
|
|
142
|
+
if (args.helpful) {
|
|
143
|
+
updates.timesHelpful = 1;
|
|
144
|
+
}
|
|
145
|
+
if (args.usefulness_score !== undefined) {
|
|
146
|
+
updates.usefulnessScore = args.usefulness_score;
|
|
147
|
+
}
|
|
148
|
+
if (Object.keys(updates).length === 0) {
|
|
149
|
+
return "No updates applied.";
|
|
150
|
+
}
|
|
151
|
+
const node = await store.getNode(nodeId);
|
|
152
|
+
if (args.helpful) {
|
|
153
|
+
updates.timesHelpful = (node.timesHelpful ?? 0) + 1;
|
|
154
|
+
}
|
|
155
|
+
await store.updateNode(nodeId, updates);
|
|
156
|
+
return `Updated node ${nodeId?.slice(0, 8)}: ${args.helpful ? "timesHelpful incremented" : ""}${args.usefulness_score !== undefined ? ` usefulnessScore set to ${args.usefulness_score}` : ""}`;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
return wrapWithTracking(t, store, "memory_rate");
|
|
160
|
+
}
|
|
161
|
+
export function MemoryGet(store) {
|
|
162
|
+
const t = tool({
|
|
163
|
+
description: "Get a memory node by ID or label.",
|
|
164
|
+
args: {
|
|
165
|
+
id: tool.schema.string().optional(),
|
|
166
|
+
label: tool.schema.string().optional(),
|
|
167
|
+
scope: tool.schema.enum(["global", "project"]).optional(),
|
|
168
|
+
},
|
|
169
|
+
async execute(args) {
|
|
170
|
+
const node = await resolveNode(store, args);
|
|
171
|
+
const result = `Scope: ${node.scope}
|
|
172
|
+
Level: ${node.level}
|
|
173
|
+
Type: ${node.type ?? "none"}
|
|
174
|
+
Importance: ${node.importance}
|
|
175
|
+
Access count: ${node.accessCount}
|
|
176
|
+
Created: ${node.createdAt.toISOString()}
|
|
177
|
+
Updated: ${node.updatedAt.toISOString()}
|
|
178
|
+
|
|
179
|
+
Content:
|
|
180
|
+
${node.content}${node.summary ? "\n\nSummary:\n" + node.summary : ""}`;
|
|
181
|
+
return wrapWithContextWarning(result);
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
return wrapWithTracking(t, store, "memory_get");
|
|
185
|
+
}
|
|
186
|
+
export function MemoryFetch(store) {
|
|
187
|
+
const t = tool({
|
|
188
|
+
description: "Fetch a specific memory node by exact label.",
|
|
189
|
+
args: {
|
|
190
|
+
label: tool.schema.string(),
|
|
191
|
+
scope: tool.schema.enum(["global", "project"]).optional(),
|
|
192
|
+
},
|
|
193
|
+
async execute(args) {
|
|
194
|
+
const scope = args.scope ?? "global";
|
|
195
|
+
try {
|
|
196
|
+
const node = await store.getNodeByLabel(scope, args.label);
|
|
197
|
+
const recencyScore = node.lastAccessed
|
|
198
|
+
? Math.exp(-(Date.now() - node.lastAccessed.getTime()) / (1000 * 60 * 60 * 24))
|
|
199
|
+
: 0;
|
|
200
|
+
const result = {
|
|
201
|
+
success: true,
|
|
202
|
+
node: {
|
|
203
|
+
id: node.id,
|
|
204
|
+
label: node.label,
|
|
205
|
+
scope: node.scope,
|
|
206
|
+
level: node.level,
|
|
207
|
+
importance: node.importance,
|
|
208
|
+
usefulnessScore: node.usefulnessScore,
|
|
209
|
+
recencyScore,
|
|
210
|
+
accessCount: node.accessCount,
|
|
211
|
+
timesUsed: node.timesUsed,
|
|
212
|
+
timesHelpful: node.timesHelpful,
|
|
213
|
+
createdAt: node.createdAt.toISOString(),
|
|
214
|
+
updatedAt: node.updatedAt.toISOString(),
|
|
215
|
+
content: node.content,
|
|
216
|
+
summary: node.summary,
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
return JSON.stringify(result, null, 2);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
const allNodes = await store.listNodes("all");
|
|
223
|
+
const similar = allNodes
|
|
224
|
+
.filter(n => n.label?.toLowerCase().includes(args.label.toLowerCase()))
|
|
225
|
+
.slice(0, 3)
|
|
226
|
+
.map(n => n.label);
|
|
227
|
+
return JSON.stringify({
|
|
228
|
+
success: false,
|
|
229
|
+
error: `Memory node not found: ${args.label}`,
|
|
230
|
+
suggestions: similar.length > 0 ? similar : undefined
|
|
231
|
+
}, null, 2);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
return wrapWithTracking(t, store, "memory_fetch");
|
|
236
|
+
}
|
|
237
|
+
export function MemoryReplace(store) {
|
|
238
|
+
const t = tool({
|
|
239
|
+
description: "Replace content in a memory node.",
|
|
240
|
+
args: {
|
|
241
|
+
id: tool.schema.string().optional(),
|
|
242
|
+
label: tool.schema.string().optional(),
|
|
243
|
+
scope: tool.schema.enum(["global", "project"]).optional(),
|
|
244
|
+
oldText: tool.schema.string(),
|
|
245
|
+
newText: tool.schema.string(),
|
|
246
|
+
},
|
|
247
|
+
async execute(args) {
|
|
248
|
+
const node = await resolveNode(store, args);
|
|
249
|
+
const oldText = args.oldText;
|
|
250
|
+
if (!oldText) {
|
|
251
|
+
throw new Error("oldText is required");
|
|
252
|
+
}
|
|
253
|
+
const content = node.content;
|
|
254
|
+
if (content.includes(oldText)) {
|
|
255
|
+
const updated = content.replace(oldText, args.newText);
|
|
256
|
+
const embedding = await tryGenerateEmbedding(updated);
|
|
257
|
+
await store.updateNode(node.id, { content: updated, embedding });
|
|
258
|
+
return `Updated memory node ${node.id.slice(0, 8)}${embedding ? " (embedding refreshed)" : ""}.`;
|
|
259
|
+
}
|
|
260
|
+
const oldLines = oldText.split("\n");
|
|
261
|
+
const oldTrimmedLines = oldLines.map(l => l.trim());
|
|
262
|
+
const contentLines = content.split("\n");
|
|
263
|
+
let matchStart = -1;
|
|
264
|
+
for (let i = 0; i <= contentLines.length - oldTrimmedLines.length; i++) {
|
|
265
|
+
let matched = true;
|
|
266
|
+
for (let j = 0; j < oldTrimmedLines.length; j++) {
|
|
267
|
+
const line = contentLines[i + j];
|
|
268
|
+
if (!line || line.trim() !== oldTrimmedLines[j]) {
|
|
269
|
+
matched = false;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (matched) {
|
|
274
|
+
matchStart = i;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (matchStart >= 0) {
|
|
279
|
+
const before = contentLines.slice(0, matchStart);
|
|
280
|
+
const after = contentLines.slice(matchStart + oldTrimmedLines.length);
|
|
281
|
+
const newLines = args.newText.split("\n");
|
|
282
|
+
const updated = [...before, ...newLines, ...after].join("\n");
|
|
283
|
+
const embedding = await tryGenerateEmbedding(updated);
|
|
284
|
+
await store.updateNode(node.id, { content: updated, embedding });
|
|
285
|
+
return `Updated memory node ${node.id.slice(0, 8)} (fuzzy match)${embedding ? " (embedding refreshed)" : ""}.`;
|
|
286
|
+
}
|
|
287
|
+
throw new Error(`Old text not found in node ${node.id}. Re-read the node with memory_get to get exact content.`);
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
return wrapWithTracking(t, store, "memory_replace");
|
|
291
|
+
}
|
|
292
|
+
export function MemoryDelete(store) {
|
|
293
|
+
const t = tool({
|
|
294
|
+
description: "Delete a memory node by ID or label.",
|
|
295
|
+
args: {
|
|
296
|
+
id: tool.schema.string().optional(),
|
|
297
|
+
label: tool.schema.string().optional(),
|
|
298
|
+
scope: tool.schema.enum(["global", "project"]).optional(),
|
|
299
|
+
},
|
|
300
|
+
async execute(args) {
|
|
301
|
+
const node = await resolveNode(store, args);
|
|
302
|
+
await store.deleteNode(node.id);
|
|
303
|
+
return `Deleted memory node ${node.id.slice(0, 8)}.`;
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
return wrapWithTracking(t, store, "memory_delete");
|
|
307
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { wrapWithTracking } from "./shared";
|
|
3
|
+
export function MemoryDashboard(store) {
|
|
4
|
+
const t = tool({
|
|
5
|
+
description: "Display memory dashboard with top nodes by usefulness, type distribution, and recent activity.",
|
|
6
|
+
args: {
|
|
7
|
+
scope: tool.schema.enum(["all", "global", "project"]).optional(),
|
|
8
|
+
limit: tool.schema.number().min(1).max(100).optional(),
|
|
9
|
+
show_tree_depth: tool.schema.boolean().optional(),
|
|
10
|
+
show_embedding_coverage: tool.schema.boolean().optional(),
|
|
11
|
+
},
|
|
12
|
+
async execute(args) {
|
|
13
|
+
const scope = (args.scope ?? "all");
|
|
14
|
+
const limit = args.limit ?? 10;
|
|
15
|
+
const showTreeDepth = args.show_tree_depth ?? true;
|
|
16
|
+
const showEmbeddingCoverage = args.show_embedding_coverage ?? true;
|
|
17
|
+
const [allNodes, stats] = await Promise.all([
|
|
18
|
+
store.listNodes(scope),
|
|
19
|
+
store.getFractalStats(scope),
|
|
20
|
+
]);
|
|
21
|
+
if (allNodes.length === 0) {
|
|
22
|
+
return "## Memory Dashboard\n\nNo nodes found in the specified scope.";
|
|
23
|
+
}
|
|
24
|
+
const lines = [
|
|
25
|
+
"## Memory Dashboard",
|
|
26
|
+
`Scope: ${scope} | Total nodes: ${allNodes.length}`,
|
|
27
|
+
"",
|
|
28
|
+
];
|
|
29
|
+
// 1. Top nodes by access count
|
|
30
|
+
const topByAccess = [...allNodes]
|
|
31
|
+
.sort((a, b) => b.accessCount - a.accessCount)
|
|
32
|
+
.slice(0, limit);
|
|
33
|
+
lines.push("### Top Nodes by Access Count");
|
|
34
|
+
lines.push("| Rank | Label | Access Count | Usefulness | Times Used | Level |");
|
|
35
|
+
lines.push("|------|-------|-------------|------------|------------|-------|");
|
|
36
|
+
topByAccess.forEach((n, i) => {
|
|
37
|
+
const label = n.label ?? n.id.slice(0, 8);
|
|
38
|
+
lines.push(`| ${i + 1} | ${label} | ${n.accessCount} | ${n.usefulnessScore?.toFixed(1) ?? "-"} | ${n.timesUsed ?? 0} | L${n.level} |`);
|
|
39
|
+
});
|
|
40
|
+
lines.push("");
|
|
41
|
+
// 2. Type distribution
|
|
42
|
+
const typeCount = new Map();
|
|
43
|
+
for (const n of allNodes) {
|
|
44
|
+
const t = n.type ?? "none";
|
|
45
|
+
typeCount.set(t, (typeCount.get(t) ?? 0) + 1);
|
|
46
|
+
}
|
|
47
|
+
lines.push("### Type Distribution");
|
|
48
|
+
lines.push("| Type | Count |");
|
|
49
|
+
lines.push("|------|-------|");
|
|
50
|
+
for (const [type, count] of [...typeCount.entries()].sort((a, b) => b[1] - a[1])) {
|
|
51
|
+
lines.push(`| ${type} | ${count} |`);
|
|
52
|
+
}
|
|
53
|
+
lines.push("");
|
|
54
|
+
// 3. Compression Health
|
|
55
|
+
lines.push("### Compression Health");
|
|
56
|
+
const stickyCount = allNodes.filter(n => n.sticky).length;
|
|
57
|
+
lines.push(`- Fractal dimension: ${stats.fractalDimension}`);
|
|
58
|
+
if (showTreeDepth) {
|
|
59
|
+
lines.push(`- Tree depth: ${stats.treeDepth}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push(`- Average children per summary: ${stats.avgChildrenPerNode?.toFixed(1) ?? "-"}`);
|
|
62
|
+
lines.push(`- Sticky nodes: ${stickyCount}`);
|
|
63
|
+
if (showEmbeddingCoverage) {
|
|
64
|
+
const withEmbeddings = allNodes.filter(n => n.embedding).length;
|
|
65
|
+
const pct = allNodes.length > 0 ? ((withEmbeddings / allNodes.length) * 100).toFixed(0) : "0";
|
|
66
|
+
lines.push(`- Nodes with embeddings: ${withEmbeddings}/${allNodes.length} (${pct}%)`);
|
|
67
|
+
}
|
|
68
|
+
lines.push("");
|
|
69
|
+
// Compression ratios
|
|
70
|
+
const ratios = [
|
|
71
|
+
stats.compressionRatios[0] ?? 0,
|
|
72
|
+
stats.compressionRatios[1] ?? 0,
|
|
73
|
+
stats.compressionRatios[2] ?? 0,
|
|
74
|
+
stats.compressionRatios[3] ?? 0,
|
|
75
|
+
stats.compressionRatios[4] ?? 0,
|
|
76
|
+
];
|
|
77
|
+
const ratioStr = ratios.map(r => r > 0 ? `${r.toFixed(1)}x` : "-").join(" → ");
|
|
78
|
+
lines.push(`- Compression ratios (L0→L4): ${ratioStr}`);
|
|
79
|
+
lines.push("");
|
|
80
|
+
// 4. Usefulness tracking
|
|
81
|
+
const topUseful = [...allNodes]
|
|
82
|
+
.sort((a, b) => (b.usefulnessScore ?? 0) - (a.usefulnessScore ?? 0))
|
|
83
|
+
.slice(0, Math.min(limit, 5));
|
|
84
|
+
lines.push("### Most Useful Nodes");
|
|
85
|
+
lines.push("| Label | Usefulness | Times Used | Times Helpful |");
|
|
86
|
+
lines.push("|-------|------------|------------|---------------|");
|
|
87
|
+
for (const n of topUseful) {
|
|
88
|
+
if ((n.usefulnessScore ?? 0) > 0) {
|
|
89
|
+
const label = n.label ?? n.id.slice(0, 8);
|
|
90
|
+
lines.push(`| ${label} | ${n.usefulnessScore?.toFixed(1) ?? "-"} | ${n.timesUsed ?? 0} | ${n.timesHelpful ?? 0} |`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
return wrapWithTracking(t, store, "memory_dashboard");
|
|
97
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { wrapWithTracking } from "./shared";
|
|
3
|
+
const COMMANDS = [
|
|
4
|
+
{ name: "memory_stats", desc: "Fractal memory statistics – nodes per level, compression ratios, tree structure" },
|
|
5
|
+
{ name: "memory_dashboard", desc: "Top nodes by access, type distribution, compression health, usefulness" },
|
|
6
|
+
{ name: "memory_list", desc: "List all memory nodes with content preview" },
|
|
7
|
+
{ name: "memory_search", desc: "Semantic / BM25 search over memory nodes" },
|
|
8
|
+
{ name: "memory_get", desc: "Get a single memory node by ID or label" },
|
|
9
|
+
{ name: "memory_fetch", desc: "Fetch a memory node by exact label (returns JSON)" },
|
|
10
|
+
{ name: "memory_set", desc: "Create or update a memory node" },
|
|
11
|
+
{ name: "memory_delete", desc: "Delete a memory node by ID or label" },
|
|
12
|
+
{ name: "memory_replace", desc: "Replace content within a memory node" },
|
|
13
|
+
{ name: "memory_drilldown", desc: "Fractal retrieval – walk from summary to source nodes" },
|
|
14
|
+
{ name: "memory_drilldown_query", desc: "Top-down drilldown by intent / query" },
|
|
15
|
+
{ name: "memory_compress", desc: "Compress nodes into level-up summaries" },
|
|
16
|
+
{ name: "memory_llm_compress", desc: "LLM-powered compression with richer summaries" },
|
|
17
|
+
{ name: "memory_extract_patterns", desc: "Extract cross-layer patterns across diverse topics" },
|
|
18
|
+
{ name: "memory_prune", desc: "Find and remove stale / unused nodes" },
|
|
19
|
+
{ name: "memory_verify", desc: "Verify node correctness and boost confidence" },
|
|
20
|
+
{ name: "memory_rate", desc: "Mark a node as helpful and adjust usefulness score" },
|
|
21
|
+
{ name: "memory_summarize", desc: "Generate an LLM prompt to summarize a node" },
|
|
22
|
+
{ name: "memory_check_context", desc: "Check token usage against context limit" },
|
|
23
|
+
{ name: "memory_total_tokens", desc: "Complete token analysis (memory + conversation)" },
|
|
24
|
+
{ name: "memory_injection_stats", desc: "Injection efficiency metrics" },
|
|
25
|
+
{ name: "memory_injection_feedback", desc: "Rate injected memory usefulness" },
|
|
26
|
+
{ name: "memory_tool_stats", desc: "Tool call statistics – durations, success rates" },
|
|
27
|
+
{ name: "memory_session_stats", desc: "Session forensics – tool sequence, files touched" },
|
|
28
|
+
{ name: "memory_reflect", desc: "Analyze a session and create lesson nodes" },
|
|
29
|
+
{ name: "memory_distill", desc: "Extract actionable rules from lesson nodes" },
|
|
30
|
+
{ name: "memory_inject", desc: "Inject relevant memories into the prompt" },
|
|
31
|
+
{ name: "memory_injection_debug", desc: "Show selected node IDs and token usage for last injection" },
|
|
32
|
+
{ name: "memory_middle_term", desc: "Retrieve middle-term context snapshots" },
|
|
33
|
+
{ name: "memory_version", desc: "Show installed plugin version" },
|
|
34
|
+
{ name: "memory_auto_test", desc: "Test auto-retrieval pipeline" },
|
|
35
|
+
{ name: "memory_detect_topics", desc: "Detect topic boundaries across nodes" },
|
|
36
|
+
{ name: "memory_generate_embeddings", desc: "Generate embeddings for nodes that lack them" },
|
|
37
|
+
{ name: "memory_cache_status", desc: "Show working-memory cache usage" },
|
|
38
|
+
{ name: "memory_help", desc: "Show this help message" },
|
|
39
|
+
];
|
|
40
|
+
export function MemoryHelp(store) {
|
|
41
|
+
const t = tool({
|
|
42
|
+
description: "Show all available memory commands and their descriptions.",
|
|
43
|
+
args: {},
|
|
44
|
+
async execute() {
|
|
45
|
+
const lines = [
|
|
46
|
+
"## Memory Plugin Commands",
|
|
47
|
+
"",
|
|
48
|
+
"```",
|
|
49
|
+
];
|
|
50
|
+
for (const cmd of COMMANDS) {
|
|
51
|
+
lines.push(` ${cmd.name.padEnd(28)} ${cmd.desc}`);
|
|
52
|
+
}
|
|
53
|
+
lines.push("```", "", "### About Fractal Memory", "");
|
|
54
|
+
lines.push("Fractal memory organizes knowledge as a hierarchy of increasingly compressed summaries:", "- **L0 (raw)** – Full content, no compression", "- **L1 (weekly)** – Weekly summaries of related L0 nodes", "- **L2 (monthly)** – Patterns extracted from L1 summaries", "- **L3 (quarterly)** – Cross-cutting themes from L2", "- **L4+ (yearly)** – Highest-level synthesis", "", "Use `memory_drilldown` to trace a summary back to its source nodes.", "Use `memory_compress` to promote nodes to the next level.", "Use `memory_search` with `min_level` / `max_level` to target specific compression levels.");
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return wrapWithTracking(t, store, "memory_help");
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./shared";
|
|
2
|
+
export * from "./core";
|
|
3
|
+
export * from "./search";
|
|
4
|
+
export * from "./compress";
|
|
5
|
+
export * from "./stats";
|
|
6
|
+
export * from "./session";
|
|
7
|
+
export * from "./reflect";
|
|
8
|
+
export { MemoryPlaybookExecute } from "./playbook";
|
|
9
|
+
export { MemorySkillLoad } from "./skill";
|
|
10
|
+
export { MemoryDashboard } from "./dashboard";
|
|
11
|
+
export { MemoryCacheStatus } from "./cache-status";
|
|
12
|
+
export { MemoryHelp } from "./help";
|