Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

## [0.9.3] — 2026-04-24

Developer-experience patch. Every disabled feature flag is now visible in the viewer, the CLI, and REST error responses, so devs no longer hit empty tabs wondering whether the install is broken or just opt-in. Adds a `doctor` command that diagnoses the whole stack in one shot and a first-run hero in the viewer that points at the magical-moment `demo` command.

### Added

- **`agentmemory doctor` command.** Runs 10 diagnostic checks in one shot: server reachability, health status, viewer port, LLM provider, embedding provider, four feature flag states, and whether the knowledge graph has data. Every failing check includes a concrete hint with the exact env var or command to fix it. Mirrors the shape of the new viewer feature-flag banners.
- **`/agentmemory/config/flags` REST endpoint.** Returns `{ version, provider, embeddingProvider, flags[] }` with per-flag `{ key, label, enabled, default, affects, needsLlm, description, enableHow, docsHref }`. Used by the viewer banner, CLI status/doctor, and anyone who wants to introspect config without parsing logs.
- **Viewer feature-flag banner system.** Compact collapsible summary row at the top of every tab (`⚠ 3 off · ⚙ 1 note · Feature flags — click to expand`). Expanded view shows per-flag card with description, exact enable command, docs link, and dismiss button. Dismissed state persists per-flag in localStorage so banners stay out of the way once acknowledged. Banners filter by the current tab's `affects` list.
- **Viewer first-run hero card.** When `sessions.length === 0`, dashboard renders an orange-accent card titled "First run → magical moment in 10 seconds" with `npx @agentmemory/agentmemory demo` as the next step. Removes the dead-empty dashboard that used to greet fresh installs.
- **Viewer footer with preset issue report.** `agentmemory viewer · v{version} · github · docs · report issue →`. The feedback link opens a GitHub issue pre-filled with version, provider name, embedding provider, flag state, and user-agent — so the first message on an issue already contains the diagnostic context that used to take three back-and-forths.
- **Richer empty states on Actions, Memories, Lessons, Crystals tabs.** Each now has a titled lead explaining what the tab is for, why it's empty, three concrete ways to populate it (MCP tool, curl, hook), and a docs link. The old one-liners ("No actions yet. Create actions via memory_action_create MCP tool") assumed too much context.
- **`status` command shows flag state.** New section in the output block lists provider (`✓ llm` / `✗ noop`), embedding provider (`✓ embeddings` / `bm25-only`), and each flag with a tick/cross. Parity with the viewer banner.
- **`AGENTMEMORY_URL` environment variable honored by CLI.** `status`, `doctor`, and related health checks now respect `AGENTMEMORY_URL=http://host:port` and extract the port from it. Previously documented but silently ignored; `--port N` was the only way to override.
- **Website install section promotes `demo` to step 2.** `npx @agentmemory/agentmemory demo` now appears between "start server" and "open viewer" on agent-memory.dev. The magical-moment command is on the critical path of the three-step install, not tucked into the README.
- **Website version auto-derived from repo package.json.** `gen-meta.mjs` picks up `src/version.ts` on `prebuild` and writes `website/lib/generated-meta.json`. Removes the stale-version drift that showed `v0.9.1` on the landing page after `v0.9.2` shipped.

### Changed

- **REST "feature not enabled" errors now return structured bodies.** Graph extraction (3 endpoints) and consolidation pipeline (1 endpoint) used to return `{ error: "Knowledge graph not enabled" }`. Now return `{ error, flag, enableHow, docsHref }` matching the viewer banner contract. Curl users get the same fix guidance as UI users.
- **Website install title: `THREE STEPS` → `THREE COMMANDS`.** Matches the new three-command install (`npx agentmemory`, `agentmemory demo`, `open viewer`).

### Fixed

- **Viewer banner scroll blocker.** Initial banner implementation rendered four full-height banner cards stacked above the dashboard, pushing all stats off-screen. Replaced with compact collapsible summary that takes ~40px of vertical space by default and only expands on click.

