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
10 changes: 10 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ Usage: agentmemory [command] [options]
Commands:
(default) Start agentmemory worker
init Copy bundled .env.example to ~/.agentmemory/.env if absent
connect [agent] Wire agentmemory into an installed agent (claude-code, codex,
cursor, gemini-cli, openclaw, hermes, pi, openhuman).
No arg = interactive picker. --all wires every detected agent.
--dry-run shows what would change. --force re-installs.
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
Expand Down Expand Up @@ -1538,6 +1542,11 @@ async function runMcp(): Promise<void> {
await import("./mcp/standalone.js");
}

async function runConnectCmd(): Promise<void> {
const { runConnect } = await import("./cli/connect/index.js");
await runConnect(args.slice(1));
}

async function runImportJsonl(): Promise<void> {
// Long-form flags that take a value. Their value tokens must be
// consumed alongside the flag so they don't leak into positional
Expand Down Expand Up @@ -1716,6 +1725,7 @@ async function runImportJsonl(): Promise<void> {

const commands: Record<string, () => Promise<void>> = {
init: runInit,
connect: runConnectCmd,
status: runStatus,
doctor: runDoctor,
demo: runDemo,
Expand Down
89 changes: 89 additions & 0 deletions src/cli/connect/claude-code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import * as p from "@clack/prompts";
import type { ConnectAdapter, ConnectOptions, ConnectResult } from "./types.js";
import {
AGENTMEMORY_MCP_BLOCK,
backupFile,
logAlreadyWired,
logBackup,
logInstalled,
readJsonSafe,
writeJsonAtomic,
} from "./util.js";

const CLAUDE_DIR = join(homedir(), ".claude");
const CLAUDE_JSON = join(homedir(), ".claude.json");

type ClaudeMcpEntry = typeof AGENTMEMORY_MCP_BLOCK;
type ClaudeConfig = {
mcpServers?: Record<string, ClaudeMcpEntry>;
[key: string]: unknown;
};

function entryMatches(entry: unknown): boolean {
if (!entry || typeof entry !== "object") return false;
const e = entry as Record<string, unknown>;
if (e["command"] !== "npx") return false;
const args = Array.isArray(e["args"]) ? (e["args"] as string[]) : [];
return args.includes("@agentmemory/mcp");
}

export const adapter: ConnectAdapter = {
name: "claude-code",
displayName: "Claude Code",
docs: "https://github.com/rohitg00/agentmemory#claude-code-one-block-paste-it",

detect(): boolean {
return existsSync(CLAUDE_DIR);
},

async install(opts: ConnectOptions): Promise<ConnectResult> {
const existing = readJsonSafe<ClaudeConfig>(CLAUDE_JSON);
const next: ClaudeConfig = existing ? { ...existing } : {};
const servers: Record<string, ClaudeMcpEntry> = {
...((next.mcpServers as Record<string, ClaudeMcpEntry>) ?? {}),
};

const alreadyHas = entryMatches(servers["agentmemory"]);
if (alreadyHas && !opts.force) {
logAlreadyWired("Claude Code", CLAUDE_JSON);
return { kind: "already-wired", mutatedPath: CLAUDE_JSON };
}

if (opts.dryRun) {
p.log.info(
`[dry-run] Would ${alreadyHas ? "overwrite" : "add"} mcpServers.agentmemory in ${CLAUDE_JSON}`,
);
return { kind: "installed", mutatedPath: CLAUDE_JSON };
}

let backupPath: string | undefined;
if (existsSync(CLAUDE_JSON)) {
backupPath = backupFile(CLAUDE_JSON, "claude-code");
logBackup(backupPath);
} else {
mkdirSync(CLAUDE_DIR, { recursive: true });
writeFileSync(CLAUDE_JSON, "{}\n", "utf-8");
}
Comment on lines +67 to +69

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 | 🟡 Minor | ⚡ Quick win

Remove unnecessary intermediate file write.

Line 68 writes an empty JSON object {} to CLAUDE_JSON, but line 73 immediately overwrites it via writeJsonAtomic. The intermediate write serves no purpose—only the directory creation on line 67 is needed before the atomic write.

♻️ Proposed fix
     } else {
       mkdirSync(CLAUDE_DIR, { recursive: true });
-      writeFileSync(CLAUDE_JSON, "{}\n", "utf-8");
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mkdirSync(CLAUDE_DIR, { recursive: true });
writeFileSync(CLAUDE_JSON, "{}\n", "utf-8");
}
mkdirSync(CLAUDE_DIR, { recursive: true });
}
🤖 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/connect/claude-code.ts` around lines 67 - 69, Remove the redundant
intermediate write of an empty JSON object: keep the directory creation using
mkdirSync(CLAUDE_DIR, { recursive: true }) but delete the
writeFileSync(CLAUDE_JSON, "{}\n", "utf-8") call and let
writeJsonAtomic(CLAUDE_JSON, data, ...) perform the initial write atomically;
update any related comments and ensure CLAUDE_DIR and CLAUDE_JSON are still
created/available before calling writeJsonAtomic.


servers["agentmemory"] = AGENTMEMORY_MCP_BLOCK;
next.mcpServers = servers;
writeJsonAtomic(CLAUDE_JSON, next);

const verify = readJsonSafe<ClaudeConfig>(CLAUDE_JSON);
if (!entryMatches(verify?.mcpServers?.["agentmemory"])) {
p.log.error(
`Verification failed: ${CLAUDE_JSON} did not contain mcpServers.agentmemory after write.`,
);
return { kind: "skipped", reason: "verification-failed" };
}

logInstalled("Claude Code", CLAUDE_JSON);
p.log.info(
"Restart Claude Code (or run `/mcp` inside a session) to pick up the new server.",
);
return { kind: "installed", mutatedPath: CLAUDE_JSON, backupPath };
},
};
112 changes: 112 additions & 0 deletions src/cli/connect/codex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join, dirname } from "node:path";
import * as p from "@clack/prompts";
import type { ConnectAdapter, ConnectOptions, ConnectResult } from "./types.js";
import {
backupFile,
logAlreadyWired,
logBackup,
logInstalled,
} from "./util.js";

const CODEX_DIR = join(homedir(), ".codex");
const CODEX_TOML = join(CODEX_DIR, "config.toml");

const TOML_BLOCK = `[mcp_servers.agentmemory]
command = "npx"
args = ["-y", "@agentmemory/mcp"]

[mcp_servers.agentmemory.env]
AGENTMEMORY_URL = "http://localhost:3111"
`;

const SECTION_HEADER = "[mcp_servers.agentmemory]";

function isWiredText(toml: string): boolean {
return toml.includes(SECTION_HEADER);
}

function stripExistingBlock(toml: string): string {
const lines = toml.split(/\r?\n/);
const out: string[] = [];
let skipping = false;
for (const line of lines) {
const trimmed = line.trim();
if (
trimmed === SECTION_HEADER ||
trimmed === "[mcp_servers.agentmemory.env]"
) {
skipping = true;
continue;
}
if (
skipping &&
trimmed.startsWith("[") &&
trimmed !== "[mcp_servers.agentmemory.env]"
) {
skipping = false;
}
if (!skipping) out.push(line);
}
return out.join("\n").replace(/\n{3,}$/, "\n\n").trimEnd() + "\n";
}

export const adapter: ConnectAdapter = {
name: "codex",
displayName: "Codex CLI",
docs: "https://github.com/rohitg00/agentmemory#codex-cli-codex-plugin-platform",

detect(): boolean {
return existsSync(CODEX_DIR);
},

async install(opts: ConnectOptions): Promise<ConnectResult> {
const exists = existsSync(CODEX_TOML);
const current = exists ? readFileSync(CODEX_TOML, "utf-8") : "";
const wired = isWiredText(current);

if (wired && !opts.force) {
logAlreadyWired("Codex CLI", CODEX_TOML);
return { kind: "already-wired", mutatedPath: CODEX_TOML };
}

if (opts.dryRun) {
p.log.info(
`[dry-run] Would ${wired ? "rewrite" : "append"} [mcp_servers.agentmemory] in ${CODEX_TOML}`,
);
return { kind: "installed", mutatedPath: CODEX_TOML };
}

let backupPath: string | undefined;
if (exists) {
backupPath = backupFile(CODEX_TOML, "codex", "toml");
logBackup(backupPath);
} else {
mkdirSync(dirname(CODEX_TOML), { recursive: true });
}

const cleaned = wired ? stripExistingBlock(current) : current;
const joiner = cleaned.length === 0 || cleaned.endsWith("\n") ? "" : "\n";
const next = `${cleaned}${joiner}${cleaned.length > 0 ? "\n" : ""}${TOML_BLOCK}`;
writeFileSync(CODEX_TOML, next, "utf-8");

const verify = readFileSync(CODEX_TOML, "utf-8");
if (!isWiredText(verify)) {
p.log.error(
`Verification failed: ${CODEX_TOML} did not contain ${SECTION_HEADER} after write.`,
);
return { kind: "skipped", reason: "verification-failed" };
}

logInstalled("Codex CLI", CODEX_TOML);
p.log.info(
"Codex picks up MCP servers on next launch. For the deeper plugin install, run: codex plugin marketplace add rohitg00/agentmemory && codex plugin install agentmemory",
);
return {
kind: "installed",
mutatedPath: CODEX_TOML,
...(backupPath !== undefined && { backupPath }),
};
},
};
11 changes: 11 additions & 0 deletions src/cli/connect/cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { homedir } from "node:os";
import { join } from "node:path";
import { createJsonMcpAdapter } from "./json-mcp-adapter.js";

export const adapter = createJsonMcpAdapter({
name: "cursor",
displayName: "Cursor",
detectDir: join(homedir(), ".cursor"),
configPath: join(homedir(), ".cursor", "mcp.json"),
docs: "https://github.com/rohitg00/agentmemory#other-agents",
});
11 changes: 11 additions & 0 deletions src/cli/connect/gemini-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { homedir } from "node:os";
import { join } from "node:path";
import { createJsonMcpAdapter } from "./json-mcp-adapter.js";

export const adapter = createJsonMcpAdapter({
name: "gemini-cli",
displayName: "Gemini CLI",
detectDir: join(homedir(), ".gemini"),
configPath: join(homedir(), ".gemini", "settings.json"),
docs: "https://github.com/rohitg00/agentmemory#other-agents",
});
45 changes: 45 additions & 0 deletions src/cli/connect/hermes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import * as p from "@clack/prompts";
import type { ConnectAdapter, ConnectOptions, ConnectResult } from "./types.js";

const HERMES_DIR = join(homedir(), ".hermes");
const HERMES_CONFIG = join(HERMES_DIR, "config.yaml");
const DOCS = "https://github.com/rohitg00/agentmemory/tree/main/integrations/hermes";

export const adapter: ConnectAdapter = {
name: "hermes",
displayName: "Hermes Agent",
docs: DOCS,

detect(): boolean {
return existsSync(HERMES_DIR);
},

async install(_opts: ConnectOptions): Promise<ConnectResult> {
p.log.warn(
"Hermes uses YAML config. Automated merge isn't implemented yet — manual install required.",
);
p.note(
[
`Add to ${HERMES_CONFIG}:`,
"",
" mcp_servers:",
" agentmemory:",
" command: npx",
' args: ["-y", "@agentmemory/mcp"]',
"",
" memory:",
" provider: agentmemory",
"",
`Full guide: ${DOCS}`,
].join("\n"),
"Hermes manual install",
);
return {
kind: "stub",
reason: "yaml-merge-not-implemented",
};
},
};
Loading