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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +493 -0
  3. package/agent/memory-hints.md +98 -0
  4. package/agent/memory-researcher.md +56 -0
  5. package/commands/memory-auto-test.md +10 -0
  6. package/commands/memory-cache-status.md +13 -0
  7. package/commands/memory-check-context.md +4 -0
  8. package/commands/memory-compress.md +13 -0
  9. package/commands/memory-dashboard.md +23 -0
  10. package/commands/memory-delete.md +24 -0
  11. package/commands/memory-detect-topics.md +28 -0
  12. package/commands/memory-distill.md +35 -0
  13. package/commands/memory-drilldown-query.md +28 -0
  14. package/commands/memory-drilldown.md +11 -0
  15. package/commands/memory-extract-patterns.md +4 -0
  16. package/commands/memory-generate-embeddings.md +26 -0
  17. package/commands/memory-get.md +26 -0
  18. package/commands/memory-help.md +55 -0
  19. package/commands/memory-injection-feedback.md +26 -0
  20. package/commands/memory-injection-stats.md +11 -0
  21. package/commands/memory-list.md +4 -0
  22. package/commands/memory-llm-compress.md +34 -0
  23. package/commands/memory-mcp.md +20 -0
  24. package/commands/memory-prune.md +4 -0
  25. package/commands/memory-rate.md +48 -0
  26. package/commands/memory-reflect.md +37 -0
  27. package/commands/memory-replace.md +26 -0
  28. package/commands/memory-retrieve.md +34 -0
  29. package/commands/memory-search.md +28 -0
  30. package/commands/memory-session-stats.md +4 -0
  31. package/commands/memory-set.md +31 -0
  32. package/commands/memory-stats.md +11 -0
  33. package/commands/memory-summarize.md +29 -0
  34. package/commands/memory-tool-stats.md +4 -0
  35. package/commands/memory-total-tokens.md +10 -0
  36. package/commands/memory-verify.md +4 -0
  37. package/commands/memory-version.md +9 -0
  38. package/dist/cache.js +39 -0
  39. package/dist/config.js +120 -0
  40. package/dist/embeddings.js +125 -0
  41. package/dist/ensure-models.js +70 -0
  42. package/dist/file-summary.js +143 -0
  43. package/dist/frontmatter.js +28 -0
  44. package/dist/hnsw-index.js +138 -0
  45. package/dist/hooks/auto-discover.js +4 -0
  46. package/dist/hooks/auto-distill.js +120 -0
  47. package/dist/hooks/auto-retrieve/content.js +47 -0
  48. package/dist/hooks/auto-retrieve/detection.js +50 -0
  49. package/dist/hooks/auto-retrieve/formatting.js +19 -0
  50. package/dist/hooks/auto-retrieve/index.js +163 -0
  51. package/dist/hooks/auto-retrieve/scoring.js +56 -0
  52. package/dist/hooks/auto-retrieve.js +1 -0
  53. package/dist/hooks/index.js +4 -0
  54. package/dist/hooks/predictive-rating.js +87 -0
  55. package/dist/journal.js +279 -0
  56. package/dist/logging.js +147 -0
  57. package/dist/management/helpers.js +227 -0
  58. package/dist/management/router.js +48 -0
  59. package/dist/management/routes.js +197 -0
  60. package/dist/management-server.js +4 -0
  61. package/dist/management-standalone.js +31 -0
  62. package/dist/mcp/logging.js +57 -0
  63. package/dist/mcp/server.js +251 -0
  64. package/dist/mcp/transform.js +48 -0
  65. package/dist/mcp-server.js +18 -0
  66. package/dist/memory.js +2 -0
  67. package/dist/ollama.js +74 -0
  68. package/dist/plugin/hooks.js +168 -0
  69. package/dist/plugin/index.js +28 -0
  70. package/dist/plugin/init.js +109 -0
  71. package/dist/plugin/state.js +75 -0
  72. package/dist/plugin/tools.js +45 -0
  73. package/dist/plugin.js +2 -0
  74. package/dist/procedural/store.js +1 -0
  75. package/dist/procedural/types.js +1 -0
  76. package/dist/seed-nodes.js +804 -0
  77. package/dist/storage/compress-ops.js +129 -0
  78. package/dist/storage/compression/formatters.js +243 -0
  79. package/dist/storage/compression/index.js +107 -0
  80. package/dist/storage/compression/patterns.js +138 -0
  81. package/dist/storage/expiration.js +66 -0
  82. package/dist/storage/index.js +1 -0
  83. package/dist/storage/injection-events.js +82 -0
  84. package/dist/storage/lifecycle.js +65 -0
  85. package/dist/storage/maintenance.js +60 -0
  86. package/dist/storage/migrations/definitions.js +374 -0
  87. package/dist/storage/migrations/index.js +21 -0
  88. package/dist/storage/navigation.js +98 -0
  89. package/dist/storage/queries/base.js +44 -0
  90. package/dist/storage/queries/links.js +32 -0
  91. package/dist/storage/queries/nodes.js +189 -0
  92. package/dist/storage/queries/search-helpers.js +239 -0
  93. package/dist/storage/scoring.js +36 -0
  94. package/dist/storage/search.js +233 -0
  95. package/dist/storage/session-tracking.js +180 -0
  96. package/dist/storage/sqlite.js +329 -0
  97. package/dist/storage/tool-usage.js +56 -0
  98. package/dist/storage/types.js +1 -0
  99. package/dist/storage/utils.js +94 -0
  100. package/dist/tools/auto-test.js +24 -0
  101. package/dist/tools/cache-status.js +36 -0
  102. package/dist/tools/compress.js +186 -0
  103. package/dist/tools/core.js +307 -0
  104. package/dist/tools/dashboard.js +97 -0
  105. package/dist/tools/help.js +59 -0
  106. package/dist/tools/index.js +12 -0
  107. package/dist/tools/inject.js +91 -0
  108. package/dist/tools/injection-debug.js +48 -0
  109. package/dist/tools/journal.js +105 -0
  110. package/dist/tools/llm-compress.js +41 -0
  111. package/dist/tools/middle-term.js +68 -0
  112. package/dist/tools/playbook.js +64 -0
  113. package/dist/tools/reflect.js +291 -0
  114. package/dist/tools/search.js +188 -0
  115. package/dist/tools/session.js +189 -0
  116. package/dist/tools/shared.js +74 -0
  117. package/dist/tools/skill.js +37 -0
  118. package/dist/tools/stats.js +256 -0
  119. package/dist/tools/version.js +13 -0
  120. package/dist/tools.js +18 -0
  121. package/dist/utils/hybridScore.js +67 -0
  122. package/management/public/app.js +1529 -0
  123. package/management/public/index.html +486 -0
  124. package/management/public/three.min.js +6 -0
  125. package/package.json +65 -0
  126. package/scripts/download-models.ts +16 -0
  127. package/scripts/postinstall.cjs +30 -0
