chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "agentmemory",
|
||||
"version": "0.9.27",
|
||||
"description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 53 MCP tools, 8 skills, real-time viewer.",
|
||||
"author": {
|
||||
"name": "Rohit Ghumare",
|
||||
"url": "https://github.com/rohitg00"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://github.com/rohitg00/agentmemory",
|
||||
"repository": "https://github.com/rohitg00/agentmemory",
|
||||
"skills": ["./skills/"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "agentmemory",
|
||||
"version": "0.9.27",
|
||||
"description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 6 hooks, 53 MCP tools, 8 skills, real-time viewer.",
|
||||
"author": {
|
||||
"name": "Rohit Ghumare",
|
||||
"url": "https://github.com/rohitg00"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://github.com/rohitg00/agentmemory",
|
||||
"repository": "https://github.com/rohitg00/agentmemory",
|
||||
"skills": "./skills/",
|
||||
"mcpServers": "./.mcp.json",
|
||||
"hooks": "./hooks/hooks.codex.json"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"agentmemory": {
|
||||
"type": "local",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@agentmemory/mcp"],
|
||||
"env": {
|
||||
"AGENTMEMORY_URL": "${AGENTMEMORY_URL:-http://localhost:3111}",
|
||||
"AGENTMEMORY_SECRET": "${AGENTMEMORY_SECRET:-}",
|
||||
"AGENTMEMORY_TOOLS": "${AGENTMEMORY_TOOLS:-all}"
|
||||
},
|
||||
"tools": ["*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"agentmemory": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@agentmemory/mcp"],
|
||||
"env": {
|
||||
"AGENTMEMORY_URL": "${AGENTMEMORY_URL:-http://localhost:3111}",
|
||||
"AGENTMEMORY_SECRET": "${AGENTMEMORY_SECRET:-}",
|
||||
"AGENTMEMORY_TOOLS": "${AGENTMEMORY_TOOLS:-all}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.mjs\"",
|
||||
"statusMessage": "agentmemory: loading session context"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/prompt-submit.mjs\"",
|
||||
"statusMessage": "agentmemory: recalling relevant memories"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|Read|Glob|Grep",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-use.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-use.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-compact.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"version": 1,
|
||||
"hooks": {
|
||||
"sessionStart": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/session-start.mjs"
|
||||
}
|
||||
],
|
||||
"userPromptSubmitted": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/prompt-submit.mjs"
|
||||
}
|
||||
],
|
||||
"preToolUse": [
|
||||
{
|
||||
"type": "command",
|
||||
"matcher": "edit|write|create|read|view|glob|grep",
|
||||
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/pre-tool-use.mjs"
|
||||
}
|
||||
],
|
||||
"postToolUse": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/post-tool-use.mjs"
|
||||
}
|
||||
],
|
||||
"postToolUseFailure": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/post-tool-failure.mjs"
|
||||
}
|
||||
],
|
||||
"preCompact": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/pre-compact.mjs"
|
||||
}
|
||||
],
|
||||
"agentStop": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/stop.mjs"
|
||||
}
|
||||
],
|
||||
"sessionEnd": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/session-end.mjs"
|
||||
}
|
||||
],
|
||||
"subagentStart": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/subagent-start.mjs"
|
||||
}
|
||||
],
|
||||
"subagentStop": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/subagent-stop.mjs"
|
||||
}
|
||||
],
|
||||
"notification": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/notification.mjs"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/prompt-submit.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|Read|Glob|Grep",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-use.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-use.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUseFailure": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-failure.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-compact.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubagentStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/subagent-start.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubagentStop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/subagent-stop.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Notification": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/notification.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"TaskCompleted": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/task-completed.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-end.mjs\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
<h1 align="center">
|
||||
<img src="https://github.com/opencode-ai.png?size=80" alt="OpenCode" width="28" height="28" align="center" />
|
||||
agentmemory for OpenCode
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Your OpenCode agents remember everything. No more re-explaining.</strong><br/>
|
||||
<sub>Persistent cross-session memory via <a href="https://github.com/rohitg00/agentmemory">agentmemory</a> — 95.2% retrieval accuracy on <a href="https://arxiv.org/abs/2410.10813">LongMemEval-S</a>.</sub>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/MCP-53_tools-1f6feb?style=flat-square" alt="53 MCP tools" />
|
||||
<img src="https://img.shields.io/badge/Plugin-22_hooks-1f6feb?style=flat-square" alt="22 hooks" />
|
||||
<img src="https://img.shields.io/badge/Commands-2_slash-1f6feb?style=flat-square" alt="2 slash commands" />
|
||||
<img src="https://img.shields.io/badge/R@5-95.2%25-00875f?style=flat-square" alt="95.2% R@5" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
### 1. Start the agentmemory server
|
||||
|
||||
```bash
|
||||
npx @agentmemory/agentmemory
|
||||
```
|
||||
|
||||
The server starts on `http://localhost:3111`.
|
||||
|
||||
### 2. Configure the MCP server
|
||||
|
||||
Add to `~/.config/opencode/opencode.json` or your project's `.opencode/opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"agentmemory": {
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "@agentmemory/mcp"],
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Install the plugin
|
||||
|
||||
Add to `~/.config/opencode/opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugin": ["./plugins/agentmemory-capture.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
Copy the plugin file from this repo:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/opencode/plugins
|
||||
cp plugin/opencode/agentmemory-capture.ts ~/.config/opencode/plugins/
|
||||
```
|
||||
|
||||
### 4. Add the slash commands
|
||||
|
||||
Copy the commands into your project or global `.opencode/commands/` directory:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/opencode/commands
|
||||
cp plugin/opencode/commands/recall.md ~/.config/opencode/commands/
|
||||
cp plugin/opencode/commands/remember.md ~/.config/opencode/commands/
|
||||
```
|
||||
|
||||
Restart OpenCode or open a new session. The plugin auto-captures everything.
|
||||
|
||||
## What gets captured
|
||||
|
||||
### Session lifecycle
|
||||
|
||||
| Event | Hook | agentmemory API |
|
||||
|---|---|---|
|
||||
| Session start | `session.created` | POST /session/start |
|
||||
| Idle → summarize | `session.idle` + `session.status` (idle) | POST /summarize |
|
||||
| Status transitions | `session.status` (idle/busy/retry) | POST /observe |
|
||||
| Compaction | `session.compacted` | POST /summarize + POST /observe |
|
||||
| Metadata updates | `session.updated` | POST /observe |
|
||||
| Code change tracking | `session.diff` | POST /observe |
|
||||
| Session delete | `session.deleted` | POST /session/end |
|
||||
| Session error | `session.error` | POST /observe |
|
||||
|
||||
### Messages & prompts
|
||||
|
||||
| Event | Hook | agentmemory API |
|
||||
|---|---|---|
|
||||
| User prompt (rich) | `chat.message` | POST /observe |
|
||||
| User prompt metadata | `message.updated` (user) | POST /observe |
|
||||
| Assistant response | `message.updated` (assistant) | POST /observe |
|
||||
| Message removed (undo) | `message.removed` | POST /observe |
|
||||
|
||||
### Parts & steps
|
||||
|
||||
| Event | Hook | agentmemory API |
|
||||
|---|---|---|
|
||||
| Subagent start | `message.part.updated` (subtask) | POST /observe |
|
||||
| Tool completed | `message.part.updated` (tool completed) | POST /observe |
|
||||
| Tool error | `message.part.updated` (tool error) | POST /observe |
|
||||
| Step finish (cost/tokens) | `message.part.updated` (step-finish) | POST /observe |
|
||||
| Reasoning trace | `message.part.updated` (reasoning) | POST /observe |
|
||||
| Patch applied | `message.part.updated` (patch) | POST /observe |
|
||||
| Auto/manual compaction | `message.part.updated` (compaction) | POST /observe |
|
||||
| Agent selection | `message.part.updated` (agent) | POST /observe |
|
||||
| API retry | `message.part.updated` (retry) | POST /observe |
|
||||
|
||||
### File enrichment pipeline
|
||||
|
||||
| Event | Hook | agentmemory API |
|
||||
|---|---|---|
|
||||
| File tool params | `tool.execute.before` → stash paths | — |
|
||||
| File edited | `file.edited` → stash paths | — |
|
||||
| File part attached | `message.part.updated` (file) → stash paths | — |
|
||||
| Enrichment inject | `experimental.chat.system.transform` | POST /enrich → `output.system[]` |
|
||||
| Memory context inject | `experimental.chat.system.transform` | POST /context → `output.system[]` |
|
||||
|
||||
### Permissions
|
||||
|
||||
| Event | Hook | agentmemory API |
|
||||
|---|---|---|
|
||||
| Permission prompt | `permission.updated` | POST /observe |
|
||||
| Permission reply | `permission.replied` | POST /observe |
|
||||
|
||||
### Tasks & commands
|
||||
|
||||
| Event | Hook | agentmemory API |
|
||||
|---|---|---|
|
||||
| Task tracking (w/ priority) | `todo.updated` | POST /observe |
|
||||
| Command executed | `command.executed` | POST /observe |
|
||||
|
||||
### Model & config
|
||||
|
||||
| Event | Hook | agentmemory API |
|
||||
|---|---|---|
|
||||
| LLM parameters | `chat.params` | POST /observe |
|
||||
| Config loaded | `config` | POST /observe |
|
||||
| Compaction (WIP) | `experimental.session.compacting` | POST /context → `output.context[]` |
|
||||
|
||||
### File enrichment + memory injection (two-layer pipeline)
|
||||
|
||||
`experimental.chat.system.transform` fires before every LLM call and injects two layers of context:
|
||||
|
||||
1. **Memory context** (once per session): calls `/agentmemory/context` and injects project profile, recent session summaries, and important past observations into the system prompt. This is the OpenCode equivalent of Claude's MEMORY.md bridge — instead of syncing to a markdown file, context is injected directly into the system prompt.
|
||||
|
||||
2. **File enrichment** (every turn with stashed files): calls `/agentmemory/enrich` with files stashed by `tool.execute.before`, `file.edited`, and `message.part.updated` (file parts). File-specific context (past observations, related bugs, semantic search) is injected into the system prompt.
|
||||
|
||||
```text
|
||||
System prompt = [OpenCode instructions] + [memory context] + [file enrichment] + [user message]
|
||||
^ ^
|
||||
first turn only every file-touching turn
|
||||
```
|
||||
|
||||
**Differences from Claude's PreToolUse:**
|
||||
|
||||
| Dimension | Claude (PreToolUse) | OpenCode (two-hop pipeline) |
|
||||
|---|---|---|
|
||||
| Injection mechanism | stdout → context window | `output.system[]` → system prompt |
|
||||
| Timing | Same turn (parallel with tool) | Next turn (before next LLM call) |
|
||||
| File set | Per-tool (immediate) | Batched (all files since last enrichment) |
|
||||
| Coverage | Edit/Write/Read/Glob/Grep only | Edit/Write/Read/Glob/Grep only |
|
||||
| What gets injected | `<agentmemory-file-context>` + bug memories | Identical `/enrich` response |
|
||||
|
||||
## MEMORY.md vs AGENTS.md: how context flows
|
||||
|
||||
Claude Code and OpenCode take fundamentally different approaches to injecting memory context into the agent's system prompt.
|
||||
|
||||
### Claude Code: file-backed bridge (two-hop)
|
||||
|
||||
```
|
||||
agentmemory ──write──▶ MEMORY.md ──read──▶ Claude system prompt
|
||||
```
|
||||
|
||||
- The `claude-bridge/sync` endpoint serializes agentmemory observations into a `MEMORY.md` file in the project root
|
||||
- Claude Code reads `MEMORY.md` on session start and prepends it to the system prompt
|
||||
- **Sync is periodic** — sessions only get fresh context when the bridge last ran (session end, pre-compact)
|
||||
- **Coupling**: memory data lives in a git-trackable file, visible to CI, team members, and other tools
|
||||
|
||||
### OpenCode: direct injection (one-hop)
|
||||
|
||||
```
|
||||
agentmemory ──push──▶ OpenCode system prompt
|
||||
```
|
||||
|
||||
- `experimental.chat.system.transform` calls `/context` at runtime and pushes the response directly into `output.system[]`
|
||||
- **Always current** — context is fetched at session start (once) and before file-touching turns (per-batch)
|
||||
- **No file intermediary** — no stale copies, no merge conflicts, no disk I/O
|
||||
- `AGENTS.md` is a static instruction file for project conventions, coding standards, and tool guidance — agentmemory does not read or write it
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
| Dimension | Claude (MEMORY.md bridge) | OpenCode (direct injection) |
|
||||
|---|---|---|
|
||||
| Freshness | Stale between syncs | Always current (fetched at call time) |
|
||||
| Visibility | Human-readable file in repo | In-memory injection only |
|
||||
| Simplicity | Two moving parts (bridge + file) | One step (API → system prompt) |
|
||||
| Team sharing | File is git-trackable, CI-friendly | Memory shared via agentmemory server API |
|
||||
| Integration | Any tool can read MEMORY.md | Requires OpenCode plugin SDK |
|
||||
|
||||
### Why OpenCode goes direct
|
||||
|
||||
agentmemory already persists everything in SQLite (`data/state_store.db`). Adding an intermediate MEMORY.md file would duplicate data, introduce sync lag, and require the model to re-parse structured context from markdown. Direct injection delivers the same data with lower latency and zero staleness — the agent always sees what agentmemory knows right now.
|
||||
|
||||
## Slash commands
|
||||
|
||||
- `/recall <query>` — Search past observations and lessons
|
||||
- `/remember <text>` — Save an insight to long-term memory
|
||||
|
||||
## Session instruction injection
|
||||
|
||||
Agentmemory usage instructions are injected into the system prompt on the first turn of every session via `experimental.chat.system.transform` (alongside memory context from `/context`). This is functionally equivalent to Claude Code's skills mechanism — the agent learns which `agentmemory_memory_*` tools to use and when, without needing separate skill invocations.
|
||||
|
||||
## What's not covered (vs Claude Code plugin)
|
||||
|
||||
| Claude feature | Reason |
|
||||
|---|---|
|
||||
| SubagentStop | OpenCode's `SubtaskPart` type has no completion/result fields; subtask lifecycle ends are not exposed as distinct events in the OpenCode SDK |
|
||||
| TaskCompleted | No team/teammate concept in OpenCode; `todo.updated` captures task state changes as a partial equivalent |
|
||||
| Stop | `session.compacted` event handler exists; `experimental.session.compacting` injection hook defined in SDK but Go binary (v1.14.41) doesn't wire it — will auto-activate when upstream implements it |
|
||||
| Skills (remember/recall/forget/session-history) | Covered by injected system instructions via `experimental.chat.system.transform` — agent receives usage guidance on first turn |
|
||||
| Consolidation pipeline (crystals/auto + consolidate-pipeline) | Now called on `session.deleted` — mirrors Claude's `CONSOLIDATION_ENABLED=true` behavior |
|
||||
| Claude MEMORY.md bridge | OpenCode-specific; OpenCode uses its own AGENTS.md mechanism, not Claude's MEMORY.md |
|
||||
|
||||
All other Claude Code hooks have direct or pipeline equivalents in this plugin. 12 of 12 Claude hook types covered.
|
||||
@@ -0,0 +1,687 @@
|
||||
import type { Plugin } from "@opencode-ai/plugin";
|
||||
|
||||
const API = process.env.AGENTMEMORY_URL || "http://localhost:3111";
|
||||
const FILE_TOOLS = new Set(["Read", "Write", "Edit", "Glob", "Grep"]);
|
||||
const FILE_KEYS = ["filePath", "file_path", "path", "file", "pattern"];
|
||||
const MAX_STASHED_FILES = 20;
|
||||
|
||||
const DEBUG = process.env.OPENCODE_AGENTMEMORY_DEBUG === "1";
|
||||
const SECRET = process.env.AGENTMEMORY_SECRET || "";
|
||||
|
||||
function authHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
if (SECRET) headers["Authorization"] = `Bearer ${SECRET}`;
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function post(path: string, body: Record<string, unknown>, timeoutMs = 5000): Promise<void> {
|
||||
try {
|
||||
await fetch(`${API}/agentmemory${path}`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
} catch (e) {
|
||||
if (DEBUG) console.error(`[agentmemory] POST ${path} failed:`, (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async function postJson(path: string, body: Record<string, unknown>): Promise<unknown | null> {
|
||||
try {
|
||||
const res = await fetch(`${API}/agentmemory${path}`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
return res.ok ? await res.json() : null;
|
||||
} catch (e) {
|
||||
if (DEBUG) console.error(`[agentmemory] POST ${path} failed:`, (e as Error).message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function observe(
|
||||
sessionId: string,
|
||||
hookType: string,
|
||||
data: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
await post("/observe", {
|
||||
hookType,
|
||||
sessionId,
|
||||
project: projectPath,
|
||||
cwd: projectPath,
|
||||
timestamp: new Date().toISOString(),
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
let activeSessionId: string | null = null;
|
||||
let pendingConfig: Record<string, unknown> | null = null;
|
||||
let projectPath: string | null = null;
|
||||
const stashedFiles = new Map<string, Set<string>>();
|
||||
const seenSubtaskIds = new Map<string, Set<string>>();
|
||||
const seenToolCallIds = new Map<string, Set<string>>();
|
||||
const contextInjectedSessions = new Set<string>();
|
||||
// cache the context returned by POST /session/start so the chat
|
||||
// system-transform hook can inject it without a second /context fetch.
|
||||
// Auto-injection now happens at session.created (immediately) AND at
|
||||
// the first prompt_submit (fallback for older OpenCode builds that
|
||||
// don't implement experimental.chat.system.transform).
|
||||
const startContextCache = new Map<string, string>();
|
||||
|
||||
function stashFor(sid: string): Set<string> {
|
||||
let s = stashedFiles.get(sid);
|
||||
if (!s) { s = new Set<string>(); stashedFiles.set(sid, s); }
|
||||
return s;
|
||||
}
|
||||
|
||||
function subtaskSetFor(sid: string): Set<string> {
|
||||
let s = seenSubtaskIds.get(sid);
|
||||
if (!s) { s = new Set<string>(); seenSubtaskIds.set(sid, s); }
|
||||
return s;
|
||||
}
|
||||
|
||||
function toolCallSetFor(sid: string): Set<string> {
|
||||
let s = seenToolCallIds.get(sid);
|
||||
if (!s) { s = new Set<string>(); seenToolCallIds.set(sid, s); }
|
||||
return s;
|
||||
}
|
||||
|
||||
function pruneSessionMaps(sid: string): void {
|
||||
stashedFiles.delete(sid);
|
||||
seenSubtaskIds.delete(sid);
|
||||
seenToolCallIds.delete(sid);
|
||||
}
|
||||
|
||||
function safeSlice(v: unknown, max: number): string {
|
||||
if (typeof v === "string") return v.slice(0, max);
|
||||
if (v == null) return "";
|
||||
try { return JSON.stringify(v).slice(0, max); } catch { return ""; }
|
||||
}
|
||||
|
||||
const AGENTMEMORY_INSTRUCTIONS = `<agentmemory-instructions>
|
||||
You have access to agentmemory for persistent cross-session memory. Use these tools proactively.
|
||||
|
||||
CORE TOOLS:
|
||||
|
||||
memory_save — Save an insight, decision, or fact to long-term memory.
|
||||
Required: content (text), concepts (2-5 comma-separated keywords), type (pattern/preference/architecture/bug/workflow/fact)
|
||||
Optional: files (comma-separated paths)
|
||||
Use when: user says "remember this", after discovering a bug, after making an architectural decision, after learning a project convention.
|
||||
|
||||
memory_recall — Search past observations by keywords.
|
||||
Use when: user says "recall", "what did we do", "do you remember", or needs context from past sessions.
|
||||
|
||||
memory_smart_search — Hybrid semantic+keyword search with progressive disclosure.
|
||||
Use when: you need the most relevant past context, fuzzy/conceptual searches, or recall doesn't find what you need.
|
||||
|
||||
memory_sessions — List recent sessions with status and observation counts.
|
||||
Use when: user asks about session/past history, "what did we work on".
|
||||
|
||||
memory_file_history — Get past observations about specific files (across all sessions).
|
||||
Use when: you're about to edit a file and want to know its history, common pitfalls, or past edits.
|
||||
|
||||
memory_lesson_save — Save a lesson learned (what worked, what to avoid).
|
||||
Use when: you discover a pattern that could help future sessions avoid mistakes.
|
||||
|
||||
memory_lesson_recall — Search lessons by query. Returns lessons sorted by confidence.
|
||||
Use when: before making a decision, check if past lessons apply.
|
||||
|
||||
memory_governance_delete — Delete specific memories. Requires explicit user confirmation.
|
||||
Use when: user says "forget this", "delete that memory".
|
||||
|
||||
memory_patterns — Detect recurring patterns across sessions.
|
||||
Use when: you want to understand project-level trends over time.
|
||||
|
||||
memory_consolidate — Run the 4-tier memory consolidation pipeline.
|
||||
Use when: you want to compress and organize accumulated session observations.
|
||||
|
||||
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.
|
||||
</agentmemory-instructions>`;
|
||||
|
||||
function extractFilePaths(args: Record<string, unknown>): string[] {
|
||||
const files: string[] = [];
|
||||
for (const key of FILE_KEYS) {
|
||||
const val = args[key];
|
||||
if (typeof val === "string" && val.length > 0) {
|
||||
files.push(val);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function extractErrorMessage(err: unknown): string {
|
||||
if (typeof err === "string") return err;
|
||||
if (err && typeof err === "object") {
|
||||
const e = err as Record<string, unknown>;
|
||||
if (typeof e.message === "string") return e.message;
|
||||
if (e.data && typeof e.data === "object") {
|
||||
const d = e.data as Record<string, unknown>;
|
||||
if (typeof d.message === "string") return d.message;
|
||||
}
|
||||
if (typeof e.name === "string") return e.name;
|
||||
try { return JSON.stringify(err); } catch { return ""; }
|
||||
}
|
||||
return String(err ?? "");
|
||||
}
|
||||
|
||||
export const AgentmemoryCapturePlugin: Plugin = async (ctx) => {
|
||||
projectPath = ctx.worktree || ctx.project?.id || process.cwd();
|
||||
|
||||
return {
|
||||
event: async ({ event }) => {
|
||||
const type = event.type;
|
||||
const props = (event as any).properties || {};
|
||||
|
||||
// ── session.created ──
|
||||
if (type === "session.created") {
|
||||
const info = props.info as Record<string, unknown> | undefined;
|
||||
activeSessionId = (info?.id as string) || props.sessionID || null;
|
||||
if (!activeSessionId) return;
|
||||
stashedFiles.set(activeSessionId, new Set());
|
||||
seenSubtaskIds.delete(activeSessionId);
|
||||
seenToolCallIds.delete(activeSessionId);
|
||||
contextInjectedSessions.delete(activeSessionId);
|
||||
// Snapshot the session id locally — `activeSessionId` is mutable
|
||||
// and another `session.created` event during the await could
|
||||
// rebind it, causing context to be cached against the wrong key.
|
||||
const sessionId = activeSessionId;
|
||||
const startResult = await postJson("/session/start", {
|
||||
sessionId,
|
||||
title: info?.title ?? null,
|
||||
parentID: info?.parentID ?? null,
|
||||
version: info?.version ?? null,
|
||||
project: projectPath,
|
||||
cwd: projectPath,
|
||||
});
|
||||
// cache the context returned at session/start so the
|
||||
// chat.system.transform hook injects it without a second fetch.
|
||||
const startCtx = (startResult as any)?.context;
|
||||
if (typeof startCtx === "string" && startCtx.length > 0) {
|
||||
startContextCache.set(sessionId, startCtx);
|
||||
}
|
||||
if (pendingConfig) {
|
||||
await observe(sessionId, "config_loaded", pendingConfig);
|
||||
pendingConfig = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── session.idle ── (summarize handled in session.status idle branch)
|
||||
|
||||
// ── session.status ──
|
||||
if (type === "session.status") {
|
||||
const status = props.status as Record<string, unknown> | undefined;
|
||||
const sid = props.sessionID || activeSessionId;
|
||||
if (!sid || !status) return;
|
||||
if (status.type === "idle") {
|
||||
await post("/summarize", { sessionId: sid });
|
||||
}
|
||||
await observe(sid, "session_status", {
|
||||
status_type: status.type,
|
||||
attempt: status.attempt ?? null,
|
||||
message: safeSlice(status.message, 2000),
|
||||
});
|
||||
}
|
||||
|
||||
// ── session.compacted ──
|
||||
if (type === "session.compacted") {
|
||||
const sid = props.sessionID || activeSessionId;
|
||||
if (sid) {
|
||||
await post("/summarize", { sessionId: sid });
|
||||
await observe(sid, "session_compacted", {});
|
||||
}
|
||||
}
|
||||
|
||||
// ── session.updated ──
|
||||
if (type === "session.updated") {
|
||||
const info = props.info as Record<string, unknown> | undefined;
|
||||
const sid = (info?.id as string) || props.sessionID || activeSessionId;
|
||||
if (!sid) return;
|
||||
await observe(sid, "session_updated", {
|
||||
title: info?.title ?? null,
|
||||
parentID: info?.parentID ?? null,
|
||||
additions: (info?.summary as any)?.additions ?? null,
|
||||
deletions: (info?.summary as any)?.deletions ?? null,
|
||||
files: (info?.summary as any)?.files ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
// ── session.diff ──
|
||||
if (type === "session.diff") {
|
||||
const sid = props.sessionID || activeSessionId;
|
||||
if (!sid || !Array.isArray(props.diff)) return;
|
||||
const diffs = props.diff as Array<Record<string, unknown>>;
|
||||
await observe(sid, "session_diff", {
|
||||
files: diffs.map(d => d.file),
|
||||
additions: diffs.reduce((s, d) => s + ((d.additions as number) || 0), 0),
|
||||
deletions: diffs.reduce((s, d) => s + ((d.deletions as number) || 0), 0),
|
||||
diffs: diffs.slice(0, 50),
|
||||
});
|
||||
}
|
||||
|
||||
// ── session.deleted ──
|
||||
if (type === "session.deleted") {
|
||||
const sid = props.info?.id || props.sessionID || activeSessionId;
|
||||
if (!sid) {
|
||||
if (DEBUG) console.error("[agentmemory] session.deleted with no session ID");
|
||||
return;
|
||||
}
|
||||
await post("/session/end", { sessionId: sid });
|
||||
post("/crystals/auto", { olderThanDays: 7 }, 30000);
|
||||
post("/consolidate-pipeline", { tier: "all", force: true }, 30000);
|
||||
if (sid === activeSessionId) activeSessionId = null;
|
||||
stashedFiles.delete(sid);
|
||||
startContextCache.delete(sid);
|
||||
seenSubtaskIds.delete(sid);
|
||||
seenToolCallIds.delete(sid);
|
||||
contextInjectedSessions.delete(sid);
|
||||
}
|
||||
|
||||
// ── session.error ──
|
||||
if (type === "session.error") {
|
||||
const sid = props.sessionID || activeSessionId;
|
||||
if (sid) {
|
||||
await observe(sid, "post_tool_failure", {
|
||||
tool_name: "session.error",
|
||||
tool_input: "",
|
||||
tool_output: safeSlice(props.error, 8000),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── message.updated ──
|
||||
if (type === "message.updated") {
|
||||
const info = props.info as Record<string, unknown> | undefined;
|
||||
if (!info) return;
|
||||
|
||||
if (info.role === "assistant") {
|
||||
const sid = props.sessionID || (info.sessionID as string) || activeSessionId;
|
||||
if (!sid) return;
|
||||
const tokens = info.tokens as Record<string, unknown> | undefined;
|
||||
const error = info.error ? extractErrorMessage(info.error) : null;
|
||||
await observe(sid, "assistant_message", {
|
||||
messageID: info.id,
|
||||
parentID: info.parentID,
|
||||
modelID: info.modelID,
|
||||
providerID: info.providerID,
|
||||
mode: info.mode,
|
||||
cost: info.cost ?? 0,
|
||||
tokens: {
|
||||
input: tokens?.input ?? 0,
|
||||
output: tokens?.output ?? 0,
|
||||
reasoning: tokens?.reasoning ?? 0,
|
||||
cache_read: (tokens?.cache as any)?.read ?? 0,
|
||||
cache_write: (tokens?.cache as any)?.write ?? 0,
|
||||
},
|
||||
finish: info.finish ?? null,
|
||||
error,
|
||||
duration_ms: (info.time && typeof (info.time as any).completed === "number")
|
||||
? (info.time as any).completed - ((info.time as any).created || 0)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── message.removed ──
|
||||
if (type === "message.removed") {
|
||||
const sid = props.sessionID || activeSessionId;
|
||||
if (sid) {
|
||||
await observe(sid, "message_removed", {
|
||||
messageID: props.messageID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── message.part.updated ──
|
||||
if (type === "message.part.updated") {
|
||||
const part = props.part as Record<string, unknown> | undefined;
|
||||
if (!part) return;
|
||||
const sid = (part.sessionID as string) || props.sessionID || activeSessionId;
|
||||
if (!sid) return;
|
||||
|
||||
if (part.type === "subtask") {
|
||||
const subtaskId = part.id as string;
|
||||
if (!subtaskId) return;
|
||||
const subtaskSet = subtaskSetFor(sid);
|
||||
if (subtaskSet.has(subtaskId)) return;
|
||||
subtaskSet.add(subtaskId);
|
||||
await observe(sid, "subagent_start", {
|
||||
subtask_id: part.id,
|
||||
agent: part.agent,
|
||||
prompt: safeSlice(part.prompt, 4000),
|
||||
description: safeSlice(part.description, 2000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (part.type === "tool") {
|
||||
const state = part.state as Record<string, unknown> | undefined;
|
||||
if (!state) return;
|
||||
const callId = part.callID as string;
|
||||
if (!callId) return;
|
||||
const toolName = part.tool as string;
|
||||
|
||||
if (state.status === "completed") {
|
||||
const callSet = toolCallSetFor(sid);
|
||||
if (callSet.has(callId)) return;
|
||||
callSet.add(callId);
|
||||
const st = state as Record<string, unknown>;
|
||||
const rawTime = (st.time as any) || {};
|
||||
const startTime = typeof rawTime.start === "number" ? rawTime.start : null;
|
||||
const endTime = typeof rawTime.end === "number" ? rawTime.end : null;
|
||||
await observe(sid, "post_tool_use", {
|
||||
tool_name: toolName,
|
||||
call_id: callId,
|
||||
tool_input: safeSlice(st.input, 4000),
|
||||
tool_output: safeSlice(st.output, 8000),
|
||||
title: st.title ?? null,
|
||||
metadata: st.metadata || {},
|
||||
duration_ms: (startTime != null && endTime != null) ? endTime - startTime : null,
|
||||
attachments: Array.isArray(st.attachments)
|
||||
? (st.attachments as Array<Record<string, unknown>>).map(a => a.filename || a.url)
|
||||
: [],
|
||||
});
|
||||
} else if (state.status === "error") {
|
||||
const callSet = toolCallSetFor(sid);
|
||||
if (callSet.has(callId)) return;
|
||||
callSet.add(callId);
|
||||
const st = state as Record<string, unknown>;
|
||||
const rawTime = (st.time as any) || {};
|
||||
const startTime = typeof rawTime.start === "number" ? rawTime.start : null;
|
||||
const endTime = typeof rawTime.end === "number" ? rawTime.end : null;
|
||||
await observe(sid, "post_tool_failure", {
|
||||
tool_name: toolName,
|
||||
call_id: callId,
|
||||
tool_input: safeSlice(st.input, 4000),
|
||||
tool_output: safeSlice(st.error, 8000),
|
||||
duration_ms: (startTime != null && endTime != null) ? endTime - startTime : null,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (part.type === "step-finish") {
|
||||
await observe(sid, "step_finish", {
|
||||
messageID: part.messageID,
|
||||
reason: part.reason ?? null,
|
||||
cost: (part as any).cost ?? 0,
|
||||
input_tokens: ((part as any).tokens?.input as number) ?? 0,
|
||||
output_tokens: ((part as any).tokens?.output as number) ?? 0,
|
||||
reasoning_tokens: ((part as any).tokens?.reasoning as number) ?? 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (part.type === "reasoning") {
|
||||
await observe(sid, "reasoning", {
|
||||
messageID: part.messageID,
|
||||
text: safeSlice((part as any).text, 4000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (part.type === "file") {
|
||||
const filename = (part as any).filename || (part as any).url || null;
|
||||
if (filename) stashFor(sid).add(filename);
|
||||
return;
|
||||
}
|
||||
|
||||
if (part.type === "patch") {
|
||||
await observe(sid, "patch_applied", {
|
||||
messageID: part.messageID,
|
||||
hash: (part as any).hash,
|
||||
files: (part as any).files || [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (part.type === "compaction") {
|
||||
await observe(sid, "compaction_event", {
|
||||
messageID: part.messageID,
|
||||
auto: (part as any).auto ?? false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (part.type === "agent") {
|
||||
await observe(sid, "agent_selected", {
|
||||
messageID: part.messageID,
|
||||
name: (part as any).name,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (part.type === "retry") {
|
||||
await observe(sid, "retry_attempt", {
|
||||
messageID: part.messageID,
|
||||
attempt: (part as any).attempt,
|
||||
error: safeSlice((part as any).error, 2000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── file.edited ──
|
||||
if (type === "file.edited") {
|
||||
const sid = props.sessionID || activeSessionId;
|
||||
if (sid && typeof props.file === "string" && props.file.length > 0) {
|
||||
const stash = stashFor(sid);
|
||||
stash.add(props.file);
|
||||
if (stash.size > MAX_STASHED_FILES) {
|
||||
const keep = [...stash].slice(-MAX_STASHED_FILES);
|
||||
stash.clear();
|
||||
for (const f of keep) stash.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── permission.updated ──
|
||||
if (type === "permission.updated") {
|
||||
const sid = props.sessionID || activeSessionId;
|
||||
if (!sid) return;
|
||||
await observe(sid, "notification", {
|
||||
notification_type: "permission_prompt",
|
||||
permission: props.type || "unknown",
|
||||
pattern: Array.isArray(props.pattern)
|
||||
? props.pattern.join(", ")
|
||||
: (props.pattern || ""),
|
||||
tool_call_id: props.callID || null,
|
||||
title: props.title || props.type || "",
|
||||
metadata: props.metadata || {},
|
||||
});
|
||||
}
|
||||
|
||||
// ── permission.replied ──
|
||||
if (type === "permission.replied") {
|
||||
const sid = props.sessionID || activeSessionId;
|
||||
if (!sid) return;
|
||||
await observe(sid, "permission_replied", {
|
||||
permission_id: props.permissionID || props.requestID || "",
|
||||
response: props.response || props.reply || "",
|
||||
});
|
||||
}
|
||||
|
||||
// ── todo.updated ──
|
||||
if (type === "todo.updated") {
|
||||
const sid = props.sessionID || activeSessionId;
|
||||
const todos = Array.isArray(props.todos) ? props.todos.slice(0, 100) : [];
|
||||
if (!sid || todos.length === 0) return;
|
||||
const completed = todos.filter((t: any) => t.status === "completed");
|
||||
const active = todos.filter((t: any) => t.status !== "completed");
|
||||
await observe(sid, "task_completed", {
|
||||
completed: completed.map((t: any) => ({ content: t.content, priority: t.priority })),
|
||||
in_progress: active.map((t: any) => ({ content: t.content, priority: t.priority })),
|
||||
total: todos.length,
|
||||
});
|
||||
}
|
||||
|
||||
// ── command.executed ──
|
||||
if (type === "command.executed") {
|
||||
const sid = props.sessionID || activeSessionId;
|
||||
if (sid) {
|
||||
await observe(sid, "command_executed", {
|
||||
name: props.name,
|
||||
arguments: props.arguments || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ── chat.message ──
|
||||
"chat.message": async (input, output) => {
|
||||
const sid = input.sessionID || activeSessionId;
|
||||
if (!sid) return;
|
||||
const parts = output.parts || [];
|
||||
const files = parts
|
||||
.filter((p: any) => p.type === "file")
|
||||
.map((p: any) => p.filename || p.url)
|
||||
.filter(Boolean);
|
||||
for (const f of files) {
|
||||
const stash = stashFor(sid);
|
||||
stash.add(f);
|
||||
if (stash.size > MAX_STASHED_FILES) {
|
||||
const keep = [...stash].slice(-MAX_STASHED_FILES);
|
||||
stash.clear();
|
||||
for (const k of keep) stash.add(k);
|
||||
}
|
||||
}
|
||||
|
||||
const textParts = parts.filter((p: any) => p.type === "text" && !p.synthetic && !p.ignored);
|
||||
const userText = textParts.map((p: any) => p.text || "").join("\n");
|
||||
|
||||
await observe(sid, "prompt_submit", {
|
||||
agent: input.agent ?? null,
|
||||
model: input.model ?? null,
|
||||
variant: input.variant ?? null,
|
||||
prompt: userText.slice(0, 8000),
|
||||
files: files.slice(0, 20),
|
||||
parts_summary: parts.map((p: any) => p.type).filter(Boolean),
|
||||
});
|
||||
},
|
||||
|
||||
// ── chat.params ──
|
||||
"chat.params": async (input, output) => {
|
||||
if (!input.model || !output) return;
|
||||
const sid = input.sessionID || activeSessionId;
|
||||
if (!sid) return;
|
||||
await observe(sid, "llm_params", {
|
||||
agent: input.agent,
|
||||
model: `${input.model.providerID}/${input.model.id}`,
|
||||
provider_url: input.model.api?.url ?? null,
|
||||
temperature: output.temperature,
|
||||
topP: output.topP,
|
||||
max_output_tokens: input.model.limit?.output ?? null,
|
||||
context_limit: input.model.limit?.context ?? null,
|
||||
cost_1k_input: input.model.cost?.input ?? 0,
|
||||
cost_1k_output: input.model.cost?.output ?? 0,
|
||||
});
|
||||
},
|
||||
|
||||
// ── tool.execute.before ──
|
||||
"tool.execute.before": async (input, output) => {
|
||||
if (!FILE_TOOLS.has(input.tool)) return;
|
||||
const sid = input.sessionID || activeSessionId;
|
||||
if (!sid) return;
|
||||
const args = output.args as Record<string, unknown> | undefined;
|
||||
if (!args) return;
|
||||
const stash = stashFor(sid);
|
||||
for (const fp of extractFilePaths(args)) {
|
||||
stash.add(fp);
|
||||
}
|
||||
if (stash.size > MAX_STASHED_FILES) {
|
||||
const keep = [...stash].slice(-MAX_STASHED_FILES);
|
||||
stash.clear();
|
||||
for (const f of keep) stash.add(f);
|
||||
}
|
||||
},
|
||||
|
||||
// ── experimental.chat.system.transform ──
|
||||
"experimental.chat.system.transform": async (input, output) => {
|
||||
const sid = input.sessionID || activeSessionId;
|
||||
if (!sid) return;
|
||||
|
||||
if (!contextInjectedSessions.has(sid)) {
|
||||
if (!Array.isArray(output.system)) return;
|
||||
output.system.push(AGENTMEMORY_INSTRUCTIONS);
|
||||
// prefer the context already fetched at session.created;
|
||||
// fall back to a fresh /context call if the cache missed (e.g.
|
||||
// session resumed across plugin reloads).
|
||||
let ctx = startContextCache.get(sid);
|
||||
if (typeof ctx !== "string" || ctx.length === 0) {
|
||||
const result = await postJson("/context", {
|
||||
sessionId: sid,
|
||||
project: projectPath,
|
||||
});
|
||||
ctx = (result as any)?.context;
|
||||
} else {
|
||||
startContextCache.delete(sid);
|
||||
}
|
||||
if (typeof ctx === "string" && ctx.length > 0) {
|
||||
output.system.push(ctx);
|
||||
}
|
||||
contextInjectedSessions.add(sid);
|
||||
}
|
||||
|
||||
const stash = stashFor(sid);
|
||||
if (stash.size === 0) return;
|
||||
const files = [...stash].slice(0, 10);
|
||||
|
||||
const enrichResult = await postJson("/enrich", {
|
||||
sessionId: sid,
|
||||
files,
|
||||
toolName: "enrich_inject",
|
||||
});
|
||||
|
||||
const enrichCtx = (enrichResult as any)?.context;
|
||||
if (typeof enrichCtx === "string" && enrichCtx.length > 0) {
|
||||
if (Array.isArray(output.system)) {
|
||||
output.system.push(enrichCtx);
|
||||
}
|
||||
for (const f of files) stash.delete(f);
|
||||
}
|
||||
},
|
||||
|
||||
// ── experimental.session.compacting (WIP) ──
|
||||
"experimental.session.compacting": async (input, output) => {
|
||||
const sid = input.sessionID || activeSessionId;
|
||||
if (!sid) return;
|
||||
|
||||
const result = await postJson("/context", {
|
||||
sessionId: sid,
|
||||
project: projectPath,
|
||||
});
|
||||
const ctx = (result as any)?.context;
|
||||
if (typeof ctx === "string" && ctx.length > 0) {
|
||||
if (Array.isArray(output.context)) {
|
||||
output.context.push(ctx);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ── config ──
|
||||
config: async (input) => {
|
||||
const payload: Record<string, unknown> = {
|
||||
theme: input.theme ?? null,
|
||||
model: input.model ?? null,
|
||||
autoupdate: input.autoupdate ?? null,
|
||||
agents: typeof input.agent === "object" && input.agent !== null && !Array.isArray(input.agent)
|
||||
? Object.keys(input.agent as Record<string, unknown>)
|
||||
: Array.isArray(input.agent) ? input.agent : [],
|
||||
mcp_servers: typeof input.mcp === "object" && input.mcp !== null && !Array.isArray(input.mcp)
|
||||
? Object.keys(input.mcp as Record<string, unknown>)
|
||||
: Array.isArray(input.mcp) ? input.mcp : [],
|
||||
providers: typeof input.provider === "object" && input.provider !== null && !Array.isArray(input.provider)
|
||||
? Object.keys(input.provider as Record<string, unknown>)
|
||||
: Array.isArray(input.provider) ? input.provider : [],
|
||||
permission: input.permission ?? null,
|
||||
};
|
||||
if (activeSessionId) {
|
||||
await observe(activeSessionId, "config_loaded", payload);
|
||||
} else {
|
||||
pendingConfig = payload;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
Search past session observations and lessons for relevant context. Wrap the `memory_smart_search` and `memory_lesson_recall` MCP tools.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/recall [query]
|
||||
```
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Call `memory_smart_search` with the query and `limit: 10` (hybrid BM25 + vector + graph search).
|
||||
2. Call `memory_lesson_recall` with the same query and `limit: 5` (lesson search).
|
||||
3. Combine results and present to the user:
|
||||
- Group by session
|
||||
- Show type, title, and narrative for each observation
|
||||
- Highlight high-importance (>= 7) observations
|
||||
- Show lessons separately with confidence scores
|
||||
4. If no results, suggest 2-3 alternative search terms.
|
||||
5. **Never hallucinate results.** Only present what the MCP tools actually return.
|
||||
@@ -0,0 +1,19 @@
|
||||
Explicitly save an insight, decision, or learning to agentmemory for future sessions. Wraps the `memory_save` MCP tool.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/remember [what to remember]
|
||||
```
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Analyze what needs to be remembered — extract the core insight, decision, or fact.
|
||||
2. Extract 2-5 searchable concepts (lowercased keyword phrases). Prefer specific terms ("jwt-refresh-rotation" over "auth").
|
||||
3. Extract relevant file paths the memory references.
|
||||
4. Call `memory_save` with:
|
||||
- `content` — full text to remember (preserve user's phrasing)
|
||||
- `concepts` — extracted concept list
|
||||
- `files` — extracted file list (empty array if none)
|
||||
- `type` — choose from: pattern, preference, architecture, bug, workflow, fact
|
||||
5. Confirm the save and show the concepts tagged so the user knows retrieval terms.
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "agentmemory-capture",
|
||||
"version": "0.9.20",
|
||||
"description": "OpenCode plugin for agentmemory — full Claude Code hook parity: session lifecycle (create/idle/status/compacted/update/diff/delete/error), messages & prompts (chat.message, message.updated user+assistant, message.removed), tool lifecycle (ToolPart states with timing), part tracking (subtask, step-finish, reasoning, file, patch, compaction, agent, retry), file enrichment pipeline, permissions, task tracking (w/ priority), commands, config & model tracking. 22 hooks, 2 slash commands.",
|
||||
"author": {
|
||||
"name": "Rohit Ghumare",
|
||||
"url": "https://github.com/rohitg00"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://github.com/rohitg00/agentmemory",
|
||||
"repository": "https://github.com/rohitg00/agentmemory"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "agentmemory",
|
||||
"version": "0.9.27",
|
||||
"description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 53 MCP tools, 15 skills, real-time viewer.",
|
||||
"author": {
|
||||
"name": "Rohit Ghumare",
|
||||
"url": "https://github.com/rohitg00"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://github.com/rohitg00/agentmemory",
|
||||
"repository": "https://github.com/rohitg00/agentmemory",
|
||||
"skills": "skills/",
|
||||
"mcpServers": ".mcp.copilot.json",
|
||||
"hooks": "hooks/hooks.copilot.json"
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
//#region src/state/schema.ts
|
||||
const KV = {
|
||||
sessions: "mem:sessions",
|
||||
observations: (sessionId) => `mem:obs:${sessionId}`,
|
||||
memories: "mem:memories",
|
||||
summaries: "mem:summaries",
|
||||
config: "mem:config",
|
||||
metrics: "mem:metrics",
|
||||
health: "mem:health",
|
||||
embeddings: (obsId) => `mem:emb:${obsId}`,
|
||||
bm25Index: "mem:index:bm25",
|
||||
relations: "mem:relations",
|
||||
profiles: "mem:profiles",
|
||||
claudeBridge: "mem:claude-bridge",
|
||||
graphNodes: "mem:graph:nodes",
|
||||
graphEdges: "mem:graph:edges",
|
||||
semantic: "mem:semantic",
|
||||
procedural: "mem:procedural",
|
||||
teamShared: (teamId) => `mem:team:${teamId}:shared`,
|
||||
teamUsers: (teamId, userId) => `mem:team:${teamId}:users:${userId}`,
|
||||
teamProfile: (teamId) => `mem:team:${teamId}:profile`,
|
||||
audit: "mem:audit",
|
||||
actions: "mem:actions",
|
||||
actionEdges: "mem:action-edges",
|
||||
leases: "mem:leases",
|
||||
routines: "mem:routines",
|
||||
routineRuns: "mem:routine-runs",
|
||||
signals: "mem:signals",
|
||||
checkpoints: "mem:checkpoints",
|
||||
mesh: "mem:mesh",
|
||||
sketches: "mem:sketches",
|
||||
facets: "mem:facets",
|
||||
sentinels: "mem:sentinels",
|
||||
crystals: "mem:crystals"
|
||||
};
|
||||
|
||||
//#endregion
|
||||
//#region src/state/keyed-mutex.ts
|
||||
const locks = /* @__PURE__ */ new Map();
|
||||
function withKeyedLock(key, fn) {
|
||||
const next = (locks.get(key) ?? Promise.resolve()).then(fn, fn);
|
||||
const cleanup = next.then(() => {}, () => {});
|
||||
locks.set(key, cleanup);
|
||||
cleanup.then(() => {
|
||||
if (locks.get(key) === cleanup) locks.delete(key);
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region src/functions/diagnostics.ts
|
||||
const ALL_CATEGORIES = [
|
||||
"actions",
|
||||
"leases",
|
||||
"sentinels",
|
||||
"sketches",
|
||||
"signals",
|
||||
"sessions",
|
||||
"memories",
|
||||
"mesh"
|
||||
];
|
||||
const TWENTY_FOUR_HOURS_MS = 1440 * 60 * 1e3;
|
||||
const ONE_HOUR_MS = 3600 * 1e3;
|
||||
function registerDiagnosticsFunction(sdk, kv) {
|
||||
sdk.registerFunction("mem::diagnose", async (data) => {
|
||||
const categories = data.categories && data.categories.length > 0 ? data.categories.filter((c) => ALL_CATEGORIES.includes(c)) : ALL_CATEGORIES;
|
||||
const checks = [];
|
||||
const now = Date.now();
|
||||
if (categories.includes("actions")) {
|
||||
const actions = await kv.list(KV.actions);
|
||||
const allEdges = await kv.list(KV.actionEdges);
|
||||
const leases = await kv.list(KV.leases);
|
||||
const actionMap = new Map(actions.map((a) => [a.id, a]));
|
||||
for (const action of actions) {
|
||||
if (action.status === "active") {
|
||||
if (!leases.some((l) => l.actionId === action.id && l.status === "active" && new Date(l.expiresAt).getTime() > now)) checks.push({
|
||||
name: `active-no-lease:${action.id}`,
|
||||
category: "actions",
|
||||
status: "warn",
|
||||
message: `Action "${action.title}" is active but has no active lease`,
|
||||
fixable: false
|
||||
});
|
||||
}
|
||||
if (action.status === "blocked") {
|
||||
const deps = allEdges.filter((e) => e.sourceActionId === action.id && e.type === "requires");
|
||||
if (deps.length > 0) {
|
||||
if (deps.every((d) => {
|
||||
const target = actionMap.get(d.targetActionId);
|
||||
return target && target.status === "done";
|
||||
})) checks.push({
|
||||
name: `blocked-deps-done:${action.id}`,
|
||||
category: "actions",
|
||||
status: "fail",
|
||||
message: `Action "${action.title}" is blocked but all dependencies are done`,
|
||||
fixable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
if (action.status === "pending") {
|
||||
const deps = allEdges.filter((e) => e.sourceActionId === action.id && e.type === "requires");
|
||||
if (deps.length > 0) {
|
||||
if (deps.some((d) => {
|
||||
const target = actionMap.get(d.targetActionId);
|
||||
return !target || target.status !== "done";
|
||||
})) checks.push({
|
||||
name: `pending-unsatisfied-deps:${action.id}`,
|
||||
category: "actions",
|
||||
status: "fail",
|
||||
message: `Action "${action.title}" is pending but has unsatisfied dependencies`,
|
||||
fixable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!checks.some((c) => c.category === "actions" && c.status !== "pass")) checks.push({
|
||||
name: "actions-ok",
|
||||
category: "actions",
|
||||
status: "pass",
|
||||
message: `All ${actions.length} actions are consistent`,
|
||||
fixable: false
|
||||
});
|
||||
}
|
||||
if (categories.includes("leases")) {
|
||||
const leases = await kv.list(KV.leases);
|
||||
const actions = await kv.list(KV.actions);
|
||||
const actionIds = new Set(actions.map((a) => a.id));
|
||||
let leaseIssues = 0;
|
||||
for (const lease of leases) {
|
||||
if (lease.status === "active" && new Date(lease.expiresAt).getTime() <= now) {
|
||||
checks.push({
|
||||
name: `expired-lease:${lease.id}`,
|
||||
category: "leases",
|
||||
status: "fail",
|
||||
message: `Lease ${lease.id} for action ${lease.actionId} expired at ${lease.expiresAt}`,
|
||||
fixable: true
|
||||
});
|
||||
leaseIssues++;
|
||||
}
|
||||
if (!actionIds.has(lease.actionId)) {
|
||||
checks.push({
|
||||
name: `orphaned-lease:${lease.id}`,
|
||||
category: "leases",
|
||||
status: "fail",
|
||||
message: `Lease ${lease.id} references non-existent action ${lease.actionId}`,
|
||||
fixable: true
|
||||
});
|
||||
leaseIssues++;
|
||||
}
|
||||
}
|
||||
if (leaseIssues === 0) checks.push({
|
||||
name: "leases-ok",
|
||||
category: "leases",
|
||||
status: "pass",
|
||||
message: `All ${leases.length} leases are healthy`,
|
||||
fixable: false
|
||||
});
|
||||
}
|
||||
if (categories.includes("sentinels")) {
|
||||
const sentinels = await kv.list(KV.sentinels);
|
||||
const actions = await kv.list(KV.actions);
|
||||
const actionIds = new Set(actions.map((a) => a.id));
|
||||
let sentinelIssues = 0;
|
||||
for (const sentinel of sentinels) {
|
||||
if (sentinel.status === "watching" && sentinel.expiresAt && new Date(sentinel.expiresAt).getTime() <= now) {
|
||||
checks.push({
|
||||
name: `expired-sentinel:${sentinel.id}`,
|
||||
category: "sentinels",
|
||||
status: "fail",
|
||||
message: `Sentinel "${sentinel.name}" expired at ${sentinel.expiresAt}`,
|
||||
fixable: true
|
||||
});
|
||||
sentinelIssues++;
|
||||
}
|
||||
for (const actionId of sentinel.linkedActionIds) if (!actionIds.has(actionId)) {
|
||||
checks.push({
|
||||
name: `sentinel-missing-action:${sentinel.id}:${actionId}`,
|
||||
category: "sentinels",
|
||||
status: "warn",
|
||||
message: `Sentinel "${sentinel.name}" references non-existent action ${actionId}`,
|
||||
fixable: false
|
||||
});
|
||||
sentinelIssues++;
|
||||
}
|
||||
}
|
||||
if (sentinelIssues === 0) checks.push({
|
||||
name: "sentinels-ok",
|
||||
category: "sentinels",
|
||||
status: "pass",
|
||||
message: `All ${sentinels.length} sentinels are healthy`,
|
||||
fixable: false
|
||||
});
|
||||
}
|
||||
if (categories.includes("sketches")) {
|
||||
const sketches = await kv.list(KV.sketches);
|
||||
let sketchIssues = 0;
|
||||
for (const sketch of sketches) if (sketch.status === "active" && new Date(sketch.expiresAt).getTime() <= now) {
|
||||
checks.push({
|
||||
name: `expired-sketch:${sketch.id}`,
|
||||
category: "sketches",
|
||||
status: "fail",
|
||||
message: `Sketch "${sketch.title}" expired at ${sketch.expiresAt}`,
|
||||
fixable: true
|
||||
});
|
||||
sketchIssues++;
|
||||
}
|
||||
if (sketchIssues === 0) checks.push({
|
||||
name: "sketches-ok",
|
||||
category: "sketches",
|
||||
status: "pass",
|
||||
message: `All ${sketches.length} sketches are healthy`,
|
||||
fixable: false
|
||||
});
|
||||
}
|
||||
if (categories.includes("signals")) {
|
||||
const signals = await kv.list(KV.signals);
|
||||
let signalIssues = 0;
|
||||
for (const signal of signals) if (signal.expiresAt && new Date(signal.expiresAt).getTime() <= now) {
|
||||
checks.push({
|
||||
name: `expired-signal:${signal.id}`,
|
||||
category: "signals",
|
||||
status: "fail",
|
||||
message: `Signal from "${signal.from}" expired at ${signal.expiresAt}`,
|
||||
fixable: true
|
||||
});
|
||||
signalIssues++;
|
||||
}
|
||||
if (signalIssues === 0) checks.push({
|
||||
name: "signals-ok",
|
||||
category: "signals",
|
||||
status: "pass",
|
||||
message: `All ${signals.length} signals are healthy`,
|
||||
fixable: false
|
||||
});
|
||||
}
|
||||
if (categories.includes("sessions")) {
|
||||
const sessions = await kv.list(KV.sessions);
|
||||
let sessionIssues = 0;
|
||||
for (const session of sessions) if (session.status === "active" && now - new Date(session.startedAt).getTime() > TWENTY_FOUR_HOURS_MS) {
|
||||
checks.push({
|
||||
name: `abandoned-session:${session.id}`,
|
||||
category: "sessions",
|
||||
status: "warn",
|
||||
message: `Session ${session.id} has been active for over 24 hours`,
|
||||
fixable: false
|
||||
});
|
||||
sessionIssues++;
|
||||
}
|
||||
if (sessionIssues === 0) checks.push({
|
||||
name: "sessions-ok",
|
||||
category: "sessions",
|
||||
status: "pass",
|
||||
message: `All ${sessions.length} sessions are healthy`,
|
||||
fixable: false
|
||||
});
|
||||
}
|
||||
if (categories.includes("memories")) {
|
||||
const memories = await kv.list(KV.memories);
|
||||
const memoryIds = new Set(memories.map((m) => m.id));
|
||||
const supersededBy = /* @__PURE__ */ new Map();
|
||||
let memoryIssues = 0;
|
||||
for (const memory of memories) if (memory.supersedes && memory.supersedes.length > 0) for (const sid of memory.supersedes) {
|
||||
if (!memoryIds.has(sid)) {
|
||||
checks.push({
|
||||
name: `memory-missing-supersedes:${memory.id}:${sid}`,
|
||||
category: "memories",
|
||||
status: "warn",
|
||||
message: `Memory "${memory.title}" supersedes non-existent memory ${sid}`,
|
||||
fixable: false
|
||||
});
|
||||
memoryIssues++;
|
||||
}
|
||||
supersededBy.set(sid, memory.id);
|
||||
}
|
||||
for (const memory of memories) if (memory.isLatest && supersededBy.has(memory.id)) {
|
||||
checks.push({
|
||||
name: `memory-stale-latest:${memory.id}`,
|
||||
category: "memories",
|
||||
status: "fail",
|
||||
message: `Memory "${memory.title}" has isLatest=true but is superseded by ${supersededBy.get(memory.id)}`,
|
||||
fixable: true
|
||||
});
|
||||
memoryIssues++;
|
||||
}
|
||||
if (memoryIssues === 0) checks.push({
|
||||
name: "memories-ok",
|
||||
category: "memories",
|
||||
status: "pass",
|
||||
message: `All ${memories.length} memories are consistent`,
|
||||
fixable: false
|
||||
});
|
||||
}
|
||||
if (categories.includes("mesh")) {
|
||||
const peers = await kv.list(KV.mesh);
|
||||
let meshIssues = 0;
|
||||
for (const peer of peers) {
|
||||
if (peer.lastSyncAt && now - new Date(peer.lastSyncAt).getTime() > ONE_HOUR_MS) {
|
||||
checks.push({
|
||||
name: `stale-peer:${peer.id}`,
|
||||
category: "mesh",
|
||||
status: "warn",
|
||||
message: `Peer "${peer.name}" last synced over 1 hour ago`,
|
||||
fixable: false
|
||||
});
|
||||
meshIssues++;
|
||||
}
|
||||
if (peer.status === "error") {
|
||||
checks.push({
|
||||
name: `error-peer:${peer.id}`,
|
||||
category: "mesh",
|
||||
status: "warn",
|
||||
message: `Peer "${peer.name}" is in error state`,
|
||||
fixable: false
|
||||
});
|
||||
meshIssues++;
|
||||
}
|
||||
}
|
||||
if (meshIssues === 0) checks.push({
|
||||
name: "mesh-ok",
|
||||
category: "mesh",
|
||||
status: "pass",
|
||||
message: `All ${peers.length} mesh peers are healthy`,
|
||||
fixable: false
|
||||
});
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
checks,
|
||||
summary: {
|
||||
pass: checks.filter((c) => c.status === "pass").length,
|
||||
warn: checks.filter((c) => c.status === "warn").length,
|
||||
fail: checks.filter((c) => c.status === "fail").length,
|
||||
fixable: checks.filter((c) => c.fixable).length
|
||||
}
|
||||
};
|
||||
});
|
||||
sdk.registerFunction("mem::heal", async (data) => {
|
||||
const dryRun = data.dryRun ?? false;
|
||||
const categories = data.categories && data.categories.length > 0 ? data.categories.filter((c) => ALL_CATEGORIES.includes(c)) : ALL_CATEGORIES;
|
||||
let fixed = 0;
|
||||
let skipped = 0;
|
||||
const details = [];
|
||||
const now = Date.now();
|
||||
if (categories.includes("actions")) {
|
||||
const actions = await kv.list(KV.actions);
|
||||
const allEdges = await kv.list(KV.actionEdges);
|
||||
const actionMap = new Map(actions.map((a) => [a.id, a]));
|
||||
for (const action of actions) {
|
||||
if (action.status === "blocked") {
|
||||
const deps = allEdges.filter((e) => e.sourceActionId === action.id && e.type === "requires");
|
||||
if (deps.length > 0) {
|
||||
if (deps.every((d) => {
|
||||
const target = actionMap.get(d.targetActionId);
|
||||
return target && target.status === "done";
|
||||
})) {
|
||||
if (dryRun) {
|
||||
details.push(`[dry-run] Would unblock action "${action.title}" (${action.id})`);
|
||||
fixed++;
|
||||
continue;
|
||||
}
|
||||
if (await withKeyedLock(`mem:action:${action.id}`, async () => {
|
||||
const fresh = await kv.get(KV.actions, action.id);
|
||||
if (!fresh || fresh.status !== "blocked") return false;
|
||||
const freshDeps = (await kv.list(KV.actionEdges)).filter((e) => e.sourceActionId === fresh.id && e.type === "requires");
|
||||
const freshActions = await kv.list(KV.actions);
|
||||
const freshMap = new Map(freshActions.map((a) => [a.id, a]));
|
||||
if (!freshDeps.every((d) => {
|
||||
const target = freshMap.get(d.targetActionId);
|
||||
return target && target.status === "done";
|
||||
})) return false;
|
||||
fresh.status = "pending";
|
||||
fresh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
||||
await kv.set(KV.actions, fresh.id, fresh);
|
||||
return true;
|
||||
})) {
|
||||
details.push(`Unblocked action "${action.title}" (${action.id})`);
|
||||
fixed++;
|
||||
} else skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (action.status === "pending") {
|
||||
const deps = allEdges.filter((e) => e.sourceActionId === action.id && e.type === "requires");
|
||||
if (deps.length > 0) {
|
||||
if (deps.some((d) => {
|
||||
const target = actionMap.get(d.targetActionId);
|
||||
return !target || target.status !== "done";
|
||||
})) {
|
||||
if (dryRun) {
|
||||
details.push(`[dry-run] Would block action "${action.title}" (${action.id})`);
|
||||
fixed++;
|
||||
continue;
|
||||
}
|
||||
if (await withKeyedLock(`mem:action:${action.id}`, async () => {
|
||||
const fresh = await kv.get(KV.actions, action.id);
|
||||
if (!fresh || fresh.status !== "pending") return false;
|
||||
const freshDeps = (await kv.list(KV.actionEdges)).filter((e) => e.sourceActionId === fresh.id && e.type === "requires");
|
||||
const freshActions = await kv.list(KV.actions);
|
||||
const freshMap = new Map(freshActions.map((a) => [a.id, a]));
|
||||
if (!freshDeps.some((d) => {
|
||||
const target = freshMap.get(d.targetActionId);
|
||||
return !target || target.status !== "done";
|
||||
})) return false;
|
||||
fresh.status = "blocked";
|
||||
fresh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
||||
await kv.set(KV.actions, fresh.id, fresh);
|
||||
return true;
|
||||
})) {
|
||||
details.push(`Blocked action "${action.title}" (${action.id})`);
|
||||
fixed++;
|
||||
} else skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (categories.includes("leases")) {
|
||||
const leases = await kv.list(KV.leases);
|
||||
const actions = await kv.list(KV.actions);
|
||||
const actionIds = new Set(actions.map((a) => a.id));
|
||||
for (const lease of leases) {
|
||||
if (lease.status === "active" && new Date(lease.expiresAt).getTime() <= now) {
|
||||
if (dryRun) {
|
||||
details.push(`[dry-run] Would expire lease ${lease.id} for action ${lease.actionId}`);
|
||||
fixed++;
|
||||
continue;
|
||||
}
|
||||
if (await withKeyedLock(`mem:action:${lease.actionId}`, async () => {
|
||||
const fresh = await kv.get(KV.leases, lease.id);
|
||||
if (!fresh || fresh.status !== "active" || new Date(fresh.expiresAt).getTime() > Date.now()) return false;
|
||||
fresh.status = "expired";
|
||||
await kv.set(KV.leases, fresh.id, fresh);
|
||||
const action = await kv.get(KV.actions, fresh.actionId);
|
||||
if (action && action.status === "active" && action.assignedTo === fresh.agentId) {
|
||||
action.status = "pending";
|
||||
action.assignedTo = void 0;
|
||||
action.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
||||
await kv.set(KV.actions, action.id, action);
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
details.push(`Expired lease ${lease.id} for action ${lease.actionId}`);
|
||||
fixed++;
|
||||
} else skipped++;
|
||||
continue;
|
||||
}
|
||||
if (!actionIds.has(lease.actionId)) {
|
||||
if (dryRun) {
|
||||
details.push(`[dry-run] Would delete orphaned lease ${lease.id}`);
|
||||
fixed++;
|
||||
continue;
|
||||
}
|
||||
await kv.delete(KV.leases, lease.id);
|
||||
details.push(`Deleted orphaned lease ${lease.id}`);
|
||||
fixed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (categories.includes("sentinels")) {
|
||||
const sentinels = await kv.list(KV.sentinels);
|
||||
for (const sentinel of sentinels) if (sentinel.status === "watching" && sentinel.expiresAt && new Date(sentinel.expiresAt).getTime() <= now) {
|
||||
if (dryRun) {
|
||||
details.push(`[dry-run] Would expire sentinel "${sentinel.name}" (${sentinel.id})`);
|
||||
fixed++;
|
||||
continue;
|
||||
}
|
||||
if (await withKeyedLock(`mem:sentinel:${sentinel.id}`, async () => {
|
||||
const fresh = await kv.get(KV.sentinels, sentinel.id);
|
||||
if (!fresh || fresh.status !== "watching") return false;
|
||||
if (!fresh.expiresAt || new Date(fresh.expiresAt).getTime() > Date.now()) return false;
|
||||
fresh.status = "expired";
|
||||
await kv.set(KV.sentinels, fresh.id, fresh);
|
||||
return true;
|
||||
})) {
|
||||
details.push(`Expired sentinel "${sentinel.name}" (${sentinel.id})`);
|
||||
fixed++;
|
||||
} else skipped++;
|
||||
}
|
||||
}
|
||||
if (categories.includes("sketches")) {
|
||||
const sketches = await kv.list(KV.sketches);
|
||||
for (const sketch of sketches) if (sketch.status === "active" && new Date(sketch.expiresAt).getTime() <= now) {
|
||||
if (dryRun) {
|
||||
details.push(`[dry-run] Would discard expired sketch "${sketch.title}" (${sketch.id})`);
|
||||
fixed++;
|
||||
continue;
|
||||
}
|
||||
if (await withKeyedLock(`mem:sketch:${sketch.id}`, async () => {
|
||||
const fresh = await kv.get(KV.sketches, sketch.id);
|
||||
if (!fresh || fresh.status !== "active" || new Date(fresh.expiresAt).getTime() > Date.now()) return false;
|
||||
const allEdges = await kv.list(KV.actionEdges);
|
||||
const actionIdSet = new Set(fresh.actionIds);
|
||||
for (const edge of allEdges) if (actionIdSet.has(edge.sourceActionId) || actionIdSet.has(edge.targetActionId)) await kv.delete(KV.actionEdges, edge.id);
|
||||
for (const actionId of fresh.actionIds) await kv.delete(KV.actions, actionId);
|
||||
fresh.status = "discarded";
|
||||
fresh.discardedAt = (/* @__PURE__ */ new Date()).toISOString();
|
||||
await kv.set(KV.sketches, fresh.id, fresh);
|
||||
return true;
|
||||
})) {
|
||||
details.push(`Discarded expired sketch "${sketch.title}" (${sketch.id})`);
|
||||
fixed++;
|
||||
} else skipped++;
|
||||
}
|
||||
}
|
||||
if (categories.includes("signals")) {
|
||||
const signals = await kv.list(KV.signals);
|
||||
for (const signal of signals) if (signal.expiresAt && new Date(signal.expiresAt).getTime() <= now) {
|
||||
if (dryRun) {
|
||||
details.push(`[dry-run] Would delete expired signal ${signal.id}`);
|
||||
fixed++;
|
||||
continue;
|
||||
}
|
||||
await kv.delete(KV.signals, signal.id);
|
||||
details.push(`Deleted expired signal ${signal.id}`);
|
||||
fixed++;
|
||||
}
|
||||
}
|
||||
if (categories.includes("memories")) {
|
||||
const memories = await kv.list(KV.memories);
|
||||
const supersededBy = /* @__PURE__ */ new Map();
|
||||
for (const memory of memories) if (memory.supersedes && memory.supersedes.length > 0) for (const sid of memory.supersedes) supersededBy.set(sid, memory.id);
|
||||
for (const memory of memories) if (memory.isLatest && supersededBy.has(memory.id)) {
|
||||
if (dryRun) {
|
||||
details.push(`[dry-run] Would set isLatest=false on memory "${memory.title}" (${memory.id})`);
|
||||
fixed++;
|
||||
continue;
|
||||
}
|
||||
if (await withKeyedLock(`mem:memory:${memory.id}`, async () => {
|
||||
const fresh = await kv.get(KV.memories, memory.id);
|
||||
if (!fresh || !fresh.isLatest) return false;
|
||||
fresh.isLatest = false;
|
||||
fresh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
||||
await kv.set(KV.memories, fresh.id, fresh);
|
||||
return true;
|
||||
})) {
|
||||
details.push(`Set isLatest=false on memory "${memory.title}" (${memory.id})`);
|
||||
fixed++;
|
||||
} else skipped++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
fixed,
|
||||
skipped,
|
||||
details
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
export { registerDiagnosticsFunction };
|
||||
//# sourceMappingURL=diagnostics.mjs.map
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from "node:child_process";
|
||||
import { basename } from "node:path";
|
||||
|
||||
//#region src/hooks/_project.ts
|
||||
function resolveProject(cwd) {
|
||||
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
|
||||
if (explicit && explicit.trim()) return explicit.trim();
|
||||
const dir = cwd && cwd.trim() ? cwd : process.cwd();
|
||||
try {
|
||||
const top = execSync("git rev-parse --show-toplevel", {
|
||||
cwd: dir,
|
||||
stdio: [
|
||||
"ignore",
|
||||
"pipe",
|
||||
"ignore"
|
||||
],
|
||||
timeout: 500
|
||||
}).toString().trim();
|
||||
if (top) return basename(top);
|
||||
} catch {}
|
||||
return basename(dir);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region src/hooks/notification.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const notificationType = data.notification_type ?? data.notificationType;
|
||||
if (notificationType !== "permission_prompt") return;
|
||||
const rawSessionId = data.session_id ?? data.sessionId;
|
||||
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : "unknown";
|
||||
fetch(`${REST_URL}/agentmemory/observe`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
hookType: "notification",
|
||||
sessionId,
|
||||
project: resolveProject(data.cwd),
|
||||
cwd: data.cwd || process.cwd(),
|
||||
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
data: {
|
||||
notification_type: notificationType,
|
||||
title: data.title,
|
||||
message: data.message
|
||||
}
|
||||
}),
|
||||
signal: AbortSignal.timeout(2e3)
|
||||
}).catch(() => {});
|
||||
setTimeout(() => process.exit(0), 500).unref();
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=notification.mjs.map
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
//#region src/hooks/post-commit.ts
|
||||
const exec = promisify(execFile);
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
const TIMEOUT_MS = 1500;
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function git(args, cwd) {
|
||||
try {
|
||||
const { stdout } = await exec("git", args, {
|
||||
cwd,
|
||||
timeout: 1500
|
||||
});
|
||||
return stdout.trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data = {};
|
||||
if (input.trim()) try {
|
||||
data = JSON.parse(input);
|
||||
} catch {}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const cwd = data.cwd || process.env["AGENTMEMORY_CWD"] || process.cwd();
|
||||
const sessionId = data.session_id || process.env["AGENTMEMORY_SESSION_ID"] || void 0;
|
||||
const sha = process.env["AGENTMEMORY_COMMIT_SHA"] || await git(["rev-parse", "HEAD"], cwd);
|
||||
if (!sha) return;
|
||||
const branch = await git([
|
||||
"rev-parse",
|
||||
"--abbrev-ref",
|
||||
"HEAD"
|
||||
], cwd);
|
||||
const repo = await git([
|
||||
"config",
|
||||
"--get",
|
||||
"remote.origin.url"
|
||||
], cwd);
|
||||
const message = await git([
|
||||
"log",
|
||||
"-1",
|
||||
"--pretty=%B",
|
||||
sha
|
||||
], cwd);
|
||||
const author = await git([
|
||||
"log",
|
||||
"-1",
|
||||
"--pretty=%an <%ae>",
|
||||
sha
|
||||
], cwd);
|
||||
const authoredAt = await git([
|
||||
"log",
|
||||
"-1",
|
||||
"--pretty=%aI",
|
||||
sha
|
||||
], cwd);
|
||||
const filesRaw = await git([
|
||||
"diff-tree",
|
||||
"--no-commit-id",
|
||||
"--name-only",
|
||||
"-r",
|
||||
sha
|
||||
], cwd);
|
||||
const files = filesRaw ? filesRaw.split("\n").filter(Boolean) : void 0;
|
||||
const body = {
|
||||
sessionId,
|
||||
sha,
|
||||
branch: branch || void 0,
|
||||
repo: repo || void 0,
|
||||
message: message || void 0,
|
||||
author: author || void 0,
|
||||
authoredAt: authoredAt || void 0,
|
||||
files
|
||||
};
|
||||
try {
|
||||
await fetch(`${REST_URL}/agentmemory/session/commit`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS)
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=post-commit.mjs.map
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from "node:child_process";
|
||||
import { basename } from "node:path";
|
||||
|
||||
//#region src/hooks/_project.ts
|
||||
function resolveProject(cwd) {
|
||||
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
|
||||
if (explicit && explicit.trim()) return explicit.trim();
|
||||
const dir = cwd && cwd.trim() ? cwd : process.cwd();
|
||||
try {
|
||||
const top = execSync("git rev-parse --show-toplevel", {
|
||||
cwd: dir,
|
||||
stdio: [
|
||||
"ignore",
|
||||
"pipe",
|
||||
"ignore"
|
||||
],
|
||||
timeout: 500
|
||||
}).toString().trim();
|
||||
if (top) return basename(top);
|
||||
} catch {}
|
||||
return basename(dir);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region src/hooks/post-tool-failure.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
if (data.is_interrupt || data.isInterrupt) return;
|
||||
const sessionId = data.session_id || data.sessionId || "unknown";
|
||||
const toolName = data.tool_name ?? data.toolName;
|
||||
const toolInput = data.tool_input ?? data.toolArgs;
|
||||
const error = data.error ?? data.errorMessage;
|
||||
fetch(`${REST_URL}/agentmemory/observe`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
hookType: "post_tool_failure",
|
||||
sessionId,
|
||||
project: resolveProject(data.cwd),
|
||||
cwd: data.cwd || process.cwd(),
|
||||
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
data: {
|
||||
tool_name: toolName,
|
||||
tool_input: typeof toolInput === "string" ? toolInput.slice(0, 4e3) : JSON.stringify(toolInput ?? "").slice(0, 4e3),
|
||||
error: typeof error === "string" ? error.slice(0, 4e3) : JSON.stringify(error ?? "").slice(0, 4e3)
|
||||
}
|
||||
}),
|
||||
signal: AbortSignal.timeout(3e3)
|
||||
}).catch(() => {});
|
||||
setTimeout(() => process.exit(0), 500).unref();
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=post-tool-failure.mjs.map
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from "node:child_process";
|
||||
import { basename } from "node:path";
|
||||
|
||||
//#region src/hooks/_project.ts
|
||||
function resolveProject(cwd) {
|
||||
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
|
||||
if (explicit && explicit.trim()) return explicit.trim();
|
||||
const dir = cwd && cwd.trim() ? cwd : process.cwd();
|
||||
try {
|
||||
const top = execSync("git rev-parse --show-toplevel", {
|
||||
cwd: dir,
|
||||
stdio: [
|
||||
"ignore",
|
||||
"pipe",
|
||||
"ignore"
|
||||
],
|
||||
timeout: 500
|
||||
}).toString().trim();
|
||||
if (top) return basename(top);
|
||||
} catch {}
|
||||
return basename(dir);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region src/hooks/post-tool-use.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const sessionId = data.session_id || data.sessionId || "unknown";
|
||||
const toolName = data.tool_name ?? data.toolName;
|
||||
const toolInput = data.tool_input ?? data.toolArgs;
|
||||
const { imageData, cleanOutput } = extractImageData(toolOutput(data));
|
||||
fetch(`${REST_URL}/agentmemory/observe`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
hookType: "post_tool_use",
|
||||
sessionId,
|
||||
project: resolveProject(data.cwd),
|
||||
cwd: data.cwd || process.cwd(),
|
||||
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
data: {
|
||||
tool_name: toolName,
|
||||
tool_input: toolInput,
|
||||
tool_output: truncate(cleanOutput, 8e3),
|
||||
...imageData ? { image_data: imageData } : {}
|
||||
}
|
||||
}),
|
||||
signal: AbortSignal.timeout(3e3)
|
||||
}).catch(() => {});
|
||||
setTimeout(() => process.exit(0), 500).unref();
|
||||
}
|
||||
function toolOutput(data) {
|
||||
if (data.tool_response !== void 0) return data.tool_response;
|
||||
if (data.tool_output !== void 0) return data.tool_output;
|
||||
const result = data.tool_result ?? data.toolResult;
|
||||
if (typeof result === "object" && result !== null) {
|
||||
const obj = result;
|
||||
return obj.text_result_for_llm ?? obj.textResultForLlm ?? result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function isBase64Image(val) {
|
||||
return typeof val === "string" && (val.startsWith("data:image/") || val.startsWith("iVBORw0KGgo") || val.startsWith("/9j/"));
|
||||
}
|
||||
function extractImageData(output) {
|
||||
if (isBase64Image(output)) return {
|
||||
imageData: output,
|
||||
cleanOutput: "[image data extracted]"
|
||||
};
|
||||
if (typeof output === "object" && output !== null && !Array.isArray(output)) {
|
||||
const obj = output;
|
||||
let imageData;
|
||||
const clean = {};
|
||||
for (const [key, val] of Object.entries(obj)) if (!imageData && isBase64Image(val)) {
|
||||
imageData = val;
|
||||
clean[key] = "[image data extracted]";
|
||||
} else clean[key] = val;
|
||||
return {
|
||||
imageData,
|
||||
cleanOutput: clean
|
||||
};
|
||||
}
|
||||
return {
|
||||
imageData: void 0,
|
||||
cleanOutput: output
|
||||
};
|
||||
}
|
||||
function truncate(value, max) {
|
||||
if (typeof value === "string" && value.length > max) return value.slice(0, max) + "\n[...truncated]";
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const str = JSON.stringify(value);
|
||||
if (str.length > max) return str.slice(0, max) + "...[truncated]";
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=post-tool-use.mjs.map
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from "node:child_process";
|
||||
import { basename } from "node:path";
|
||||
|
||||
//#region src/hooks/_project.ts
|
||||
function resolveProject(cwd) {
|
||||
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
|
||||
if (explicit && explicit.trim()) return explicit.trim();
|
||||
const dir = cwd && cwd.trim() ? cwd : process.cwd();
|
||||
try {
|
||||
const top = execSync("git rev-parse --show-toplevel", {
|
||||
cwd: dir,
|
||||
stdio: [
|
||||
"ignore",
|
||||
"pipe",
|
||||
"ignore"
|
||||
],
|
||||
timeout: 500
|
||||
}).toString().trim();
|
||||
if (top) return basename(top);
|
||||
} catch {}
|
||||
return basename(dir);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region src/hooks/pre-compact.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const sessionId = data.session_id || data.sessionId || "unknown";
|
||||
const project = resolveProject(data.cwd);
|
||||
if (process.env["CLAUDE_MEMORY_BRIDGE"] === "true") try {
|
||||
await fetch(`${REST_URL}/agentmemory/claude-bridge/sync`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({}),
|
||||
signal: AbortSignal.timeout(5e3)
|
||||
});
|
||||
} catch {}
|
||||
try {
|
||||
const res = await fetch(`${REST_URL}/agentmemory/context`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
project,
|
||||
budget: 1500
|
||||
}),
|
||||
signal: AbortSignal.timeout(5e3)
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
if (result.context) process.stdout.write(result.context);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=pre-compact.mjs.map
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
//#region src/hooks/pre-tool-use.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const INJECT_CONTEXT = process.env["AGENTMEMORY_INJECT_CONTEXT"] === "true";
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
if (!INJECT_CONTEXT) return;
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const toolName = typeof data.tool_name === "string" ? data.tool_name : typeof data.toolName === "string" ? data.toolName : void 0;
|
||||
if (!toolName) return;
|
||||
const normalizedToolName = toolName.toLowerCase();
|
||||
if (![
|
||||
"edit",
|
||||
"write",
|
||||
"create",
|
||||
"read",
|
||||
"view",
|
||||
"glob",
|
||||
"grep"
|
||||
].includes(normalizedToolName)) return;
|
||||
const rawToolInput = data.tool_input ?? data.toolArgs;
|
||||
const toolInput = typeof rawToolInput === "object" && rawToolInput !== null && !Array.isArray(rawToolInput) ? rawToolInput : {};
|
||||
const files = [];
|
||||
const fileKeys = normalizedToolName === "grep" ? ["path", "file"] : [
|
||||
"file_path",
|
||||
"path",
|
||||
"file",
|
||||
"pattern"
|
||||
];
|
||||
for (const key of fileKeys) {
|
||||
const val = toolInput[key];
|
||||
if (typeof val === "string" && val.length > 0) files.push(val);
|
||||
}
|
||||
if (files.length === 0) return;
|
||||
const terms = [];
|
||||
if (normalizedToolName === "grep" || normalizedToolName === "glob") {
|
||||
const pattern = toolInput["pattern"];
|
||||
if (typeof pattern === "string" && pattern.length > 0) terms.push(pattern);
|
||||
}
|
||||
const rawSessionId = data.session_id || data.sessionId;
|
||||
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : "unknown";
|
||||
const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
|
||||
try {
|
||||
const res = await fetch(`${REST_URL}/agentmemory/enrich`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
files,
|
||||
terms,
|
||||
toolName,
|
||||
...project !== void 0 && { project }
|
||||
}),
|
||||
signal: AbortSignal.timeout(2e3)
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
if (result.context) process.stdout.write(result.context);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=pre-tool-use.mjs.map
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from "node:child_process";
|
||||
import { basename } from "node:path";
|
||||
|
||||
//#region src/hooks/_project.ts
|
||||
function resolveProject(cwd) {
|
||||
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
|
||||
if (explicit && explicit.trim()) return explicit.trim();
|
||||
const dir = cwd && cwd.trim() ? cwd : process.cwd();
|
||||
try {
|
||||
const top = execSync("git rev-parse --show-toplevel", {
|
||||
cwd: dir,
|
||||
stdio: [
|
||||
"ignore",
|
||||
"pipe",
|
||||
"ignore"
|
||||
],
|
||||
timeout: 500
|
||||
}).toString().trim();
|
||||
if (top) return basename(top);
|
||||
} catch {}
|
||||
return basename(dir);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region src/hooks/prompt-submit.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const sessionId = data.session_id || data.sessionId || "unknown";
|
||||
fetch(`${REST_URL}/agentmemory/observe`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
hookType: "prompt_submit",
|
||||
sessionId,
|
||||
project: resolveProject(data.cwd),
|
||||
cwd: data.cwd || process.cwd(),
|
||||
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
data: { prompt: data.prompt ?? data.userPrompt }
|
||||
}),
|
||||
signal: AbortSignal.timeout(3e3)
|
||||
}).catch(() => {});
|
||||
setTimeout(() => process.exit(0), 500).unref();
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=prompt-submit.mjs.map
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env node
|
||||
//#region src/hooks/session-end.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const sessionId = data.session_id || data.sessionId || "unknown";
|
||||
fetch(`${REST_URL}/agentmemory/session/end`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({ sessionId }),
|
||||
signal: AbortSignal.timeout(3e4)
|
||||
}).catch(() => {});
|
||||
if (process.env["CONSOLIDATION_ENABLED"] === "true") {
|
||||
fetch(`${REST_URL}/agentmemory/crystals/auto`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({ olderThanDays: 0 }),
|
||||
signal: AbortSignal.timeout(6e4)
|
||||
}).catch(() => {});
|
||||
fetch(`${REST_URL}/agentmemory/consolidate-pipeline`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
tier: "all",
|
||||
force: true
|
||||
}),
|
||||
signal: AbortSignal.timeout(12e4)
|
||||
}).catch(() => {});
|
||||
}
|
||||
if (process.env["CLAUDE_MEMORY_BRIDGE"] === "true") fetch(`${REST_URL}/agentmemory/claude-bridge/sync`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
signal: AbortSignal.timeout(3e4)
|
||||
}).catch(() => {});
|
||||
setTimeout(() => process.exit(0), 1500).unref();
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=session-end.mjs.map
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from "node:child_process";
|
||||
import { basename } from "node:path";
|
||||
|
||||
//#region src/hooks/_project.ts
|
||||
function resolveProject(cwd) {
|
||||
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
|
||||
if (explicit && explicit.trim()) return explicit.trim();
|
||||
const dir = cwd && cwd.trim() ? cwd : process.cwd();
|
||||
try {
|
||||
const top = execSync("git rev-parse --show-toplevel", {
|
||||
cwd: dir,
|
||||
stdio: [
|
||||
"ignore",
|
||||
"pipe",
|
||||
"ignore"
|
||||
],
|
||||
timeout: 500
|
||||
}).toString().trim();
|
||||
if (top) return basename(top);
|
||||
} catch {}
|
||||
return basename(dir);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region src/hooks/session-start.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const INJECT_CONTEXT = process.env["AGENTMEMORY_INJECT_CONTEXT"] === "true";
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
const INJECT_TIMEOUT_MS = 1500;
|
||||
const REGISTER_TIMEOUT_MS = 800;
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const sessionId = data.session_id || data.sessionId || `ses_${Date.now().toString(36)}`;
|
||||
const cwd = data.cwd || process.cwd();
|
||||
const project = resolveProject(data.cwd);
|
||||
const url = `${REST_URL}/agentmemory/session/start`;
|
||||
const init = {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
project,
|
||||
cwd
|
||||
})
|
||||
};
|
||||
if (!INJECT_CONTEXT) {
|
||||
fetch(url, {
|
||||
...init,
|
||||
signal: AbortSignal.timeout(REGISTER_TIMEOUT_MS)
|
||||
}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
...init,
|
||||
signal: AbortSignal.timeout(INJECT_TIMEOUT_MS)
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
if (result.context) process.stdout.write(result.context);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=session-start.mjs.map
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
//#region src/hooks/stop.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const sessionId = data.session_id || data.sessionId || "unknown";
|
||||
fetch(`${REST_URL}/agentmemory/summarize`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({ sessionId }),
|
||||
signal: AbortSignal.timeout(12e4)
|
||||
}).catch(() => {});
|
||||
fetch(`${REST_URL}/agentmemory/session/end`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({ sessionId }),
|
||||
signal: AbortSignal.timeout(5e3)
|
||||
}).catch(() => {});
|
||||
setTimeout(() => process.exit(0), 1500).unref();
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=stop.mjs.map
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from "node:child_process";
|
||||
import { basename } from "node:path";
|
||||
|
||||
//#region src/hooks/_project.ts
|
||||
function resolveProject(cwd) {
|
||||
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
|
||||
if (explicit && explicit.trim()) return explicit.trim();
|
||||
const dir = cwd && cwd.trim() ? cwd : process.cwd();
|
||||
try {
|
||||
const top = execSync("git rev-parse --show-toplevel", {
|
||||
cwd: dir,
|
||||
stdio: [
|
||||
"ignore",
|
||||
"pipe",
|
||||
"ignore"
|
||||
],
|
||||
timeout: 500
|
||||
}).toString().trim();
|
||||
if (top) return basename(top);
|
||||
} catch {}
|
||||
return basename(dir);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region src/hooks/subagent-start.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
const TIMEOUT_MS = 800;
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const sessionId = data.session_id || data.sessionId || "unknown";
|
||||
const agentId = data.agent_id || data.agentName;
|
||||
const agentType = data.agent_type || data.agentDisplayName || data.agentName;
|
||||
fetch(`${REST_URL}/agentmemory/observe`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
hookType: "subagent_start",
|
||||
sessionId,
|
||||
project: resolveProject(data.cwd),
|
||||
cwd: data.cwd || process.cwd(),
|
||||
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
data: {
|
||||
agent_id: agentId,
|
||||
agent_type: agentType
|
||||
}
|
||||
}),
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS)
|
||||
}).catch(() => {});
|
||||
setTimeout(() => process.exit(0), 500).unref();
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=subagent-start.mjs.map
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from "node:child_process";
|
||||
import { basename } from "node:path";
|
||||
|
||||
//#region src/hooks/_project.ts
|
||||
function resolveProject(cwd) {
|
||||
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
|
||||
if (explicit && explicit.trim()) return explicit.trim();
|
||||
const dir = cwd && cwd.trim() ? cwd : process.cwd();
|
||||
try {
|
||||
const top = execSync("git rev-parse --show-toplevel", {
|
||||
cwd: dir,
|
||||
stdio: [
|
||||
"ignore",
|
||||
"pipe",
|
||||
"ignore"
|
||||
],
|
||||
timeout: 500
|
||||
}).toString().trim();
|
||||
if (top) return basename(top);
|
||||
} catch {}
|
||||
return basename(dir);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region src/hooks/subagent-stop.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const sessionId = data.session_id || data.sessionId || "unknown";
|
||||
const agentId = data.agent_id || data.agentName;
|
||||
const agentType = data.agent_type || data.agentDisplayName || data.agentName;
|
||||
const lastMsg = typeof data.last_assistant_message === "string" ? data.last_assistant_message.slice(0, 4e3) : "";
|
||||
fetch(`${REST_URL}/agentmemory/observe`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
hookType: "subagent_stop",
|
||||
sessionId,
|
||||
project: resolveProject(data.cwd),
|
||||
cwd: data.cwd || process.cwd(),
|
||||
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
data: {
|
||||
agent_id: agentId,
|
||||
agent_type: agentType,
|
||||
last_message: lastMsg
|
||||
}
|
||||
}),
|
||||
signal: AbortSignal.timeout(2e3)
|
||||
}).catch(() => {});
|
||||
setTimeout(() => process.exit(0), 500).unref();
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=subagent-stop.mjs.map
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from "node:child_process";
|
||||
import { basename } from "node:path";
|
||||
|
||||
//#region src/hooks/_project.ts
|
||||
function resolveProject(cwd) {
|
||||
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
|
||||
if (explicit && explicit.trim()) return explicit.trim();
|
||||
const dir = cwd && cwd.trim() ? cwd : process.cwd();
|
||||
try {
|
||||
const top = execSync("git rev-parse --show-toplevel", {
|
||||
cwd: dir,
|
||||
stdio: [
|
||||
"ignore",
|
||||
"pipe",
|
||||
"ignore"
|
||||
],
|
||||
timeout: 500
|
||||
}).toString().trim();
|
||||
if (top) return basename(top);
|
||||
} catch {}
|
||||
return basename(dir);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
//#region src/hooks/task-completed.ts
|
||||
function isSdkChildContext(payload) {
|
||||
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
return payload.entrypoint === "sdk-ts";
|
||||
}
|
||||
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
|
||||
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
|
||||
function authHeaders() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
|
||||
return h;
|
||||
}
|
||||
async function main() {
|
||||
let input = "";
|
||||
for await (const chunk of process.stdin) input += chunk;
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(input);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (isSdkChildContext(data)) return;
|
||||
const sessionId = data.session_id || "unknown";
|
||||
fetch(`${REST_URL}/agentmemory/observe`, {
|
||||
method: "POST",
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
hookType: "task_completed",
|
||||
sessionId,
|
||||
project: resolveProject(data.cwd),
|
||||
cwd: data.cwd || process.cwd(),
|
||||
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
data: {
|
||||
task_id: data.task_id,
|
||||
task_subject: data.task_subject,
|
||||
task_description: typeof data.task_description === "string" ? data.task_description.slice(0, 2e3) : "",
|
||||
teammate_name: data.teammate_name,
|
||||
team_name: data.team_name
|
||||
}
|
||||
}),
|
||||
signal: AbortSignal.timeout(2e3)
|
||||
}).catch(() => {});
|
||||
setTimeout(() => process.exit(0), 500).unref();
|
||||
}
|
||||
main();
|
||||
|
||||
//#endregion
|
||||
export { };
|
||||
//# sourceMappingURL=task-completed.mjs.map
|
||||
@@ -0,0 +1,38 @@
|
||||
# Troubleshooting agentmemory skills
|
||||
|
||||
Shared recovery steps for all user-invocable agentmemory skills. Each skill's
|
||||
Troubleshooting section points here instead of duplicating the block.
|
||||
|
||||
## "MCP tool not available"
|
||||
|
||||
If a `memory_*` MCP tool does not appear, the stdio MCP shim never started.
|
||||
Walk these in order:
|
||||
|
||||
1. Run `/plugin list` in the host and confirm `agentmemory` shows as enabled.
|
||||
2. Restart the host. The plugin's `.mcp.json` is only read on startup, so a
|
||||
freshly installed or re-enabled plugin will not register tools mid-session.
|
||||
3. Check `/mcp` and confirm the `agentmemory` server shows a live connection.
|
||||
|
||||
## REST fallback
|
||||
|
||||
When the MCP tools stay unavailable but the daemon is running, call the REST
|
||||
API directly:
|
||||
|
||||
1. Set `AGENTMEMORY_URL` to the daemon base URL (default `http://localhost:3111`).
|
||||
2. Add `Authorization: Bearer $AGENTMEMORY_SECRET` ONLY when `AGENTMEMORY_SECRET`
|
||||
is set. The default localhost daemon is open and rejects a stray header.
|
||||
|
||||
Endpoint map by skill:
|
||||
|
||||
| Skill | REST call |
|
||||
| --------------- | --------------------------------------------------------------- |
|
||||
| remember | `POST /agentmemory/remember` |
|
||||
| recall | `POST /agentmemory/smart-search` |
|
||||
| recap | `GET /agentmemory/sessions` + `POST /agentmemory/smart-search` |
|
||||
| handoff | `GET /agentmemory/sessions` + `POST /agentmemory/smart-search` |
|
||||
| session-history | `GET /agentmemory/sessions` |
|
||||
| commit-context | `GET /agentmemory/session/by-commit?sha=<sha>` |
|
||||
| commit-history | `GET /agentmemory/commits` (URL-encode every query param) |
|
||||
|
||||
The daemon reads `.mcp.json` on startup only, so any port or auth change needs a
|
||||
restart before either transport sees it.
|
||||
@@ -0,0 +1,28 @@
|
||||
# agentmemory connect adapters reference
|
||||
|
||||
Generated from `src/cli/connect/index.ts`. Do not edit the block below by hand; run `npm run skills:gen` after adding or removing an adapter.
|
||||
|
||||
<!-- AUTOGEN:agents START - generated by scripts/skills/generate.ts, do not edit by hand -->
|
||||
`agentmemory connect <agent>` wires the memory server into a host agent. 18 adapters:
|
||||
|
||||
| Agent | Name | Protocol |
|
||||
| --- | --- | --- |
|
||||
| Antigravity | `antigravity` | Using MCP via mcp_config.json. Antigravity replaces Gemini CLI (sunset 2026-06-18). |
|
||||
| Claude Code | `claude-code` | Using MCP. Hooks are also available, see https://github.com/rohitg00/agentmemory#claude-code-one-block-paste-it. |
|
||||
| Cline | `cline` | Using MCP via ~/.cline/mcp.json (CLI). VS Code users: add the same block via Cline Settings → MCP Servers → Edit JSON. |
|
||||
| Codex CLI | `codex` | Using MCP. Hooks ship via the Codex plugin; on Codex Desktop, also pass --with-hooks to install the global hooks.json workaround for openai/codex#16430. |
|
||||
| Continue | `continue` | Using MCP via ~/.continue/config.yaml (preferred) or config.json (legacy, only when no yaml). |
|
||||
| GitHub Copilot CLI | `copilot-cli` | Using MCP. Install the plugin too for full hooks/skills coverage. |
|
||||
| Cursor | `cursor` | Using MCP (the only protocol Cursor speaks). Memory bridge runs at :3111 underneath. |
|
||||
| Droid (Factory.ai) | `droid` | Using MCP via ~/.factory/mcp.json. The `/mcp` slash command inside droid lists configured servers. |
|
||||
| Gemini CLI | `gemini-cli` | Using MCP (the only protocol Gemini CLI speaks). Memory bridge runs at :3111 underneath. |
|
||||
| Hermes Agent | `hermes` | Using MCP. Hooks are also available, see https://github.com/rohitg00/agentmemory/tree/main/integrations/hermes. |
|
||||
| Kiro | `kiro` | Using MCP via ~/.kiro/settings/mcp.json (user-level). Workspace overrides live in .kiro/settings/mcp.json. |
|
||||
| OpenClaw | `openclaw` | Using MCP. Hooks are also available, see https://github.com/rohitg00/agentmemory/tree/main/integrations/openclaw. |
|
||||
| OpenCode | `opencode` | Using MCP via ~/.config/opencode/opencode.json (top-level `mcp` key). For full auto-capture, also install the bundled plugin in plugin/opencode/. |
|
||||
| OpenHuman | `openhuman` | Using native hooks (REST API at :3111). MCP not required. |
|
||||
| pi | `pi` | Using native hooks (REST API at :3111). MCP not required. |
|
||||
| Qwen Code | `qwen` | Using MCP via ~/.qwen/settings.json. Qwen Code's hook system can also be wired separately, see docs. |
|
||||
| Warp | `warp` | Using MCP via ~/.warp/.mcp.json. Skills auto-discover from .claude/skills/ if the Claude Code plugin is also installed. |
|
||||
| Zed | `zed` | Using MCP via ~/.config/zed/settings.json (key: context_servers). |
|
||||
<!-- AUTOGEN:agents END -->
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: agentmemory-agents
|
||||
description: How agentmemory wires into host coding agents via the connect command. Use when installing agentmemory into a specific agent, when asked which agents are supported, or when a connect adapter writes the wrong config path.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
`agentmemory connect <agent>` merges the memory server into a host agent's config and preserves any existing servers. REST is the underlying protocol; for MCP-only hosts the adapter wires the stdio MCP bridge.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
agentmemory connect claude-code # or cursor, codex, gemini-cli, ...
|
||||
```
|
||||
|
||||
After wiring, restart the host or run its MCP reload (for example `/mcp` in Claude Code) so it picks up the server. Then confirm the agent lists agentmemory's tools.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Detect the calling agent. If unknown, default to `claude-code`.
|
||||
2. Run `agentmemory connect <name>` using a name from the table in REFERENCE.md.
|
||||
3. Verify: the host should show the full tool set with a server running. Only 7 tools means the MCP shim could not reach a server (see ../_shared/TROUBLESHOOTING.md).
|
||||
|
||||
## Notes
|
||||
|
||||
- The action skills (remember, recall, and the rest) are installed separately with `npx skills add rohitg00/agentmemory`. `connect` makes tools available; skills teach the agent when to use them.
|
||||
- Windows: use WSL2. Native Windows runs the server but `connect` is not supported there.
|
||||
|
||||
## See also
|
||||
|
||||
- agentmemory-mcp-tools, agentmemory-rest-api, agentmemory-hooks.
|
||||
|
||||
## Reference
|
||||
|
||||
The full adapter list with display names and protocol notes lives in REFERENCE.md, generated from `src/cli/connect/`.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: agentmemory-architecture
|
||||
description: How agentmemory is built, the iii engine primitives it runs on, its storage model, ports, and the viewer. Use when reasoning about how memory is stored or retrieved end to end, when extending the system, or when answering how agentmemory works under the hood.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
agentmemory is a memory server for coding agents. It runs locally, captures observations, indexes them for hybrid retrieval, and serves them back over REST and MCP. It is built on the iii engine.
|
||||
|
||||
## iii primitives
|
||||
|
||||
Everything is a function, a trigger, or worker state on the iii engine. There is no separate plugin system; the worker registers functions (`mem::*`) and HTTP triggers (`api::*`) and the engine routes calls. agentmemory does not bypass iii; new capability is a new function plus a trigger.
|
||||
|
||||
## Retrieval model
|
||||
|
||||
Recall is hybrid: BM25 keyword search plus vector similarity plus graph expansion over linked concepts. The default install needs no API key because embeddings run on-device and BM25 needs none. An LLM provider only adds richer summaries and auto-injection, both opt-in.
|
||||
|
||||
## Storage and lifecycle
|
||||
|
||||
Memories carry content, concepts, files, importance, and timestamps, grouped into sessions and optionally linked to commits. A lifecycle of capture, compress, consolidate, and forget keeps the store useful over time rather than letting it grow unbounded.
|
||||
|
||||
## Ports
|
||||
|
||||
REST is the anchor at 3111. Streams = N+1 (3112), viewer = N+2 (3113), engine = N+46023 (49134). `--instance N` shifts the whole block by N*100.
|
||||
|
||||
## Viewer
|
||||
|
||||
A real-time web viewer at `http://localhost:3113` shows memory building as sessions run. Useful for demos and for confirming capture is working.
|
||||
|
||||
## See also
|
||||
|
||||
- agentmemory-mcp-tools and agentmemory-rest-api for the surfaces.
|
||||
- agentmemory-hooks for automatic capture.
|
||||
- agentmemory-config for ports and feature flags.
|
||||
@@ -0,0 +1,42 @@
|
||||
# agentmemory configuration reference
|
||||
|
||||
Generated by scanning `src/` for `AGENTMEMORY_*` usage. Do not edit the block below by hand; run `npm run skills:gen` after adding or removing a variable. Internal markers ending in two underscores are excluded.
|
||||
|
||||
<!-- AUTOGEN:env START - generated by scripts/skills/generate.ts, do not edit by hand -->
|
||||
Configuration is read from the environment and from `~/.agentmemory/.env` (no `export` prefix). 34 recognized variables:
|
||||
|
||||
- `AGENTMEMORY_AGENT_SCOPE`
|
||||
- `AGENTMEMORY_ALLOW_AGENT_SDK`
|
||||
- `AGENTMEMORY_AUTO_COMPRESS`
|
||||
- `AGENTMEMORY_COMMIT_SHA`
|
||||
- `AGENTMEMORY_COPILOT_MCP_BLOCK`
|
||||
- `AGENTMEMORY_CWD`
|
||||
- `AGENTMEMORY_DEBUG`
|
||||
- `AGENTMEMORY_DROP_STALE_INDEX`
|
||||
- `AGENTMEMORY_EXPORT_ROOT`
|
||||
- `AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS`
|
||||
- `AGENTMEMORY_FORCE_PROXY`
|
||||
- `AGENTMEMORY_GRAPH_WEIGHT`
|
||||
- `AGENTMEMORY_III_CONFIG`
|
||||
- `AGENTMEMORY_III_VERSION`
|
||||
- `AGENTMEMORY_IMAGE_EMBEDDINGS`
|
||||
- `AGENTMEMORY_IMAGE_STORE_MAX_BYTES`
|
||||
- `AGENTMEMORY_INJECT_CONTEXT`
|
||||
- `AGENTMEMORY_LLM_TIMEOUT_MS`
|
||||
- `AGENTMEMORY_MCP_BLOCK`
|
||||
- `AGENTMEMORY_PROBE_TIMEOUT_MS`
|
||||
- `AGENTMEMORY_PROJECT_NAME`
|
||||
- `AGENTMEMORY_PROVIDER`
|
||||
- `AGENTMEMORY_REFLECT`
|
||||
- `AGENTMEMORY_SDK_CHILD`
|
||||
- `AGENTMEMORY_SECRET`
|
||||
- `AGENTMEMORY_SESSION_ID`
|
||||
- `AGENTMEMORY_SLOTS`
|
||||
- `AGENTMEMORY_SUPPRESS_COST_WARNING`
|
||||
- `AGENTMEMORY_TOOLS`
|
||||
- `AGENTMEMORY_URL`
|
||||
- `AGENTMEMORY_USE_DOCKER`
|
||||
- `AGENTMEMORY_VERBOSE`
|
||||
- `AGENTMEMORY_VIEWER_HOST`
|
||||
- `AGENTMEMORY_VIEWER_URL`
|
||||
<!-- AUTOGEN:env END -->
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: agentmemory-config
|
||||
description: agentmemory configuration, environment variables, ports, and feature flags. Use when enabling a feature, changing ports, setting an API key, configuring auth, or explaining why a feature is off by default.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
agentmemory reads configuration from the environment and from `~/.agentmemory/.env` (one `KEY=value` per line, no `export` prefix). Restart the server after changing it.
|
||||
|
||||
## Quick start
|
||||
|
||||
Enable richer memory and set a provider key in `~/.agentmemory/.env`:
|
||||
|
||||
```env
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
AGENTMEMORY_AUTO_COMPRESS=true
|
||||
AGENTMEMORY_INJECT_CONTEXT=true
|
||||
```
|
||||
|
||||
## Defaults worth knowing
|
||||
|
||||
- No API key is required. Without one, agentmemory runs zero-LLM with BM25 plus local embeddings.
|
||||
- Token-spending features ship OFF on purpose: `AGENTMEMORY_AUTO_COMPRESS` (LLM summaries) and `AGENTMEMORY_INJECT_CONTEXT` (auto context injection) both cost tokens proportional to tool-use frequency.
|
||||
- Tool visibility: `AGENTMEMORY_TOOLS=all` (default) or `core` for the lean set.
|
||||
- Auth: set `AGENTMEMORY_SECRET` to require `Authorization: Bearer` on the REST API.
|
||||
|
||||
## Ports
|
||||
|
||||
REST is the anchor at 3111. Streams = N+1 (3112), viewer = N+2 (3113), engine = N+46023 (49134). Relocate the whole block with `--port <N>` or `--instance <N>`.
|
||||
|
||||
## See also
|
||||
|
||||
- agentmemory-rest-api for how the secret is used.
|
||||
- agentmemory-architecture for the port quartet rationale.
|
||||
|
||||
## Reference
|
||||
|
||||
The full recognized-variable list lives in REFERENCE.md, generated by scanning `src/`.
|
||||
@@ -0,0 +1,20 @@
|
||||
# agentmemory hooks reference
|
||||
|
||||
Generated from `plugin/hooks/hooks.json`. Do not edit the block below by hand; run `npm run skills:gen` after changing the hook registration.
|
||||
|
||||
<!-- AUTOGEN:hooks START - generated by scripts/skills/generate.ts, do not edit by hand -->
|
||||
The Claude Code plugin registers hooks on 12 lifecycle events to capture observations automatically:
|
||||
|
||||
- `Notification`
|
||||
- `PostToolUse`
|
||||
- `PostToolUseFailure`
|
||||
- `PreCompact`
|
||||
- `PreToolUse`
|
||||
- `SessionEnd`
|
||||
- `SessionStart`
|
||||
- `Stop`
|
||||
- `SubagentStart`
|
||||
- `SubagentStop`
|
||||
- `TaskCompleted`
|
||||
- `UserPromptSubmit`
|
||||
<!-- AUTOGEN:hooks END -->
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: agentmemory-hooks
|
||||
description: The agentmemory plugin hooks that capture observations automatically across the agent session lifecycle. Use when explaining how memory gets captured without manual saves, when debugging missing observations, or when tuning what gets recorded.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
The Claude Code plugin registers lifecycle hooks so memory is captured automatically. You do not have to call `memory_save` for routine work; the hooks observe tool use, prompts, and session boundaries and write observations for you.
|
||||
|
||||
## Quick start
|
||||
|
||||
Install the plugin and the hooks register themselves:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add rohitg00/agentmemory
|
||||
/plugin install agentmemory
|
||||
```
|
||||
|
||||
Watch observations land live at `http://localhost:3113`.
|
||||
|
||||
## What the hooks do
|
||||
|
||||
- Session start and end frame each unit of work and let `handoff` resume it.
|
||||
- Tool-use hooks capture what changed and why, the raw material for `recall` and `recap`.
|
||||
- Prompt-submit captures intent. Pre-compact preserves context before the host trims it.
|
||||
- A post-commit hook links commits to sessions, which powers `commit-context` and `commit-history`.
|
||||
|
||||
## Important
|
||||
|
||||
- Capture is on by default and is zero-LLM. Turning observations into LLM summaries (`AGENTMEMORY_AUTO_COMPRESS`) and injecting them back into context (`AGENTMEMORY_INJECT_CONTEXT`) are separate opt-ins because they spend tokens.
|
||||
- If observations are missing, confirm the plugin is enabled and the server is running. See ../_shared/TROUBLESHOOTING.md.
|
||||
|
||||
## See also
|
||||
|
||||
- agentmemory-config for the capture and injection flags.
|
||||
- The handoff, recap, and session-history skills consume what these hooks record.
|
||||
|
||||
## Reference
|
||||
|
||||
The exact registered hook events live in REFERENCE.md, generated from `plugin/hooks/hooks.json`.
|
||||
@@ -0,0 +1,65 @@
|
||||
# agentmemory MCP tools reference
|
||||
|
||||
Generated from `src/mcp/tools-registry.ts`. Do not edit the block below by hand; run `npm run skills:gen` after changing the registry.
|
||||
|
||||
<!-- AUTOGEN:tools START - generated by scripts/skills/generate.ts, do not edit by hand -->
|
||||
agentmemory exposes 53 MCP tools. 8 are in the lean core set (`--tools core` or `AGENTMEMORY_TOOLS=core`); the rest load with `--tools all` (default).
|
||||
|
||||
| Tool | Core | Parameters | Purpose |
|
||||
| --- | --- | --- | --- |
|
||||
| `memory_action_create` | | `title`*: string, `description`: string, `priority`: number, `project`: string, `tags`: string, `parentId`: string, `requires`: string | Create an actionable work item with typed dependencies. Actions track what agents need to do and how work items relate to each other. |
|
||||
| `memory_action_update` | | `actionId`*: string, `status`: string, `result`: string, `priority`: number | Update an action's status, priority, or details. Set status to 'done' to complete it and unblock dependent actions. |
|
||||
| `memory_audit` | | `operation`: string, `limit`: number | View the audit trail of memory operations. |
|
||||
| `memory_checkpoint` | | `operation`*: string, `name`: string, `checkpointId`: string, `status`: string, `type`: string, `linkedActionIds`: string | Create or resolve an external checkpoint (CI result, approval, deploy status) that gates action progress. |
|
||||
| `memory_claude_bridge_sync` | | `direction`*: string | Sync memory state to/from Claude Code's native MEMORY.md file. |
|
||||
| `memory_commit_lookup` | | `sha`*: string | Look up the agent session(s) that produced a specific git commit, given its SHA. Returns the commit metadata and linked sessions. |
|
||||
| `memory_commits` | | `branch`: string, `repo`: string, `limit`: number | List recent commits linked to agent sessions, optionally filtered by branch or repo. |
|
||||
| `memory_compress_file` | | `filePath`*: string | Compress a markdown file to reduce token usage while preserving headings, URLs, and code blocks. Creates a .original.md backup before writing. |
|
||||
| `memory_consolidate` | yes | `tier`: string | Run the 4-tier memory consolidation pipeline (working -> episodic -> semantic -> procedural). |
|
||||
| `memory_crystallize` | | `actionIds`*: string, `project`: string, `sessionId`: string | Compress completed action chains into compact crystal digests using LLM summarization. Extracts narrative, key outcomes, files affected, and lessons. |
|
||||
| `memory_diagnose` | yes | `categories`: string | Run health checks across all subsystems (actions, leases, sentinels, sketches, signals, sessions, memories, mesh). Identifies stuck, orphaned, and inconsistent state. |
|
||||
| `memory_export` | | none | Export all memory data as JSON. |
|
||||
| `memory_facet_query` | | `matchAll`: string, `matchAny`: string, `targetType`: string | Query targets by facet tags with AND/OR logic. Find all actions tagged priority:urgent AND team:backend. |
|
||||
| `memory_facet_tag` | | `targetId`*: string, `targetType`*: string, `dimension`*: string, `value`*: string | Attach a structured tag (dimension:value) to an action, memory, or observation for multi-dimensional categorization. |
|
||||
| `memory_file_history` | | `files`*: string, `sessionId`: string | Get past observations about specific files. |
|
||||
| `memory_frontier` | | `project`: string, `agentId`: string, `limit`: number | Get all unblocked actions ranked by priority and urgency. Returns the frontier of actionable work with no unsatisfied dependencies. |
|
||||
| `memory_governance_delete` | | `memoryIds`*: string, `reason`: string | Delete specific memories with audit trail. |
|
||||
| `memory_graph_query` | | `startNodeId`: string, `nodeType`: string, `maxDepth`: number, `query`: string | Query the knowledge graph for entities and relationships. |
|
||||
| `memory_heal` | | `categories`: string, `dryRun`: string | Auto-fix all fixable issues found by diagnostics. Unblocks stuck actions, expires stale leases, cleans up orphaned data. |
|
||||
| `memory_insight_list` | | `project`: string, `minConfidence`: number, `limit`: number | List synthesized insights, higher-order observations derived from patterns across memories, lessons, and crystals. |
|
||||
| `memory_lease` | | `actionId`*: string, `agentId`*: string, `operation`*: string, `result`: string, `ttlMs`: number | Acquire, release, or renew an exclusive lease on an action. Prevents multiple agents from working on the same thing. |
|
||||
| `memory_lesson_recall` | | `query`*: string, `project`: string, `minConfidence`: number, `limit`: number | Search lessons by query. Returns lessons sorted by confidence and recency. Use to check what the agent has learned before making decisions. |
|
||||
| `memory_lesson_save` | yes | `content`*: string, `context`: string, `confidence`: number, `project`: string, `tags`: string | Save a lesson learned from this session. Lessons have confidence scores that strengthen when reinforced and decay when not used. Duplicate content auto-strengthens the existing lesson. |
|
||||
| `memory_mesh_sync` | | `peerId`: string, `direction`: string | Sync memories and actions with peer agentmemory instances for multi-agent collaboration. |
|
||||
| `memory_next` | | `project`: string, `agentId`: string | Get the single most important next action to work on. Combines dependency resolution, priority, and recency into a score. |
|
||||
| `memory_obsidian_export` | | `vaultDir`: string, `types`: string | Export memories, lessons, and crystals as Obsidian-compatible Markdown files with YAML frontmatter and wikilinks for graph view. |
|
||||
| `memory_patterns` | | `project`: string | Detect recurring patterns across sessions. |
|
||||
| `memory_profile` | | `project`*: string, `refresh`: string | User/project profile with top concepts and file patterns. |
|
||||
| `memory_recall` | yes | `query`*: string, `limit`: number, `format`: string, `token_budget`: number | Search past session observations for relevant context. Use when you need to recall what happened in previous sessions, find past decisions, or look up how a file was modified before. |
|
||||
| `memory_reflect` | yes | `project`: string, `maxClusters`: number | Traverse the knowledge graph, group related memories by concept clusters, and synthesize higher-order insights via LLM. Returns new and reinforced insights. |
|
||||
| `memory_relations` | | `memoryId`*: string, `maxHops`: number, `minConfidence`: number | Query the memory relationship graph. |
|
||||
| `memory_routine_run` | | `routineId`*: string, `project`: string, `initiatedBy`: string | Instantiate a frozen workflow routine, creating actions for each step with proper dependencies. |
|
||||
| `memory_save` | yes | `content`*: string, `type`: string, `concepts`: string, `files`: string, `project`: string | Explicitly save an important insight, decision, or pattern to long-term memory. |
|
||||
| `memory_sentinel_create` | | `name`*: string, `type`*: string, `config`: string, `linkedActionIds`: string, `expiresInMs`: number | Create an event-driven sentinel that watches for conditions (webhook, timer, threshold, pattern, approval) and auto-unblocks gated actions when triggered. |
|
||||
| `memory_sentinel_trigger` | | `sentinelId`*: string, `result`: string | Externally fire a sentinel, providing an optional result payload. Unblocks any gated actions. |
|
||||
| `memory_sessions` | yes | none | List recent sessions with their status and observation counts. |
|
||||
| `memory_signal_read` | | `agentId`*: string, `unreadOnly`: string, `threadId`: string, `limit`: number | Read messages for an agent. Marks delivered messages as read. |
|
||||
| `memory_signal_send` | | `from`*: string, `to`: string, `content`*: string, `type`: string, `replyTo`: string | Send a message to another agent or broadcast. Supports threading, typed messages, and TTL expiration. |
|
||||
| `memory_sketch_create` | | `title`*: string, `description`: string, `expiresInMs`: number, `project`: string | Create an ephemeral action graph for exploratory work. Auto-expires after TTL. Can be promoted to permanent actions or discarded. |
|
||||
| `memory_sketch_promote` | | `sketchId`*: string, `project`: string | Promote a sketch's ephemeral actions to permanent actions. Makes the exploratory work official. |
|
||||
| `memory_slot_append` | | `label`*: string, `text`*: string | Append text to an existing slot. Fails with 413 if the append would exceed the slot's sizeLimit, agent must compact via memory_slot_replace first. |
|
||||
| `memory_slot_create` | | `label`*: string, `content`: string, `sizeLimit`: number, `description`: string, `pinned`: string, `scope`: string | Create a new slot. Reject if a slot with the same label already exists. |
|
||||
| `memory_slot_delete` | | `label`*: string | Delete a slot. Seeded default slots can be deleted unless marked readOnly. |
|
||||
| `memory_slot_get` | | `label`*: string | Read a single slot by label. |
|
||||
| `memory_slot_list` | | none | List all memory slots (pinned + project + global). Slots are editable, size-limited memory units the agent can read and modify across sessions. |
|
||||
| `memory_slot_replace` | | `label`*: string, `content`*: string | Replace slot content in place. Fails if content exceeds sizeLimit. |
|
||||
| `memory_smart_search` | yes | `query`*: string, `expandIds`: string, `limit`: number | Hybrid semantic+keyword search with progressive disclosure. |
|
||||
| `memory_snapshot_create` | | `message`: string | Create a git-versioned snapshot of current memory state. |
|
||||
| `memory_team_feed` | | `limit`: number | Get recent shared items from all team members. |
|
||||
| `memory_team_share` | | `itemId`*: string, `itemType`*: string | Share a memory or observation with team members. |
|
||||
| `memory_timeline` | | `anchor`*: string, `project`: string, `before`: number, `after`: number | Chronological observations around an anchor point. |
|
||||
| `memory_verify` | | `id`*: string | Verify a memory or observation by tracing its citation chain back to source observations and session context. Returns provenance information including confidence scores. |
|
||||
| `memory_vision_search` | | `queryText`: string, `queryImageRef`: string, `queryImageBase64`: string, `topK`: number, `sessionId`: string | Cross-modal image search via CLIP embeddings. Pass queryText to find screenshots matching a description, or queryImageBase64/queryImageRef to find similar images. Requires AGENTMEMORY_IMAGE_EMBEDDINGS=true. |
|
||||
|
||||
`*` marks required parameters.
|
||||
<!-- AUTOGEN:tools END -->
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: agentmemory-mcp-tools
|
||||
description: Map of every agentmemory MCP tool, what each does, and its parameters. Use when choosing which memory tool to call, when a tool name or argument is unclear, or when answering what agentmemory can do via MCP.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
agentmemory exposes its full capability set as MCP tools. This skill is the index: it tells you which tool to reach for and where to find exact parameters.
|
||||
|
||||
## Quick start
|
||||
|
||||
Save then recall:
|
||||
|
||||
1. `memory_save` with `content` (the insight), `concepts` (comma-separated keywords), `files` (comma-separated paths).
|
||||
2. `memory_smart_search` with `query` and `limit` to retrieve it later. This runs hybrid BM25 plus vector plus graph-expanded search.
|
||||
|
||||
## Tool families
|
||||
|
||||
- Capture: `memory_save`, `memory_observe` flows, `memory_compress_file`.
|
||||
- Retrieve: `memory_smart_search`, `memory_recall`, `memory_file_history`, `memory_timeline`, `memory_vision_search`.
|
||||
- Sessions and commits: `memory_sessions`, `memory_commits`, `memory_commit_lookup`.
|
||||
- Knowledge and graph: `memory_lesson_save`, `memory_lesson_recall`, `memory_graph_query`, `memory_relations`, `memory_patterns`, `memory_crystallize`.
|
||||
- Structured slots: `memory_slot_create`, `memory_slot_append`, `memory_slot_get`, `memory_slot_list`, `memory_slot_replace`, `memory_slot_delete`.
|
||||
- Governance and health: `memory_governance_delete`, `memory_audit`, `memory_verify`, `memory_heal`, `memory_diagnose`.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Pick the narrowest tool for the task. Prefer `memory_smart_search` for open recall, `memory_recall` when you already have a focused query, `memory_sessions` for session listings.
|
||||
2. Look up exact parameter names and which are required in REFERENCE.md before calling.
|
||||
3. Pass only documented fields. REST handlers whitelist fields and drop unknown ones.
|
||||
|
||||
## See also
|
||||
|
||||
- agentmemory-rest-api for the HTTP equivalents.
|
||||
- agentmemory-config for tool-visibility and feature flags.
|
||||
- The user-invocable action skills (remember, recall, recap, handoff, forget) wrap the most common tools.
|
||||
|
||||
## Reference
|
||||
|
||||
Full tool table with parameters and the core-set marking lives in REFERENCE.md, generated from source so it never drifts.
|
||||
@@ -0,0 +1,129 @@
|
||||
# agentmemory REST API reference
|
||||
|
||||
Generated from `src/triggers/api.ts`. Do not edit the block below by hand; run `npm run skills:gen` after changing the registered endpoints.
|
||||
|
||||
<!-- AUTOGEN:rest START - generated by scripts/skills/generate.ts, do not edit by hand -->
|
||||
The REST API is the primary surface. All paths are under `http://localhost:3111` (override with `--port`). When `AGENTMEMORY_SECRET` is set, send `Authorization: Bearer $AGENTMEMORY_SECRET`; localhost is otherwise open.
|
||||
|
||||
117 registered endpoints:
|
||||
|
||||
| Method | Path |
|
||||
| --- | --- |
|
||||
| POST | `/agentmemory/actions` |
|
||||
| POST | `/agentmemory/actions/edges` |
|
||||
| GET | `/agentmemory/actions/get` |
|
||||
| POST | `/agentmemory/actions/update` |
|
||||
| GET | `/agentmemory/audit` |
|
||||
| POST | `/agentmemory/auto-forget` |
|
||||
| GET | `/agentmemory/branch/detect` |
|
||||
| GET | `/agentmemory/branch/sessions` |
|
||||
| GET | `/agentmemory/branch/worktrees` |
|
||||
| POST | `/agentmemory/cascade-update` |
|
||||
| POST | `/agentmemory/checkpoints` |
|
||||
| POST | `/agentmemory/checkpoints/resolve` |
|
||||
| GET | `/agentmemory/claude-bridge/read` |
|
||||
| POST | `/agentmemory/claude-bridge/sync` |
|
||||
| GET | `/agentmemory/commits` |
|
||||
| POST | `/agentmemory/compress-file` |
|
||||
| GET | `/agentmemory/config/flags` |
|
||||
| POST | `/agentmemory/consolidate` |
|
||||
| POST | `/agentmemory/consolidate-pipeline` |
|
||||
| POST | `/agentmemory/context` |
|
||||
| GET | `/agentmemory/crystals` |
|
||||
| POST | `/agentmemory/crystals/auto` |
|
||||
| POST | `/agentmemory/crystals/create` |
|
||||
| POST | `/agentmemory/diagnostics` |
|
||||
| GET | `/agentmemory/diagnostics/followup` |
|
||||
| POST | `/agentmemory/diagnostics/heal` |
|
||||
| POST | `/agentmemory/enrich` |
|
||||
| POST | `/agentmemory/evict` |
|
||||
| POST | `/agentmemory/evolve` |
|
||||
| GET | `/agentmemory/export` |
|
||||
| POST | `/agentmemory/facets` |
|
||||
| POST | `/agentmemory/facets/query` |
|
||||
| POST | `/agentmemory/facets/remove` |
|
||||
| GET | `/agentmemory/facets/stats` |
|
||||
| POST | `/agentmemory/file-context` |
|
||||
| POST | `/agentmemory/flow/compress` |
|
||||
| POST | `/agentmemory/forget` |
|
||||
| GET | `/agentmemory/frontier` |
|
||||
| POST | `/agentmemory/generate-rules` |
|
||||
| POST | `/agentmemory/governance/bulk-delete` |
|
||||
| DELETE | `/agentmemory/governance/memories` |
|
||||
| POST | `/agentmemory/graph/build` |
|
||||
| POST | `/agentmemory/graph/extract` |
|
||||
| POST | `/agentmemory/graph/query` |
|
||||
| POST | `/agentmemory/graph/reset` |
|
||||
| POST | `/agentmemory/graph/snapshot-rebuild` |
|
||||
| GET | `/agentmemory/graph/stats` |
|
||||
| GET | `/agentmemory/health` |
|
||||
| POST | `/agentmemory/import` |
|
||||
| GET | `/agentmemory/insights` |
|
||||
| POST | `/agentmemory/insights/search` |
|
||||
| POST | `/agentmemory/leases/acquire` |
|
||||
| POST | `/agentmemory/leases/release` |
|
||||
| POST | `/agentmemory/leases/renew` |
|
||||
| POST | `/agentmemory/lessons` |
|
||||
| POST | `/agentmemory/lessons/search` |
|
||||
| POST | `/agentmemory/lessons/strengthen` |
|
||||
| GET | `/agentmemory/livez` |
|
||||
| GET | `/agentmemory/memories` |
|
||||
| GET | `/agentmemory/memories/:id` |
|
||||
| GET | `/agentmemory/mesh/export` |
|
||||
| POST | `/agentmemory/mesh/peers` |
|
||||
| POST | `/agentmemory/mesh/receive` |
|
||||
| POST | `/agentmemory/mesh/sync` |
|
||||
| POST | `/agentmemory/migrate` |
|
||||
| GET | `/agentmemory/next` |
|
||||
| GET | `/agentmemory/observations` |
|
||||
| POST | `/agentmemory/observe` |
|
||||
| POST | `/agentmemory/obsidian/export` |
|
||||
| POST | `/agentmemory/patterns` |
|
||||
| GET | `/agentmemory/procedural` |
|
||||
| GET | `/agentmemory/profile` |
|
||||
| POST | `/agentmemory/reflect` |
|
||||
| POST | `/agentmemory/relations` |
|
||||
| POST | `/agentmemory/remember` |
|
||||
| POST | `/agentmemory/replay/import-jsonl` |
|
||||
| GET | `/agentmemory/replay/load` |
|
||||
| GET | `/agentmemory/replay/sessions` |
|
||||
| POST | `/agentmemory/routines` |
|
||||
| POST | `/agentmemory/routines/run` |
|
||||
| GET | `/agentmemory/routines/status` |
|
||||
| POST | `/agentmemory/search` |
|
||||
| GET | `/agentmemory/semantic` |
|
||||
| POST | `/agentmemory/sentinels` |
|
||||
| POST | `/agentmemory/sentinels/cancel` |
|
||||
| POST | `/agentmemory/sentinels/check` |
|
||||
| POST | `/agentmemory/sentinels/trigger` |
|
||||
| GET | `/agentmemory/session/by-commit` |
|
||||
| POST | `/agentmemory/session/commit` |
|
||||
| POST | `/agentmemory/session/end` |
|
||||
| POST | `/agentmemory/session/start` |
|
||||
| GET | `/agentmemory/sessions` |
|
||||
| GET | `/agentmemory/signals` |
|
||||
| POST | `/agentmemory/signals/send` |
|
||||
| POST | `/agentmemory/sketches` |
|
||||
| POST | `/agentmemory/sketches/add` |
|
||||
| POST | `/agentmemory/sketches/discard` |
|
||||
| POST | `/agentmemory/sketches/gc` |
|
||||
| POST | `/agentmemory/sketches/promote` |
|
||||
| GET | `/agentmemory/slot` |
|
||||
| POST | `/agentmemory/slot/append` |
|
||||
| POST | `/agentmemory/slot/reflect` |
|
||||
| POST | `/agentmemory/slot/replace` |
|
||||
| GET | `/agentmemory/slots` |
|
||||
| POST | `/agentmemory/smart-search` |
|
||||
| POST | `/agentmemory/snapshot/create` |
|
||||
| POST | `/agentmemory/snapshot/restore` |
|
||||
| GET | `/agentmemory/snapshots` |
|
||||
| POST | `/agentmemory/summarize` |
|
||||
| GET | `/agentmemory/team/feed` |
|
||||
| GET | `/agentmemory/team/profile` |
|
||||
| POST | `/agentmemory/team/share` |
|
||||
| POST | `/agentmemory/timeline` |
|
||||
| POST | `/agentmemory/verify` |
|
||||
| GET | `/agentmemory/viewer` |
|
||||
| POST | `/agentmemory/vision-embed` |
|
||||
| POST | `/agentmemory/vision-search` |
|
||||
<!-- AUTOGEN:rest END -->
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: agentmemory-rest-api
|
||||
description: The agentmemory HTTP REST API surface, the primary protocol for talking to the memory server. Use when calling agentmemory over HTTP, when MCP is unavailable and you need a fallback, or when integrating a host that does not speak MCP.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
REST is agentmemory's primary surface. MCP is a bridge on top of it. Every memory operation has an HTTP endpoint under `http://localhost:3111/agentmemory/*`.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# liveness
|
||||
curl -fsS http://localhost:3111/agentmemory/livez
|
||||
|
||||
# save
|
||||
curl -X POST http://localhost:3111/agentmemory/remember \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"content":"chose JWT refresh rotation","concepts":["jwt-refresh-rotation"]}'
|
||||
|
||||
# recall
|
||||
curl -X POST http://localhost:3111/agentmemory/smart-search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"auth token strategy","limit":5}'
|
||||
```
|
||||
|
||||
## Auth
|
||||
|
||||
By default localhost is open and no auth is needed. When `AGENTMEMORY_SECRET` is set, every request needs `Authorization: Bearer $AGENTMEMORY_SECRET`. See agentmemory-config.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Save returns `201`, reads return `200`, validation errors return `400`.
|
||||
- Handlers whitelist body fields and drop unknown ones, so passing extra keys is safe but ignored.
|
||||
- The port is configurable with `--port` or `--instance`; streams, viewer, and engine derive from it.
|
||||
|
||||
## See also
|
||||
|
||||
- agentmemory-mcp-tools for the MCP equivalents.
|
||||
- agentmemory-config for the port quartet and the secret.
|
||||
|
||||
## Reference
|
||||
|
||||
The full endpoint list with methods lives in REFERENCE.md, generated from `src/triggers/api.ts`.
|
||||
@@ -0,0 +1,71 @@
|
||||
# commit-context worked examples
|
||||
|
||||
## 1. Line range with a linked session
|
||||
|
||||
User: "Why is this retry loop here?" (pointing at lines 40-52 of refresh.ts)
|
||||
|
||||
Find the SHA:
|
||||
|
||||
```bash
|
||||
git blame -L 40,52 src/auth/refresh.ts
|
||||
# 9a1b2c3d (dev 2026-06-07) ... retry on revoked token
|
||||
```
|
||||
|
||||
Look it up:
|
||||
|
||||
```json
|
||||
memory_commit_lookup { "sha": "9a1b2c3d4e5f60718293a4b5c6d7e8f901234567" }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"commit": { "sha": "9a1b2c3d...", "short": "9a1b2c3", "branch": "main",
|
||||
"author": "dev", "message": "rotate refresh tokens" },
|
||||
"sessions": [ { "id": "7f3a9c21", "project": "app", "observationCount": 14,
|
||||
"summary": "Reworked refresh rotation" } ]
|
||||
}
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> `9a1b2c3` on `main` by dev: "rotate refresh tokens". Linked to session
|
||||
> `7f3a9c2` "Auth refresh rework" (14 obs). The retry loop handles a token
|
||||
> revoked mid-flight, per the session's high-importance observations.
|
||||
|
||||
## 2. Function lookup, commit predates linking
|
||||
|
||||
User: "What was the agent doing when it wrote validateScope?"
|
||||
|
||||
```bash
|
||||
git log -L :validateScope:src/auth/scope.ts -n 1
|
||||
# 1122aabb ...
|
||||
```
|
||||
|
||||
```json
|
||||
memory_commit_lookup { "sha": "1122aabbccddeeff00112233445566778899aabb" }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{ "commit": null }
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> `1122aab` predates agent session linking, so there is no recorded session.
|
||||
> From `git show`: it added `validateScope` to enforce per-token scopes. I can
|
||||
> show the full diff if useful.
|
||||
|
||||
## 3. Bare path
|
||||
|
||||
User: "Give me context on src/middleware/limit.ts."
|
||||
|
||||
```bash
|
||||
git log -n 1 -- src/middleware/limit.ts
|
||||
```
|
||||
|
||||
Take the SHA, run `memory_commit_lookup`, present commit plus linked session in
|
||||
the same shape as example 1.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: commit-context
|
||||
description: Trace a file, function, or line back to the agent session that produced its current commit. Use when the user asks "why is this code here", "what was the agent doing when this changed", "who wrote this", or wants context on a specific location in the codebase.
|
||||
argument-hint: "[file, function, or line]"
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
The user wants commit context for: $ARGUMENTS
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
git blame -L 40,52 src/auth/refresh.ts # -> SHA 9a1b2c3d
|
||||
```
|
||||
|
||||
```json
|
||||
memory_commit_lookup { "sha": "9a1b2c3d4e5f60718293a4b5c6d7e8f901234567" }
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
9a1b2c3 on main by dev: "rotate refresh tokens"
|
||||
Linked session 7f3a9c2 "Auth refresh rework", 14 obs.
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
Report only what git and the lookup return. When the lookup gives `commit: null`,
|
||||
the commit predates session linking; do not invent intent.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Find the SHA: `git blame -L <start>,<end> <file>` for a line range;
|
||||
`git log -L :<function>:<file>` for a function; `git log -n 1 -- <file>` for a
|
||||
bare path.
|
||||
2. Look it up: `memory_commit_lookup { "sha": "<full-sha>" }`.
|
||||
3. Present the commit (sha, short sha, branch, author, message), the linked
|
||||
session(s) (id, project, started/ended, observation count, summary), and the
|
||||
importance >= 7 observations via `memory_recall` when available.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
WRONG: lookup returns `{ "commit": null }`, you narrate "the agent was
|
||||
refactoring auth" from the diff alone.
|
||||
|
||||
RIGHT: "This commit predates session linking, so there is no recorded agent
|
||||
session. From `git show`: it changed token rotation in refresh.ts."
|
||||
|
||||
## Checklist
|
||||
|
||||
- SHA came from git blame/log, not a guess.
|
||||
- `commit: null` reported as "predates linking", no fabricated session.
|
||||
- Session details quote the lookup response verbatim.
|
||||
- No intent claimed beyond what observations state.
|
||||
|
||||
## See also
|
||||
|
||||
- `commit-history`: list many agent-linked commits at once.
|
||||
- `recall`: dig deeper into the linked session's observations.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See ../_shared/TROUBLESHOOTING.md if `memory_commit_lookup` is not available.
|
||||
@@ -0,0 +1,71 @@
|
||||
# commit-history worked examples
|
||||
|
||||
## 1. Branch filter
|
||||
|
||||
User: "Show agent commits on main."
|
||||
|
||||
Invocation:
|
||||
|
||||
```json
|
||||
memory_commits { "branch": "main", "limit": 100 }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"commits": [
|
||||
{ "short": "9a1b2c3", "branch": "main", "authoredAt": "2026-06-07T09:12:00Z",
|
||||
"message": "rotate refresh tokens", "sessionIds": ["7f3a9c21"],
|
||||
"observationCount": 14, "files": 3 },
|
||||
{ "short": "b21d004", "branch": "main", "authoredAt": "2026-06-05T14:40:00Z",
|
||||
"message": "rate limiter audit", "sessionIds": ["b21d004e"],
|
||||
"observationCount": 9, "files": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> - `9a1b2c3` main 2026-06-07 "rotate refresh tokens", session `7f3a9c2` (14 obs, 3 files)
|
||||
> - `b21d004` main 2026-06-05 "rate limiter audit", session `b21d004` (9 obs, 1 file)
|
||||
|
||||
## 2. Bare number as limit
|
||||
|
||||
User: "commit-history 5"
|
||||
|
||||
Treat `5` as the limit:
|
||||
|
||||
```json
|
||||
memory_commits { "limit": 5 }
|
||||
```
|
||||
|
||||
Render the five newest linked commits in the same format.
|
||||
|
||||
## 3. Empty result
|
||||
|
||||
User: "Show agent commits on release-2.0."
|
||||
|
||||
```json
|
||||
memory_commits { "branch": "release-2.0", "limit": 100 }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{ "commits": [] }
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> No agent-linked commits on `release-2.0`. Drop the branch filter to see all
|
||||
> linked commits, or try a different branch.
|
||||
|
||||
REST fallback for this same call, with encoding:
|
||||
|
||||
```http
|
||||
GET /agentmemory/commits?branch=release-2.0&limit=100
|
||||
```
|
||||
|
||||
Build it with `URLSearchParams` so a branch like `feat/a&b` becomes
|
||||
`feat%2Fa%26b` rather than breaking the query.
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: commit-history
|
||||
description: List recent git commits linked to agent sessions, optionally filtered by branch or repo. Use when the user asks "show agent commits", "what has the agent shipped", "list linked commits", or wants commits with their session context.
|
||||
argument-hint: "[branch=... repo=... limit=...]"
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
The user wants a list of agent-linked commits. Filter args: $ARGUMENTS
|
||||
|
||||
## Quick start
|
||||
|
||||
```json
|
||||
memory_commits { "branch": "main", "limit": 20 }
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
9a1b2c3 main 2026-06-07 "rotate refresh tokens" · session 7f3a9c2 (14 obs)
|
||||
b21d004 main 2026-06-05 "rate limiter audit" · session b21d004 (9 obs)
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
Render only the commits the tool returned, newest first. An empty result means
|
||||
the filter matched nothing, not that work is missing.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Parse `$ARGUMENTS` for `branch=<name>`, `repo=<url-or-fragment>`,
|
||||
`limit=<n>`. A bare numeric token is the limit. Defaults: no branch, no repo,
|
||||
limit 100, max 500.
|
||||
2. Call `memory_commits` with the parsed filters.
|
||||
3. Render reverse-chronologically: short sha, branch, authored timestamp, first
|
||||
line of the message, linked session id(s) (first 8) with observation counts,
|
||||
and file count when `files` is present.
|
||||
4. Empty result: tell the user the filter matched nothing and suggest dropping
|
||||
the branch or repo filter.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
WRONG (REST fallback): concatenate `?branch=` + raw branch name, so a name with
|
||||
`?`, `&`, or `#` corrupts the query string.
|
||||
|
||||
RIGHT: URL-encode every value with `URLSearchParams`/`encodeURIComponent` before
|
||||
appending to `GET /agentmemory/commits`.
|
||||
|
||||
## Checklist
|
||||
|
||||
- Filters parsed; bare number treated as limit; limit capped at 500.
|
||||
- Output is reverse-chronological.
|
||||
- Session ids and observation counts come straight from the response.
|
||||
- REST fallback URL-encodes branch, repo, and limit.
|
||||
|
||||
## See also
|
||||
|
||||
- `commit-context`: drill into one commit's session.
|
||||
- `recall`: search the observations behind a linked session.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See ../_shared/TROUBLESHOOTING.md if `memory_commits` is not available.
|
||||
@@ -0,0 +1,75 @@
|
||||
# forget worked examples
|
||||
|
||||
## 1. Delete a leaked secret
|
||||
|
||||
User: "Forget that note where I pasted the API key."
|
||||
|
||||
Search:
|
||||
|
||||
```json
|
||||
memory_smart_search { "query": "api key", "limit": 20 }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{ "id": "abc12345", "sessionId": "7f3a9c21", "title": "Pasted staging API key",
|
||||
"narrative": "STAGING_KEY=sk-live-..." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Present and gate:
|
||||
|
||||
> Found 1 match:
|
||||
> - `abc12345` (session `7f3a9c2`) "Pasted staging API key"
|
||||
>
|
||||
> Delete this memory? (yes/no)
|
||||
|
||||
After the user says yes:
|
||||
|
||||
```json
|
||||
memory_governance_delete { "memoryIds": ["abc12345"], "reason": "user privacy request" }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{ "deleted": 1 }
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> Deleted 1 memory (`abc12345`).
|
||||
|
||||
## 2. Drop a whole session's observations
|
||||
|
||||
User: "Delete everything from the throwaway spike session."
|
||||
|
||||
Search, identify the session, then collect every memory id in it:
|
||||
|
||||
```json
|
||||
memory_smart_search { "query": "spike prototype throwaway", "limit": 20 }
|
||||
```
|
||||
|
||||
Response has 4 results all with `sessionId: c98f1100`. Show all four, get a yes,
|
||||
then pass every id:
|
||||
|
||||
```json
|
||||
memory_governance_delete {
|
||||
"memoryIds": ["m1", "m2", "m3", "m4"],
|
||||
"reason": "drop throwaway spike session"
|
||||
}
|
||||
```
|
||||
|
||||
Never send `{ "sessionId": "c98f1100" }`; the MCP deletes by memory id only.
|
||||
|
||||
## 3. User declines
|
||||
|
||||
User: "Actually, on second thought, keep them."
|
||||
|
||||
Do not call delete. Confirm:
|
||||
|
||||
> Understood, nothing deleted. The 4 memories from session `c98f110` are intact.
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: forget
|
||||
description: Delete specific observations from agentmemory after showing them and getting explicit confirmation. Use when the user says "forget this", "delete memory", "remove that note", or wants to scrub specific data for privacy.
|
||||
argument-hint: "[what to forget - session ID, file path, or search term]"
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
The user wants to remove data from agentmemory: $ARGUMENTS
|
||||
|
||||
## Quick start
|
||||
|
||||
```json
|
||||
memory_smart_search { "query": "old api key in config", "limit": 20 }
|
||||
```
|
||||
|
||||
Show the matches, get a yes, then:
|
||||
|
||||
```json
|
||||
memory_governance_delete { "memoryIds": ["abc12345", "def67890"], "reason": "user privacy request" }
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
Found 2 matching memories. Confirmed. Deleted 2 memories.
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
This is destructive and irreversible. Show exactly what will be deleted and get
|
||||
an explicit yes before calling delete. Delete by memory ID, never a bare session.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Search with `memory_smart_search`, the user's text as `query`, `limit: 20`.
|
||||
2. Show what matched: session ids, memory ids, titles. Ask for explicit
|
||||
confirmation. Do not proceed on silence or a vague "sure, whatever".
|
||||
3. On confirmation, call `memory_governance_delete` with `memoryIds` (array or
|
||||
comma-separated string) and optional `reason` (default `plugin skill request`).
|
||||
4. To drop a whole session, collect every memory id in that session from the
|
||||
search results and pass them all. The MCP does not accept a bare `sessionId`.
|
||||
5. Report the deletion count back.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
WRONG: search returns matches, you immediately call `memory_governance_delete`
|
||||
without showing them or waiting for a yes.
|
||||
|
||||
RIGHT: list the matches, ask "Delete these 2? (yes/no)", and only delete after
|
||||
an explicit yes.
|
||||
|
||||
## Checklist
|
||||
|
||||
- Matches were shown to the user before any delete.
|
||||
- An explicit yes was received, not assumed.
|
||||
- `memoryIds` holds real ids from the search, never a bare `sessionId`.
|
||||
- Final message states the actual count deleted.
|
||||
|
||||
## See also
|
||||
|
||||
- `remember`: the write side; forget is its undo.
|
||||
- `recall`: find the exact memory id before deleting.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See ../_shared/TROUBLESHOOTING.md if `memory_smart_search` or `memory_governance_delete` is not available.
|
||||
@@ -0,0 +1,56 @@
|
||||
# handoff worked examples
|
||||
|
||||
## 1. Resume with an open question
|
||||
|
||||
User: "Where were we?"
|
||||
|
||||
Project cwd: `/Users/dev/app`. Invocation:
|
||||
|
||||
```json
|
||||
memory_sessions { "limit": 20 }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessions": [
|
||||
{ "id": "7f3a9c21", "cwd": "/Users/dev/app", "title": "Auth refresh rework",
|
||||
"status": "completed", "startedAt": "2026-06-07T09:00:00Z",
|
||||
"summary": "Reworked refresh rotation. Open: should logout revoke all device tokens?",
|
||||
"concepts": ["jwt-refresh-rotation", "logout"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Pull supporting context:
|
||||
|
||||
```json
|
||||
memory_recall { "query": "jwt-refresh-rotation logout", "limit": 10 }
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> Resuming `7f3a9c2` "Auth refresh rework".
|
||||
> **Open question:** should logout revoke all device tokens or just the current one?
|
||||
> Touched `auth/refresh.ts`, `auth/logout.ts`. Refresh rotation is done.
|
||||
> **Next step:** decide the revoke scope, then finish `auth/logout.ts`.
|
||||
|
||||
## 2. Sibling-repo guard
|
||||
|
||||
Project cwd: `/Users/dev/repo-a`. Sessions include one with `cwd:
|
||||
/Users/dev/repo-a-staging`.
|
||||
|
||||
A raw prefix would match `repo-a-staging`. The boundary check rejects it because
|
||||
`/Users/dev/repo-a-staging` does not equal `/Users/dev/repo-a` and does not start
|
||||
with `/Users/dev/repo-a/`. Pick the real `repo-a` session instead, or fall back
|
||||
to the most recent session overall if none matches.
|
||||
|
||||
## 3. Empty session
|
||||
|
||||
User: "Resume."
|
||||
|
||||
If the matched session has zero observations:
|
||||
|
||||
> The most recent session `c98f110` for this project has no recorded
|
||||
> observations. Nothing to hand off. Want to start fresh from the current state?
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: handoff
|
||||
description: Resume the most recent agent session for the current working directory, leading with any unanswered question. Use when the user says "where were we", "resume", "handoff", "pick up where I left off", or starts a session with no fresh context.
|
||||
argument-hint: "[optional cwd override]"
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
The user wants to resume work. Optional cwd override: $ARGUMENTS
|
||||
|
||||
## Quick start
|
||||
|
||||
```json
|
||||
memory_sessions { "limit": 20 }
|
||||
```
|
||||
|
||||
Pick the most recent session whose `cwd` matches this project, then:
|
||||
`memory_recall { "query": "<session top concepts>", "limit": 10 }`.
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
Resuming 7f3a9c2 "Auth refresh rework".
|
||||
Open question: should logout revoke all device tokens or just the current one?
|
||||
Next step: decide revoke scope, then update auth/logout.ts.
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
Match the session by directory boundary, not raw prefix, so a sibling repo never
|
||||
gets mistaken for this one. Never invent observations for an empty session.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Resolve the project path: if `$ARGUMENTS` is given, normalize it to absolute
|
||||
(`path.resolve(process.cwd(), $ARGUMENTS)`); else use the cwd.
|
||||
2. Call `memory_sessions`. Pick the most recent session whose normalized `cwd`
|
||||
matches by directory boundary: equality, OR `cwd.startsWith(projectPath + sep)`,
|
||||
OR `projectPath.startsWith(cwd + sep)`. Prefer `completed` over `abandoned`.
|
||||
No match: fall back to the single most recent session overall.
|
||||
3. If the session ended on an unanswered user-facing question, surface it FIRST.
|
||||
Look in `summary` or recent `conversation` observations whose `narrative`
|
||||
ends in `?`.
|
||||
4. Summarize: title/summary, key files, key decisions or errors, using
|
||||
`memory_recall` on the top concepts, limit 10.
|
||||
5. End with one concrete "next step?" pointer.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
WRONG: `session.cwd.startsWith(projectPath)` matches `/repo-a-staging` when the
|
||||
project is `/repo-a`, resuming the wrong repo's session.
|
||||
|
||||
RIGHT: `session.cwd === projectPath || session.cwd.startsWith(projectPath + sep)`,
|
||||
a directory-boundary check that cannot cross sibling repos.
|
||||
|
||||
## Checklist
|
||||
|
||||
- cwd override resolved to an absolute, normalized path.
|
||||
- Match used a directory-boundary check, not a raw prefix.
|
||||
- Unanswered question (if any) leads the response.
|
||||
- Empty session is reported plainly, with an offer to start fresh.
|
||||
|
||||
## See also
|
||||
|
||||
- `recap`, `session-history`, `recall`: same session data, broader views.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See ../_shared/TROUBLESHOOTING.md if `memory_sessions` or `memory_recall` is not available.
|
||||
@@ -0,0 +1,74 @@
|
||||
# recall worked examples
|
||||
|
||||
## 1. Hit on a past decision
|
||||
|
||||
User: "Did we ever decide how to cache sessions?"
|
||||
|
||||
Invocation:
|
||||
|
||||
```json
|
||||
memory_smart_search { "query": "session caching decision", "limit": 10 }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{ "sessionId": "7f3a9c21", "type": "decision", "title": "Drop Redis for iii state store",
|
||||
"narrative": "We decided to drop Redis and use the iii-engine state store for session caching.",
|
||||
"importance": 8 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> Yes. In session `7f3a9c21` you decided to drop Redis and use the iii-engine
|
||||
> state store for session caching (importance 8).
|
||||
|
||||
## 2. Multiple results across sessions
|
||||
|
||||
User: "Recall anything about the rate limiter."
|
||||
|
||||
Invocation:
|
||||
|
||||
```json
|
||||
memory_smart_search { "query": "rate limiter", "limit": 10 }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{ "sessionId": "b21d004e", "type": "code", "title": "limit.ts counts per-IP",
|
||||
"narrative": "Rate limiter counts per-IP, not per-user.", "importance": 6 },
|
||||
{ "sessionId": "c98f1100", "type": "conversation", "title": "Discussed sliding window",
|
||||
"narrative": "Considered a sliding-window limiter, deferred.", "importance": 4 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Present them grouped by session, leading with the per-IP code note.
|
||||
|
||||
## 3. No results
|
||||
|
||||
User: "Recall the GraphQL schema migration."
|
||||
|
||||
Invocation:
|
||||
|
||||
```json
|
||||
memory_smart_search { "query": "graphql schema migration", "limit": 10 }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{ "results": [] }
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> No memories matched that. Try `schema change`, `graphql`, or `migration` as
|
||||
> narrower terms, or `recap this week` to scan recent sessions.
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: recall
|
||||
description: Search agentmemory for past observations, sessions, and learnings about a topic using hybrid BM25 plus vector plus graph search. Use when the user says "recall", "what did we do about", "did we ever", "have we seen", or needs context from past sessions.
|
||||
argument-hint: "[search query]"
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
The user wants to recall past context about: $ARGUMENTS
|
||||
|
||||
## Quick start
|
||||
|
||||
```json
|
||||
memory_smart_search { "query": "jwt refresh token rotation", "limit": 10 }
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
2 results across 2 sessions.
|
||||
[importance 8] decision · "Rotate refresh tokens on every use" (session 7f3a9c21)
|
||||
[importance 5] code · "limit.ts counts per-IP" (session b21d004e)
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
Only surface what the tool returned. Never fabricate an observation, a session
|
||||
id, or an importance score. If nothing comes back, say so.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Call `memory_smart_search` with the user's text as `query` and `limit: 10`.
|
||||
Pass `project` when the user scopes to a specific repo.
|
||||
2. Group results by session.
|
||||
3. For each observation show its type, title, and narrative.
|
||||
4. Lead with the high-signal observations (importance >= 7).
|
||||
5. If zero results, suggest 2-3 alternative search terms and stop. Do not guess.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
WRONG: results are empty, so you write "We probably discussed token expiry last
|
||||
week" from assumption.
|
||||
|
||||
RIGHT: "No memories matched that query. Try `refresh token`, `session expiry`,
|
||||
or `auth rotation`."
|
||||
|
||||
## Checklist
|
||||
|
||||
- Every observation shown came from the tool response.
|
||||
- Results grouped by session, high-importance first.
|
||||
- Empty results trigger alternative-term suggestions, not invention.
|
||||
- No session id or score was paraphrased or rounded.
|
||||
|
||||
## See also
|
||||
|
||||
- `remember`: the write side; recall retrieves what it stores.
|
||||
- `recap`, `handoff`, `session-history`: session-scoped views of the same data.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See ../_shared/TROUBLESHOOTING.md if `memory_smart_search` is not available.
|
||||
@@ -0,0 +1,60 @@
|
||||
# recap worked examples
|
||||
|
||||
## 1. This week
|
||||
|
||||
User: "Recap this week."
|
||||
|
||||
Window: last 7 days. Invocation:
|
||||
|
||||
```json
|
||||
memory_sessions { "limit": 30 }
|
||||
```
|
||||
|
||||
Response (after filtering to cwd and the 7-day window):
|
||||
|
||||
```json
|
||||
{
|
||||
"sessions": [
|
||||
{ "id": "7f3a9c21", "cwd": "/Users/dev/app", "title": "Auth refresh rework",
|
||||
"startedAt": "2026-06-07T09:00:00Z", "observationCount": 14, "status": "completed",
|
||||
"concepts": ["jwt-refresh-rotation", "auth-flow"] },
|
||||
{ "id": "b21d004e", "cwd": "/Users/dev/app", "title": "Rate limiter audit",
|
||||
"startedAt": "2026-06-05T14:00:00Z", "observationCount": 9, "status": "completed",
|
||||
"concepts": ["rate-limiter", "per-ip-bug"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Per session, pull highlights:
|
||||
|
||||
```json
|
||||
memory_recall { "query": "jwt-refresh-rotation auth-flow", "limit": 3 }
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> **2026-06-07**
|
||||
> - `7f3a9c2` Auth refresh rework, 14 obs, completed
|
||||
> - [8] Rotate refresh tokens on every use
|
||||
>
|
||||
> **2026-06-05**
|
||||
> - `b21d004e` Rate limiter audit, 9 obs, completed
|
||||
> - [7] limit.ts counts per-IP, not per-user
|
||||
>
|
||||
> 2 sessions across 2 days, 23 observations.
|
||||
|
||||
## 2. Bare number
|
||||
|
||||
User: "recap 3"
|
||||
|
||||
Treat as `last 3`. Call `memory_sessions { "limit": 3 }`, group by date, same format.
|
||||
|
||||
## 3. Empty window
|
||||
|
||||
User: "Recap today."
|
||||
|
||||
If `memory_sessions` returns no session whose `startedAt` is today and whose
|
||||
`cwd` matches:
|
||||
|
||||
> No sessions today for this project. The most recent was yesterday, `7f3a9c2`
|
||||
> Auth refresh rework. Want a recap of that instead?
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: recap
|
||||
description: Summarize the last N agent sessions for the current project, grouped by date, with highlight observations per session. Use when the user asks "recap", "what have we been doing", "today", "this week", or wants a rollup of recent work.
|
||||
argument-hint: "[last N | today | this week]"
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
The user wants a recap. Time window args: $ARGUMENTS
|
||||
|
||||
## Quick start
|
||||
|
||||
```json
|
||||
memory_sessions { "limit": 30 }
|
||||
```
|
||||
|
||||
Then per surviving session: `memory_recall { "query": "<top concepts>", "limit": 3 }`.
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
2026-06-07
|
||||
7f3a9c2 · "Auth refresh rework" · 14 obs · completed
|
||||
- [8] Rotate refresh tokens on every use
|
||||
3 sessions across 2 days, 41 observations.
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
Only summarize sessions and observations the tools returned. An empty window is
|
||||
a real answer, not a prompt to invent activity.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Parse `$ARGUMENTS`: `today` = current local date; `this week` = last 7 days;
|
||||
`last <n>` or bare numeric = most recent N; empty = `last 10`.
|
||||
2. Call `memory_sessions`, filter to the current project (match `cwd` against the
|
||||
working directory), apply the window, sort by `startedAt` descending.
|
||||
3. Group survivors by local calendar date (YYYY-MM-DD).
|
||||
4. Per session list id (first 8), title or first prompt, observation count,
|
||||
status. Indent 2-3 highlights (importance >= 7) from `memory_recall`.
|
||||
5. End with "N sessions across M days, K observations."
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
WRONG: window is empty, so you summarize "a productive week of auth work" from
|
||||
memory of the conversation.
|
||||
|
||||
RIGHT: "No sessions in the last 7 days for this project."
|
||||
|
||||
## Checklist
|
||||
|
||||
- Window parsed correctly from the argument.
|
||||
- Sessions filtered to the current project's cwd.
|
||||
- Highlights come from `memory_recall`, not paraphrase.
|
||||
- Totals line reflects the actual counts shown.
|
||||
|
||||
## See also
|
||||
|
||||
- `handoff`, `session-history`, `recall`: same session data, different lens.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See ../_shared/TROUBLESHOOTING.md if `memory_sessions` or `memory_recall` is not available.
|
||||
@@ -0,0 +1,67 @@
|
||||
# remember worked examples
|
||||
|
||||
## 1. Saving a decision
|
||||
|
||||
User: "Remember that we decided to drop Redis and use the iii-engine state store for session caching."
|
||||
|
||||
Invocation:
|
||||
|
||||
```json
|
||||
memory_save {
|
||||
"content": "We decided to drop Redis and use the iii-engine state store for session caching to remove a moving part.",
|
||||
"concepts": "session-caching, iii-state-store, dropped-redis",
|
||||
"files": ""
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{ "id": "7f3a9c21", "concepts": ["session-caching", "iii-state-store", "dropped-redis"], "saved": true }
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> Saved. Tagged it `session-caching`, `iii-state-store`, `dropped-redis`, so a
|
||||
> later recall on caching or Redis will surface this decision.
|
||||
|
||||
## 2. Saving a code-specific gotcha
|
||||
|
||||
User: "Save this: the rate limiter in middleware/limit.ts counts per-IP, not per-user, so logged-in users on shared IPs collide."
|
||||
|
||||
Invocation:
|
||||
|
||||
```json
|
||||
memory_save {
|
||||
"content": "The rate limiter in middleware/limit.ts counts per-IP, not per-user, so logged-in users on shared IPs collide.",
|
||||
"concepts": "rate-limiter, per-ip-bug, shared-ip-collision",
|
||||
"files": "src/middleware/limit.ts"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{ "id": "b21d004e", "concepts": ["rate-limiter", "per-ip-bug", "shared-ip-collision"], "saved": true }
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> Saved memory `b21d004e` referencing `src/middleware/limit.ts`. Concepts:
|
||||
> `rate-limiter`, `per-ip-bug`, `shared-ip-collision`.
|
||||
|
||||
## 3. Vague request, no files
|
||||
|
||||
User: "Remember this for later."
|
||||
|
||||
Ask once for the substance, then save:
|
||||
|
||||
```json
|
||||
memory_save {
|
||||
"content": "Staging deploys must run the migration job before the app rollout, never after.",
|
||||
"concepts": "staging-deploy, migration-ordering, rollout-sequence",
|
||||
"files": ""
|
||||
}
|
||||
```
|
||||
|
||||
Present the confirmation with the concepts echoed back.
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: remember
|
||||
description: Save an insight, decision, or learning to agentmemory's long-term storage with searchable concept tags. Use when the user says "remember this", "save this", "note that", "don't forget", or wants to preserve knowledge for future sessions.
|
||||
argument-hint: "[what to remember]"
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
The user wants to save this to long-term memory: $ARGUMENTS
|
||||
|
||||
## Quick start
|
||||
|
||||
```json
|
||||
memory_save {
|
||||
"content": "We rotate JWT refresh tokens on every use; the old token is revoked server-side in auth/refresh.ts.",
|
||||
"concepts": "jwt-refresh-rotation, token-revocation, auth-flow",
|
||||
"files": "src/auth/refresh.ts"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
Saved memory abc12345 with 3 concepts: jwt-refresh-rotation, token-revocation, auth-flow.
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
A memory is only as useful as the terms that retrieve it. Tag with specific
|
||||
concepts so a future `recall` finds it, and preserve the user's own phrasing.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Pull the core insight, decision, or fact out of `$ARGUMENTS`.
|
||||
2. Extract 2-5 lowercased concept phrases. Prefer specific over generic
|
||||
(`jwt-refresh-rotation` beats `auth`).
|
||||
3. Extract referenced file paths (absolute or repo-relative). Empty if none.
|
||||
4. Call `memory_save` with `content`, `concepts` (comma-separated string), and
|
||||
`files` (comma-separated string).
|
||||
5. Confirm the save and echo the concepts so the user knows the retrieval terms.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
WRONG: `concepts: "stuff, code, notes"` (generic tags nothing can find later).
|
||||
|
||||
RIGHT: `concepts: "jwt-refresh-rotation, token-revocation"` (specific, retrievable).
|
||||
|
||||
## Checklist
|
||||
|
||||
- Content preserves the user's phrasing, not a paraphrase.
|
||||
- Concepts are specific, lowercased, 2-5 items.
|
||||
- File paths are real references, not guesses.
|
||||
- Confirmation echoes the exact concepts tagged.
|
||||
|
||||
## See also
|
||||
|
||||
- `recall`: retrieve what you save here (the pair to this skill).
|
||||
- `forget`: remove a memory you saved by mistake.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See ../_shared/TROUBLESHOOTING.md if `memory_save` is not available.
|
||||
@@ -0,0 +1,59 @@
|
||||
# session-history worked examples
|
||||
|
||||
## 1. Standard timeline
|
||||
|
||||
User: "Show me the session history."
|
||||
|
||||
Invocation:
|
||||
|
||||
```json
|
||||
memory_sessions { "limit": 20 }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessions": [
|
||||
{ "id": "7f3a9c21", "project": "app", "startedAt": "2026-06-07T09:00:00Z",
|
||||
"status": "completed", "observationCount": 14,
|
||||
"summary": "Reworked refresh rotation",
|
||||
"highlights": [ { "type": "decision", "title": "Rotate refresh tokens on every use" } ] },
|
||||
{ "id": "b21d004e", "project": "app", "startedAt": "2026-06-05T14:00:00Z",
|
||||
"status": "completed", "observationCount": 9,
|
||||
"highlights": [ { "type": "code", "title": "limit.ts counts per-IP" } ] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> - `7f3a9c2` app, 2026-06-07 09:00, completed, 14 obs
|
||||
> - decision: Rotate refresh tokens on every use (summary: Reworked refresh rotation)
|
||||
> - `b21d004` app, 2026-06-05 14:00, completed, 9 obs
|
||||
> - code: limit.ts counts per-IP
|
||||
|
||||
## 2. Session with no observations
|
||||
|
||||
If a returned session has `observationCount: 0`, list it but say so:
|
||||
|
||||
> - `c98f110` app, 2026-06-04 11:00, abandoned, 0 obs (no recorded work)
|
||||
|
||||
## 3. Empty history
|
||||
|
||||
User: "What did we do last time?"
|
||||
|
||||
```json
|
||||
memory_sessions { "limit": 20 }
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{ "sessions": [] }
|
||||
```
|
||||
|
||||
Present:
|
||||
|
||||
> No recorded sessions yet for this project. Once you work with memory capture
|
||||
> on, they will show here. Use `remember` to save a note now.
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: session-history
|
||||
description: Show what happened in recent past sessions on this project as a clean timeline. Use when the user asks "what did we do last time", "session history", "past sessions", or wants an overview of previous work.
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
The user wants an overview of recent sessions on this project.
|
||||
|
||||
## Quick start
|
||||
|
||||
```json
|
||||
memory_sessions { "limit": 20 }
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
7f3a9c2 · app · 2026-06-07 09:00 · completed · 14 obs
|
||||
- decision: Rotate refresh tokens on every use
|
||||
b21d004 · app · 2026-06-05 14:00 · completed · 9 obs
|
||||
- code: limit.ts counts per-IP
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
Only show sessions and observations the tool returned. An empty history is a
|
||||
real answer, never a cue to invent past work.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Call `memory_sessions` with `limit: 20` for a meaningful window.
|
||||
2. Present in reverse chronological order: session id (first 8), project, start
|
||||
time, status.
|
||||
3. For sessions with observations, show the key highlights (type plus title).
|
||||
4. Note the total observation count per session.
|
||||
5. When a session summary exists, surface its title and the key decisions.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
WRONG: the tool returns two sessions, you describe "several sessions of steady
|
||||
progress" and add ones you remember from the conversation.
|
||||
|
||||
RIGHT: show exactly the two sessions returned, each with its real id, status, and
|
||||
observation count.
|
||||
|
||||
## Checklist
|
||||
|
||||
- Every session shown came from the tool response.
|
||||
- Order is reverse-chronological.
|
||||
- Per-session observation counts match the response.
|
||||
- No session or highlight was invented or merged.
|
||||
|
||||
## See also
|
||||
|
||||
- `recap`: same data grouped by date with highlights.
|
||||
- `handoff`: jump straight into the most recent session.
|
||||
- `recall`: search across all sessions by topic.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See ../_shared/TROUBLESHOOTING.md if `memory_sessions` is not available.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: write-agentmemory-skill
|
||||
description: The house format and rules for writing or updating an agentmemory skill. Use when adding a new skill, restructuring an existing one, or reviewing a skill contribution for consistency.
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
agentmemory skills follow one tiered format so they stay skimmable, accurate, and current. Match it exactly.
|
||||
|
||||
## Directory layout
|
||||
|
||||
```text
|
||||
plugin/skills/<name>/
|
||||
SKILL.md (required, under 100 lines)
|
||||
REFERENCE.md (optional, dense facts; auto-generate data tables)
|
||||
EXAMPLES.md (optional, worked transcripts)
|
||||
```
|
||||
|
||||
## SKILL.md rules
|
||||
|
||||
- Frontmatter: `name`, `description`, optional `argument-hint`, and `user-invocable`. Set `user-invocable: true` only for skills the user runs as a slash command; reference and knowledge skills are `false`.
|
||||
- Description is two sentences and the only thing the agent sees when deciding to load the skill. Sentence one states the capability. Sentence two starts "Use when" and lists concrete triggers. Keep it distinct from sibling skills, under 1024 chars, third person.
|
||||
- Body order: Quick start (one concrete example), Why (the governing principle), Workflow (numbered steps with decision gates), Anti-patterns (a WRONG vs RIGHT callout for the top mistake), Checklist, See also (cross-link siblings), Reference or Troubleshooting pointer.
|
||||
- Stay under 100 lines. Move dense facts to REFERENCE.md and examples to EXAMPLES.md.
|
||||
- Cross-references link one level deep only. Shared recovery steps live in `../_shared/TROUBLESHOOTING.md`, never inlined.
|
||||
|
||||
## Keep it current
|
||||
|
||||
Facts that exist in source (tool names and parameters, REST endpoints, env vars, connect adapters, hook events) are generated, never hand-typed. Edit the source, then run `npm run skills:gen`. CI runs `npm run skills:check` and fails on drift, so generated tables cannot fall behind the code.
|
||||
|
||||
## Style
|
||||
|
||||
No external or competitor product names. No emojis. No em-dashes. No filler. State the thing and stop.
|
||||
|
||||
## Checklist
|
||||
|
||||
- Description has a "Use when" sentence with real triggers.
|
||||
- SKILL.md is under 100 lines.
|
||||
- No time-sensitive claims and no duplicated troubleshooting block.
|
||||
- Concrete example present; generated facts come from the generator.
|
||||
- Cross-links resolve and go one level deep.
|
||||
Reference in New Issue
Block a user