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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
<picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/stat-tools.svg"><img src="assets/tags/stat-tools.svg" alt="53 MCP tools" height="38" /></picture>
<picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/stat-hooks.svg"><img src="assets/tags/stat-hooks.svg" alt="12 auto hooks" height="38" /></picture>
<picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/stat-deps.svg"><img src="assets/tags/stat-deps.svg" alt="0 external DBs" height="38" /></picture>
<picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/stat-tests.svg"><img src="assets/tags/stat-tests.svg" alt="1,390+ tests passing" height="38" /></picture>
<picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/stat-tests.svg"><img src="assets/tags/stat-tests.svg" alt="1,423+ tests passing" height="38" /></picture>
</p>

<p align="center">
Expand Down Expand Up @@ -93,6 +93,7 @@ npm install -g @agentmemory/agentmemory # once — bare `agentmemory` o
# sudo npm install -g @agentmemory/agentmemory
agentmemory # start the memory server on :3111
agentmemory demo # seed sample sessions + prove recall
agentmemory demo --serve # one command: boot server, run demo, tear down (no second terminal)
agentmemory connect claude-code # wire MCP into your agent (also: copilot-cli, codex, cursor, gemini-cli, ...)
npx skills add rohitg00/agentmemory -y # install 8 native skills so your agent knows when to use the tools
```
Expand Down Expand Up @@ -1141,7 +1142,7 @@ Full registry: [workers.iii.dev](https://workers.iii.dev). Every worker there co
| Prometheus / Grafana | iii OTEL + health monitor |
| Custom plugin systems | `iii worker add <name>` |

**174 source files · ~37,800 LOC · 1,390+ tests · 258 functions · 44 KV scopes** — all on three primitives. No `agentmemory plugin install`. The plugin system is iii itself.
**174 source files · ~37,800 LOC · 1,423+ tests · 258 functions · 44 KV scopes** — all on three primitives. No `agentmemory plugin install`. The plugin system is iii itself.

---

Expand Down Expand Up @@ -1465,7 +1466,7 @@ Full endpoint list: [`src/triggers/api.ts`](src/triggers/api.ts)
```bash
npm run dev # Hot reload
npm run build # Production build
npm test # 1,390+ tests
npm test # 1,423+ tests
npm run test:integration # API tests (requires running services)
```

Expand Down
87 changes: 76 additions & 11 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ import { isFirstRun, readPrefs, resetPrefs, writePrefs } from "./cli/preferences
import { runOnboarding } from "./cli/onboarding.js";
import { setBootVerbose } from "./logger.js";
import { VERSION } from "./version.js";
import { getAllTools, ESSENTIAL_TOOLS } from "./mcp/tools-registry.js";

const ALL_TOOLS_COUNT = getAllTools().length;
const CORE_TOOLS_COUNT = getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name)).length;

const __dirname = dirname(fileURLToPath(import.meta.url));
const args = process.argv.slice(2);
Expand Down Expand Up @@ -136,7 +140,9 @@ Commands:
--dry-run: show what each fix would do, don't execute
remove Cleanly uninstall agentmemory (pidfile, state, .env, binaries).
--force: skip confirmations · --keep-data: keep memory data
demo Seed sample sessions and show recall in action
demo [--serve] Seed sample sessions and show recall in action.
--serve boots the server, runs the demo, and stops it
in one command (no second terminal).
upgrade Upgrade local deps + iii runtime (best effort)
stop [--force] Stop the running iii-engine started by this CLI.
--force bypasses the Docker-heuristic guard and signals
Expand All @@ -152,7 +158,7 @@ Options:
--help, -h Show this help
--verbose, -v Show engine stderr, boot log, and diagnostic info
--reset Wipe ~/.agentmemory/preferences.json and re-run onboarding
--tools all|core Tool visibility (default: all = 51 tools; core = 8 essentials)
--tools all|core Tool visibility (default: all = ${ALL_TOOLS_COUNT} tools; core = ${CORE_TOOLS_COUNT} essentials)
--no-engine Skip auto-starting iii-engine
--port <N> Override REST port (default: 3111). Streams (N+1), viewer
(N+2), and iii engine (N+46023) auto-derive from N so a
Expand Down Expand Up @@ -1197,15 +1203,11 @@ async function main() {
if (attachedBin) {
const detected = iiiBinVersion(attachedBin);
if (detected && detected !== IIPINNED_VERSION) {
p.log.error(
`Attached iii-engine appears to be v${detected} (from ${attachedBin}) ` +
`but agentmemory v${VERSION} hard-pins v${IIPINNED_VERSION}. ` +
`Engine API drift causes runtime failures (e.g. state::list-not-found on v0.13.0+). ` +
`Stop the running engine (\`agentmemory stop --force\`) and re-run \`agentmemory\` ` +
`to install the pinned engine into ~/.agentmemory/bin without touching ${attachedBin}. ` +
`Or set AGENTMEMORY_III_VERSION=${detected} to override at your own risk.`,
p.log.warn(
`iii on PATH is v${detected} (from ${attachedBin}) but agentmemory v${VERSION} pins v${IIPINNED_VERSION}. ` +
`agentmemory will use its own pinned engine in ~/.agentmemory/bin and leaves ${attachedBin} untouched. ` +
`If you want agentmemory to track a different engine, set AGENTMEMORY_III_VERSION=${detected}.`,
);
process.exit(1);
}
}
adoptRunningEngine();
Expand Down Expand Up @@ -2100,19 +2102,82 @@ async function runInit() {
p.outro(`Edit ${target} and you're set.`);
}