@@ -0,0 +1,74 @@
1
+ import { estimateTokens } from "../embeddings";
2
+ export const CONTEXT_LIMIT = 128000;
3
+ export const WARN_THRESHOLD = 0.8;
4
+ export const MAX_RECENT_CALLS = 50;
5
+ export const recentCalls = [];
6
+ export let pruneCallCounter = 0;
7
+ export let lastSearchResults = [];
8
+ export async function resolveNode(store, args) {
9
+ if (args.id) {
10
+ try {
11
+ return await store.getNode(args.id);
12
+ }
13
+ catch { /* Try prefix fallback */
14
+ const prefixNode = await store.getNodeByPrefix(args.id);
15
+ if (prefixNode) {
16
+ return prefixNode;
17
+ }
18
+ throw new Error(`Memory node not found: ${args.id}`);
19
+ }
20
+ }
21
+ if (args.label) {
22
+ const scope = (args.scope ?? "project");
23
+ return await store.getNodeByLabel(scope, args.label);
24
+ }
25
+ throw new Error("Must provide either id or label");
26
+ }
27
+ export function wrapWithContextWarning(result, extraTokens = 0) {
28
+ const tokens = estimateTokens(result) + extraTokens;
29
+ const ratio = tokens / CONTEXT_LIMIT;
30
+ if (ratio >= WARN_THRESHOLD) {
31
+ return result + `\n\n---\n⚠️ Context at ${(ratio * 100).toFixed(0)}% (~${tokens.toLocaleString()} tokens). Run memory_drilldown to compress, or memory_compress to create summaries.`;
32
+ }
33
+ return result;
34
+ }
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ export function wrapWithTracking(toolDef, store, toolName) {
37
+ if (!store)
38
+ return toolDef;
39
+ const originalExecute = toolDef.execute;
40
+ if (!originalExecute)
41
+ return toolDef;
42
+ toolDef.execute = async (...args) => {
43
+ let contextWarning = false;
44
+ let result;
45
+ const startTime = performance.now();
46
+ try {
47
+ result = await originalExecute(...args);
48
+ const durationMs = performance.now() - startTime;
49
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result);
50
+ const tokens = estimateTokens(resultStr);
51
+ contextWarning = tokens / CONTEXT_LIMIT >= WARN_THRESHOLD;
52
+ recentCalls.push(toolName);
53
+ if (recentCalls.length > MAX_RECENT_CALLS) {
54
+ recentCalls.shift();
55
+ }
56
+ pruneCallCounter++;
57
+ if (pruneCallCounter >= 100) {
58
+ pruneCallCounter = 0;
59
+ try {
60
+ await store.pruneUsageLog();
61
+ }
62
+ catch { /* ignore */ }
63
+ }
64
+ await store.logToolCall(toolName, tokens, contextWarning, true, durationMs);
65
+ }
66
+ catch (err) {
67
+ const durationMs = performance.now() - startTime;
68
+ await store.logToolCall(toolName, 0, false, false, durationMs).catch(() => { });
69
+ throw err;
70
+ }
71
+ return result;
72
+ };
73
+ return toolDef;
74
+ }
@@ -0,0 +1,37 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { wrapWithTracking } from "./shared";
3
+ export function MemorySkillLoad(store) {
4
+ const t = tool({
5
+ description: "Load a skill's full instructions by name. Works for all skills (built-in and memory-stored). Call with the skill name to get the full markdown content.",
6
+ args: {
7
+ name: tool.schema.string(),
8
+ },
9
+ async execute(args) {
10
+ const cleanName = (args.name ?? "").replace(/^skill:/, "").trim();
11
+ if (!cleanName) {
12
+ return "Error: skill name is required. Usage: memory_skill_load(name=\"skill-name\")";
13
+ }
14
+ for (const scope of ["global", "project"]) {
15
+ try {
16
+ const node = await store.getNodeByLabel(scope, `skill:${cleanName}`);
17
+ return `<skill_content name="${cleanName}">\n${node.content}\n</skill_content>`;
18
+ }
19
+ catch {
20
+ // Not found in this scope
21
+ }
22
+ }
23
+ const allNodes = await store.listNodes("all");
24
+ const skills = allNodes.filter(n => n.type === "skill");
25
+ const names = skills
26
+ .map(s => s.label?.replace(/^skill:/, ""))
27
+ .filter((n) => !!n)
28
+ .sort()
29
+ .join(", ");
30
+ if (names) {
31
+ return `Skill '${cleanName}' not found. Available skills:\n\n${names}\n\n${skills.length} total skills. Use memory_skill_load(name="skill-name") to load one.`;
32
+ }
33
+ return `Skill '${cleanName}' not found. No skills available in memory database.`;
34
+ },
35
+ });
36
+ return wrapWithTracking(t, store, "memory_skill_load");
37
+ }
@@ -0,0 +1,256 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { estimateTokens } from "../embeddings";
3
+ import { CONTEXT_LIMIT, WARN_THRESHOLD, wrapWithTracking } from "./shared";
4
+ export function MemoryStats(store) {
5
+ const t = tool({
6
+ description: "Get fractal memory statistics: nodes per level, compression ratios, fractal dimension, tree structure.",
7
+ args: {
8
+ scope: tool.schema.enum(["all", "global", "project"]).optional(),
9
+ },
10
+ async execute(args) {
11
+ const scope = (args.scope ?? "all");
12
+ const stats = await store.getFractalStats(scope);
13
+ const lines = [
14
+ "## Fractal Memory Statistics",
15
+ "",
16
+ `Total nodes: ${stats.totalNodes}`,
17
+ `Global: ${stats.scopes.global} | Project: ${stats.scopes.project}`,
18
+ "",
19
+ "### Nodes per Level",
20
+ `L0 (raw): ${stats.nodesPerLevel[0]}`,
21
+ `L1 (weekly): ${stats.nodesPerLevel[1]}`,
22
+ `L2 (monthly): ${stats.nodesPerLevel[2]}`,
23
+ `L3 (quarter): ${stats.nodesPerLevel[3]}`,
24
+ `L4+ (yearly): ${stats.nodesPerLevel[4] + stats.nodesPerLevel[5]}`,
25
+ "",
26
+ "### Compression Metrics",
27
+ `Fractal dimension: ${stats.fractalDimension}`,
28
+ `Average children per summary: ${stats.avgChildrenPerNode}`,
29
+ `Tree depth: ${stats.treeDepth}`,
30
+ `Nodes with embeddings: ${stats.hasEmbeddings}`,
31
+ "",
32
+ "### Compression Ratios (L0→L1→L2→L3→L4)",
33
+ ];
34
+ const ratios = [
35
+ stats.compressionRatios[0] ?? 0,
36
+ stats.compressionRatios[1] ?? 0,
37
+ stats.compressionRatios[2] ?? 0,
38
+ stats.compressionRatios[3] ?? 0,
39
+ stats.compressionRatios[4] ?? 0,
40
+ ];
41
+ lines.push(ratios.map(r => r > 0 ? `${r.toFixed(1)}x` : "-").join(" → "));
42
+ return lines.join("\n");
43
+ },
44
+ });
45
+ return wrapWithTracking(t, store, "memory_stats");
46
+ }
47
+ export function MemoryInjectionStats(store) {
48
+ const t = tool({
49
+ description: "Get injection efficiency metrics: tracks how well injected memory is being used by the agent.",
50
+ args: {
51
+ limit: tool.schema.number().min(1).max(1000).optional().describe("Number of recent injections to show (default 10)"),
52
+ session_id: tool.schema.string().optional().describe("Get metrics for specific session only"),
53
+ },
54
+ async execute(args) {
55
+ const limit = args.limit ?? 10;
56
+ let metrics;
57
+ if (args.session_id) {
58
+ const sessionMetrics = await store.getSessionMetrics(args.session_id);
59
+ if (!sessionMetrics) {
60
+ metrics = [];
61
+ }
62
+ else {
63
+ metrics = [{
64
+ sessionId: args.session_id,
65
+ timestamp: Date.now(),
66
+ injectedNodeCount: sessionMetrics.totalInjections,
67
+ injectedTokens: 0,
68
+ injectionMode: "session-summary",
69
+ toolCalls: sessionMetrics.totalToolCalls,
70
+ effectivenessScore: sessionMetrics.avgEffectiveness ?? undefined,
71
+ }];
72
+ }
73
+ }
74
+ else {
75
+ metrics = await store.getInjectionMetrics(limit);
76
+ }
77
+ if (metrics.length === 0) {
78
+ return "No injection metrics recorded yet. Metrics are collected automatically when memory is injected.";
79
+ }
80
+ const lines = [
81
+ "## Injection Efficiency Metrics",
82
+ "",
83
+ `Tracking ${metrics.length} injection(s)`,
84
+ "",
85
+ ];
86
+ const totalToolCalls = metrics.reduce((sum, m) => sum + (m.toolCalls ?? 0), 0);
87
+ const avgNodesPerInjection = metrics.reduce((sum, m) => sum + m.injectedNodeCount, 0) / metrics.length;
88
+ const avgEffectiveness = metrics.filter(m => m.effectivenessScore !== null)
89
+ .reduce((sum, m, _, arr) => sum + (m.effectivenessScore ?? 0) / arr.length, 0);
90
+ lines.push("### Summary");
91
+ lines.push(`Total tool calls triggered: ${totalToolCalls}`);
92
+ lines.push(`Avg nodes per injection: ${avgNodesPerInjection.toFixed(1)}`);
93
+ if (avgEffectiveness > 0) {
94
+ lines.push(`Avg effectiveness score: ${(avgEffectiveness * 100).toFixed(0)}%`);
95
+ }
96
+ lines.push("");
97
+ lines.push("### Recent Injections");
98
+ for (const m of metrics.slice(0, 5)) {
99
+ const date = m.timestamp ? new Date(m.timestamp).toLocaleString() : "N/A";
100
+ const toolCallsStr = m.toolCalls ?? 0;
101
+ const effectivenessStr = (m.effectivenessScore !== undefined && m.effectivenessScore !== null) ? `, effectiveness: ${(m.effectivenessScore * 100).toFixed(0)}%` : '';
102
+ lines.push(`- ${date}: ${m.injectedNodeCount} nodes, ${toolCallsStr} tool calls${effectivenessStr}`);
103
+ }
104
+ lines.push("");
105
+ lines.push("_Metrics are collected automatically when memory is injected. Use memory_verify after tasks to improve effectiveness scores._");
106
+ return lines.join("\n");
107
+ },
108
+ });
109
+ return wrapWithTracking(t, store, "memory_injection_stats");
110
+ }
111
+ export function MemoryInjectionFeedback(store) {
112
+ const t = tool({
113
+ description: "Rate the usefulness of injected memories after completing a task. Upvote helpful injections, downvote irrelevant ones. This helps improve future injection relevance.",
114
+ args: {
115
+ session_id: tool.schema.string().describe("Session ID to provide feedback for (find via memory_injection_stats)"),
116
+ upvotes: tool.schema.number().min(0).describe("Number of helpful injections"),
117
+ downvotes: tool.schema.number().min(0).describe("Number of irrelevant/inutile injections"),
118
+ task_outcome: tool.schema.enum(["success", "partial", "failed"]).optional().describe("How well the task went"),
119
+ needed_nodes: tool.schema.array(tool.schema.string()).optional().describe("Labels of nodes that would have been helpful but weren't injected"),
120
+ },
121
+ async execute(args) {
122
+ try {
123
+ await store.recordInjectionFeedback(args.session_id, args.upvotes, args.downvotes, args.task_outcome, args.needed_nodes);
124
+ return `Feedback recorded: ${args.upvotes} upvotes, ${args.downvotes} downvotes for session ${args.session_id}`;
125
+ }
126
+ catch (err) {
127
+ return `Failed to record feedback: ${err}`;
128
+ }
129
+ },
130
+ });
131
+ return wrapWithTracking(t, store, "memory_injection_feedback");
132
+ }
133
+ export function MemoryCheckContext(store) {
134
+ const t = tool({
135
+ description: "Check token usage of memory nodes and warn if approaching context limit. Helps decide when to compress or use memory_drilldown.",
136
+ args: {
137
+ scope: tool.schema.enum(["all", "global", "project"]).optional(),
138
+ threshold: tool.schema.number().min(0).max(1).optional(),
139
+ node_ids: tool.schema.array(tool.schema.string()).optional(),
140
+ },
141
+ async execute(args) {
142
+ const scope = args.scope ?? "all";
143
+ const threshold = args.threshold ?? WARN_THRESHOLD;
144
+ let nodes = [];
145
+ if (args.node_ids && args.node_ids.length > 0) {
146
+ for (const id of args.node_ids) {
147
+ try {
148
+ const node = await store.getNode(id);
149
+ nodes.push(node);
150
+ }
151
+ catch {
152
+ try {
153
+ const prefixNode = await store.getNodeByPrefix(id);
154
+ if (prefixNode)
155
+ nodes.push(prefixNode);
156
+ }
157
+ catch { /* skip */ }
158
+ }
159
+ }
160
+ }
161
+ else {
162
+ nodes = await store.listNodes(scope);
163
+ }
164
+ const totalTokens = nodes.reduce((sum, n) => sum + estimateTokens(n.content), 0);
165
+ const ratio = totalTokens / CONTEXT_LIMIT;
166
+ const lines = [
167
+ "## Memory Context Check",
168
+ "",
169
+ `Total nodes: ${nodes.length}`,
170
+ `Estimated tokens: ${totalTokens.toLocaleString()} / ${CONTEXT_LIMIT.toLocaleString()} (${(ratio * 100).toFixed(1)}%)`,
171
+ "",
172
+ ];
173
+ if (ratio >= threshold) {
174
+ const warningLines = [
175
+ `⚠️ Context at ${(ratio * 100).toFixed(0)}% — above threshold (${(threshold * 100).toFixed(0)}%)`,
176
+ "",
177
+ "To reduce token usage:",
178
+ "- Run memory_drilldown on specific nodes to get summaries",
179
+ "- Run memory_compress(scope=\"project\", force=true) to create L1 summaries",
180
+ "- Run memory_drilldown on the created summaries to retrieve compressed content",
181
+ ];
182
+ const nodesByLevel = {};
183
+ let rawTokens = 0;
184
+ for (const n of nodes) {
185
+ nodesByLevel[n.level] = (nodesByLevel[n.level] ?? 0) + 1;
186
+ if (n.level === 0)
187
+ rawTokens += estimateTokens(n.content);
188
+ }
189
+ if (nodesByLevel[0] ?? 0 > 0) {
190
+ warningLines.push(`\n${nodesByLevel[0]} L0 nodes (~${rawTokens.toLocaleString()} tokens) could be compressed.`);
191
+ }
192
+ lines.push(...warningLines);
193
+ }
194
+ else {
195
+ lines.push(`✅ Context at ${(ratio * 100).toFixed(0)}% — below threshold (${(threshold * 100).toFixed(0)}%). No action needed.`);
196
+ }
197
+ if (nodes.length > 0 && nodes.length <= 20) {
198
+ lines.push("", "### Node Breakdown");
199
+ const nodesByLevel = {};
200
+ for (const n of nodes) {
201
+ if (!nodesByLevel[n.level])
202
+ nodesByLevel[n.level] = [];
203
+ nodesByLevel[n.level].push(n);
204
+ }
205
+ for (const [level, lvlNodes] of Object.entries(nodesByLevel).sort((a, b) => Number(a[0]) - Number(b[0]))) {
206
+ const lvlTokens = lvlNodes.reduce((s, n) => s + estimateTokens(n.content), 0);
207
+ lines.push(`L${level}: ${lvlNodes.length} nodes (~${lvlTokens.toLocaleString()} tokens)`);
208
+ }
209
+ }
210
+ return lines.join("\n");
211
+ },
212
+ });
213
+ return wrapWithTracking(t, store, "memory_check_context");
214
+ }
215
+ export function MemoryToolStats(store) {
216
+ const t = tool({
217
+ description: "Get tool call statistics: usage counts, durations, token output, and efficiency patterns. Shows which tools are fastest and most token-efficient.",
218
+ args: {},
219
+ async execute(_args) {
220
+ const patterns = await store.getToolPatterns("all");
221
+ if (patterns.length === 0) {
222
+ return "No tool call data yet. Statistics are collected automatically as tools are used.";
223
+ }
224
+ const lines = [
225
+ "## Tool Statistics (last 30 days)",
226
+ "",
227
+ "### Per-Tool Breakdown",
228
+ "",
229
+ "| Tool | Calls | Avg Duration | Avg Tokens | Success |",
230
+ "|------|-------|-------------|------------|---------|",
231
+ ];
232
+ for (const p of patterns) {
233
+ const duration = p.avgDurationMs > 0 ? `${p.avgDurationMs}ms` : "—";
234
+ lines.push(`| ${p.toolName} | ${p.count} | ${duration} | ${p.avgTokens} | ${p.successRate}% |`);
235
+ }
236
+ lines.push("");
237
+ lines.push("### Efficiency Analysis");
238
+ const slowest = patterns.filter(p => p.avgDurationMs > 0).sort((a, b) => b.avgDurationMs - a.avgDurationMs);
239
+ const heaviest = patterns.sort((a, b) => b.avgTokens - a.avgTokens);
240
+ if (slowest.length > 0) {
241
+ const top = slowest[0];
242
+ lines.push(`**Slowest:** ${top.toolName} (${top.avgDurationMs}ms avg)`);
243
+ }
244
+ if (heaviest.length > 0) {
245
+ const top = heaviest[0];
246
+ lines.push(`**Most tokens:** ${top.toolName} (${top.avgTokens} tokens avg)`);
247
+ }
248
+ const failed = patterns.filter(p => p.successRate < 100);
249
+ if (failed.length > 0) {
250
+ lines.push("**Failures:** " + failed.map(f => `${f.toolName} (${100 - f.successRate}%)`).join(", "));
251
+ }
252
+ return lines.join("\n");
253
+ },
254
+ });
255
+ return wrapWithTracking(t, store, "memory_tool_stats");
256
+ }
@@ -0,0 +1,13 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { wrapWithTracking } from "./shared";
3
+ const VERSION = "0.2.0";
4
+ export function MemoryVersion(store) {
5
+ const t = tool({
6
+ description: "Show the installed version of the Fractal Memory plugin.",
7
+ args: {},
8
+ async execute() {
9
+ return `Fractal Memory plugin version: ${VERSION}`;
10
+ },
11
+ });
12
+ return wrapWithTracking(t, store, "memory_version");
13
+ }
package/dist/tools.js ADDED
@@ -0,0 +1,18 @@
1
+ export { MemoryInjectionDebug } from "./tools/injection-debug";
2
+ export { MemoryInject } from "./tools/inject";
3
+ export { MemoryMiddleTerm } from "./tools/middle-term";
4
+ export { JournalWrite, JournalRead, JournalSearch } from "./tools/journal";
5
+ export { wrapWithContextWarning, wrapWithTracking, recentCalls, pruneCallCounter, lastSearchResults, CONTEXT_LIMIT, WARN_THRESHOLD, MAX_RECENT_CALLS } from "./tools/shared";
6
+ export { MemoryList, MemorySet, MemoryRate, MemoryGet, MemoryFetch, MemoryReplace, MemoryDelete } from "./tools/core";
7
+ export { MemoryDrilldown, MemorySearch, MemoryDrilldownQuery } from "./tools/search";
8
+ export { MemoryPrune, MemoryVerify, MemoryCompress, MemoryExtractPatterns, MemorySummarize, MemoryGenerateEmbeddings } from "./tools/compress";
9
+ export { MemoryLlmCompress, MemoryDetectTopics } from "./tools/llm-compress";
10
+ export { MemoryStats, MemoryInjectionStats, MemoryInjectionFeedback, MemoryCheckContext, MemoryToolStats } from "./tools/stats";
11
+ export { MemorySessionStats, MemoryTotalTokens } from "./tools/session";
12
+ export { MemoryReflect, MemoryDistill } from "./tools/reflect";
13
+ export { MemoryDashboard } from "./tools/dashboard";
14
+ export { MemorySkillLoad } from "./tools/skill";
15
+ export { MemoryCacheStatus } from "./tools/cache-status";
16
+ export { MemoryHelp } from "./tools/help";
17
+ export { MemoryVersion } from "./tools/version";
18
+ export { createMemoryAutoTest } from "./tools/auto-test";
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Tokenize a string into lowercase word tokens.
3
+ */
4
+ export function tokenize(text) {
5
+ return text
6
+ .toLowerCase()
7
+ .replace(/[^\w\s]/g, " ")
8
+ .split(/\s+/)
9
+ .filter(Boolean);
10
+ }
11
+ /**
12
+ * Compute the hybrid relevance score for a memory node.
13
+ *
14
+ * score = 0.5 * embeddingSimilarity
15
+ * + 0.3 * importance
16
+ * + 0.2 * confidence
17
+ * + recencyBoost
18
+ * + keywordBoost
19
+ */
20
+ export function computeHybridScore(node, queryEmbedding, queryTerms) {
21
+ // ---- Embedding similarity (cosine) ----
22
+ let embeddingScore = 0;
23
+ if (queryEmbedding && queryEmbedding.length && node.embedding?.length) {
24
+ let dot = 0, normA = 0, normB = 0;
25
+ const len = Math.min(queryEmbedding.length, node.embedding.length);
26
+ for (let i = 0; i < len; i++) {
27
+ const qi = queryEmbedding[i] ?? 0;
28
+ const ni = node.embedding[i] ?? 0;
29
+ dot += qi * ni;
30
+ normA += qi * qi;
31
+ normB += ni * ni;
32
+ }
33
+ embeddingScore = dot / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-8);
34
+ }
35
+ const importance = node.importance ?? 0.5;
36
+ const confidence = node.confidence ?? 0.5;
37
+ // ---- Recency boost ----
38
+ const now = Date.now();
39
+ const DAY_MS = 24 * 60 * 60 * 1000;
40
+ let recencyBoost = 0;
41
+ if (node.updatedAt instanceof Date) {
42
+ const daysSince = (now - node.updatedAt.getTime()) / DAY_MS;
43
+ if (daysSince < 1)
44
+ recencyBoost = 0.1;
45
+ else if (daysSince < 7)
46
+ recencyBoost = 0.05;
47
+ }
48
+ // ---- Keyword boost (overlap between query terms and node content/label) ----
49
+ let keywordBoost = 0;
50
+ if (queryTerms.length) {
51
+ const nodeTokens = tokenize(node.content + " " + (node.label ?? ""));
52
+ let overlap = 0;
53
+ for (const term of queryTerms) {
54
+ if (nodeTokens.includes(term))
55
+ overlap++;
56
+ }
57
+ if (overlap > 0) {
58
+ keywordBoost = Math.min(0.15, (overlap / queryTerms.length) * 0.15);
59
+ }
60
+ }
61
+ // ---- Final hybrid combination ----
62
+ return (0.5 * embeddingScore +
63
+ 0.3 * importance +
64
+ 0.2 * confidence +
65
+ recencyBoost +
66
+ keywordBoost);
67
+ }