diff --git a/README.md b/README.md index 87e6b3f0d..30483919b 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ 53 MCP tools 12 auto hooks 0 external DBs - 1,390+ tests passing + 1,423+ tests passing

@@ -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 ``` @@ -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 ` | -**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. --- @@ -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) ``` diff --git a/src/cli.ts b/src/cli.ts index acac3d971..ae5be0035 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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); @@ -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 @@ -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 Override REST port (default: 3111). Streams (N+1), viewer (N+2), and iii engine (N+46023) auto-derive from N so a @@ -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(); @@ -2100,19 +2102,82 @@ async function runInit() { p.outro(`Edit ${target} and you're set.`); } +async function startServerForDemo(): Promise<() => Promise> { + 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(findEnginePidsByPort(port)); + const pidfilePid = readEnginePidfile(); + if (pidfilePid) pids.add(pidfilePid); + for (const pid of pids) { + await signalAndWait(pid, "SIGTERM", 3000).catch(() => {}); + } + clearEnginePidfile(); + clearEngineState(); + clearWorkerPidfile(); + }; +} + 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 = 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(); diff --git a/src/mcp/standalone.ts b/src/mcp/standalone.ts index f4683747f..dd66ecb1d 100644 --- a/src/mcp/standalone.ts +++ b/src/mcp/standalone.ts @@ -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`, ); } } @@ -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", { diff --git a/src/mcp/tools-registry.ts b/src/mcp/tools-registry.ts index c86d899bb..c4df3499c 100644 --- a/src/mcp/tools-registry.ts +++ b/src/mcp/tools-registry.ts @@ -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", @@ -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"; diff --git a/test/tool-count-consistency.test.ts b/test/tool-count-consistency.test.ts new file mode 100644 index 000000000..19f0e3a5e --- /dev/null +++ b/test/tool-count-consistency.test.ts @@ -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"); + }); +});