async function startServerForDemo(): Promise<() => Promise<void>> {
if (await isAgentmemoryReady()) {
return async () => {};
}

const startedEngine = !(await isEngineRunning());
if (startedEngine) {
const ok = await startEngine();
if (!ok) {
p.log.error("Could not start iii-engine for the demo.");
p.note(installInstructions().join("\n"), "Setup required");
process.exit(1);
}
if (!(await waitForEngine(15000))) {
p.log.error("iii-engine did not become ready within 15s.");
process.exit(1);
}
}

await import("./index.js");
if (!(await waitForAgentmemoryReady(15000))) {
p.log.error("agentmemory worker did not become ready within 15s.");
process.exit(1);
}

return async () => {
if (!startedEngine) return;
const port = getRestPort();
const state = readEngineState();
if (state?.kind === "docker") {
await stopDockerEngine(state.composeFile, port).catch(() => {});
return;
}
const pids = new Set<number>(findEnginePidsByPort(port));
const pidfilePid = readEnginePidfile();
if (pidfilePid) pids.add(pidfilePid);
for (const pid of pids) {
await signalAndWait(pid, "SIGTERM", 3000).catch(() => {});
}
clearEnginePidfile();
clearEngineState();
clearWorkerPidfile();
};
Comment on lines +2124 to +2147

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

demo --serve teardown does not stop the worker started in this process

At Line 2124, import("./index.js") starts the worker in the current CLI process, but teardown (Lines 2131-2147) only targets external engine pids and becomes a no-op when startedEngine is false. This breaks the “one-command demo then tear down” contract and can leave the worker running after the demo completes.

Also applies to: 2158-2173

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli.ts` around lines 2124 - 2147, Importing "./index.js" can start an
in-process worker that isn’t stopped by the current teardown logic; modify the
code so the import returns a handle or exported stop function from index.js
(e.g., export stopWorker/closeServer) and capture it where import("./index.js")
is awaited, then in the teardown blocks (the one that runs when startedEngine is
false and the later block at 2158-2173) call that stop function (await
stopWorker()) before returning so the in-process worker is properly shut down;
alternatively, set a shared startedEngine flag or store the worker pid from the
import and use signalAndWait/clearWorkerPidfile/clearEngineState accordingly to
ensure both external and in-process workers are stopped.

}

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

if (!(await isAgentmemoryReady())) {
const serve = args.includes("--serve");
let teardown: () => Promise<void> = async () => {};

if (serve) {
teardown = await startServerForDemo();
} else if (!(await isAgentmemoryReady())) {
p.log.error(
`agentmemory worker not reachable on port ${port} (livez probe failed). Something may be on the port but it isn't serving /agentmemory/*.`,
);
p.log.info("Start it with: npx @agentmemory/agentmemory");
p.log.info("Or run a one-command demo with: npx @agentmemory/agentmemory demo --serve");
process.exit(1);
}