[0.9.3]: https://github.com/rohitg00/agentmemory/compare/v0.9.2...v0.9.3

## [0.9.2] — 2026-04-22

Safety + import-pipeline patch. Kills the infinite Stop-hook recursion loop that burned Claude Pro tokens on unkeyed installs, repairs every empty viewer tab after `import-jsonl`, derives lessons and crystals automatically from imported sessions, and opens up OpenAI-compatible embedding endpoints.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agentmemory/agentmemory",
"version": "0.9.2",
"version": "0.9.3",
"description": "Persistent memory for AI coding agents, powered by iii-engine's three primitives",
"type": "module",
"main": "dist/index.mjs",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agentmemory/mcp",
"version": "0.9.2",
"version": "0.9.3",
"description": "Standalone MCP server for agentmemory — thin shim that re-exposes @agentmemory/agentmemory's MCP entrypoint",
"type": "module",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agentmemory",
"version": "0.9.2",
"version": "0.9.3",
"description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 51 MCP tools, 4 skills, real-time viewer.",
"author": {
"name": "Rohit Ghumare",
Expand Down
177 changes: 159 additions & 18 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ Usage: agentmemory [command] [options]

Commands:
(default) Start agentmemory worker
status Show connection status, memory count, and health
status Show connection status, memory count, flags, and health
doctor Run diagnostic checks (server, flags, graph, providers)
demo Seed sample sessions and show recall in action
upgrade Upgrade local deps + iii runtime (best effort)
mcp Start standalone MCP server (no engine required)
Expand All @@ -43,10 +44,15 @@ Options:
--no-engine Skip auto-starting iii-engine
--port <N> Override REST port (default: 3111)

Environment:
AGENTMEMORY_URL Full REST base URL (e.g. http://localhost:3111).
Honored by status, doctor, and MCP shim commands.

Quick start:
npx @agentmemory/agentmemory # start with local iii-engine or Docker
npx @agentmemory/agentmemory status # check health
npx @agentmemory/agentmemory demo # try it in 30 seconds (needs server running)
npx @agentmemory/agentmemory demo # see semantic recall in 30 seconds
npx @agentmemory/agentmemory doctor # diagnose config + feature flags
npx @agentmemory/agentmemory status # health + memory count + flags
npx @agentmemory/agentmemory upgrade # upgrade agentmemory + iii runtime
npx @agentmemory/agentmemory mcp # standalone MCP server (no engine)
npx @agentmemory/mcp # same as above (shim package)
Expand All @@ -67,12 +73,37 @@ if (portIdx !== -1 && args[portIdx + 1]) {
const skipEngine = args.includes("--no-engine");

function getRestPort(): number {
const url = process.env["AGENTMEMORY_URL"];
if (url) {
try {
const parsed = new URL(url).port;
if (parsed) return parseInt(parsed, 10);
} catch {}
}
return parseInt(process.env["III_REST_PORT"] || "3111", 10) || 3111;
}

function getBaseUrl(): string {
const url = process.env["AGENTMEMORY_URL"];
if (url) return url.replace(/\/+$/, "");
return `http://localhost:${getRestPort()}`;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function getViewerUrl(): string {
const envUrl = process.env["AGENTMEMORY_VIEWER_URL"];
if (envUrl) return envUrl.replace(/\/+$/, "");
try {
const u = new URL(getBaseUrl());
const vPort = (parseInt(u.port || "3111", 10) || 3111) + 2;
return `${u.protocol}//${u.hostname}:${vPort}`;
} catch {
return `http://localhost:${getRestPort() + 2}`;
}
}

async function isEngineRunning(): Promise<boolean> {
try {
await fetch(`http://localhost:${getRestPort()}/`, {
await fetch(`${getBaseUrl()}/`, {
signal: AbortSignal.timeout(2000),
});
return true;
Expand All @@ -83,7 +114,7 @@ async function isEngineRunning(): Promise<boolean> {

async function isAgentmemoryReady(): Promise<boolean> {
try {
const res = await fetch(`http://localhost:${getRestPort()}/agentmemory/livez`, {
const res = await fetch(`${getBaseUrl()}/agentmemory/livez`, {
signal: AbortSignal.timeout(2000),
});
return res.ok;
Expand Down Expand Up @@ -382,32 +413,42 @@ async function main() {
await import("./index.js");
}

async function apiFetch<T = unknown>(base: string, path: string, timeoutMs = 5000): Promise<T | null> {
try {
const res = await fetch(`${base}/agentmemory/${path}`, { signal: AbortSignal.timeout(timeoutMs) });
return (await res.json()) as T;
} catch {
return null;
}
}
Comment on lines +416 to +423
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

apiFetch ignores HTTP error responses.

apiFetch calls res.json() without first checking res.ok, so a 4xx/5xx response with a JSON error body (e.g., the new structured { error, flag, enableHow, docsHref } payload from "not enabled" routes) is returned as a successful result. This silently feeds malformed objects into runStatus/runDoctor (e.g., flags?.flags becomes undefined, health?.status is missing, doctor reports green/red incorrectly).

🛡️ Suggested fix
 async function apiFetch<T = unknown>(base: string, path: string, timeoutMs = 5000): Promise<T | null> {
   try {
     const res = await fetch(`${base}/agentmemory/${path}`, { signal: AbortSignal.timeout(timeoutMs) });
+    if (!res.ok) return null;
     return (await res.json()) as T;
   } catch {
     return null;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli.ts` around lines 404 - 411, The apiFetch function currently returns
res.json() for all HTTP responses which treats 4xx/5xx error payloads as
successful results; update apiFetch (the async function apiFetch<T =
unknown>(base: string, path: string, timeoutMs = 5000)) to check res.ok before
returning the parsed body—if res.ok is false, parse the JSON error body (or
text) to capture details and then return null (or propagate a structured error)
so callers like runStatus/runDoctor don't receive malformed success objects;
ensure the logic uses the same AbortSignal.timeout and still handles network
exceptions in the existing catch path.


async function runStatus() {
const port = getRestPort();
const base = `http://localhost:${port}`;
const base = getBaseUrl();
p.intro("agentmemory status");

const up = await isEngineRunning();
if (!up) {
p.log.error(`Not running — no response on port ${port}`);
p.log.error(`Not running — no response at ${base}`);
p.log.info("Start with: npx @agentmemory/agentmemory");
process.exit(1);
}

try {
const [healthRes, sessionsRes, graphRes, memoriesRes] = await Promise.all([
fetch(`${base}/agentmemory/health`, { signal: AbortSignal.timeout(5000) }).then((r) => r.json()).catch(() => null),
fetch(`${base}/agentmemory/sessions`, { signal: AbortSignal.timeout(5000) }).then((r) => r.json()).catch(() => null),
fetch(`${base}/agentmemory/graph/stats`, { signal: AbortSignal.timeout(5000) }).then((r) => r.json()).catch(() => null),
fetch(`${base}/agentmemory/export`, { signal: AbortSignal.timeout(5000) }).then((r) => r.json()).catch(() => null),
const [healthRes, sessionsRes, graphRes, memoriesRes, flagsRes] = await Promise.all([
apiFetch<any>(base, "health"),
apiFetch<any>(base, "sessions"),
apiFetch<any>(base, "graph/stats"),
apiFetch<any>(base, "export"),
apiFetch<any>(base, "config/flags"),
]);

const h = healthRes?.health;
const status = healthRes?.status || "unknown";
const version = healthRes?.version || "?";
const sessions = Array.isArray(sessionsRes?.sessions) ? sessionsRes.sessions.length : 0;
const nodes = graphRes?.nodes || 0;
const edges = graphRes?.edges || 0;
const nodes = Number(graphRes?.totalNodes ?? graphRes?.nodes ?? graphRes?.nodeCount ?? 0);
const edges = Number(graphRes?.totalEdges ?? graphRes?.edges ?? graphRes?.edgeCount ?? 0);
const cb = healthRes?.circuitBreaker?.state || "closed";
const heapMB = h?.memory ? Math.round(h.memory.heapUsed / 1048576) : 0;
const uptime = h?.uptimeSeconds ? Math.round(h.uptimeSeconds) : 0;
Expand All @@ -419,7 +460,7 @@ async function runStatus() {
const tokensSaved = estFullTokens - estInjectedTokens;
const pctSaved = estFullTokens > 0 ? Math.round((tokensSaved / estFullTokens) * 100) : 0;

p.log.success(`Connected — v${version} on port ${port}`);
p.log.success(`Connected — v${version} at ${base}`);

const lines = [
`Health: ${status === "healthy" ? "✓ healthy" : status}`,
Expand All @@ -430,7 +471,7 @@ async function runStatus() {
`Circuit: ${cb}`,
`Heap: ${heapMB} MB`,
`Uptime: ${uptime}s`,
`Viewer: http://localhost:${port + 2}`,
`Viewer: ${getViewerUrl()}`,
];

if (obsCount > 0) {
Expand All @@ -440,13 +481,112 @@ async function runStatus() {
lines.push(` Injected: ~${estInjectedTokens.toLocaleString()} tokens`);
}

if (flagsRes) {
const provider = flagsRes.provider === "llm" ? "✓ llm" : "✗ noop (no key)";
const embed = flagsRes.embeddingProvider === "embeddings" ? "✓ embeddings" : "bm25-only";
const flagRows = (flagsRes.flags || []).map((f: { key: string; enabled: boolean; label: string }) =>
` ${f.enabled ? "✓" : "✗"} ${f.key.padEnd(32)} ${f.label}`
);
lines.push("");
lines.push(`Provider: ${provider}`);
lines.push(`Embeddings: ${embed}`);
lines.push(`Flags:`);
flagRows.forEach((r: string) => lines.push(r));
}

p.note(lines.join("\n"), "agentmemory");
} catch (err) {
p.log.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
}

type DoctorCheck = { name: string; ok: boolean; hint?: string };

function formatChecks(checks: DoctorCheck[]): string {
return checks
.map((c) => `${c.ok ? "✓" : "✗"} ${c.name}${c.hint ? `\n ${c.hint}` : ""}`)
.join("\n");
}

async function runDoctor() {
p.intro("agentmemory doctor");
const base = getBaseUrl();
const viewerUrl = getViewerUrl();
const checks: DoctorCheck[] = [];

const serverUp = await isEngineRunning();
checks.push({
name: "Server reachable",
ok: serverUp,
hint: serverUp ? undefined : `Start with: npx @agentmemory/agentmemory (tried ${base})`,
});

if (!serverUp) {
p.note(formatChecks(checks), "server unreachable");
process.exit(1);
}

const [health, flags, graph] = await Promise.all([
apiFetch<any>(base, "health", 3000),
apiFetch<any>(base, "config/flags", 3000),
apiFetch<any>(base, "graph/stats", 3000),
]);

const viewerUp = await fetch(viewerUrl, { signal: AbortSignal.timeout(2000) })
.then((r) => r.ok)
.catch(() => false);

const hasLlm = flags?.provider === "llm";
const hasEmbed = flags?.embeddingProvider === "embeddings";
const graphNodeCount = Number(graph?.totalNodes ?? graph?.nodes ?? graph?.nodeCount ?? 0);
const graphHas = graphNodeCount > 0;

checks.push(
{
name: "Health status",
ok: health?.status === "healthy",
hint: health?.status === "healthy" ? undefined : `Status: ${health?.status || "unknown"}`,
},
{
name: "Viewer reachable",
ok: viewerUp,
hint: viewerUp ? undefined : `${viewerUrl} not responding`,
},
{
name: "LLM provider",
ok: hasLlm,
hint: hasLlm ? undefined : "export ANTHROPIC_API_KEY=sk-ant-... (or GEMINI/OPENROUTER/MINIMAX) then restart",
},
{
name: "Embedding provider",
ok: hasEmbed,
hint: hasEmbed ? undefined : "Running BM25-only. Add OPENAI_API_KEY / VOYAGE_API_KEY / COHERE_API_KEY / OLLAMA_HOST for semantic recall",
},
);

for (const f of (flags?.flags || []) as { label: string; enabled: boolean; enableHow: string }[]) {
checks.push({ name: f.label, ok: f.enabled, hint: f.enabled ? undefined : f.enableHow });
}

checks.push({
name: "Knowledge graph populated",
ok: graphHas,
hint: graphHas ? undefined : "Graph is empty. Run a session with GRAPH_EXTRACTION_ENABLED=true, or POST /agentmemory/graph/extract",
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const passed = checks.filter((c) => c.ok).length;
const total = checks.length;
p.note(formatChecks(checks), `${passed}/${total} checks passing`);

if (passed === total) {
p.outro("✓ All checks passed. agentmemory is healthy.");
} else {
p.outro(`${total - passed} issue(s) — follow hints above to fix.`);
process.exit(1);
}
}

type DemoObservation = {
toolName: string;
toolInput: Record<string, string>;
Expand Down Expand Up @@ -677,7 +817,7 @@ async function runDemo() {
`Notice: searching "database performance optimization"`,
`found the N+1 query fix — keyword matching can't do that.`,
"",
`Viewer: http://localhost:${port + 2}`,
`Viewer: ${getViewerUrl()}`,
`Clean up with: curl -X DELETE "${base}/agentmemory/sessions?project=${demoProject}"`,
];

Expand Down Expand Up @@ -911,7 +1051,7 @@ async function runImportJsonl(): Promise<void> {
`imported ${json.imported ?? 0} file(s), ${json.observations ?? 0} observation(s) across ${json.sessionIds?.length || 0} session(s)`,
);
if (json.sessionIds && json.sessionIds.length > 0) {
p.log.info(`View at http://localhost:${port + 2} → Replay tab`);
p.log.info(`View at ${getViewerUrl()} → Replay tab`);
}
} catch (err) {
spinner.stop("failed");
Expand All @@ -926,6 +1066,7 @@ async function runImportJsonl(): Promise<void> {

const commands: Record<string, () => Promise<void>> = {
status: runStatus,
doctor: runDoctor,
demo: runDemo,
upgrade: runUpgrade,
mcp: runMcp,
Expand Down
2 changes: 1 addition & 1 deletion src/functions/export-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export function registerExportImportFunction(sdk: ISdk, kv: StateKV): void {
const strategy = data.strategy || "merge";
const importData = data.exportData;

const supportedVersions = new Set(["0.3.0", "0.4.0", "0.5.0", "0.6.0", "0.6.1", "0.7.0", "0.7.2", "0.7.3", "0.7.4", "0.7.5", "0.7.6", "0.7.7", "0.7.9", "0.8.0", "0.8.1", "0.8.2", "0.8.3", "0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10", "0.8.11", "0.8.12", "0.8.13", "0.9.0", "0.9.1", "0.9.2"]);
const supportedVersions = new Set(["0.3.0", "0.4.0", "0.5.0", "0.6.0", "0.6.1", "0.7.0", "0.7.2", "0.7.3", "0.7.4", "0.7.5", "0.7.6", "0.7.7", "0.7.9", "0.8.0", "0.8.1", "0.8.2", "0.8.3", "0.8.4", "0.8.5", "0.8.6", "0.8.7", "0.8.8", "0.8.9", "0.8.10", "0.8.11", "0.8.12", "0.8.13", "0.9.0", "0.9.1", "0.9.2", "0.9.3"]);
if (!supportedVersions.has(importData.version)) {
return {
success: false,
Expand Down
Loading
Loading