opencodekit 0.13.2 → 0.14.1

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