try {
await runDemoBody(base);
} finally {
await teardown();
}

if (serve) {
process.exit(0);
}
}

async function runDemoBody(base: string) {
const demoProject = "/tmp/agentmemory-demo";
const sessions = buildDemoSessions();

Expand Down
5 changes: 3 additions & 2 deletions src/mcp/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ function announceMode(handle: Handle): void {
`[@agentmemory/mcp] proxying to agentmemory server at ${handle.baseUrl}\n`,
);
} else {
const fullToolCount = getAllTools().length;
process.stderr.write(
`[@agentmemory/mcp] no server reachable at ${displayAgentmemoryUrl()}; falling back to local InMemoryKV\n`,
`[@agentmemory/mcp] no server reachable at ${displayAgentmemoryUrl()}; running reduced LOCAL FALLBACK with ${IMPLEMENTED_TOOLS.size} of ${fullToolCount} tools. Start 'npx @agentmemory/agentmemory' (and point AGENTMEMORY_URL at it) to unlock all ${fullToolCount} tools.\n`,
);
}
}
Expand Down Expand Up @@ -338,7 +339,7 @@ async function handleProxyGeneric(
handle: ProxyHandle,
): Promise<{ content: Array<{ type: string; text: string }> }> {
// Forward to the server's full MCP surface so non-Claude clients can
// reach all 51 tools (lessons, sentinels, slots, signals, graph, …)
// reach all 53 tools (lessons, sentinels, slots, signals, graph, …)
// instead of being capped at the 7 IMPLEMENTED_TOOLS set baked into
// this shim. The server validates arguments per tool.
const result = (await handle.call("/agentmemory/mcp/call", {
Expand Down
8 changes: 4 additions & 4 deletions src/mcp/tools-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@ export const V010_SLOTS_TOOLS: McpToolDef[] = [
},
];

const ESSENTIAL_TOOLS = new Set([
export const ESSENTIAL_TOOLS = new Set([
"memory_save",
"memory_recall",
"memory_consolidate",
Expand All @@ -950,9 +950,9 @@ export function getAllTools(): McpToolDef[] {
}

// default switched from "core" (8 essential tools) to "all"
// (full 51-tool surface). README and plugin manifests have always
// advertised 51 tools "in proxy mode"; the old default left OpenCode /
// Claude Code users seeing 8 with no indication the other 43 existed.
// (full 53-tool surface). README and plugin manifests have always
// advertised 53 tools "in proxy mode"; the old default left OpenCode /
// Claude Code users seeing 8 with no indication the other tools existed.
// Users who want the lean essentials can still set AGENTMEMORY_TOOLS=core.
export function getVisibleTools(): McpToolDef[] {
const mode = process.env["AGENTMEMORY_TOOLS"] || "all";
Expand Down
43 changes: 43 additions & 0 deletions test/tool-count-consistency.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, it, expect, vi } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";

vi.mock("../src/logger.js", () => ({
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
}));

import { getAllTools, ESSENTIAL_TOOLS } from "../src/mcp/tools-registry.js";

const ROOT = join(import.meta.dirname, "..");
const EXPECTED_TOOL_COUNT = 53;

function readText(relativePath: string): string {
return readFileSync(join(ROOT, relativePath), "utf-8");
}

describe("Tool count consistency", () => {
it("registry exposes the expected number of tools", () => {
expect(getAllTools().length).toBe(EXPECTED_TOOL_COUNT);
});

it("cli help derives the tool counts from the registry", () => {
const cli = readText("src/cli.ts");
expect(cli).toContain("const ALL_TOOLS_COUNT = getAllTools().length;");
expect(cli).toContain(
"(default: all = ${ALL_TOOLS_COUNT} tools; core = ${CORE_TOOLS_COUNT} essentials)",
);
expect(cli).not.toMatch(/all\s*=\s*51 tools/);
});

it("core tool count derives from the registry", () => {
const coreCount = getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name)).length;
expect(coreCount).toBe(ESSENTIAL_TOOLS.size);
expect(coreCount).toBeGreaterThan(0);
});

it("README advertises the same tool count as the registry", () => {
const readme = readText("README.md");
expect(readme).toContain(`${EXPECTED_TOOL_COUNT} MCP tools`);
expect(readme).not.toContain("51 MCP tools");
});
});