opencodekit 0.17.13 → 0.18.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/dist/index.js +4 -6
- package/dist/template/.opencode/dcp.jsonc +81 -81
- package/dist/template/.opencode/memory/memory.db +0 -0
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +199 -23
- package/dist/template/.opencode/opencode.json.tui-migration.bak +1380 -0
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plugin/lib/capture.ts +177 -0
- package/dist/template/.opencode/plugin/lib/context.ts +194 -0
- package/dist/template/.opencode/plugin/lib/curator.ts +234 -0
- package/dist/template/.opencode/plugin/lib/db/maintenance.ts +312 -0
- package/dist/template/.opencode/plugin/lib/db/observations.ts +299 -0
- package/dist/template/.opencode/plugin/lib/db/pipeline.ts +520 -0
- package/dist/template/.opencode/plugin/lib/db/schema.ts +356 -0
- package/dist/template/.opencode/plugin/lib/db/types.ts +211 -0
- package/dist/template/.opencode/plugin/lib/distill.ts +376 -0
- package/dist/template/.opencode/plugin/lib/inject.ts +126 -0
- package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +188 -0
- package/dist/template/.opencode/plugin/lib/memory-db.ts +54 -936
- package/dist/template/.opencode/plugin/lib/memory-helpers.ts +202 -0
- package/dist/template/.opencode/plugin/lib/memory-hooks.ts +240 -0
- package/dist/template/.opencode/plugin/lib/memory-tools.ts +341 -0
- package/dist/template/.opencode/plugin/memory.ts +56 -60
- package/dist/template/.opencode/plugin/sessions.ts +372 -93
- package/dist/template/.opencode/tui.json +15 -0
- package/package.json +1 -1
- package/dist/template/.opencode/tool/action-queue.ts +0 -313
- package/dist/template/.opencode/tool/memory-admin.ts +0 -445
- package/dist/template/.opencode/tool/memory-get.ts +0 -143
- package/dist/template/.opencode/tool/memory-read.ts +0 -45
- package/dist/template/.opencode/tool/memory-search.ts +0 -264
- package/dist/template/.opencode/tool/memory-timeline.ts +0 -105
- package/dist/template/.opencode/tool/memory-update.ts +0 -63
- package/dist/template/.opencode/tool/observation.ts +0 -357
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Plugin — Core Tools
|
|
3
|
+
*
|
|
4
|
+
* observation, memory-search, memory-get, memory-read, memory-update, memory-timeline
|
|
5
|
+
*
|
|
6
|
+
* Uses factory pattern: createCoreTools(deps) returns tool definitions
|
|
7
|
+
* that can be spread into plugin's tool:{} export.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readdir } from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
13
|
+
import {
|
|
14
|
+
type ConfidenceLevel,
|
|
15
|
+
checkFTS5Available,
|
|
16
|
+
getMemoryFile,
|
|
17
|
+
getObservationsByIds,
|
|
18
|
+
getTimelineAroundObservation,
|
|
19
|
+
type ObservationSource,
|
|
20
|
+
type ObservationType,
|
|
21
|
+
searchDistillationsFTS,
|
|
22
|
+
searchObservationsFTS,
|
|
23
|
+
storeObservation,
|
|
24
|
+
upsertMemoryFile,
|
|
25
|
+
} from "./memory-db.js";
|
|
26
|
+
import {
|
|
27
|
+
autoDetectFiles,
|
|
28
|
+
formatObservation,
|
|
29
|
+
parseCSV,
|
|
30
|
+
safeReadFile,
|
|
31
|
+
TYPE_ICONS,
|
|
32
|
+
VALID_TYPES,
|
|
33
|
+
} from "./memory-helpers.js";
|
|
34
|
+
|
|
35
|
+
interface CoreToolDeps {
|
|
36
|
+
handoffDir: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createCoreTools(deps: CoreToolDeps) {
|
|
40
|
+
const { handoffDir } = deps;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
observation: tool({
|
|
44
|
+
description: `Create a structured observation for future reference.\n\t\n\tPurpose:\n\t- Capture decisions, bugs, features, patterns, discoveries, learnings, or warnings\n\t- Auto-detects file references from content (file:line, \`path\`, src/, .opencode/)\n\t- Stores in SQLite with FTS5 index for fast search\n\t- Supports enhanced schema: facts, subtitle, files_read/files_modified\n\t\n\tConfidence guidance:\n\t- high: verified by tests, logs, or direct inspection (default)\n\t- medium: likely, but not fully verified\n\t- low: uncertain or speculative\n\t\n\tType-specific examples:\n\tdecision\n\tobservation({\n\t type: "decision",\n\t title: "Use JWT for auth",\n\t narrative: "Chose JWT for stateless auth across services.",\n\t facts: "stateless, scalable",\n\t concepts: "authentication, jwt",\n\t confidence: "high"\n\t})\n\t\n\tbugfix\n\tobservation({\n\t type: "bugfix",\n\t title: "Fix null pointer on login",\n\t narrative: "Guarded optional user in src/auth.ts:42 to prevent crash.",\n\t files_modified: "src/auth.ts",\n\t concepts: "auth, null-check",\n\t confidence: "high"\n\t})\n\t\n\tfeature\n\tobservation({\n\t type: "feature",\n\t title: "Add CLI --dry-run",\n\t narrative: "Introduce dry-run mode to show planned changes without writing.",\n\t files_modified: "src/commands/init.ts",\n\t concepts: "cli, ux",\n\t confidence: "medium"\n\t})\n\t\n\tpattern\n\tobservation({\n\t type: "pattern",\n\t title: "Use zod for input validation",\n\t narrative: "All command inputs validated with zod schemas before execute.",\n\t concepts: "validation, zod",\n\t confidence: "high"\n\t})\n\t\n\tdiscovery\n\tobservation({\n\t type: "discovery",\n\t title: "Build copies .opencode/ to dist/template/",\n\t narrative: "Found rsync step in build.ts that bundles .opencode/.",\n\t files_read: "build.ts",\n\t confidence: "high"\n\t})\n\t\n\tlearning\n\tobservation({\n\t type: "learning",\n\t title: "Bun test respects --watch",\n\t narrative: "Observed bun test --watch keeps runner active during edits.",\n\t confidence: "medium"\n\t})\n\t\n\twarning\n\tobservation({\n\t type: "warning",\n\t title: "Do not edit dist/ directly",\n\t narrative: "dist/ is built output and overwritten on build.",\n\t concepts: "build, generated",\n\t confidence: "high"\n\t})`,
|
|
45
|
+
args: {
|
|
46
|
+
type: tool.schema
|
|
47
|
+
.string()
|
|
48
|
+
.describe(
|
|
49
|
+
"Observation type: decision, bugfix, feature, pattern, discovery, learning, warning",
|
|
50
|
+
),
|
|
51
|
+
title: tool.schema.string().describe("Brief title"),
|
|
52
|
+
subtitle: tool.schema.string().optional().describe("Optional subtitle"),
|
|
53
|
+
facts: tool.schema
|
|
54
|
+
.string()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Comma-separated key facts"),
|
|
57
|
+
narrative: tool.schema.string().optional().describe("Detailed content"),
|
|
58
|
+
content: tool.schema
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("DEPRECATED: Use 'narrative'"),
|
|
62
|
+
concepts: tool.schema
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("Comma-separated concept tags"),
|
|
66
|
+
files_read: tool.schema
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("Comma-separated files read"),
|
|
70
|
+
files_modified: tool.schema
|
|
71
|
+
.string()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("Comma-separated files modified"),
|
|
74
|
+
files: tool.schema
|
|
75
|
+
.string()
|
|
76
|
+
.optional()
|
|
77
|
+
.describe("DEPRECATED: Use 'files_modified'"),
|
|
78
|
+
bead_id: tool.schema.string().optional().describe("Related bead ID"),
|
|
79
|
+
confidence: tool.schema
|
|
80
|
+
.string()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe("high, medium, low"),
|
|
83
|
+
supersedes: tool.schema
|
|
84
|
+
.string()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("ID this supersedes"),
|
|
87
|
+
source: tool.schema
|
|
88
|
+
.string()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("manual, curator, imported"),
|
|
91
|
+
},
|
|
92
|
+
execute: async (args) => {
|
|
93
|
+
const obsType = args.type as ObservationType;
|
|
94
|
+
if (!VALID_TYPES.includes(obsType)) {
|
|
95
|
+
return `Error: Invalid type "${args.type}". Valid: ${VALID_TYPES.join(", ")}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const confidence = (args.confidence ?? "high") as ConfidenceLevel;
|
|
99
|
+
if (!["high", "medium", "low"].includes(confidence)) {
|
|
100
|
+
return `Error: Invalid confidence "${args.confidence}". Valid: high, medium, low`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const narrative = args.narrative ?? args.content;
|
|
104
|
+
const filesModifiedRaw = args.files_modified ?? args.files;
|
|
105
|
+
const facts = parseCSV(args.facts);
|
|
106
|
+
const concepts = parseCSV(args.concepts);
|
|
107
|
+
let filesRead = parseCSV(args.files_read);
|
|
108
|
+
const filesModified = parseCSV(filesModifiedRaw);
|
|
109
|
+
|
|
110
|
+
if (narrative) {
|
|
111
|
+
const detected = autoDetectFiles(narrative);
|
|
112
|
+
if (detected.length > 0) {
|
|
113
|
+
const existing = new Set([
|
|
114
|
+
...(filesRead ?? []),
|
|
115
|
+
...(filesModified ?? []),
|
|
116
|
+
]);
|
|
117
|
+
const newRefs = detected.filter((f) => !existing.has(f));
|
|
118
|
+
if (newRefs.length > 0)
|
|
119
|
+
filesRead = [...(filesRead ?? []), ...newRefs];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let supersedes: number | undefined;
|
|
124
|
+
if (args.supersedes) {
|
|
125
|
+
const parsed = Number.parseInt(args.supersedes, 10);
|
|
126
|
+
if (!Number.isNaN(parsed)) supersedes = parsed;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const source = (args.source ?? "manual") as ObservationSource;
|
|
130
|
+
|
|
131
|
+
const id = storeObservation({
|
|
132
|
+
type: obsType,
|
|
133
|
+
title: args.title,
|
|
134
|
+
subtitle: args.subtitle,
|
|
135
|
+
facts,
|
|
136
|
+
narrative,
|
|
137
|
+
concepts,
|
|
138
|
+
files_read: filesRead,
|
|
139
|
+
files_modified: filesModified,
|
|
140
|
+
confidence,
|
|
141
|
+
bead_id: args.bead_id,
|
|
142
|
+
supersedes,
|
|
143
|
+
source,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return `${TYPE_ICONS[obsType] ?? "\uD83D\uDCCC"} Observation #${id} stored [${obsType}] "${args.title}" (confidence: ${confidence}, source: ${source})`;
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
|
|
150
|
+
"memory-search": tool({
|
|
151
|
+
description: `Search memory across observations and markdown archives.\n\t\n\tPurpose:\n\t- Fast, ranked search across all observations in SQLite (when FTS5 is available)\n\t- Returns compact index (~50-100 tokens per result) for progressive disclosure\n\t- Use memory-get for full details after identifying relevant observations\n\t\n\tFTS5 availability:\n\t- Auto-detected at runtime; if unavailable, observation searches fall back to file scan\n\t\n\tSearch modes and hints:\n\t- "observations" (default): Best for decisions, bugs, learnings; uses FTS5 ranking when available\n\t- "handoffs": Use for past session handoffs and summaries\n\t- "research": Use for research notes and external findings\n\t- "templates": Use for memory templates and boilerplate references\n\t- "beads": Use for task artifacts in .beads/artifacts\n\t- "all": Use when you are unsure where info lives; searches SQLite + markdown + beads\n\t\n\tExample:\n\tmemory-search({ query: "authentication" })\n\tmemory-search({ query: "auth", type: "decision", limit: 5 })`,
|
|
152
|
+
args: {
|
|
153
|
+
query: tool.schema.string().describe("Search query"),
|
|
154
|
+
type: tool.schema
|
|
155
|
+
.string()
|
|
156
|
+
.optional()
|
|
157
|
+
.describe("Filter by type or scope"),
|
|
158
|
+
limit: tool.schema
|
|
159
|
+
.number()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe("Max results (default: 10)"),
|
|
162
|
+
},
|
|
163
|
+
execute: async (args) => {
|
|
164
|
+
const query = args.query.trim();
|
|
165
|
+
if (!query) return "Error: Empty search query";
|
|
166
|
+
const limit = args.limit ?? 10;
|
|
167
|
+
const scope = args.type ?? "observations";
|
|
168
|
+
const lines: string[] = [];
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
scope === "observations" ||
|
|
172
|
+
scope === "all" ||
|
|
173
|
+
VALID_TYPES.includes(scope as ObservationType)
|
|
174
|
+
) {
|
|
175
|
+
const typeFilter = VALID_TYPES.includes(scope as ObservationType)
|
|
176
|
+
? (scope as ObservationType)
|
|
177
|
+
: undefined;
|
|
178
|
+
if (checkFTS5Available()) {
|
|
179
|
+
const results = searchObservationsFTS(query, {
|
|
180
|
+
type: typeFilter,
|
|
181
|
+
limit,
|
|
182
|
+
});
|
|
183
|
+
if (results.length > 0) {
|
|
184
|
+
lines.push(`## Observations (${results.length} results)\n`);
|
|
185
|
+
lines.push("| ID | Type | Title | Date |");
|
|
186
|
+
lines.push("|---|---|---|---|");
|
|
187
|
+
for (const r of results)
|
|
188
|
+
lines.push(
|
|
189
|
+
`| ${r.id} | ${r.type} | ${r.title} | ${r.created_at.slice(0, 10)} |`,
|
|
190
|
+
);
|
|
191
|
+
lines.push("");
|
|
192
|
+
for (const r of results.slice(0, 3)) {
|
|
193
|
+
if (r.snippet) lines.push(`**#${r.id}**: ${r.snippet}`);
|
|
194
|
+
}
|
|
195
|
+
lines.push(
|
|
196
|
+
`\nUse \`memory-get({ ids: "${results
|
|
197
|
+
.slice(0, 3)
|
|
198
|
+
.map((r) => r.id)
|
|
199
|
+
.join(",")}" })\` for full details.`,
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
lines.push("No observation matches found.");
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
lines.push("FTS5 not available. Use memory-admin to check status.");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (scope === "distillations" || scope === "all") {
|
|
210
|
+
const distResults = searchDistillationsFTS(query, limit);
|
|
211
|
+
if (distResults.length > 0) {
|
|
212
|
+
lines.push(`\n## Distillations (${distResults.length} results)\n`);
|
|
213
|
+
for (const d of distResults) {
|
|
214
|
+
lines.push(
|
|
215
|
+
`- **Session ${d.session_id.slice(0, 8)}** (${d.message_count} msgs): ${d.snippet}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (scope === "handoffs" || scope === "all") {
|
|
222
|
+
try {
|
|
223
|
+
const handoffFiles = await readdir(handoffDir);
|
|
224
|
+
const matches: string[] = [];
|
|
225
|
+
for (const f of handoffFiles.filter((n) => n.endsWith(".md"))) {
|
|
226
|
+
const content = await safeReadFile(path.join(handoffDir, f));
|
|
227
|
+
if (content.toLowerCase().includes(query.toLowerCase()))
|
|
228
|
+
matches.push(f);
|
|
229
|
+
}
|
|
230
|
+
if (matches.length > 0) {
|
|
231
|
+
lines.push(`\n## Handoffs (${matches.length} matches)\n`);
|
|
232
|
+
for (const m of matches.slice(0, 5)) lines.push(`- ${m}`);
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
/* No handoffs directory */
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return lines.length > 0 ? lines.join("\n") : "No results found.";
|
|
240
|
+
},
|
|
241
|
+
}),
|
|
242
|
+
|
|
243
|
+
"memory-get": tool({
|
|
244
|
+
description: `Get full observation details by ID.\n\t\n\tPurpose:\n\t- Progressive disclosure: fetch full details after identifying relevant observations via search\n\t- Get complete narrative, facts, and metadata\n\t- Supports multiple IDs for batch retrieval\n\t\n\tExample:\n\tmemory-get({ ids: "42" }) // Single observation\n\tmemory-get({ ids: "1,5,10" }) // Multiple observations`,
|
|
245
|
+
args: {
|
|
246
|
+
ids: tool.schema.string().describe("Comma-separated observation IDs"),
|
|
247
|
+
},
|
|
248
|
+
execute: async (args) => {
|
|
249
|
+
const idList = args.ids
|
|
250
|
+
.split(",")
|
|
251
|
+
.map((s) => Number.parseInt(s.trim(), 10))
|
|
252
|
+
.filter((n) => !Number.isNaN(n));
|
|
253
|
+
if (idList.length === 0) return "Error: No valid IDs provided";
|
|
254
|
+
const observations = getObservationsByIds(idList);
|
|
255
|
+
if (observations.length === 0)
|
|
256
|
+
return "No observations found for given IDs.";
|
|
257
|
+
return observations
|
|
258
|
+
.map((obs) => formatObservation(obs))
|
|
259
|
+
.join("\n\n---\n\n");
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
|
|
263
|
+
"memory-read": tool({
|
|
264
|
+
description: `Read memory files for persistent cross-session context.\n\t\n\tPurpose:\n\t- Retrieve project state, learnings, and active tasks\n\t- Reads from SQLite database\n\t- Supports subdirectories: handoffs/, research/\n\t\n\tExample:\n\tmemory-read({ file: "handoffs/2024-01-20-phase-1" })\n\tmemory-read({ file: "research/2024-01-topic" })`,
|
|
265
|
+
args: {
|
|
266
|
+
file: tool.schema.string().optional().describe("Memory file path"),
|
|
267
|
+
},
|
|
268
|
+
execute: async (args) => {
|
|
269
|
+
const filePath = (args.file ?? "").replace(/\.md$/, "");
|
|
270
|
+
if (!filePath) return "Error: No file path provided";
|
|
271
|
+
const row = getMemoryFile(filePath);
|
|
272
|
+
return row ? row.content : `Memory file "${filePath}" not found.`;
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
|
|
276
|
+
"memory-update": tool({
|
|
277
|
+
description: `Update memory files with new learnings, progress, or context.\n\t\n\tPurpose:\n\t- Write or append to project memory in SQLite\n\t- Supports subdirectories (e.g., 'research/2024-01-topic')\n\t- Two modes: 'replace' (overwrite) or 'append' (add to end)\n\t\n\tExample:\n\tmemory-update({ file: "research/session-findings", content: "..." })\n\tmemory-update({ file: "handoffs/phase-2", content: "...", mode: "append" })`,
|
|
278
|
+
args: {
|
|
279
|
+
file: tool.schema.string().describe("Memory file to update"),
|
|
280
|
+
content: tool.schema.string().describe("Content to write or append"),
|
|
281
|
+
mode: tool.schema
|
|
282
|
+
.string()
|
|
283
|
+
.optional()
|
|
284
|
+
.describe("replace (default) or append"),
|
|
285
|
+
},
|
|
286
|
+
execute: async (args) => {
|
|
287
|
+
const filePath = args.file.replace(/\.md$/, "");
|
|
288
|
+
const mode = (args.mode ?? "replace") as "replace" | "append";
|
|
289
|
+
let finalContent = args.content;
|
|
290
|
+
if (mode === "append") {
|
|
291
|
+
finalContent = `\n---\n_Updated: ${new Date().toISOString()}_\n\n${args.content}`;
|
|
292
|
+
}
|
|
293
|
+
upsertMemoryFile(filePath, finalContent, mode);
|
|
294
|
+
return `Memory file "${filePath}" updated (mode: ${mode}).`;
|
|
295
|
+
},
|
|
296
|
+
}),
|
|
297
|
+
|
|
298
|
+
"memory-timeline": tool({
|
|
299
|
+
description: `Get chronological context around an observation.\n\t\n\tPurpose:\n\t- Progressive disclosure: see what was happening before/after a specific observation\n\t- Understand decision context over time\n\t- Navigate memory timeline\n\t\n\tExample:\n\tmemory-timeline({ anchor_id: 42, depth_before: 5, depth_after: 5 })`,
|
|
300
|
+
args: {
|
|
301
|
+
anchor_id: tool.schema.number().describe("ID of the observation"),
|
|
302
|
+
depth_before: tool.schema
|
|
303
|
+
.number()
|
|
304
|
+
.optional()
|
|
305
|
+
.describe("Earlier observations (default: 5)"),
|
|
306
|
+
depth_after: tool.schema
|
|
307
|
+
.number()
|
|
308
|
+
.optional()
|
|
309
|
+
.describe("Later observations (default: 5)"),
|
|
310
|
+
},
|
|
311
|
+
execute: async (args) => {
|
|
312
|
+
const { anchor, before, after } = getTimelineAroundObservation(
|
|
313
|
+
args.anchor_id,
|
|
314
|
+
args.depth_before ?? 5,
|
|
315
|
+
args.depth_after ?? 5,
|
|
316
|
+
);
|
|
317
|
+
if (!anchor) return `Observation #${args.anchor_id} not found.`;
|
|
318
|
+
const lines: string[] = [];
|
|
319
|
+
if (before.length > 0) {
|
|
320
|
+
lines.push("### Earlier");
|
|
321
|
+
for (const b of before)
|
|
322
|
+
lines.push(
|
|
323
|
+
` ${b.id}: [${b.type}] ${b.title} (${b.created_at.slice(0, 10)})`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
lines.push(
|
|
327
|
+
`\n### ${TYPE_ICONS[anchor.type] ?? "\uD83D\uDCCC"} Current: #${anchor.id} [${anchor.type}] ${anchor.title}`,
|
|
328
|
+
);
|
|
329
|
+
if (anchor.narrative) lines.push(anchor.narrative.slice(0, 200));
|
|
330
|
+
if (after.length > 0) {
|
|
331
|
+
lines.push("\n### Later");
|
|
332
|
+
for (const a of after)
|
|
333
|
+
lines.push(
|
|
334
|
+
` ${a.id}: [${a.type}] ${a.title} (${a.created_at.slice(0, 10)})`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return lines.join("\n");
|
|
338
|
+
},
|
|
339
|
+
}),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
@@ -1,27 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory Plugin
|
|
2
|
+
* Unified Memory Plugin v2
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Consolidates all memory tools + hooks + compaction into a single plugin.
|
|
5
|
+
*
|
|
6
|
+
* Systems:
|
|
7
|
+
* 1. Capture — message.updated → temporal_messages
|
|
8
|
+
* 2. Distillation — session.idle → TF-IDF compression
|
|
9
|
+
* 3. Curator — session.idle → pattern-matched observations
|
|
10
|
+
* 4. LTM Injection — system.transform → relevance-scored knowledge
|
|
11
|
+
* 5. Context Management — messages.transform → token budget enforcement
|
|
12
|
+
*
|
|
13
|
+
* Tools: observation, memory-search, memory-get, memory-read,
|
|
14
|
+
* memory-update, memory-timeline, memory-admin
|
|
15
|
+
*
|
|
16
|
+
* Module structure:
|
|
17
|
+
* memory.ts — Plugin entry (this file)
|
|
18
|
+
* lib/memory-helpers.ts — Constants, compaction utilities, formatting
|
|
19
|
+
* lib/memory-tools.ts — Core tools (observation, search, get, read, update, timeline)
|
|
20
|
+
* lib/memory-admin-tools.ts — Admin tools (memory-admin)
|
|
21
|
+
* lib/memory-hooks.ts — Event hooks, transforms, compaction
|
|
22
|
+
* lib/memory-db.ts — Database barrel (re-exports from lib/db/*.ts)
|
|
23
|
+
* lib/capture.ts — Message capture module
|
|
24
|
+
* lib/distill.ts — Heuristic distillation engine
|
|
25
|
+
* lib/curator.ts — Pattern-based knowledge extraction
|
|
26
|
+
* lib/inject.ts — LTM relevance scoring + injection
|
|
27
|
+
* lib/context.ts — Context window management
|
|
9
28
|
*/
|
|
10
29
|
|
|
30
|
+
import path from "node:path";
|
|
11
31
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
32
|
+
import { createAdminTools } from "./lib/memory-admin-tools.js";
|
|
33
|
+
import { createHooks } from "./lib/memory-hooks.js";
|
|
34
|
+
import { createCoreTools } from "./lib/memory-tools.js";
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Plugin Export
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
export const MemoryPlugin: Plugin = async ({ client, directory }) => {
|
|
41
|
+
const memoryDir = path.join(directory, ".opencode", "memory");
|
|
42
|
+
const handoffDir = path.join(memoryDir, "handoffs");
|
|
18
43
|
|
|
19
|
-
|
|
44
|
+
// Logging + toast helpers
|
|
20
45
|
const log = async (message: string, level: "info" | "warn" = "info") => {
|
|
21
46
|
await client.app
|
|
22
|
-
.log({
|
|
23
|
-
body: { service: "memory", level, message },
|
|
24
|
-
})
|
|
47
|
+
.log({ body: { service: "memory", level, message } })
|
|
25
48
|
.catch(() => {});
|
|
26
49
|
};
|
|
27
50
|
|
|
@@ -40,56 +63,29 @@ export const MemoryPlugin: Plugin = async ({ client }) => {
|
|
|
40
63
|
},
|
|
41
64
|
});
|
|
42
65
|
} catch {
|
|
43
|
-
|
|
66
|
+
/* Toast API unavailable */
|
|
44
67
|
}
|
|
45
68
|
};
|
|
46
69
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// Optimize FTS5 index for fast search
|
|
51
|
-
try {
|
|
52
|
-
if (checkFTS5Available()) {
|
|
53
|
-
optimizeFTS5();
|
|
54
|
-
await log("FTS5 index optimized");
|
|
55
|
-
}
|
|
56
|
-
} catch (err) {
|
|
57
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
58
|
-
await log(`FTS5 optimization failed: ${msg}`, "warn");
|
|
59
|
-
}
|
|
70
|
+
// Assemble tools
|
|
71
|
+
const coreTools = createCoreTools({ handoffDir });
|
|
72
|
+
const adminTools = createAdminTools({ directory });
|
|
60
73
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
} catch (err) {
|
|
73
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
74
|
-
await log(`WAL checkpoint failed: ${msg}`, "warn");
|
|
75
|
-
}
|
|
76
|
-
},
|
|
74
|
+
// Assemble hooks
|
|
75
|
+
const hooks = createHooks({
|
|
76
|
+
memoryDir,
|
|
77
|
+
handoffDir,
|
|
78
|
+
directory,
|
|
79
|
+
showToast,
|
|
80
|
+
log,
|
|
81
|
+
});
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
// Warn on session errors
|
|
86
|
-
"session.error": async () => {
|
|
87
|
-
await showToast(
|
|
88
|
-
"Session Error",
|
|
89
|
-
"Consider saving important learnings with observation tool",
|
|
90
|
-
"warning",
|
|
91
|
-
);
|
|
83
|
+
return {
|
|
84
|
+
tool: {
|
|
85
|
+
...coreTools,
|
|
86
|
+
...adminTools,
|
|
92
87
|
},
|
|
88
|
+
...hooks,
|
|
93
89
|
};
|
|
94
90
|
};
|
|
95
91
|
|