@scitrera/memorylayer-opencode-plugin 0.1.22 → 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.
@@ -1,63 +0,0 @@
1
- /**
2
- * Event hook for MemoryLayer OpenCode plugin.
3
- *
4
- * Handles context compaction by preserving critical memory state
5
- * and committing working memory before context is lost.
6
- */
7
-
8
- import { getClient, checkHealth } from "../shared/client.js";
9
-
10
- /**
11
- * Handle session compacting — commit working memory and checkpoint sandbox.
12
- *
13
- * OpenCode's `experimental.session.compacting` hook is the equivalent
14
- * of Claude Code's PreCompact. We commit working memory and return
15
- * context strings that survive the compaction.
16
- */
17
- export async function handleCompacting(_sessionID: string): Promise<string[]> {
18
- const context: string[] = [];
19
-
20
- const healthy = await checkHealth();
21
- if (!healthy) return context;
22
-
23
- const client = getClient();
24
- const clientSessionId = client.getSessionId();
25
-
26
- if (clientSessionId) {
27
- // Commit working memory to long-term storage before compaction
28
- try {
29
- await client.commitSession(clientSessionId, { importance_threshold: 0.3 });
30
- context.push(
31
- "[MemoryLayer] Working memory committed to long-term storage before compaction. " +
32
- "Use `memory_recall` to retrieve prior context. " +
33
- "Use `memory_context_inspect` to check server-side sandbox variables."
34
- );
35
- } catch {
36
- // Commit may fail, but we should still try to preserve context
37
- }
38
-
39
- // Checkpoint sandbox state if active
40
- try {
41
- const status = await client.contextStatus() as { exists?: boolean; variable_count?: number };
42
- if (status.exists && (status.variable_count ?? 0) > 0) {
43
- await client.contextCheckpoint();
44
- context.push(
45
- "[MemoryLayer] Server-side sandbox state checkpointed. " +
46
- "Variables persist across compaction — use `memory_context_inspect` to re-orient."
47
- );
48
- }
49
- } catch {
50
- // Sandbox checkpoint is best-effort
51
- }
52
- }
53
-
54
- // Always include recovery instructions
55
- if (context.length === 0) {
56
- context.push(
57
- "[MemoryLayer] Context compaction occurred. Use `memory_recall` to retrieve " +
58
- "prior context and `memory_context_inspect` to check sandbox state."
59
- );
60
- }
61
-
62
- return context;
63
- }
@@ -1,147 +0,0 @@
1
- /**
2
- * User message hook for MemoryLayer OpenCode plugin.
3
- *
4
- * Detects patterns in user messages and performs intelligent recall
5
- * to inject relevant memories into the conversation context.
6
- */
7
-
8
- import { getClient, checkHealth } from "../shared/client.js";
9
- import { formatRecallResult } from "../shared/formatters.js";
10
- import {
11
- wasRecallDoneThisTurn,
12
- markRecallDone,
13
- resetRecallStatus,
14
- setCurrentTopic,
15
- setCurrentPrompt,
16
- } from "../shared/state.js";
17
-
18
- /** Pattern categories and their recall queries */
19
- const PATTERNS = {
20
- preference: {
21
- regex: /\b(which\s+\w+\s+should|should\s+we\s+use|what\s+do\s+we\s+use|how\s+do\s+we|what'?s\s+our|preferred|convention|do\s+we\s+have\s+a|what'?s\s+the\s+(default|standard|preferred))\b/i,
22
- queries: ["directive", "preference", "convention"],
23
- },
24
- recall: {
25
- regex: /\b(remember|recall|what did we|how did we|remind me|what was the|what were the|what do you (know|remember))\b/i,
26
- queries: ["context", "history", "decision"],
27
- },
28
- analysis: {
29
- regex: /\b(review|assess|analyze|evaluate|audit|investigate|explore|research|compare|status|state of|readiness|gap analysis)\b/i,
30
- queries: ["status", "assessment", "gaps", "problems"],
31
- },
32
- implementation: {
33
- regex: /\b(implement|build|create|add|fix|refactor|update|change|modify)\b/i,
34
- queries: ["patterns", "solutions", "issues"],
35
- },
36
- error: {
37
- regex: /\b(error|bug|issue|broken|failing|crash|exception|doesn't work|not working)\b/i,
38
- queries: ["fix", "error", "solution"],
39
- },
40
- };
41
-
42
- /**
43
- * Detect which pattern category matches the prompt
44
- */
45
- function detectPattern(prompt: string): keyof typeof PATTERNS | null {
46
- for (const [category, config] of Object.entries(PATTERNS)) {
47
- if (config.regex.test(prompt)) {
48
- return category as keyof typeof PATTERNS;
49
- }
50
- }
51
- return null;
52
- }
53
-
54
- /**
55
- * Extract key terms from the prompt for recall query
56
- */
57
- function extractKeyTerms(prompt: string): string {
58
- const stopWords = new Set([
59
- "the", "a", "an", "is", "are", "was", "were", "be", "been",
60
- "have", "has", "had", "do", "does", "did", "will", "would",
61
- "could", "should", "may", "might", "must", "can", "this",
62
- "that", "these", "those", "i", "you", "we", "they", "it",
63
- "my", "your", "our", "their", "its", "please", "help", "me",
64
- "want", "need", "like", "to", "for", "with", "on", "in", "at",
65
- ]);
66
-
67
- const words = prompt.toLowerCase()
68
- .replace(/[^\w\s]/g, " ")
69
- .split(/\s+/)
70
- .filter(w => w.length > 2 && !stopWords.has(w));
71
-
72
- return words.slice(0, 5).join(" ");
73
- }
74
-
75
- /**
76
- * Get guidance text for each pattern type
77
- */
78
- function getPatternGuidance(pattern: keyof typeof PATTERNS): string {
79
- const guidance: Record<string, string> = {
80
- preference: "This is a preference/convention question. Relevant directives and decisions found:",
81
- recall: "Recalled memories matching this request:",
82
- analysis: "This request involves analysis/research. Relevant prior context found:",
83
- implementation: "This request involves implementation. Relevant patterns/solutions found:",
84
- error: "This request mentions an error/bug. Relevant prior fixes found:",
85
- };
86
- return guidance[pattern] || "Relevant memories found:";
87
- }
88
-
89
- /**
90
- * Extract user message text from message parts.
91
- */
92
- export function extractMessageText(parts: Array<{ type: string; text?: string }>): string {
93
- return parts
94
- .filter(p => p.type === "text" && p.text)
95
- .map(p => p.text!)
96
- .join("\n");
97
- }
98
-
99
- /**
100
- * Handle a new user message — detect patterns and recall relevant memories.
101
- *
102
- * Returns additional context text to inject, or null if no recall needed.
103
- */
104
- export async function handleUserMessage(messageText: string): Promise<string | null> {
105
- if (!messageText) return null;
106
-
107
- // Reset recall status for new user turn
108
- resetRecallStatus();
109
-
110
- // Persist the user's prompt for intent detection in tool hooks
111
- setCurrentPrompt(messageText);
112
-
113
- // Skip if recall was already done this turn
114
- if (wasRecallDoneThisTurn()) return null;
115
-
116
- // Check server health
117
- const healthy = await checkHealth();
118
- if (!healthy) return null;
119
-
120
- // Store the user's topic for cross-hook context
121
- const keyTerms = extractKeyTerms(messageText);
122
- if (keyTerms) {
123
- setCurrentTopic(keyTerms);
124
- }
125
-
126
- // Detect pattern category
127
- const pattern = detectPattern(messageText);
128
- if (!pattern) return null;
129
-
130
- try {
131
- const patternQueries = PATTERNS[pattern].queries;
132
- const query = `${keyTerms} ${patternQueries[0]}`.trim();
133
-
134
- const client = getClient();
135
- const result = await client.recall({ query, limit: 10 });
136
- markRecallDone(query);
137
-
138
- if (result.memories.length === 0) return null;
139
-
140
- const guidance = getPatternGuidance(pattern);
141
- const recallOutput = formatRecallResult(result, query);
142
-
143
- return `${guidance}\n\n${recallOutput}`;
144
- } catch {
145
- return null;
146
- }
147
- }
@@ -1,127 +0,0 @@
1
- /**
2
- * Session lifecycle hooks for MemoryLayer OpenCode plugin.
3
- *
4
- * Handles session initialization (briefing, directives, sandbox state)
5
- * and session teardown (commit working memory, end session).
6
- */
7
-
8
- import type { Memory } from "@scitrera/memorylayer-mcp-server";
9
- import { getClient, checkHealth } from "../shared/client.js";
10
- import { formatSessionStart } from "../shared/formatters.js";
11
- import { markRecallDone, resetRecallStatus, updateSessionInfo } from "../shared/state.js";
12
-
13
- /**
14
- * Check for existing sandbox state from a prior session or pre-compaction.
15
- */
16
- async function checkSandboxState(client: ReturnType<typeof getClient>): Promise<Record<string, unknown> | null> {
17
- try {
18
- const status = await client.contextStatus() as { exists?: boolean; variable_count?: number };
19
- if (status.exists && (status.variable_count ?? 0) > 0) {
20
- return await client.contextInspect({});
21
- }
22
- } catch {
23
- // Context environment may not be available
24
- }
25
- return null;
26
- }
27
-
28
- /**
29
- * Initialize a MemoryLayer session and return formatted context for system prompt injection.
30
- *
31
- * Called from the `experimental.chat.system.transform` hook on the first message
32
- * of a session, or from the `event` hook on session start.
33
- */
34
- export async function initializeSession(topic?: string): Promise<string | null> {
35
- const healthy = await checkHealth();
36
- if (!healthy) {
37
- return "MemoryLayer server not reachable. Memory features unavailable this session.";
38
- }
39
-
40
- try {
41
- const client = getClient();
42
-
43
- // Start server session
44
- let sessionId: string | undefined;
45
- try {
46
- const sessionResult = await client.startSession({ ttl_seconds: 3600 });
47
- sessionId = sessionResult.session_id;
48
- const workspaceId = client.getWorkspaceId();
49
- if (workspaceId) {
50
- updateSessionInfo(workspaceId, sessionId);
51
- }
52
- } catch {
53
- // Session start failed, continue without session management
54
- }
55
-
56
- // Reset recall status for new session
57
- resetRecallStatus();
58
-
59
- // Run briefing, directive recall, and sandbox check in parallel
60
- const [briefingResult, directiveResult, sandboxResult] = await Promise.allSettled([
61
- client.getBriefing({ limit: 10, includeMemories: false }),
62
- client.recall({
63
- query: "user directives and preferences",
64
- subtypes: ["directive", "preference"],
65
- limit: 10,
66
- }),
67
- checkSandboxState(client),
68
- ]);
69
-
70
- const briefing = briefingResult.status === "fulfilled" ? briefingResult.value : null;
71
- const directives: Memory[] =
72
- directiveResult.status === "fulfilled"
73
- ? directiveResult.value.memories
74
- : [];
75
- const sandboxState = sandboxResult.status === "fulfilled" ? sandboxResult.value : null;
76
-
77
- // If there's a topic, recall for it too
78
- let topicRecall = null;
79
- if (topic) {
80
- try {
81
- topicRecall = await client.recall({ query: topic, limit: 10, detail_level: "abstract" });
82
- markRecallDone(topic);
83
- } catch {
84
- // Topic recall is optional
85
- }
86
- }
87
-
88
- return formatSessionStart(briefing, directives, topicRecall, topic, sandboxState);
89
- } catch (error) {
90
- return `MemoryLayer session error: ${error instanceof Error ? error.message : error}`;
91
- }
92
- }
93
-
94
- /**
95
- * Finalize the MemoryLayer session — commit working memory and end session.
96
- *
97
- * Called when the OpenCode session ends. Best-effort: never throws.
98
- */
99
- export async function finalizeSession(): Promise<void> {
100
- const healthy = await checkHealth();
101
- if (!healthy) return;
102
-
103
- const client = getClient();
104
- const sessionId = client.getSessionId();
105
- if (!sessionId) return;
106
-
107
- try {
108
- // Commit working memory to long-term storage
109
- try {
110
- await client.commitSession(sessionId, { importance_threshold: 0.5 });
111
- } catch {
112
- // Commit may fail if session expired
113
- }
114
-
115
- // End the server session
116
- try {
117
- await client.endSession(sessionId, {
118
- commit: true,
119
- importance_threshold: 0.5,
120
- });
121
- } catch {
122
- // Session may already be ended
123
- }
124
- } catch {
125
- // Best-effort — don't fail on shutdown
126
- }
127
- }
package/src/hooks/tool.ts DELETED
@@ -1,207 +0,0 @@
1
- /**
2
- * Tool execution hooks for MemoryLayer OpenCode plugin.
3
- *
4
- * Before-tool: Injects relevant memory context for write/delegation tools.
5
- * After-tool: Silently captures tool observations as working memory.
6
- */
7
-
8
- import { getClient, checkHealth } from "../shared/client.js";
9
- import { formatRecallResult, formatStorageGuidance } from "../shared/formatters.js";
10
- import {
11
- shouldSkipTool,
12
- buildObservation,
13
- type ObservationData,
14
- } from "../shared/observation.js";
15
- import {
16
- wasQueryRecalledThisTurn,
17
- markRecallDone,
18
- getCurrentTopic,
19
- getCurrentPrompt,
20
- } from "../shared/state.js";
21
-
22
- // ---------------------------------------------------------------------------
23
- // Before-tool hook
24
- // ---------------------------------------------------------------------------
25
-
26
- /**
27
- * Handle pre-tool execution for task/delegation tools.
28
- * Returns context text to prepend to tool description, or null.
29
- */
30
- async function handleTaskTool(toolArgs: Record<string, unknown>): Promise<string | null> {
31
- const taskPrompt = (toolArgs.prompt || toolArgs.description || "") as string;
32
- if (!taskPrompt) {
33
- return "RECALL-FIRST RULE: Consider using `memory_recall` before delegating to subagent. Subagents cannot access MemoryLayer.";
34
- }
35
-
36
- const query = taskPrompt.substring(0, 100);
37
- if (wasQueryRecalledThisTurn(query)) {
38
- return "Recall already done for this topic. Include relevant memories in subagent prompt.";
39
- }
40
-
41
- const healthy = await checkHealth();
42
- if (!healthy) return null;
43
-
44
- try {
45
- const client = getClient();
46
- const result = await client.recall({ query, limit: 5 });
47
- markRecallDone(query);
48
-
49
- if (result.memories.length === 0) {
50
- return "No relevant memories found for this task. Proceeding with delegation.";
51
- }
52
-
53
- const recallOutput = formatRecallResult(result, query);
54
- return `INCLUDE IN SUBAGENT PROMPT - Relevant context from memory:\n\n${recallOutput}`;
55
- } catch {
56
- return "Memory recall failed. Consider manual recall before delegation.";
57
- }
58
- }
59
-
60
- /**
61
- * Handle pre-tool execution for edit/write tools.
62
- * Returns context text, or null.
63
- */
64
- async function handleEditWriteTool(toolArgs: Record<string, unknown>): Promise<string | null> {
65
- const filePath = (toolArgs.file_path || toolArgs.path || toolArgs.file || "") as string;
66
- if (!filePath) return null;
67
-
68
- const filename = filePath.split("/").pop() || filePath;
69
- const topic = getCurrentTopic();
70
- const query = topic ? `${filename} ${topic}` : `${filename} patterns solutions`;
71
-
72
- if (wasQueryRecalledThisTurn(query)) return null;
73
-
74
- const healthy = await checkHealth();
75
- if (!healthy) return null;
76
-
77
- try {
78
- const client = getClient();
79
- const result = await client.recall({ query, limit: 3 });
80
-
81
- if (result.memories.length === 0) return null;
82
-
83
- markRecallDone(query);
84
- return `Relevant context for ${filename}:\n\n${formatRecallResult(result, filename)}`;
85
- } catch {
86
- return null;
87
- }
88
- }
89
-
90
- /**
91
- * Handle before-tool execution.
92
- *
93
- * In OpenCode's hook system, this is called via "tool.execute.before" with
94
- * (input, output) where we can mutate output.args. We use the return value
95
- * pattern here and let the entry point handle injection.
96
- *
97
- * Returns additional context text, or null.
98
- */
99
- export async function handleToolBefore(
100
- toolName: string,
101
- toolArgs: Record<string, unknown>
102
- ): Promise<string | null> {
103
- switch (toolName) {
104
- case "task":
105
- return handleTaskTool(toolArgs);
106
-
107
- case "edit":
108
- case "write":
109
- case "multiedit":
110
- case "apply_patch":
111
- return handleEditWriteTool(toolArgs);
112
-
113
- default:
114
- return null;
115
- }
116
- }
117
-
118
- // ---------------------------------------------------------------------------
119
- // After-tool hook
120
- // ---------------------------------------------------------------------------
121
-
122
- /**
123
- * Store observation asynchronously (fire-and-forget)
124
- */
125
- async function storeObservationAsync(obs: ObservationData): Promise<void> {
126
- const client = getClient();
127
- const sessionId = client.getSessionId();
128
- if (!sessionId) return;
129
-
130
- const controller = new AbortController();
131
- const timeout = setTimeout(() => controller.abort(), 3000);
132
-
133
- try {
134
- await client.setWorkingMemory(sessionId, `obs_${obs.contentHash}`, {
135
- type: obs.type,
136
- title: obs.title,
137
- tool: obs.toolName,
138
- files_read: obs.filesRead,
139
- files_modified: obs.filesModified,
140
- facts: obs.facts,
141
- concepts: obs.concepts,
142
- intent: obs.intent,
143
- summary: obs.summary,
144
- captured_at: new Date().toISOString(),
145
- });
146
- } catch {
147
- // Silent failure — never block tool execution
148
- } finally {
149
- clearTimeout(timeout);
150
- }
151
- }
152
-
153
- /**
154
- * Check if bash output indicates a significant action
155
- */
156
- function isSignificantBashOutput(
157
- toolArgs: Record<string, unknown>,
158
- toolOutput: string
159
- ): boolean {
160
- const command = (toolArgs.command || "") as string;
161
-
162
- // Git commits
163
- if (/git\s+commit/i.test(command)) return true;
164
-
165
- // Build commands with errors
166
- if (/npm\s+run\s+build|cargo\s+build|make\b|tsc\b|bun\s+build/i.test(command)) {
167
- return /error|fail/i.test(toolOutput);
168
- }
169
-
170
- return false;
171
- }
172
-
173
- /**
174
- * Handle after-tool execution — capture observations as working memory.
175
- *
176
- * Returns additional context guidance for significant events, or null.
177
- */
178
- export async function handleToolAfter(
179
- toolName: string,
180
- toolArgs: Record<string, unknown>,
181
- toolOutput: string
182
- ): Promise<string | null> {
183
- if (shouldSkipTool(toolName)) return null;
184
-
185
- const currentPrompt = getCurrentPrompt();
186
- const obs = buildObservation(toolName, toolArgs, toolOutput, currentPrompt);
187
-
188
- if (!obs) return null;
189
-
190
- // Fire-and-forget storage
191
- storeObservationAsync(obs).catch(() => {});
192
-
193
- // Return guidance for significant events
194
- if (toolName === "bash" && isSignificantBashOutput(toolArgs, toolOutput)) {
195
- const command = (toolArgs.command || "") as string;
196
-
197
- if (/git\s+commit/i.test(command)) {
198
- return formatStorageGuidance("bash", true);
199
- }
200
-
201
- if (/build|tsc|make|bun\s+build/i.test(command)) {
202
- return "Build had errors. Consider storing the issue with `memory_remember` (subtype: error) for future reference.";
203
- }
204
- }
205
-
206
- return null;
207
- }