opencodekit 0.16.10 → 0.16.13
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 +1 -1
- package/dist/template/.opencode/AGENTS.md +85 -32
- package/dist/template/.opencode/agent/build.md +24 -2
- package/dist/template/.opencode/command/handoff.md +37 -0
- package/dist/template/.opencode/command/start.md +6 -0
- package/dist/template/.opencode/command/status.md +33 -7
- package/dist/template/.opencode/dcp.jsonc +79 -79
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +905 -885
- package/dist/template/.opencode/plugin/compaction.ts +52 -51
- package/dist/template/.opencode/plugin/memory.ts +22 -551
- package/dist/template/.opencode/plugin/skill-mcp.ts +514 -486
- package/dist/template/.opencode/plugin/swarm-enforcer.ts +43 -119
- package/dist/template/.opencode/skill/agent-teams/SKILL.md +242 -0
- package/dist/template/.opencode/skill/compaction/SKILL.md +338 -0
- package/dist/template/.opencode/skill/jira/SKILL.md +177 -50
- package/dist/template/.opencode/skill/jira/mcp.json +2 -10
- package/package.json +1 -1
- package/dist/template/.opencode/plugin/env-ctx.ts +0 -34
- package/dist/template/.opencode/plugin/lsp.ts +0 -301
- package/dist/template/.opencode/plugin/truncator.ts +0 -59
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory Plugin
|
|
2
|
+
* Memory Plugin — Database Maintenance & UX Notifications
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. Toast
|
|
8
|
-
* 4.
|
|
9
|
-
*
|
|
10
|
-
* Uses OpenCode's official event system (v1.1.2+):
|
|
11
|
-
* - file.edited: When OpenCode edits files
|
|
12
|
-
* - file.watcher.updated: External file changes
|
|
13
|
-
* - session.idle: Session completed
|
|
14
|
-
* - tui.toast.show: Display notifications
|
|
15
|
-
*
|
|
16
|
-
* Inspired by: Supermemory, Nia, Graphiti, GKG
|
|
4
|
+
* Handles infrastructure tasks that the agent shouldn't manage:
|
|
5
|
+
* 1. FTS5 index optimization on session idle
|
|
6
|
+
* 2. WAL checkpoint when file gets large
|
|
7
|
+
* 3. Toast notification when observations are saved
|
|
8
|
+
* 4. Toast warning on session errors
|
|
17
9
|
*/
|
|
18
10
|
|
|
19
|
-
import fsPromises from "node:fs/promises";
|
|
20
|
-
import path from "node:path";
|
|
21
11
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
22
12
|
import {
|
|
23
13
|
checkFTS5Available,
|
|
@@ -26,497 +16,52 @@ import {
|
|
|
26
16
|
optimizeFTS5,
|
|
27
17
|
} from "./lib/memory-db.js";
|
|
28
18
|
|
|
29
|
-
|
|
30
|
-
// Configuration
|
|
31
|
-
// ============================================================================
|
|
32
|
-
|
|
33
|
-
const MEMORY_DIR = ".opencode/memory";
|
|
34
|
-
const SRC_DIR = "src";
|
|
35
|
-
// All extensions supported by OpenCode LSP (https://opencode.ai/docs/lsp/)
|
|
36
|
-
const CODE_EXTENSIONS = [
|
|
37
|
-
// TypeScript/JavaScript
|
|
38
|
-
".ts",
|
|
39
|
-
".tsx",
|
|
40
|
-
".js",
|
|
41
|
-
".jsx",
|
|
42
|
-
".mjs",
|
|
43
|
-
".cjs",
|
|
44
|
-
".mts",
|
|
45
|
-
".cts",
|
|
46
|
-
// Web frameworks
|
|
47
|
-
".vue",
|
|
48
|
-
".svelte",
|
|
49
|
-
".astro",
|
|
50
|
-
// Python
|
|
51
|
-
".py",
|
|
52
|
-
".pyi",
|
|
53
|
-
// Go
|
|
54
|
-
".go",
|
|
55
|
-
// Rust
|
|
56
|
-
".rs",
|
|
57
|
-
// C/C++
|
|
58
|
-
".c",
|
|
59
|
-
".cpp",
|
|
60
|
-
".cc",
|
|
61
|
-
".cxx",
|
|
62
|
-
".h",
|
|
63
|
-
".hpp",
|
|
64
|
-
".hh",
|
|
65
|
-
".hxx",
|
|
66
|
-
// Java/Kotlin
|
|
67
|
-
".java",
|
|
68
|
-
".kt",
|
|
69
|
-
".kts",
|
|
70
|
-
// C#/F#
|
|
71
|
-
".cs",
|
|
72
|
-
".fs",
|
|
73
|
-
".fsi",
|
|
74
|
-
".fsx",
|
|
75
|
-
// Ruby
|
|
76
|
-
".rb",
|
|
77
|
-
".rake",
|
|
78
|
-
".gemspec",
|
|
79
|
-
// PHP
|
|
80
|
-
".php",
|
|
81
|
-
// Elixir
|
|
82
|
-
".ex",
|
|
83
|
-
".exs",
|
|
84
|
-
// Clojure
|
|
85
|
-
".clj",
|
|
86
|
-
".cljs",
|
|
87
|
-
".cljc",
|
|
88
|
-
// Shell
|
|
89
|
-
".sh",
|
|
90
|
-
".bash",
|
|
91
|
-
".zsh",
|
|
92
|
-
// Swift/Objective-C
|
|
93
|
-
".swift",
|
|
94
|
-
".m",
|
|
95
|
-
".mm",
|
|
96
|
-
// Other
|
|
97
|
-
".lua",
|
|
98
|
-
".dart",
|
|
99
|
-
".gleam",
|
|
100
|
-
".nix",
|
|
101
|
-
".ml",
|
|
102
|
-
".mli",
|
|
103
|
-
".zig",
|
|
104
|
-
".zon",
|
|
105
|
-
".prisma",
|
|
106
|
-
".tf",
|
|
107
|
-
".tfvars",
|
|
108
|
-
".typ",
|
|
109
|
-
".yaml",
|
|
110
|
-
".yml",
|
|
111
|
-
];
|
|
112
|
-
const CODE_CHANGE_DEBOUNCE_MS = 10000; // 10 seconds for toast responsiveness
|
|
113
|
-
|
|
114
|
-
// Default trigger patterns (case-insensitive)
|
|
115
|
-
const DEFAULT_PATTERNS = [
|
|
116
|
-
// English
|
|
117
|
-
"remember\\s+(this|that)",
|
|
118
|
-
"save\\s+(this|that)",
|
|
119
|
-
"don'?t\\s+forget",
|
|
120
|
-
"note\\s+(this|that)",
|
|
121
|
-
"keep\\s+(this\\s+)?in\\s+mind",
|
|
122
|
-
"store\\s+this",
|
|
123
|
-
"log\\s+this",
|
|
124
|
-
"write\\s+(this\\s+)?down",
|
|
125
|
-
"make\\s+a\\s+note",
|
|
126
|
-
"add\\s+to\\s+memory",
|
|
127
|
-
"commit\\s+to\\s+memory",
|
|
128
|
-
"take\\s+note",
|
|
129
|
-
"jot\\s+(this\\s+)?down",
|
|
130
|
-
"file\\s+this\\s+away",
|
|
131
|
-
"bookmark\\s+this",
|
|
132
|
-
"pin\\s+this",
|
|
133
|
-
"important\\s+to\\s+remember",
|
|
134
|
-
"for\\s+future\\s+reference",
|
|
135
|
-
"never\\s+forget",
|
|
136
|
-
"always\\s+remember",
|
|
137
|
-
|
|
138
|
-
// Vietnamese
|
|
139
|
-
"nhớ\\s+(điều\\s+)?này",
|
|
140
|
-
"ghi\\s+nhớ",
|
|
141
|
-
"lưu\\s+(lại\\s+)?(điều\\s+)?này",
|
|
142
|
-
"đừng\\s+quên",
|
|
143
|
-
"không\\s+được\\s+quên",
|
|
144
|
-
"ghi\\s+chú",
|
|
145
|
-
"note\\s+lại",
|
|
146
|
-
"save\\s+lại",
|
|
147
|
-
"để\\s+ý",
|
|
148
|
-
"chú\\s+ý",
|
|
149
|
-
"quan\\s+trọng",
|
|
150
|
-
"cần\\s+nhớ",
|
|
151
|
-
"phải\\s+nhớ",
|
|
152
|
-
"luôn\\s+nhớ",
|
|
153
|
-
"ghi\\s+lại",
|
|
154
|
-
"lưu\\s+ý",
|
|
155
|
-
"đánh\\s+dấu",
|
|
156
|
-
"bookmark\\s+lại",
|
|
157
|
-
];
|
|
158
|
-
|
|
159
|
-
const DEFAULT_NUDGE = `
|
|
160
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
161
|
-
[MEMORY TRIGGER DETECTED]
|
|
162
|
-
|
|
163
|
-
The user wants you to remember something. You MUST:
|
|
164
|
-
|
|
165
|
-
1. Extract the key information to save
|
|
166
|
-
2. Call the \`observation\` tool with:
|
|
167
|
-
- type: Choose from "learning", "decision", "pattern", "preference", or "warning"
|
|
168
|
-
- title: A concise, searchable title
|
|
169
|
-
- content: The full context of what to remember
|
|
170
|
-
- concepts: Keywords for semantic search (comma-separated)
|
|
171
|
-
|
|
172
|
-
Example:
|
|
173
|
-
\`\`\`typescript
|
|
174
|
-
observation({
|
|
175
|
-
type: "preference",
|
|
176
|
-
title: "Use bun instead of npm",
|
|
177
|
-
content: "This project uses bun as the package manager, not npm",
|
|
178
|
-
concepts: "bun, npm, package-manager"
|
|
179
|
-
});
|
|
180
|
-
\`\`\`
|
|
181
|
-
|
|
182
|
-
After saving, confirm: "✓ Saved to memory for future reference."
|
|
183
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
184
|
-
`;
|
|
185
|
-
|
|
186
|
-
// ============================================================================
|
|
187
|
-
// Types
|
|
188
|
-
// ============================================================================
|
|
189
|
-
|
|
190
|
-
interface MemoryConfig {
|
|
191
|
-
enabled?: boolean;
|
|
192
|
-
patterns?: string[];
|
|
193
|
-
nudgeTemplate?: string;
|
|
194
|
-
watcherEnabled?: boolean;
|
|
195
|
-
toastEnabled?: boolean;
|
|
196
|
-
sessionSummaryEnabled?: boolean;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ============================================================================
|
|
200
|
-
// Helpers
|
|
201
|
-
// ============================================================================
|
|
202
|
-
|
|
203
|
-
async function loadConfig(): Promise<MemoryConfig> {
|
|
204
|
-
const configPaths = [
|
|
205
|
-
path.join(process.cwd(), ".opencode", "memory.json"),
|
|
206
|
-
path.join(process.cwd(), ".opencode", "memory.jsonc"),
|
|
207
|
-
];
|
|
208
|
-
|
|
209
|
-
for (const configPath of configPaths) {
|
|
210
|
-
try {
|
|
211
|
-
const content = await fsPromises.readFile(configPath, "utf-8");
|
|
212
|
-
const jsonContent = content.replace(/\/\/.*$|\/\*[\s\S]*?\*\//gm, "");
|
|
213
|
-
return JSON.parse(jsonContent);
|
|
214
|
-
} catch {
|
|
215
|
-
// Config doesn't exist
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return {};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function buildPattern(patterns: string[]): RegExp {
|
|
223
|
-
return new RegExp(`(${patterns.join("|")})`, "i");
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function extractContext(message: string, matchIndex: number): string {
|
|
227
|
-
const before = message.substring(0, matchIndex);
|
|
228
|
-
const after = message.substring(matchIndex);
|
|
229
|
-
|
|
230
|
-
const sentenceStart = Math.max(
|
|
231
|
-
before.lastIndexOf(".") + 1,
|
|
232
|
-
before.lastIndexOf("!") + 1,
|
|
233
|
-
before.lastIndexOf("?") + 1,
|
|
234
|
-
0,
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
const afterPeriod = after.search(/[.!?]/);
|
|
238
|
-
const sentenceEnd =
|
|
239
|
-
afterPeriod === -1 ? message.length : matchIndex + afterPeriod + 1;
|
|
240
|
-
|
|
241
|
-
return message.substring(sentenceStart, sentenceEnd).trim();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function isCodeFile(filePath: string): boolean {
|
|
245
|
-
return CODE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ============================================================================
|
|
249
|
-
// Plugin
|
|
250
|
-
// ============================================================================
|
|
251
|
-
// Plugin
|
|
252
|
-
// ============================================================================
|
|
253
|
-
|
|
254
|
-
export const MemoryPlugin: Plugin = async ({ client, $: _$ }) => {
|
|
255
|
-
const config = await loadConfig();
|
|
256
|
-
|
|
257
|
-
if (config.enabled === false) {
|
|
258
|
-
return {};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Logger helper
|
|
19
|
+
export const MemoryPlugin: Plugin = async ({ client }) => {
|
|
262
20
|
const log = async (message: string, level: "info" | "warn" = "info") => {
|
|
263
21
|
await client.app
|
|
264
22
|
.log({
|
|
265
|
-
body: {
|
|
266
|
-
service: "memory",
|
|
267
|
-
level,
|
|
268
|
-
message,
|
|
269
|
-
},
|
|
23
|
+
body: { service: "memory", level, message },
|
|
270
24
|
})
|
|
271
25
|
.catch(() => {});
|
|
272
26
|
};
|
|
273
27
|
|
|
274
|
-
// Toast notification helper using TUI (cross-platform)
|
|
275
28
|
const showToast = async (
|
|
276
29
|
title: string,
|
|
277
30
|
message: string,
|
|
278
|
-
variant: "info" | "
|
|
31
|
+
variant: "info" | "warning" = "info",
|
|
279
32
|
) => {
|
|
280
|
-
if (config.toastEnabled === false) return;
|
|
281
|
-
|
|
282
33
|
try {
|
|
283
|
-
// Use OpenCode's TUI toast API (cross-platform, integrated)
|
|
284
34
|
await client.tui.showToast({
|
|
285
35
|
body: {
|
|
286
36
|
title: `Memory: ${title}`,
|
|
287
37
|
message,
|
|
288
38
|
variant,
|
|
289
|
-
duration: variant === "
|
|
39
|
+
duration: variant === "warning" ? 8000 : 5000,
|
|
290
40
|
},
|
|
291
41
|
});
|
|
292
|
-
|
|
293
|
-
// Also log for debugging
|
|
294
|
-
await log(`[TOAST] ${title}: ${message}`, "info");
|
|
295
42
|
} catch {
|
|
296
|
-
// Toast
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// -------------------------------------------------------------------------
|
|
301
|
-
// Part 1: Keyword Detection
|
|
302
|
-
// -------------------------------------------------------------------------
|
|
303
|
-
|
|
304
|
-
const patterns = config.patterns || DEFAULT_PATTERNS;
|
|
305
|
-
const triggerPattern = buildPattern(patterns);
|
|
306
|
-
const nudgeTemplate = config.nudgeTemplate || DEFAULT_NUDGE;
|
|
307
|
-
const processedMessages = new Set<string>();
|
|
308
|
-
|
|
309
|
-
// -------------------------------------------------------------------------
|
|
310
|
-
// Part 2: Code-Change Awareness with Toast Notifications
|
|
311
|
-
// -------------------------------------------------------------------------
|
|
312
|
-
|
|
313
|
-
let codeChangeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
314
|
-
const pendingCodeChanges = new Set<string>();
|
|
315
|
-
|
|
316
|
-
const flagStaleObservations = async (changedFiles: string[]) => {
|
|
317
|
-
const obsDir = path.join(process.cwd(), MEMORY_DIR, "observations");
|
|
318
|
-
|
|
319
|
-
try {
|
|
320
|
-
const files = await fsPromises.readdir(obsDir);
|
|
321
|
-
const observations = files.filter((f) => f.endsWith(".md"));
|
|
322
|
-
|
|
323
|
-
let flaggedCount = 0;
|
|
324
|
-
const flaggedTitles: string[] = [];
|
|
325
|
-
|
|
326
|
-
for (const obsFile of observations) {
|
|
327
|
-
const obsPath = path.join(obsDir, obsFile);
|
|
328
|
-
const content = await fsPromises.readFile(obsPath, "utf-8");
|
|
329
|
-
|
|
330
|
-
// Skip already flagged
|
|
331
|
-
if (content.includes("needs_review: true")) continue;
|
|
332
|
-
|
|
333
|
-
// Check if observation references any changed files
|
|
334
|
-
const referencesChanged = changedFiles.some((changedFile) => {
|
|
335
|
-
const normalizedChanged = changedFile.replace(/\\/g, "/");
|
|
336
|
-
return (
|
|
337
|
-
content.includes(normalizedChanged) ||
|
|
338
|
-
content.includes(path.basename(changedFile))
|
|
339
|
-
);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
if (referencesChanged) {
|
|
343
|
-
// Extract title from content
|
|
344
|
-
const titleMatch = content.match(/^# .* (.+)$/m);
|
|
345
|
-
const title = titleMatch ? titleMatch[1] : obsFile;
|
|
346
|
-
|
|
347
|
-
// Add needs_review flag
|
|
348
|
-
const updatedContent = content.replace(
|
|
349
|
-
/^(---\n)/,
|
|
350
|
-
`$1needs_review: true\nreview_reason: "Related code changed on ${new Date().toISOString().split("T")[0]}"\n`,
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
if (updatedContent !== content) {
|
|
354
|
-
await fsPromises.writeFile(obsPath, updatedContent, "utf-8");
|
|
355
|
-
flaggedCount++;
|
|
356
|
-
flaggedTitles.push(title);
|
|
357
|
-
await log(`Flagged observation for review: ${obsFile}`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Show toast notification if observations were flagged
|
|
363
|
-
if (flaggedCount > 0) {
|
|
364
|
-
const message =
|
|
365
|
-
flaggedCount === 1
|
|
366
|
-
? `"${flaggedTitles[0]}" may be outdated`
|
|
367
|
-
: `${flaggedCount} observations may be outdated`;
|
|
368
|
-
|
|
369
|
-
await showToast("Review Needed", message, "warning");
|
|
370
|
-
}
|
|
371
|
-
} catch (err) {
|
|
372
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
373
|
-
await log(`Failed to flag stale observations: ${msg}`, "warn");
|
|
43
|
+
// Toast API unavailable, continue silently
|
|
374
44
|
}
|
|
375
45
|
};
|
|
376
46
|
|
|
377
|
-
const processCodeChanges = async () => {
|
|
378
|
-
const changedFiles = Array.from(pendingCodeChanges);
|
|
379
|
-
pendingCodeChanges.clear();
|
|
380
|
-
|
|
381
|
-
if (changedFiles.length > 0) {
|
|
382
|
-
await log(`Processing ${changedFiles.length} code change(s)`);
|
|
383
|
-
await flagStaleObservations(changedFiles);
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
const handleCodeChange = (filePath: string) => {
|
|
388
|
-
pendingCodeChanges.add(filePath);
|
|
389
|
-
|
|
390
|
-
if (codeChangeTimer) {
|
|
391
|
-
clearTimeout(codeChangeTimer);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
codeChangeTimer = setTimeout(() => {
|
|
395
|
-
codeChangeTimer = null;
|
|
396
|
-
processCodeChanges();
|
|
397
|
-
}, CODE_CHANGE_DEBOUNCE_MS);
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
// -------------------------------------------------------------------------
|
|
401
|
-
// Part 4: Session Idle Summary
|
|
402
|
-
// -------------------------------------------------------------------------
|
|
403
|
-
|
|
404
|
-
// -------------------------------------------------------------------------
|
|
405
|
-
// Return Event Hooks (Official OpenCode Event System)
|
|
406
|
-
// -------------------------------------------------------------------------
|
|
407
|
-
|
|
408
|
-
// Return Event Hooks (Official OpenCode Event System)
|
|
409
|
-
// -------------------------------------------------------------------------
|
|
410
|
-
|
|
411
47
|
return {
|
|
412
|
-
//
|
|
413
|
-
"message.updated": async ({ event }) => {
|
|
414
|
-
// Only process user messages
|
|
415
|
-
const message = event.properties?.message;
|
|
416
|
-
if (!message || message.role !== "user") return;
|
|
417
|
-
|
|
418
|
-
const parts = message.parts;
|
|
419
|
-
if (!parts) return;
|
|
420
|
-
|
|
421
|
-
// Get text content
|
|
422
|
-
const textParts = parts.filter(
|
|
423
|
-
(p: {
|
|
424
|
-
type: string;
|
|
425
|
-
text?: string;
|
|
426
|
-
}): p is {
|
|
427
|
-
type: "text";
|
|
428
|
-
text: string;
|
|
429
|
-
} => p.type === "text",
|
|
430
|
-
);
|
|
431
|
-
if (textParts.length === 0) return;
|
|
432
|
-
|
|
433
|
-
const fullText = textParts.map((p: { text: string }) => p.text).join(" ");
|
|
434
|
-
|
|
435
|
-
// Avoid processing same message twice
|
|
436
|
-
const messageHash = `${message.role}:${fullText.substring(0, 100)}`;
|
|
437
|
-
if (processedMessages.has(messageHash)) return;
|
|
438
|
-
processedMessages.add(messageHash);
|
|
439
|
-
|
|
440
|
-
// Limit cache size
|
|
441
|
-
if (processedMessages.size > 100) {
|
|
442
|
-
const first = processedMessages.values().next().value;
|
|
443
|
-
if (first) processedMessages.delete(first);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Check for trigger keywords
|
|
447
|
-
const match = fullText.match(triggerPattern);
|
|
448
|
-
if (!match) return;
|
|
449
|
-
|
|
450
|
-
const context = extractContext(fullText, match.index || 0);
|
|
451
|
-
await log(
|
|
452
|
-
`Detected memory trigger: "${match[0]}" in: "${context.substring(0, 50)}..."`,
|
|
453
|
-
);
|
|
454
|
-
|
|
455
|
-
// Note: The nudge would be injected via chat.message hook if available
|
|
456
|
-
},
|
|
457
|
-
|
|
458
|
-
// Hook: file.edited - OpenCode edited a file
|
|
459
|
-
"file.edited": async ({ event }) => {
|
|
460
|
-
const filePath = event.properties?.file || event.properties?.path;
|
|
461
|
-
if (!filePath) return;
|
|
462
|
-
|
|
463
|
-
const absolutePath =
|
|
464
|
-
typeof filePath === "string" && path.isAbsolute(filePath)
|
|
465
|
-
? filePath
|
|
466
|
-
: path.join(process.cwd(), filePath);
|
|
467
|
-
|
|
468
|
-
// Check if it's a code file in src/
|
|
469
|
-
if (isCodeFile(absolutePath) && absolutePath.includes(`/${SRC_DIR}/`)) {
|
|
470
|
-
await log(`Code edited by OpenCode: ${filePath}`);
|
|
471
|
-
handleCodeChange(absolutePath);
|
|
472
|
-
}
|
|
473
|
-
},
|
|
474
|
-
|
|
475
|
-
// Hook: file.watcher.updated - External file changes
|
|
476
|
-
"file.watcher.updated": async ({ event }) => {
|
|
477
|
-
const filePath = event.properties?.file || event.properties?.path;
|
|
478
|
-
if (!filePath) return;
|
|
479
|
-
|
|
480
|
-
const absolutePath =
|
|
481
|
-
typeof filePath === "string" && path.isAbsolute(filePath)
|
|
482
|
-
? filePath
|
|
483
|
-
: path.join(process.cwd(), filePath);
|
|
484
|
-
|
|
485
|
-
// Ignore node_modules and vector_db
|
|
486
|
-
if (
|
|
487
|
-
absolutePath.includes("node_modules") ||
|
|
488
|
-
absolutePath.includes("vector_db")
|
|
489
|
-
) {
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Code file changes
|
|
494
|
-
if (isCodeFile(absolutePath) && absolutePath.includes(`/${SRC_DIR}/`)) {
|
|
495
|
-
await log(`External code change: ${filePath}`);
|
|
496
|
-
handleCodeChange(absolutePath);
|
|
497
|
-
}
|
|
498
|
-
},
|
|
499
|
-
|
|
500
|
-
// Hook: session.idle - Session completed + FTS5 optimization
|
|
48
|
+
// FTS5 optimization + WAL checkpoint on session idle
|
|
501
49
|
"session.idle": async () => {
|
|
502
|
-
//
|
|
503
|
-
// Run FTS5 optimize at session end to keep search fast
|
|
50
|
+
// Optimize FTS5 index for fast search
|
|
504
51
|
try {
|
|
505
52
|
if (checkFTS5Available()) {
|
|
506
53
|
optimizeFTS5();
|
|
507
54
|
await log("FTS5 index optimized");
|
|
508
55
|
}
|
|
509
56
|
} catch (err) {
|
|
510
|
-
const
|
|
511
|
-
await log(`FTS5 optimization failed: ${
|
|
57
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
58
|
+
await log(`FTS5 optimization failed: ${msg}`, "warn");
|
|
512
59
|
}
|
|
513
60
|
|
|
514
|
-
//
|
|
515
|
-
// Checkpoint WAL to main DB when it gets too large
|
|
61
|
+
// Checkpoint WAL if it's grown past 1MB
|
|
516
62
|
try {
|
|
517
63
|
const sizes = getDatabaseSizes();
|
|
518
64
|
if (sizes.wal > 1024 * 1024) {
|
|
519
|
-
// WAL > 1MB
|
|
520
65
|
const result = checkpointWAL();
|
|
521
66
|
if (result.checkpointed) {
|
|
522
67
|
await log(
|
|
@@ -525,49 +70,19 @@ export const MemoryPlugin: Plugin = async ({ client, $: _$ }) => {
|
|
|
525
70
|
}
|
|
526
71
|
}
|
|
527
72
|
} catch (err) {
|
|
528
|
-
const
|
|
529
|
-
await log(`WAL checkpoint failed: ${
|
|
73
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
74
|
+
await log(`WAL checkpoint failed: ${msg}`, "warn");
|
|
530
75
|
}
|
|
531
|
-
|
|
532
|
-
// ===== Session Summary Prompt =====
|
|
533
|
-
if (config.sessionSummaryEnabled === false) return;
|
|
534
|
-
|
|
535
|
-
await log("Session idle - prompting memory summary");
|
|
536
|
-
await showToast(
|
|
537
|
-
"Session Ending",
|
|
538
|
-
"Consider saving key learnings before ending",
|
|
539
|
-
"info",
|
|
540
|
-
);
|
|
541
76
|
},
|
|
542
77
|
|
|
543
|
-
//
|
|
544
|
-
"tool.execute.after": async (input,
|
|
78
|
+
// Toast when an observation is saved
|
|
79
|
+
"tool.execute.after": async (input, _output) => {
|
|
545
80
|
if (input.tool === "observation") {
|
|
546
|
-
await showToast("
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// LSP Nudge Injection: If tool output contains LSP hints, wrap them in a prompt
|
|
550
|
-
if (
|
|
551
|
-
output.output.includes("lsp_lsp_goto_definition") ||
|
|
552
|
-
output.output.includes("lsp_lsp_document_symbols") ||
|
|
553
|
-
output.output.includes("lsp_lsp_find_references")
|
|
554
|
-
) {
|
|
555
|
-
// Avoid double injection
|
|
556
|
-
if (output.output.includes("[LSP NAVIGATION AVAILABLE]")) return;
|
|
557
|
-
|
|
558
|
-
const LSP_PROMPT = `
|
|
559
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
560
|
-
[LSP NAVIGATION AVAILABLE]
|
|
561
|
-
|
|
562
|
-
The tool output contains actionable LSP navigation links (🔍).
|
|
563
|
-
You can use these to immediately jump to the relevant code context.
|
|
564
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
565
|
-
`;
|
|
566
|
-
output.output += LSP_PROMPT;
|
|
81
|
+
await showToast("Saved", "Observation added to memory");
|
|
567
82
|
}
|
|
568
83
|
},
|
|
569
84
|
|
|
570
|
-
//
|
|
85
|
+
// Warn on session errors
|
|
571
86
|
"session.error": async () => {
|
|
572
87
|
await showToast(
|
|
573
88
|
"Session Error",
|
|
@@ -575,51 +90,7 @@ You can use these to immediately jump to the relevant code context.
|
|
|
575
90
|
"warning",
|
|
576
91
|
);
|
|
577
92
|
},
|
|
578
|
-
|
|
579
|
-
// Hook: Inject nudge for memory triggers (chat.message if available)
|
|
580
|
-
"chat.message": async (input, output) => {
|
|
581
|
-
const { sessionID, messageID } = input;
|
|
582
|
-
const { message, parts } = output;
|
|
583
|
-
|
|
584
|
-
// Only process user messages
|
|
585
|
-
if (message.role !== "user") return;
|
|
586
|
-
|
|
587
|
-
// Get text content
|
|
588
|
-
const textParts = parts.filter(
|
|
589
|
-
(p): p is Extract<typeof p, { type: "text" }> => p.type === "text",
|
|
590
|
-
);
|
|
591
|
-
if (textParts.length === 0) return;
|
|
592
|
-
|
|
593
|
-
const fullText = textParts.map((p) => p.text).join(" ");
|
|
594
|
-
|
|
595
|
-
// Avoid processing same message twice (use different cache for this hook)
|
|
596
|
-
const messageKey = `chat:${fullText.substring(0, 100)}`;
|
|
597
|
-
if (processedMessages.has(messageKey)) return;
|
|
598
|
-
processedMessages.add(messageKey);
|
|
599
|
-
|
|
600
|
-
// Check for trigger keywords
|
|
601
|
-
const match = fullText.match(triggerPattern);
|
|
602
|
-
if (!match) return;
|
|
603
|
-
|
|
604
|
-
const context = extractContext(fullText, match.index || 0);
|
|
605
|
-
await log(
|
|
606
|
-
`Detected memory trigger: "${match[0]}" in: "${context.substring(0, 50)}..."`,
|
|
607
|
-
);
|
|
608
|
-
|
|
609
|
-
// Inject the nudge as a synthetic text part
|
|
610
|
-
const partId = `memory-nudge-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
611
|
-
|
|
612
|
-
parts.push({
|
|
613
|
-
id: partId,
|
|
614
|
-
sessionID,
|
|
615
|
-
messageID: messageID || "",
|
|
616
|
-
type: "text",
|
|
617
|
-
text: nudgeTemplate,
|
|
618
|
-
synthetic: true,
|
|
619
|
-
} as import("@opencode-ai/sdk").Part);
|
|
620
|
-
},
|
|
621
93
|
};
|
|
622
94
|
};
|
|
623
95
|
|
|
624
|
-
// Default export for OpenCode plugin loader
|
|
625
96
|
export default MemoryPlugin;
|