@scitrera/memorylayer-opencode-plugin 0.1.22
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/README.md +178 -0
- package/dist/src/hooks/event.d.ts +15 -0
- package/dist/src/hooks/event.d.ts.map +1 -0
- package/dist/src/hooks/event.js +53 -0
- package/dist/src/hooks/event.js.map +1 -0
- package/dist/src/hooks/message.d.ts +20 -0
- package/dist/src/hooks/message.d.ts.map +1 -0
- package/dist/src/hooks/message.js +128 -0
- package/dist/src/hooks/message.js.map +1 -0
- package/dist/src/hooks/session.d.ts +20 -0
- package/dist/src/hooks/session.d.ts.map +1 -0
- package/dist/src/hooks/session.js +121 -0
- package/dist/src/hooks/session.js.map +1 -0
- package/dist/src/hooks/tool.d.ts +23 -0
- package/dist/src/hooks/tool.d.ts.map +1 -0
- package/dist/src/hooks/tool.js +168 -0
- package/dist/src/hooks/tool.js.map +1 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +176 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/shared/client.d.ts +27 -0
- package/dist/src/shared/client.d.ts.map +1 -0
- package/dist/src/shared/client.js +69 -0
- package/dist/src/shared/client.js.map +1 -0
- package/dist/src/shared/formatters.d.ts +33 -0
- package/dist/src/shared/formatters.d.ts.map +1 -0
- package/dist/src/shared/formatters.js +187 -0
- package/dist/src/shared/formatters.js.map +1 -0
- package/dist/src/shared/observation.d.ts +59 -0
- package/dist/src/shared/observation.d.ts.map +1 -0
- package/dist/src/shared/observation.js +406 -0
- package/dist/src/shared/observation.js.map +1 -0
- package/dist/src/shared/state.d.ts +66 -0
- package/dist/src/shared/state.d.ts.map +1 -0
- package/dist/src/shared/state.js +144 -0
- package/dist/src/shared/state.js.map +1 -0
- package/dist/src/shared/types.d.ts +141 -0
- package/dist/src/shared/types.d.ts.map +1 -0
- package/dist/src/shared/types.js +9 -0
- package/dist/src/shared/types.js.map +1 -0
- package/opencode.example.json +15 -0
- package/package.json +52 -0
- package/src/commands/recall.md +55 -0
- package/src/commands/remember.md +37 -0
- package/src/commands/setup.md +47 -0
- package/src/commands/status.md +59 -0
- package/src/hooks/event.ts +63 -0
- package/src/hooks/message.ts +147 -0
- package/src/hooks/session.ts +127 -0
- package/src/hooks/tool.ts +207 -0
- package/src/index.ts +197 -0
- package/src/shared/client.ts +73 -0
- package/src/shared/formatters.ts +237 -0
- package/src/shared/observation.ts +447 -0
- package/src/shared/state.ts +159 -0
- package/src/shared/types.ts +144 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# /memorylayer-status
|
|
2
|
+
|
|
3
|
+
Check MemoryLayer connection status and workspace information.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/memorylayer-status
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Behavior
|
|
12
|
+
|
|
13
|
+
When this command is invoked:
|
|
14
|
+
|
|
15
|
+
1. **Check Server Connection**
|
|
16
|
+
- Use `memory_briefing` to verify the MCP server is connected
|
|
17
|
+
- Report connection status
|
|
18
|
+
|
|
19
|
+
2. **Display Workspace Info**
|
|
20
|
+
- Show the current workspace name (auto-detected from directory/git)
|
|
21
|
+
- Show memory statistics from the briefing
|
|
22
|
+
|
|
23
|
+
3. **Report Plugin Status**
|
|
24
|
+
- Confirm which hooks are active
|
|
25
|
+
|
|
26
|
+
## Response Format
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
MemoryLayer Status
|
|
30
|
+
==================
|
|
31
|
+
Server: Connected (http://localhost:61001)
|
|
32
|
+
Workspace: my-project (auto-detected from git)
|
|
33
|
+
|
|
34
|
+
Statistics:
|
|
35
|
+
- Total memories: 42
|
|
36
|
+
- Recent (24h): 5
|
|
37
|
+
- Associations: 18
|
|
38
|
+
|
|
39
|
+
Plugin Hooks Active:
|
|
40
|
+
- system.transform: Yes (injects briefing at session start)
|
|
41
|
+
- chat.message: Yes (pattern-based recall)
|
|
42
|
+
- tool.before/after: Yes (context injection + observation capture)
|
|
43
|
+
- session.compacting: Yes (preserves context before compaction)
|
|
44
|
+
|
|
45
|
+
All systems operational.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If there are issues:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
MemoryLayer Status
|
|
52
|
+
==================
|
|
53
|
+
Server: NOT CONNECTED
|
|
54
|
+
|
|
55
|
+
Troubleshooting:
|
|
56
|
+
1. Start the server: memorylayer serve
|
|
57
|
+
2. Check MEMORYLAYER_URL environment variable
|
|
58
|
+
3. Run /memorylayer-setup for guided configuration
|
|
59
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
}
|