opencode-agent-kit 1.0.18 → 1.0.19
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 +18 -2
- package/bin/commands/init.mjs +29 -2
- package/package.json +2 -1
- package/template/.opencode/commands/recall.md +19 -0
- package/template/.opencode/commands/remember.md +19 -0
- package/template/.opencode/hooks/agentmemory-start.sh +17 -0
- package/template/.opencode/instructions/INSTRUCTIONS.md +49 -0
- package/template/.opencode/plugins/agentmemory-capture.ts +651 -0
- package/template/.opencode/skills/agentmemory/SKILL.md +97 -0
- package/template/opencode.json +32 -12
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# Agent Kit — Setup Guide
|
|
6
6
|
|
|
7
|
-
Complete setup guide for the **Agent Kit** — a portable multi-stack AI agent system for OpenCode. Includes 13 specialized agents,
|
|
7
|
+
Complete setup guide for the **Agent Kit** — a portable multi-stack AI agent system for OpenCode. Includes 13 specialized agents, 63 skill playbooks, 39 slash commands, and 8 MCP servers.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npx opencode-agent-kit init # One command. Full team.
|
|
@@ -191,6 +191,10 @@ After installing `.opencode/`, the following slash commands are available:
|
|
|
191
191
|
|
|
192
192
|
# Quality
|
|
193
193
|
/sonarqube-scan [options] # SonarQube quality scan (issues, security, coverage)
|
|
194
|
+
|
|
195
|
+
# Memory
|
|
196
|
+
/recall [query] # Search past observations and lessons
|
|
197
|
+
/remember [text] # Save insight/decision to persistent memory
|
|
194
198
|
```
|
|
195
199
|
|
|
196
200
|
## Using the `.opencode/` Folder
|
|
@@ -340,6 +344,7 @@ Skills are stored in `.opencode/skills/` (local in the repo) — no need to sear
|
|
|
340
344
|
| SEO Specialist | frontend-patterns, web-design-guidelines, nuxt-ui |
|
|
341
345
|
| **Android Developer** | coding-standards, android-jetpack-compose, edge-to-edge, navigation-3, firebase-basics, play-billing, camera1-to-camerax, r8-analyzer, migrate-xml-views-to-jetpack-compose, gpc-setup, gpc-release-flow, gpc-preflight, gpc-vitals-monitoring |
|
|
342
346
|
| **Flutter Developer** | coding-standards, flutter (patterns), 10 Flutter skills, 9 Dart skills, firebase-basics |
|
|
347
|
+
| **All Agents** | agentmemory (persistent cross-session memory, 53 MCP tools) |
|
|
343
348
|
|
|
344
349
|
### Skills Not Required for Core Stack
|
|
345
350
|
|
|
@@ -363,6 +368,7 @@ These can be kept if your team uses multi-stack, but are optional.
|
|
|
363
368
|
- `verification-loop` — Agent verification cycle
|
|
364
369
|
- `nutrient-document-processing` — Document processing API
|
|
365
370
|
- `project-guidelines-example` — Project guidelines example
|
|
371
|
+
- `agentmemory` — Persistent cross-session memory with 53 MCP tools
|
|
366
372
|
|
|
367
373
|
## Skill Locations
|
|
368
374
|
|
|
@@ -431,8 +437,9 @@ From `.opencode/config.json`, agents use the following MCP servers:
|
|
|
431
437
|
| `nuxt-ui` | remote | enabled | Nuxt UI component docs & examples |
|
|
432
438
|
| `playwright` | stdio | enabled | Browser automation & E2E testing |
|
|
433
439
|
| `postman` | remote | enabled | Postman API management (collections, requests, docs) |
|
|
434
|
-
| `figma` |
|
|
440
|
+
| `figma` | remote | disabled | Figma design file access (optional) |
|
|
435
441
|
| `stitch` | remote | disabled | Google Stitch AI design generation (optional) |
|
|
442
|
+
| `agentmemory`| local | enabled | Persistent cross-session memory (53 memory tools) |
|
|
436
443
|
|
|
437
444
|
To enable Figma MCP:
|
|
438
445
|
|
|
@@ -440,6 +447,15 @@ To enable Figma MCP:
|
|
|
440
447
|
export FIGMA_ACCESS_TOKEN="your-token"
|
|
441
448
|
```
|
|
442
449
|
|
|
450
|
+
To enable agentmemory:
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
npm install -g @agentmemory/agentmemory
|
|
454
|
+
agentmemory # Start the memory server on :3111
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Open `http://localhost:3113` for the real-time memory viewer.
|
|
458
|
+
|
|
443
459
|
To enable Google Stitch MCP:
|
|
444
460
|
|
|
445
461
|
```bash
|
package/bin/commands/init.mjs
CHANGED
|
@@ -222,12 +222,35 @@ export async function init(options) {
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
// 7.6 Install agentmemory globally
|
|
226
|
+
if (!skipInstall) {
|
|
227
|
+
console.log(` 🧠 Installing agentmemory (persistent memory)...`);
|
|
228
|
+
try {
|
|
229
|
+
execSync(`agentmemory --version`, { stdio: "pipe" });
|
|
230
|
+
console.log(` ✓ agentmemory already installed`);
|
|
231
|
+
} catch {
|
|
232
|
+
try {
|
|
233
|
+
execSync(`npm install -g @agentmemory/agentmemory`, {
|
|
234
|
+
stdio: "pipe",
|
|
235
|
+
timeout: 60000,
|
|
236
|
+
});
|
|
237
|
+
console.log(` ✓ agentmemory installed globally`);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.error(` ⚠ agentmemory global install failed: ${err.message}`);
|
|
240
|
+
console.error(
|
|
241
|
+
` Run "npm install -g @agentmemory/agentmemory" manually`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
225
247
|
// 8. Update .gitignore
|
|
226
248
|
const gitignorePath = join(targetDir, ".gitignore");
|
|
227
249
|
const gitignoreEntries = [
|
|
228
250
|
".opencode/*",
|
|
229
251
|
"opencode.json",
|
|
230
252
|
"opencode.example.json",
|
|
253
|
+
"data/",
|
|
231
254
|
];
|
|
232
255
|
if (!existsSync(gitignorePath)) {
|
|
233
256
|
writeFileSync(gitignorePath, gitignoreEntries.join("\n") + "\n", "utf-8");
|
|
@@ -268,8 +291,12 @@ export async function init(options) {
|
|
|
268
291
|
console.log(` • .opencode/commands/ — 35+ slash commands`);
|
|
269
292
|
console.log(` • .opencode/rules/ — Scoped coding rules`);
|
|
270
293
|
console.log(` • .opencode/contexts/ — Dev/review/research contexts`);
|
|
271
|
-
console.log(` • .opencode/docs
|
|
294
|
+
console.log(` • .opencode/docs/ — Agent documentation`);
|
|
295
|
+
console.log(` • .opencode/plugins/ — agentmemory capture plugin (22 hooks)`);
|
|
296
|
+
console.log(` • .opencode/hooks/ — agentmemory auto-start wrapper`);
|
|
297
|
+
console.log(` • agentmemory (global) — Persistent cross-session memory`);
|
|
272
298
|
console.log(`\n Next steps:`);
|
|
273
299
|
console.log(` cd ${targetDir}`);
|
|
274
|
-
console.log(` opencode
|
|
300
|
+
console.log(` opencode # agentmemory auto-starts on first use`);
|
|
301
|
+
console.log(` # Viewer: http://localhost:3113`);
|
|
275
302
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-agent-kit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"description": "Multi-stack OpenCode agent toolkit — 13 specialized AI agents (Nuxt, React, Node.js, Laravel, CI3, Android, Flutter, DevOps, SEO, SonarQube) with 62 skills, 37 commands, and 7 MCP servers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"devops",
|
|
44
44
|
"seo",
|
|
45
45
|
"sonarqube",
|
|
46
|
+
"agentmemory",
|
|
46
47
|
"coding-agent",
|
|
47
48
|
"mcp",
|
|
48
49
|
"playwright",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Search past session observations and lessons for relevant context. Wraps the `memory_smart_search` and `memory_lesson_recall` MCP tools.
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
/recall [query]
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Instructions
|
|
10
|
+
|
|
11
|
+
1. Call `memory_smart_search` with the query and `limit: 10` (hybrid BM25 + vector + graph search).
|
|
12
|
+
2. Call `memory_lesson_recall` with the same query and `limit: 5` (lesson search).
|
|
13
|
+
3. Combine results and present to the user:
|
|
14
|
+
- Group by session
|
|
15
|
+
- Show type, title, and narrative for each observation
|
|
16
|
+
- Highlight high-importance observations
|
|
17
|
+
- Show lessons separately with confidence scores
|
|
18
|
+
4. If no results, suggest 2-3 alternative search terms.
|
|
19
|
+
5. **Never hallucinate results.** Only present what the MCP tools actually return.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Explicitly save an insight, decision, or learning to agentmemory for future sessions. Wraps the `memory_save` MCP tool.
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
/remember [what to remember]
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Instructions
|
|
10
|
+
|
|
11
|
+
1. Analyze what needs to be remembered — extract the core insight, decision, or fact.
|
|
12
|
+
2. Extract 2-5 searchable concepts (lowercased keyword phrases). Prefer specific terms ("jwt-refresh-rotation" over "auth").
|
|
13
|
+
3. Extract relevant file paths the memory references.
|
|
14
|
+
4. Call `memory_save` with:
|
|
15
|
+
- `content` — full text to remember (preserve user's phrasing)
|
|
16
|
+
- `concepts` — extracted concept list
|
|
17
|
+
- `files` — extracted file list (empty array if none)
|
|
18
|
+
- `type` — choose from: pattern, preference, architecture, bug, workflow, fact
|
|
19
|
+
5. Confirm the save and show the concepts tagged so the user knows retrieval terms.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
AGENTMEMORY_URL="${AGENTMEMORY_URL:-http://localhost:3111}"
|
|
5
|
+
HEALTH_URL="$AGENTMEMORY_URL/agentmemory/livez"
|
|
6
|
+
|
|
7
|
+
if ! curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
|
|
8
|
+
npx @agentmemory/agentmemory > /dev/null 2>&1 &
|
|
9
|
+
for i in $(seq 1 15); do
|
|
10
|
+
if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then
|
|
11
|
+
break
|
|
12
|
+
fi
|
|
13
|
+
sleep 1
|
|
14
|
+
done
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
exec npx -y @agentmemory/mcp "$@"
|
|
@@ -380,6 +380,55 @@ flutter analyze # Static analysis
|
|
|
380
380
|
|
|
381
381
|
---
|
|
382
382
|
|
|
383
|
+
## Agentmemory: Persistent Cross-Session Memory
|
|
384
|
+
|
|
385
|
+
agentmemory provides persistent memory for all agents. It captures session history, saves decisions/insights, and injects relevant context from past sessions into the current session.
|
|
386
|
+
|
|
387
|
+
### Prerequisites
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
npm install -g @agentmemory/agentmemory # Install globally
|
|
391
|
+
agentmemory # Start server on :3111
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### MCP Tools (53 tools)
|
|
395
|
+
|
|
396
|
+
All agents have access to agentmemory MCP tools prefixed with `agentmemory_memory_`:
|
|
397
|
+
|
|
398
|
+
| Tool | Purpose |
|
|
399
|
+
|------|---------|
|
|
400
|
+
| `memory_save` | Save insights, decisions, facts to long-term memory |
|
|
401
|
+
| `memory_recall` | Search past observations by keywords |
|
|
402
|
+
| `memory_smart_search` | Hybrid semantic+keyword search for conceptual queries |
|
|
403
|
+
| `memory_sessions` | List recent sessions with status and observation counts |
|
|
404
|
+
| `memory_file_history` | Get past observations about specific files |
|
|
405
|
+
| `memory_lesson_save` | Save a lesson learned with confidence scoring |
|
|
406
|
+
| `memory_lesson_recall` | Search lessons by query, sorted by confidence |
|
|
407
|
+
| `memory_governance_delete` | Delete specific memories (requires confirmation) |
|
|
408
|
+
| `memory_patterns` | Detect recurring patterns across sessions |
|
|
409
|
+
| `memory_consolidate` | Run 4-tier memory consolidation pipeline |
|
|
410
|
+
|
|
411
|
+
### Available Commands
|
|
412
|
+
|
|
413
|
+
- `/recall [query]` — Search past observations and lessons
|
|
414
|
+
- `/remember [text]` — Explicitly save an insight to long-term memory
|
|
415
|
+
|
|
416
|
+
### Auto-Capture Plugin
|
|
417
|
+
|
|
418
|
+
The `agentmemory-capture.ts` plugin (registered in `opencode.json`) captures 22 lifecycle events automatically:
|
|
419
|
+
- Session lifecycle: created, idle, compacted, updated, deleted, error
|
|
420
|
+
- Messages & prompts: user messages, assistant responses, removed messages
|
|
421
|
+
- Parts & steps: subagent starts, tool calls, reasoning, step-finish, patches, compaction events
|
|
422
|
+
- File enrichment: auto-injects file-specific context into system prompt
|
|
423
|
+
- Permissions: captures permission prompts and replies
|
|
424
|
+
- Tasks & commands: captures todo changes and command execution
|
|
425
|
+
|
|
426
|
+
### Skills
|
|
427
|
+
|
|
428
|
+
The `agentmemory` skill (`.opencode/skills/agentmemory/SKILL.md`) teaches agents when and how to use the memory tools effectively.
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
383
432
|
## opencode-agent-kit Version Check
|
|
384
433
|
|
|
385
434
|
If `.opencode/.kit-version` exists, your agent toolkit has a recorded installed version.
|
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
const API = process.env.AGENTMEMORY_URL || "http://localhost:3111";
|
|
4
|
+
const FILE_TOOLS = new Set(["Read", "Write", "Edit", "Glob", "Grep"]);
|
|
5
|
+
const FILE_KEYS = ["filePath", "file_path", "path", "file", "pattern"];
|
|
6
|
+
const MAX_STASHED_FILES = 20;
|
|
7
|
+
|
|
8
|
+
const DEBUG = process.env.OPENCODE_AGENTMEMORY_DEBUG === "1";
|
|
9
|
+
const SECRET = process.env.AGENTMEMORY_SECRET || "";
|
|
10
|
+
|
|
11
|
+
function authHeaders(): Record<string, string> {
|
|
12
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
13
|
+
if (SECRET) headers["Authorization"] = `Bearer ${SECRET}`;
|
|
14
|
+
return headers;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function post(path: string, body: Record<string, unknown>, timeoutMs = 5000): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
await fetch(`${API}/agentmemory${path}`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: authHeaders(),
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
24
|
+
});
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (DEBUG) console.error(`[agentmemory] POST ${path} failed:`, (e as Error).message);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function postJson(path: string, body: Record<string, unknown>): Promise<unknown | null> {
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(`${API}/agentmemory${path}`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: authHeaders(),
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
signal: AbortSignal.timeout(5000),
|
|
37
|
+
});
|
|
38
|
+
return res.ok ? await res.json() : null;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (DEBUG) console.error(`[agentmemory] POST ${path} failed:`, (e as Error).message);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function observe(
|
|
46
|
+
sessionId: string,
|
|
47
|
+
hookType: string,
|
|
48
|
+
data: Record<string, unknown>,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
await post("/observe", {
|
|
51
|
+
hookType,
|
|
52
|
+
sessionId,
|
|
53
|
+
project: projectPath,
|
|
54
|
+
cwd: projectPath,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
data,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let activeSessionId: string | null = null;
|
|
61
|
+
let pendingConfig: Record<string, unknown> | null = null;
|
|
62
|
+
let projectPath: string | null = null;
|
|
63
|
+
const stashedFiles = new Map<string, Set<string>>();
|
|
64
|
+
const seenSubtaskIds = new Map<string, Set<string>>();
|
|
65
|
+
const seenToolCallIds = new Map<string, Set<string>>();
|
|
66
|
+
const contextInjectedSessions = new Set<string>();
|
|
67
|
+
const startContextCache = new Map<string, string>();
|
|
68
|
+
|
|
69
|
+
function stashFor(sid: string): Set<string> {
|
|
70
|
+
let s = stashedFiles.get(sid);
|
|
71
|
+
if (!s) { s = new Set<string>(); stashedFiles.set(sid, s); }
|
|
72
|
+
return s;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function subtaskSetFor(sid: string): Set<string> {
|
|
76
|
+
let s = seenSubtaskIds.get(sid);
|
|
77
|
+
if (!s) { s = new Set<string>(); seenSubtaskIds.set(sid, s); }
|
|
78
|
+
return s;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toolCallSetFor(sid: string): Set<string> {
|
|
82
|
+
let s = seenToolCallIds.get(sid);
|
|
83
|
+
if (!s) { s = new Set<string>(); seenToolCallIds.set(sid, s); }
|
|
84
|
+
return s;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pruneSessionMaps(sid: string): void {
|
|
88
|
+
stashedFiles.delete(sid);
|
|
89
|
+
seenSubtaskIds.delete(sid);
|
|
90
|
+
seenToolCallIds.delete(sid);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function safeSlice(v: unknown, max: number): string {
|
|
94
|
+
if (typeof v === "string") return v.slice(0, max);
|
|
95
|
+
if (v == null) return "";
|
|
96
|
+
try { return JSON.stringify(v).slice(0, max); } catch { return ""; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const AGENTMEMORY_INSTRUCTIONS = `<agentmemory-instructions>
|
|
100
|
+
You have access to agentmemory for persistent cross-session memory. Use these tools proactively.
|
|
101
|
+
|
|
102
|
+
CORE TOOLS:
|
|
103
|
+
|
|
104
|
+
memory_save — Save an insight, decision, or fact to long-term memory.
|
|
105
|
+
Required: content (text), concepts (2-5 comma-separated keywords), type (pattern/preference/architecture/bug/workflow/fact)
|
|
106
|
+
Optional: files (comma-separated paths)
|
|
107
|
+
Use when: user says "remember this", after discovering a bug, after making an architectural decision, after learning a project convention.
|
|
108
|
+
|
|
109
|
+
memory_recall — Search past observations by keywords.
|
|
110
|
+
Use when: user says "recall", "what did we do", "do you remember", or needs context from past sessions.
|
|
111
|
+
|
|
112
|
+
memory_smart_search — Hybrid semantic+keyword search with progressive disclosure.
|
|
113
|
+
Use when: you need the most relevant past context, fuzzy/conceptual searches, or recall doesn't find what you need.
|
|
114
|
+
|
|
115
|
+
memory_sessions — List recent sessions with status and observation counts.
|
|
116
|
+
Use when: user asks about session/past history, "what did we work on".
|
|
117
|
+
|
|
118
|
+
memory_file_history — Get past observations about specific files (across all sessions).
|
|
119
|
+
Use when: you're about to edit a file and want to know its history, common pitfalls, or past edits.
|
|
120
|
+
|
|
121
|
+
memory_lesson_save — Save a lesson learned (what worked, what to avoid).
|
|
122
|
+
Use when: you discover a pattern that could help future sessions avoid mistakes.
|
|
123
|
+
|
|
124
|
+
memory_lesson_recall — Search lessons by query. Returns lessons sorted by confidence.
|
|
125
|
+
Use when: before making a decision, check if past lessons apply.
|
|
126
|
+
|
|
127
|
+
memory_governance_delete — Delete specific memories. Requires explicit user confirmation.
|
|
128
|
+
Use when: user says "forget this", "delete that memory".
|
|
129
|
+
|
|
130
|
+
memory_patterns — Detect recurring patterns across sessions.
|
|
131
|
+
Use when: you want to understand project-level trends over time.
|
|
132
|
+
|
|
133
|
+
memory_consolidate — Run the 4-tier memory consolidation pipeline.
|
|
134
|
+
Use when: you want to compress and organize accumulated session observations.
|
|
135
|
+
|
|
136
|
+
All memory tools start with \`agentmemory_memory_\`. Use the exact names as they appear in your tool list. Tool results are JSON. Always check what was returned before presenting to the user.
|
|
137
|
+
</agentmemory-instructions>`;
|
|
138
|
+
|
|
139
|
+
function extractFilePaths(args: Record<string, unknown>): string[] {
|
|
140
|
+
const files: string[] = [];
|
|
141
|
+
for (const key of FILE_KEYS) {
|
|
142
|
+
const val = args[key];
|
|
143
|
+
if (typeof val === "string" && val.length > 0) {
|
|
144
|
+
files.push(val);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return files;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function extractErrorMessage(err: unknown): string {
|
|
151
|
+
if (typeof err === "string") return err;
|
|
152
|
+
if (err && typeof err === "object") {
|
|
153
|
+
const e = err as Record<string, unknown>;
|
|
154
|
+
if (typeof e.message === "string") return e.message;
|
|
155
|
+
if (e.data && typeof e.data === "object") {
|
|
156
|
+
const d = e.data as Record<string, unknown>;
|
|
157
|
+
if (typeof d.message === "string") return d.message;
|
|
158
|
+
}
|
|
159
|
+
if (typeof e.name === "string") return e.name;
|
|
160
|
+
try { return JSON.stringify(err); } catch { return ""; }
|
|
161
|
+
}
|
|
162
|
+
return String(err ?? "");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const AgentmemoryCapturePlugin: Plugin = async (ctx) => {
|
|
166
|
+
projectPath = ctx.worktree || ctx.project?.id || process.cwd();
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
event: async ({ event }) => {
|
|
170
|
+
const type = event.type;
|
|
171
|
+
const props = (event as any).properties || {};
|
|
172
|
+
|
|
173
|
+
if (type === "session.created") {
|
|
174
|
+
const info = props.info as Record<string, unknown> | undefined;
|
|
175
|
+
activeSessionId = (info?.id as string) || props.sessionID || null;
|
|
176
|
+
if (!activeSessionId) return;
|
|
177
|
+
stashedFiles.set(activeSessionId, new Set());
|
|
178
|
+
seenSubtaskIds.delete(activeSessionId);
|
|
179
|
+
seenToolCallIds.delete(activeSessionId);
|
|
180
|
+
contextInjectedSessions.delete(activeSessionId);
|
|
181
|
+
const sessionId = activeSessionId;
|
|
182
|
+
const startResult = await postJson("/session/start", {
|
|
183
|
+
sessionId,
|
|
184
|
+
title: info?.title ?? null,
|
|
185
|
+
parentID: info?.parentID ?? null,
|
|
186
|
+
version: info?.version ?? null,
|
|
187
|
+
project: projectPath,
|
|
188
|
+
cwd: projectPath,
|
|
189
|
+
});
|
|
190
|
+
const startCtx = (startResult as any)?.context;
|
|
191
|
+
if (typeof startCtx === "string" && startCtx.length > 0) {
|
|
192
|
+
startContextCache.set(sessionId, startCtx);
|
|
193
|
+
}
|
|
194
|
+
if (pendingConfig) {
|
|
195
|
+
await observe(sessionId, "config_loaded", pendingConfig);
|
|
196
|
+
pendingConfig = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (type === "session.status") {
|
|
201
|
+
const status = props.status as Record<string, unknown> | undefined;
|
|
202
|
+
const sid = props.sessionID || activeSessionId;
|
|
203
|
+
if (!sid || !status) return;
|
|
204
|
+
if (status.type === "idle") {
|
|
205
|
+
await post("/summarize", { sessionId: sid });
|
|
206
|
+
}
|
|
207
|
+
await observe(sid, "session_status", {
|
|
208
|
+
status_type: status.type,
|
|
209
|
+
attempt: status.attempt ?? null,
|
|
210
|
+
message: safeSlice(status.message, 2000),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (type === "session.compacted") {
|
|
215
|
+
const sid = props.sessionID || activeSessionId;
|
|
216
|
+
if (sid) {
|
|
217
|
+
await post("/summarize", { sessionId: sid });
|
|
218
|
+
await observe(sid, "session_compacted", {});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (type === "session.updated") {
|
|
223
|
+
const info = props.info as Record<string, unknown> | undefined;
|
|
224
|
+
const sid = (info?.id as string) || props.sessionID || activeSessionId;
|
|
225
|
+
if (!sid) return;
|
|
226
|
+
await observe(sid, "session_updated", {
|
|
227
|
+
title: info?.title ?? null,
|
|
228
|
+
parentID: info?.parentID ?? null,
|
|
229
|
+
additions: (info?.summary as any)?.additions ?? null,
|
|
230
|
+
deletions: (info?.summary as any)?.deletions ?? null,
|
|
231
|
+
files: (info?.summary as any)?.files ?? null,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (type === "session.diff") {
|
|
236
|
+
const sid = props.sessionID || activeSessionId;
|
|
237
|
+
if (!sid || !Array.isArray(props.diff)) return;
|
|
238
|
+
const diffs = props.diff as Array<Record<string, unknown>>;
|
|
239
|
+
await observe(sid, "session_diff", {
|
|
240
|
+
files: diffs.map(d => d.file),
|
|
241
|
+
additions: diffs.reduce((s, d) => s + ((d.additions as number) || 0), 0),
|
|
242
|
+
deletions: diffs.reduce((s, d) => s + ((d.deletions as number) || 0), 0),
|
|
243
|
+
diffs: diffs.slice(0, 50),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (type === "session.deleted") {
|
|
248
|
+
const sid = props.info?.id || props.sessionID || activeSessionId;
|
|
249
|
+
if (!sid) {
|
|
250
|
+
if (DEBUG) console.error("[agentmemory] session.deleted with no session ID");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
await post("/session/end", { sessionId: sid });
|
|
254
|
+
post("/crystals/auto", { olderThanDays: 7 }, 30000);
|
|
255
|
+
post("/consolidate-pipeline", { tier: "all", force: true }, 30000);
|
|
256
|
+
if (sid === activeSessionId) activeSessionId = null;
|
|
257
|
+
stashedFiles.delete(sid);
|
|
258
|
+
startContextCache.delete(sid);
|
|
259
|
+
seenSubtaskIds.delete(sid);
|
|
260
|
+
seenToolCallIds.delete(sid);
|
|
261
|
+
contextInjectedSessions.delete(sid);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (type === "session.error") {
|
|
265
|
+
const sid = props.sessionID || activeSessionId;
|
|
266
|
+
if (sid) {
|
|
267
|
+
await observe(sid, "post_tool_failure", {
|
|
268
|
+
tool_name: "session.error",
|
|
269
|
+
tool_input: "",
|
|
270
|
+
tool_output: safeSlice(props.error, 8000),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (type === "message.updated") {
|
|
276
|
+
const info = props.info as Record<string, unknown> | undefined;
|
|
277
|
+
if (!info) return;
|
|
278
|
+
|
|
279
|
+
if (info.role === "assistant") {
|
|
280
|
+
const sid = props.sessionID || (info.sessionID as string) || activeSessionId;
|
|
281
|
+
if (!sid) return;
|
|
282
|
+
const tokens = info.tokens as Record<string, unknown> | undefined;
|
|
283
|
+
const error = info.error ? extractErrorMessage(info.error) : null;
|
|
284
|
+
await observe(sid, "assistant_message", {
|
|
285
|
+
messageID: info.id,
|
|
286
|
+
parentID: info.parentID,
|
|
287
|
+
modelID: info.modelID,
|
|
288
|
+
providerID: info.providerID,
|
|
289
|
+
mode: info.mode,
|
|
290
|
+
cost: info.cost ?? 0,
|
|
291
|
+
tokens: {
|
|
292
|
+
input: tokens?.input ?? 0,
|
|
293
|
+
output: tokens?.output ?? 0,
|
|
294
|
+
reasoning: tokens?.reasoning ?? 0,
|
|
295
|
+
cache_read: (tokens?.cache as any)?.read ?? 0,
|
|
296
|
+
cache_write: (tokens?.cache as any)?.write ?? 0,
|
|
297
|
+
},
|
|
298
|
+
finish: info.finish ?? null,
|
|
299
|
+
error,
|
|
300
|
+
duration_ms: (info.time && typeof (info.time as any).completed === "number")
|
|
301
|
+
? (info.time as any).completed - ((info.time as any).created || 0)
|
|
302
|
+
: null,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (type === "message.removed") {
|
|
308
|
+
const sid = props.sessionID || activeSessionId;
|
|
309
|
+
if (sid) {
|
|
310
|
+
await observe(sid, "message_removed", {
|
|
311
|
+
messageID: props.messageID,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (type === "message.part.updated") {
|
|
317
|
+
const part = props.part as Record<string, unknown> | undefined;
|
|
318
|
+
if (!part) return;
|
|
319
|
+
const sid = (part.sessionID as string) || props.sessionID || activeSessionId;
|
|
320
|
+
if (!sid) return;
|
|
321
|
+
|
|
322
|
+
if (part.type === "subtask") {
|
|
323
|
+
const subtaskId = part.id as string;
|
|
324
|
+
if (!subtaskId) return;
|
|
325
|
+
const subtaskSet = subtaskSetFor(sid);
|
|
326
|
+
if (subtaskSet.has(subtaskId)) return;
|
|
327
|
+
subtaskSet.add(subtaskId);
|
|
328
|
+
await observe(sid, "subagent_start", {
|
|
329
|
+
subtask_id: part.id,
|
|
330
|
+
agent: part.agent,
|
|
331
|
+
prompt: safeSlice(part.prompt, 4000),
|
|
332
|
+
description: safeSlice(part.description, 2000),
|
|
333
|
+
});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (part.type === "tool") {
|
|
338
|
+
const state = part.state as Record<string, unknown> | undefined;
|
|
339
|
+
if (!state) return;
|
|
340
|
+
const callId = part.callID as string;
|
|
341
|
+
if (!callId) return;
|
|
342
|
+
const toolName = part.tool as string;
|
|
343
|
+
|
|
344
|
+
if (state.status === "completed") {
|
|
345
|
+
const callSet = toolCallSetFor(sid);
|
|
346
|
+
if (callSet.has(callId)) return;
|
|
347
|
+
callSet.add(callId);
|
|
348
|
+
const st = state as Record<string, unknown>;
|
|
349
|
+
const rawTime = (st.time as any) || {};
|
|
350
|
+
const startTime = typeof rawTime.start === "number" ? rawTime.start : null;
|
|
351
|
+
const endTime = typeof rawTime.end === "number" ? rawTime.end : null;
|
|
352
|
+
await observe(sid, "post_tool_use", {
|
|
353
|
+
tool_name: toolName,
|
|
354
|
+
call_id: callId,
|
|
355
|
+
tool_input: safeSlice(st.input, 4000),
|
|
356
|
+
tool_output: safeSlice(st.output, 8000),
|
|
357
|
+
title: st.title ?? null,
|
|
358
|
+
metadata: st.metadata || {},
|
|
359
|
+
duration_ms: (startTime != null && endTime != null) ? endTime - startTime : null,
|
|
360
|
+
attachments: Array.isArray(st.attachments)
|
|
361
|
+
? (st.attachments as Array<Record<string, unknown>>).map(a => a.filename || a.url)
|
|
362
|
+
: [],
|
|
363
|
+
});
|
|
364
|
+
} else if (state.status === "error") {
|
|
365
|
+
const callSet = toolCallSetFor(sid);
|
|
366
|
+
if (callSet.has(callId)) return;
|
|
367
|
+
callSet.add(callId);
|
|
368
|
+
const st = state as Record<string, unknown>;
|
|
369
|
+
const rawTime = (st.time as any) || {};
|
|
370
|
+
const startTime = typeof rawTime.start === "number" ? rawTime.start : null;
|
|
371
|
+
const endTime = typeof rawTime.end === "number" ? rawTime.end : null;
|
|
372
|
+
await observe(sid, "post_tool_failure", {
|
|
373
|
+
tool_name: toolName,
|
|
374
|
+
call_id: callId,
|
|
375
|
+
tool_input: safeSlice(st.input, 4000),
|
|
376
|
+
tool_output: safeSlice(st.error, 8000),
|
|
377
|
+
duration_ms: (startTime != null && endTime != null) ? endTime - startTime : null,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (part.type === "step-finish") {
|
|
384
|
+
await observe(sid, "step_finish", {
|
|
385
|
+
messageID: part.messageID,
|
|
386
|
+
reason: part.reason ?? null,
|
|
387
|
+
cost: (part as any).cost ?? 0,
|
|
388
|
+
input_tokens: ((part as any).tokens?.input as number) ?? 0,
|
|
389
|
+
output_tokens: ((part as any).tokens?.output as number) ?? 0,
|
|
390
|
+
reasoning_tokens: ((part as any).tokens?.reasoning as number) ?? 0,
|
|
391
|
+
});
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (part.type === "reasoning") {
|
|
396
|
+
await observe(sid, "reasoning", {
|
|
397
|
+
messageID: part.messageID,
|
|
398
|
+
text: safeSlice((part as any).text, 4000),
|
|
399
|
+
});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (part.type === "file") {
|
|
404
|
+
const filename = (part as any).filename || (part as any).url || null;
|
|
405
|
+
if (filename) stashFor(sid).add(filename);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (part.type === "patch") {
|
|
410
|
+
await observe(sid, "patch_applied", {
|
|
411
|
+
messageID: part.messageID,
|
|
412
|
+
hash: (part as any).hash,
|
|
413
|
+
files: (part as any).files || [],
|
|
414
|
+
});
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (part.type === "compaction") {
|
|
419
|
+
await observe(sid, "compaction_event", {
|
|
420
|
+
messageID: part.messageID,
|
|
421
|
+
auto: (part as any).auto ?? false,
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (part.type === "agent") {
|
|
427
|
+
await observe(sid, "agent_selected", {
|
|
428
|
+
messageID: part.messageID,
|
|
429
|
+
name: (part as any).name,
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (part.type === "retry") {
|
|
435
|
+
await observe(sid, "retry_attempt", {
|
|
436
|
+
messageID: part.messageID,
|
|
437
|
+
attempt: (part as any).attempt,
|
|
438
|
+
error: safeSlice((part as any).error, 2000),
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (type === "file.edited") {
|
|
445
|
+
const sid = props.sessionID || activeSessionId;
|
|
446
|
+
if (sid && typeof props.file === "string" && props.file.length > 0) {
|
|
447
|
+
const stash = stashFor(sid);
|
|
448
|
+
stash.add(props.file);
|
|
449
|
+
if (stash.size > MAX_STASHED_FILES) {
|
|
450
|
+
const keep = [...stash].slice(-MAX_STASHED_FILES);
|
|
451
|
+
stash.clear();
|
|
452
|
+
for (const f of keep) stash.add(f);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (type === "permission.updated") {
|
|
458
|
+
const sid = props.sessionID || activeSessionId;
|
|
459
|
+
if (!sid) return;
|
|
460
|
+
await observe(sid, "notification", {
|
|
461
|
+
notification_type: "permission_prompt",
|
|
462
|
+
permission: props.type || "unknown",
|
|
463
|
+
pattern: Array.isArray(props.pattern)
|
|
464
|
+
? props.pattern.join(", ")
|
|
465
|
+
: (props.pattern || ""),
|
|
466
|
+
tool_call_id: props.callID || null,
|
|
467
|
+
title: props.title || props.type || "",
|
|
468
|
+
metadata: props.metadata || {},
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (type === "permission.replied") {
|
|
473
|
+
const sid = props.sessionID || activeSessionId;
|
|
474
|
+
if (!sid) return;
|
|
475
|
+
await observe(sid, "permission_replied", {
|
|
476
|
+
permission_id: props.permissionID || props.requestID || "",
|
|
477
|
+
response: props.response || props.reply || "",
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (type === "todo.updated") {
|
|
482
|
+
const sid = props.sessionID || activeSessionId;
|
|
483
|
+
const todos = Array.isArray(props.todos) ? props.todos.slice(0, 100) : [];
|
|
484
|
+
if (!sid || todos.length === 0) return;
|
|
485
|
+
const completed = todos.filter((t: any) => t.status === "completed");
|
|
486
|
+
const active = todos.filter((t: any) => t.status !== "completed");
|
|
487
|
+
await observe(sid, "task_completed", {
|
|
488
|
+
completed: completed.map((t: any) => ({ content: t.content, priority: t.priority })),
|
|
489
|
+
in_progress: active.map((t: any) => ({ content: t.content, priority: t.priority })),
|
|
490
|
+
total: todos.length,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (type === "command.executed") {
|
|
495
|
+
const sid = props.sessionID || activeSessionId;
|
|
496
|
+
if (sid) {
|
|
497
|
+
await observe(sid, "command_executed", {
|
|
498
|
+
name: props.name,
|
|
499
|
+
arguments: props.arguments || "",
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
"chat.message": async (input, output) => {
|
|
506
|
+
const sid = input.sessionID || activeSessionId;
|
|
507
|
+
if (!sid) return;
|
|
508
|
+
const parts = output.parts || [];
|
|
509
|
+
const files = parts
|
|
510
|
+
.filter((p: any) => p.type === "file")
|
|
511
|
+
.map((p: any) => p.filename || p.url)
|
|
512
|
+
.filter(Boolean);
|
|
513
|
+
for (const f of files) {
|
|
514
|
+
const stash = stashFor(sid);
|
|
515
|
+
stash.add(f);
|
|
516
|
+
if (stash.size > MAX_STASHED_FILES) {
|
|
517
|
+
const keep = [...stash].slice(-MAX_STASHED_FILES);
|
|
518
|
+
stash.clear();
|
|
519
|
+
for (const k of keep) stash.add(k);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const textParts = parts.filter((p: any) => p.type === "text" && !p.synthetic && !p.ignored);
|
|
524
|
+
const userText = textParts.map((p: any) => p.text || "").join("\n");
|
|
525
|
+
|
|
526
|
+
await observe(sid, "prompt_submit", {
|
|
527
|
+
agent: input.agent ?? null,
|
|
528
|
+
model: input.model ?? null,
|
|
529
|
+
variant: input.variant ?? null,
|
|
530
|
+
prompt: userText.slice(0, 8000),
|
|
531
|
+
files: files.slice(0, 20),
|
|
532
|
+
parts_summary: parts.map((p: any) => p.type).filter(Boolean),
|
|
533
|
+
});
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
"chat.params": async (input, output) => {
|
|
537
|
+
if (!input.model || !output) return;
|
|
538
|
+
const sid = input.sessionID || activeSessionId;
|
|
539
|
+
if (!sid) return;
|
|
540
|
+
await observe(sid, "llm_params", {
|
|
541
|
+
agent: input.agent,
|
|
542
|
+
model: `${input.model.providerID}/${input.model.id}`,
|
|
543
|
+
provider_url: input.model.api?.url ?? null,
|
|
544
|
+
temperature: output.temperature,
|
|
545
|
+
topP: output.topP,
|
|
546
|
+
max_output_tokens: input.model.limit?.output ?? null,
|
|
547
|
+
context_limit: input.model.limit?.context ?? null,
|
|
548
|
+
cost_1k_input: input.model.cost?.input ?? 0,
|
|
549
|
+
cost_1k_output: input.model.cost?.output ?? 0,
|
|
550
|
+
});
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
"tool.execute.before": async (input, output) => {
|
|
554
|
+
if (!FILE_TOOLS.has(input.tool)) return;
|
|
555
|
+
const sid = input.sessionID || activeSessionId;
|
|
556
|
+
if (!sid) return;
|
|
557
|
+
const args = output.args as Record<string, unknown> | undefined;
|
|
558
|
+
if (!args) return;
|
|
559
|
+
const stash = stashFor(sid);
|
|
560
|
+
for (const fp of extractFilePaths(args)) {
|
|
561
|
+
stash.add(fp);
|
|
562
|
+
}
|
|
563
|
+
if (stash.size > MAX_STASHED_FILES) {
|
|
564
|
+
const keep = [...stash].slice(-MAX_STASHED_FILES);
|
|
565
|
+
stash.clear();
|
|
566
|
+
for (const f of keep) stash.add(f);
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
571
|
+
const sid = input.sessionID || activeSessionId;
|
|
572
|
+
if (!sid) return;
|
|
573
|
+
|
|
574
|
+
if (!contextInjectedSessions.has(sid)) {
|
|
575
|
+
if (!Array.isArray(output.system)) return;
|
|
576
|
+
output.system.push(AGENTMEMORY_INSTRUCTIONS);
|
|
577
|
+
let ctx = startContextCache.get(sid);
|
|
578
|
+
if (typeof ctx !== "string" || ctx.length === 0) {
|
|
579
|
+
const result = await postJson("/context", {
|
|
580
|
+
sessionId: sid,
|
|
581
|
+
project: projectPath,
|
|
582
|
+
});
|
|
583
|
+
ctx = (result as any)?.context;
|
|
584
|
+
} else {
|
|
585
|
+
startContextCache.delete(sid);
|
|
586
|
+
}
|
|
587
|
+
if (typeof ctx === "string" && ctx.length > 0) {
|
|
588
|
+
output.system.push(ctx);
|
|
589
|
+
}
|
|
590
|
+
contextInjectedSessions.add(sid);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const stash = stashFor(sid);
|
|
594
|
+
if (stash.size === 0) return;
|
|
595
|
+
const files = [...stash].slice(0, 10);
|
|
596
|
+
|
|
597
|
+
const enrichResult = await postJson("/enrich", {
|
|
598
|
+
sessionId: sid,
|
|
599
|
+
files,
|
|
600
|
+
toolName: "enrich_inject",
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
const enrichCtx = (enrichResult as any)?.context;
|
|
604
|
+
if (typeof enrichCtx === "string" && enrichCtx.length > 0) {
|
|
605
|
+
if (Array.isArray(output.system)) {
|
|
606
|
+
output.system.push(enrichCtx);
|
|
607
|
+
}
|
|
608
|
+
for (const f of files) stash.delete(f);
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
|
|
612
|
+
"experimental.session.compacting": async (input, output) => {
|
|
613
|
+
const sid = input.sessionID || activeSessionId;
|
|
614
|
+
if (!sid) return;
|
|
615
|
+
|
|
616
|
+
const result = await postJson("/context", {
|
|
617
|
+
sessionId: sid,
|
|
618
|
+
project: projectPath,
|
|
619
|
+
});
|
|
620
|
+
const ctx = (result as any)?.context;
|
|
621
|
+
if (typeof ctx === "string" && ctx.length > 0) {
|
|
622
|
+
if (Array.isArray(output.context)) {
|
|
623
|
+
output.context.push(ctx);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
config: async (input) => {
|
|
629
|
+
const payload: Record<string, unknown> = {
|
|
630
|
+
theme: input.theme ?? null,
|
|
631
|
+
model: input.model ?? null,
|
|
632
|
+
autoupdate: input.autoupdate ?? null,
|
|
633
|
+
agents: typeof input.agent === "object" && input.agent !== null && !Array.isArray(input.agent)
|
|
634
|
+
? Object.keys(input.agent as Record<string, unknown>)
|
|
635
|
+
: Array.isArray(input.agent) ? input.agent : [],
|
|
636
|
+
mcp_servers: typeof input.mcp === "object" && input.mcp !== null && !Array.isArray(input.mcp)
|
|
637
|
+
? Object.keys(input.mcp as Record<string, unknown>)
|
|
638
|
+
: Array.isArray(input.mcp) ? input.mcp : [],
|
|
639
|
+
providers: typeof input.provider === "object" && input.provider !== null && !Array.isArray(input.provider)
|
|
640
|
+
? Object.keys(input.provider as Record<string, unknown>)
|
|
641
|
+
: Array.isArray(input.provider) ? input.provider : [],
|
|
642
|
+
permission: input.permission ?? null,
|
|
643
|
+
};
|
|
644
|
+
if (activeSessionId) {
|
|
645
|
+
await observe(activeSessionId, "config_loaded", payload);
|
|
646
|
+
} else {
|
|
647
|
+
pendingConfig = payload;
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentmemory
|
|
3
|
+
description: Persistent cross-session memory for AI coding agents. Use when working across multiple sessions, recalling past context, saving decisions, or searching historical observations.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agentmemory Skill
|
|
7
|
+
|
|
8
|
+
Provides persistent memory capabilities for coding agents using the agentmemory MCP server.
|
|
9
|
+
|
|
10
|
+
## When to Activate
|
|
11
|
+
|
|
12
|
+
- Starting a new session that builds on previous work
|
|
13
|
+
- User asks "remember this", "do you recall", "what did we do last time"
|
|
14
|
+
- Before making architectural decisions that past context could inform
|
|
15
|
+
- After discovering a bug, to save the pattern for future prevention
|
|
16
|
+
- After making a design decision, to persist the rationale
|
|
17
|
+
- When the user asks about project history or session timeline
|
|
18
|
+
|
|
19
|
+
## Core Tools
|
|
20
|
+
|
|
21
|
+
### memory_save
|
|
22
|
+
Save an insight, decision, or fact to long-term memory.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
memory_save(
|
|
26
|
+
content: "We chose jose over jsonwebtoken for Edge Runtime compatibility",
|
|
27
|
+
concepts: ["jwt-auth", "edge-runtime", "jose-library", "auth-middleware"],
|
|
28
|
+
files: ["src/middleware/auth.ts"],
|
|
29
|
+
type: "architecture"
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### memory_recall
|
|
34
|
+
Search past observations by exact keywords.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
memory_recall(query: "jwt auth setup", limit: 5)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### memory_smart_search
|
|
41
|
+
Hybrid semantic+keyword search. Use for fuzzy or conceptual queries.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
memory_smart_search(query: "how did we handle database performance", limit: 10)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### memory_sessions
|
|
48
|
+
List recent sessions with status and observation counts.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
memory_sessions(limit: 10)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### memory_file_history
|
|
55
|
+
Get past observations about specific files across all sessions. Call before editing a file.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
memory_file_history(files: ["src/middleware/auth.ts"])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### memory_lesson_save / memory_lesson_recall
|
|
62
|
+
Save and retrieve lessons learned with confidence scoring.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
memory_lesson_save(
|
|
66
|
+
content: "Always validate JWT expiry before decoding payload",
|
|
67
|
+
concepts: ["jwt-validation", "security"],
|
|
68
|
+
domain: "backend"
|
|
69
|
+
)
|
|
70
|
+
memory_lesson_recall(query: "jwt security", limit: 5)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### memory_governance_delete
|
|
74
|
+
Delete specific memories. Requires explicit user confirmation.
|
|
75
|
+
|
|
76
|
+
### memory_patterns
|
|
77
|
+
Detect recurring patterns across sessions.
|
|
78
|
+
|
|
79
|
+
### memory_consolidate
|
|
80
|
+
Run the 4-tier memory consolidation pipeline.
|
|
81
|
+
|
|
82
|
+
## Behavior Rules
|
|
83
|
+
|
|
84
|
+
1. **Proactive recall**: Before implementing new features or fixing bugs, check agentmemory for relevant past context.
|
|
85
|
+
2. **Auto-save**: After discovering a bug, making an architectural decision, or learning a project convention, save it.
|
|
86
|
+
3. **Session awareness**: Use `memory_sessions` when the user asks about project history or past work.
|
|
87
|
+
4. **File context**: Use `memory_file_history` before editing files to surface past issues or decisions about that file.
|
|
88
|
+
5. **Never hallucinate**: Only present what the MCP tools return. Report "no results found" when appropriate.
|
|
89
|
+
6. **Tool prefix**: All agentmemory tools use the `agentmemory_memory_` prefix in the tool list. Use the exact names as listed.
|
|
90
|
+
|
|
91
|
+
## Integration Notes
|
|
92
|
+
|
|
93
|
+
- Server runs on `http://localhost:3111` by default
|
|
94
|
+
- Real-time viewer at `http://localhost:3113`
|
|
95
|
+
- Start the server with `npx @agentmemory/agentmemory`
|
|
96
|
+
- 53 MCP tools available when server is running
|
|
97
|
+
- 22 auto-capture hooks via plugin record session lifecycle automatically
|
package/template/opencode.json
CHANGED
|
@@ -72,8 +72,15 @@
|
|
|
72
72
|
"Authorization": "Bearer ${SONARQUBE_TOKEN}",
|
|
73
73
|
"SONARQUBE_TOOLSETS": "analysis,issues,security-hotspots,quality-gates,rules,duplications,measures,dependency-risks,coverage,sources,languages,portfolios,system,webhooks"
|
|
74
74
|
}
|
|
75
|
+
},
|
|
76
|
+
"agentmemory": {
|
|
77
|
+
"type": "local",
|
|
78
|
+
"command": ["bash", ".opencode/hooks/agentmemory-start.sh"],
|
|
79
|
+
"enabled": true,
|
|
80
|
+
"description": "Persistent cross-session memory with 53 tools (save, recall, smart search, sessions, file history, lessons)"
|
|
75
81
|
}
|
|
76
82
|
},
|
|
83
|
+
"plugin": [".opencode/plugins/agentmemory-capture.ts"],
|
|
77
84
|
"agent": {
|
|
78
85
|
"leader": {
|
|
79
86
|
"description": "IT Leader & Technical Project Manager — analyzes requirements, designs architecture, decomposes tasks, delegates to subagents, and unifies outputs",
|
|
@@ -98,6 +105,7 @@
|
|
|
98
105
|
"figma_*": "ask",
|
|
99
106
|
"playwright_*": "allow",
|
|
100
107
|
"postman_*": "allow",
|
|
108
|
+
"agentmemory_*": "allow",
|
|
101
109
|
"task": { "*": "allow" }
|
|
102
110
|
}
|
|
103
111
|
},
|
|
@@ -121,7 +129,8 @@
|
|
|
121
129
|
"nuxt_*": "allow",
|
|
122
130
|
"nuxt-ui_*": "allow",
|
|
123
131
|
"figma_*": "ask",
|
|
124
|
-
"playwright_*": "allow"
|
|
132
|
+
"playwright_*": "allow",
|
|
133
|
+
"agentmemory_*": "allow"
|
|
125
134
|
}
|
|
126
135
|
},
|
|
127
136
|
"frontend-react": {
|
|
@@ -142,7 +151,8 @@
|
|
|
142
151
|
"npx playwright*": "allow"
|
|
143
152
|
},
|
|
144
153
|
"figma_*": "ask",
|
|
145
|
-
"playwright_*": "allow"
|
|
154
|
+
"playwright_*": "allow",
|
|
155
|
+
"agentmemory_*": "allow"
|
|
146
156
|
}
|
|
147
157
|
},
|
|
148
158
|
"backend": {
|
|
@@ -161,7 +171,8 @@
|
|
|
161
171
|
"bun *": "allow",
|
|
162
172
|
"yarn *": "allow"
|
|
163
173
|
},
|
|
164
|
-
"postman_*": "allow"
|
|
174
|
+
"postman_*": "allow",
|
|
175
|
+
"agentmemory_*": "allow"
|
|
165
176
|
}
|
|
166
177
|
},
|
|
167
178
|
"ci3": {
|
|
@@ -179,7 +190,8 @@
|
|
|
179
190
|
"git diff": "allow",
|
|
180
191
|
"git log*": "allow"
|
|
181
192
|
},
|
|
182
|
-
"postman_*": "allow"
|
|
193
|
+
"postman_*": "allow",
|
|
194
|
+
"agentmemory_*": "allow"
|
|
183
195
|
}
|
|
184
196
|
},
|
|
185
197
|
"laravel": {
|
|
@@ -197,7 +209,8 @@
|
|
|
197
209
|
"git diff": "allow",
|
|
198
210
|
"git log*": "allow"
|
|
199
211
|
},
|
|
200
|
-
"postman_*": "allow"
|
|
212
|
+
"postman_*": "allow",
|
|
213
|
+
"agentmemory_*": "allow"
|
|
201
214
|
}
|
|
202
215
|
},
|
|
203
216
|
"designer": {
|
|
@@ -217,7 +230,8 @@
|
|
|
217
230
|
},
|
|
218
231
|
"stitch_*": "allow",
|
|
219
232
|
"figma_*": "ask",
|
|
220
|
-
"nuxt-ui_*": "allow"
|
|
233
|
+
"nuxt-ui_*": "allow",
|
|
234
|
+
"agentmemory_*": "allow"
|
|
221
235
|
}
|
|
222
236
|
},
|
|
223
237
|
"reviewer": {
|
|
@@ -237,7 +251,8 @@
|
|
|
237
251
|
"yarn *": "allow",
|
|
238
252
|
"npx playwright*": "allow"
|
|
239
253
|
},
|
|
240
|
-
"playwright_*": "allow"
|
|
254
|
+
"playwright_*": "allow",
|
|
255
|
+
"agentmemory_*": "allow"
|
|
241
256
|
}
|
|
242
257
|
},
|
|
243
258
|
"database": {
|
|
@@ -255,7 +270,8 @@
|
|
|
255
270
|
"pnpm *": "allow",
|
|
256
271
|
"bun *": "allow",
|
|
257
272
|
"yarn *": "allow"
|
|
258
|
-
}
|
|
273
|
+
},
|
|
274
|
+
"agentmemory_*": "allow"
|
|
259
275
|
}
|
|
260
276
|
},
|
|
261
277
|
"devops": {
|
|
@@ -274,7 +290,8 @@
|
|
|
274
290
|
"bun *": "allow",
|
|
275
291
|
"yarn *": "allow",
|
|
276
292
|
"docker *": "allow"
|
|
277
|
-
}
|
|
293
|
+
},
|
|
294
|
+
"agentmemory_*": "allow"
|
|
278
295
|
}
|
|
279
296
|
},
|
|
280
297
|
"seo": {
|
|
@@ -292,7 +309,8 @@
|
|
|
292
309
|
"git diff": "allow",
|
|
293
310
|
"git log*": "allow"
|
|
294
311
|
},
|
|
295
|
-
"nuxt_*": "allow"
|
|
312
|
+
"nuxt_*": "allow",
|
|
313
|
+
"agentmemory_*": "allow"
|
|
296
314
|
}
|
|
297
315
|
},
|
|
298
316
|
"android": {
|
|
@@ -310,7 +328,8 @@
|
|
|
310
328
|
"gradle *": "allow"
|
|
311
329
|
},
|
|
312
330
|
"figma_*": "ask",
|
|
313
|
-
"playwright_*": "allow"
|
|
331
|
+
"playwright_*": "allow",
|
|
332
|
+
"agentmemory_*": "allow"
|
|
314
333
|
}
|
|
315
334
|
},
|
|
316
335
|
"flutter": {
|
|
@@ -328,7 +347,8 @@
|
|
|
328
347
|
"dart *": "allow"
|
|
329
348
|
},
|
|
330
349
|
"figma_*": "ask",
|
|
331
|
-
"playwright_*": "allow"
|
|
350
|
+
"playwright_*": "allow",
|
|
351
|
+
"agentmemory_*": "allow"
|
|
332
352
|
}
|
|
333
353
|
}
|
|
334
354
|
}
|