claude-mem 源码分析:安装流程与多平台集成

04 — 安装流程与多平台集成

npx claude-mem install 10 步自动化序列,通过平台适配器(Cursor rules/Gemini CLI/OpenCode)实现一套记忆核心多端复用,uid%100 端口策略解决多用户隔离。

一、npx 入口总览

src/npx-cli/index.ts(184 行)是整个 npx 命令的路由层,逻辑极简:

1
2
3
4
5
// index.ts:10-14
const command =
  firstArg.startsWith('-') && !HELP_OR_VERSION_FLAGS.has(firstArg)
    ? 'install'       // 裸标志(如 --provider claude)直接触发 install
    : firstArg;       // 否则取第一个参数作为子命令

子命令列表(index.ts:79-178):

子命令 功能 备注
install / 默认 安装向导 核心流程
repair 重跑 Bun/uv 安装 修复运行时
update / upgrade 升级到最新版本 复用 runInstallCommand
uninstall / remove 卸载 独立模块
start / stop / restart / status Worker 守护进程管理 需要 Bun
search <query> 搜索记忆 需要 Bun
transcript watch 启动 transcript 监视器

💡 Tip 裸标志自动变 install npx claude-mem --provider claude 这种写法能直接工作,因为 index.ts 检测到首参数以 - 开头且不是 -h/--help/-v/--version 时,自动把 command 设为 'install'。这是很友好的 CI 非交互安装用法。


二、npx claude-mem install 全流程

2.1 主流程时序

2.2 已安装检测(install.ts:971-983)

1
2
3
4
5
// install.ts:971-972
const alreadyInstalled = existsSync(
  join(marketplaceDir, 'plugin', '.claude-plugin', 'plugin.json')
);
// 同时读取现有版本号,区分 "重装" 还是 "升级" 提示

检测的是 ~/.claude/plugins/marketplaces/thedotmack/plugin/.claude-plugin/plugin.json 是否存在。文件即事实,不依赖注册表。

2.3 插件三目录架构

安装流程涉及三个目录,各有职责:

目录 路径 用途
Marketplace ~/.claude/plugins/marketplaces/thedotmack/ Claude Code 实际读取的插件目录
Cache ~/.claude/plugins/cache/thedotmack/claude-mem/<version>/ 版本快照,hooks 脚本优先读取
NPM Package npx 临时目录 安装源,cpSync 后可丢弃
1
2
3
4
5
6
7
8
9
// install.ts:521-553: copyPluginToMarketplace
// 白名单复制,只拷贝必要目录
const allowedTopLevelEntries = [
  'plugin', 'package.json', 'dist', '.agents', '.mcp.json', 'README.md', ...
];

// install.ts:556-563: copyPluginToCache
// 只复制 plugin/ 目录到 cache/<version>/
cpSync(sourcePluginDirectory, cachePath, { recursive: true });

💡 Tip Cache 目录的版本保留意义 plugin/hooks/hooks.json 中的 bash 命令会优先查 cache 目录(按时间倒序 ls -dt,取最新版本),然后才回退到 marketplace。这样即使 marketplace 被覆盖升级,旧版本仍在 cache,hooks 不会断档。npx 安装新版本后,hooks 下次触发时自动切换,无需重启 Claude Code。

2.4 Claude Code 的 hooks.json 是静态资产

重要理解:Claude Code 的 hooks.json 不是由 install.ts 动态写入的,而是随插件文件 cpSync 过去的:

1
2
3
plugin/hooks/hooks.json  (仓库里的源文件)
    ↓ cpSync
~/.claude/plugins/marketplaces/thedotmack/plugin/hooks/hooks.json

install.ts 通过 registerPlugin() / enablePluginInClaudeSettings() 把这个目录注册到 Claude Code 的插件系统,Claude Code 自己在启动时扫描并加载 hooks.json。

2.5 关闭 Claude 内置 Auto-Memory(install.ts:150-161)

1
2
3
4
5
6
7
8
export function disableClaudeAutoMemory(): boolean {
  const settings = readJsonSafe(claudeSettingsPath(), {});
  if (env.CLAUDE_CODE_DISABLE_AUTO_MEMORY === '1') return false; // 幂等

  settings.env = { ...env, CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1' };
  writeJsonFileAtomic(claudeSettingsPath(), settings);
  return true;
}

写入 ~/.claude/settings.jsonenv 块。注释解释了原因:Claude Code 内置的 MEMORY.md 系统会"创建用户无法控制的影子状态,并与 claude-mem 竞争 context window tokens"(对应 anthropics/claude-code#23544)。


三、多 IDE 集成矩阵

3.1 IDE 检测策略(ide-detection.ts:40-129)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export function detectInstalledIDEs(): IDEInfo[] {
  return [
    // 命令行工具:which/where 探测
    { id: 'claude-code', detected: isCommandInPath('claude'), ... },
    { id: 'copilot-cli', detected: isCommandInPath('copilot'), ... },
    // 桌面 IDE:检查配置目录
    { id: 'cursor',     detected: existsSync(join(home, '.cursor')), ... },
    { id: 'windsurf',   detected: existsSync(join(home, '.codeium', 'windsurf')), ... },
    // VS Code 扩展:扫描 extensions 目录
    { id: 'roo-code',   detected: hasVscodeExtension('roo-code'), ... },
    // 共 12 种 IDE
  ];
}

💡 Tip 无副作用的存在性检测 不通过进程列表或注册表检测,而是检查配置目录/命令是否存在。简单、无权限要求、跨平台。

3.2 三种集成机制

IDE 集成方式 写入位置 功能完整度
Claude Code Plugin API ~/.claude/plugins/ ★★★★★ 6个生命周期
Cursor hooks.json + MCP + MDC ~/.cursor/ ★★★★☆
Gemini CLI hooks.json ~/.gemini/ ★★★★☆
Windsurf hooks.json ~/.codeium/windsurf/ ★★★☆☆
Codex CLI native hooks ~/.codex/ ★★★☆☆
OpenCode/OpenClaw Plugin 包 各自目录 ★★★☆☆
Copilot/Goose/Roo/Warp MCP only IDE 配置目录 ★★☆☆☆

四、Cursor 集成深度解析

4.1 与 Claude Code 的本质区别

维度 Claude Code Cursor
集成方式 Plugin API(Claude Code 主动加载) 原生 hooks.json(install 时主动写入)
记忆注入时机 SessionStart hook,直接注入 stdout .cursor/rules/*.mdc,下次 session 生效
安装位置 ~/.claude/plugins/ ~/.cursor/hooks.json
附加功能 写 MDC 规则文件 + 配置 MCP Server

4.2 Cursor hooks.json 结构(CursorHooksInstaller.ts:236-256)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const makeHookCommand = (command: string) =>
  `"${escapedBunPath}" "${escapedWorkerPath}" hook cursor ${command}`;
//  ↑ 绝对路径               ↑ 同一个 worker-service.cjs     ↑ IDE 标识

const hooksJson: CursorHooksJson = {
  version: 1,
  hooks: {
    beforeSubmitPrompt: [
      { command: makeHookCommand('session-init') },  // 初始化 session
      { command: makeHookCommand('context') }         // 注入历史记忆
    ],
    afterMCPExecution:  [{ command: makeHookCommand('observation') }],
    afterShellExecution:[{ command: makeHookCommand('observation') }],
    afterFileEdit:      [{ command: makeHookCommand('file-edit') }],
    stop:               [{ command: makeHookCommand('summarize') }]
  }
};
// 写入 ~/.cursor/hooks.json

💡 Tip 统一 Worker,IDE 只是参数 Cursor 和 Claude Code 调用的是同一个 worker-service.cjs。Worker 收到 hook cursorhook claude-code 时区分来源,但核心记忆逻辑完全复用。增加新 IDE 支持时,只需要在 IDE 侧写 hooks,Worker 几乎不用改。

4.3 Cursor 独有的上下文注入机制(CursorHooksInstaller.ts:305-342)

Claude Code 可以在 SessionStart 时通过 stdout 把记忆直接注入 AI 的 system context,但 Cursor 没有这个 API。claude-mem 的绕法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
安装时:生成占位文件 .cursor/rules/claude-mem-context.mdc
         ↓ alwaysApply: true → Cursor 每次 chat 自动携带

每次 session 结束后:
  stop hook → summarize → Worker 压缩本次观测
  → Worker 调用 /api/context/inject
  → writeContextFile(workspacePath, context)
  → 更新 .cursor/rules/claude-mem-context.mdc

下次 session 开始时:
  Cursor 自动读取 rules/ 目录 → 携带最新记忆
1
2
3
4
5
6
7
// MDC 文件头(metadata)
const placeholder = `---
alwaysApply: true
description: "Claude-mem context from past sessions (auto-updated)"
---
# Memory Context from Past Sessions
*No context yet. Complete your first session...*`;

⚠️ Warning Cursor 记忆延迟一个 Session Claude Code 在当前 session 的任意 UserPromptSubmit 都能注入记忆,而 Cursor 的记忆注入要等到下一个 session 才生效(当前 session 结束后才更新文件)。这是 Cursor API 的限制,不是 Bug。

4.4 三级安装目标与 MCP Server 配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 三级安装 scope
type CursorInstallTarget = 'project' | 'user' | 'enterprise';
// project    → .cursor/              (只影响当前工程)
// user       → ~/.cursor/            (默认,影响所有项目)
// enterprise → /Library/.../Cursor   (macOS 企业级,需要 sudo)

// 额外配置 MCP server(~/.cursor/mcp.json)
config.mcpServers['claude-mem'] = {
  command: 'node',
  args: [mcpServerPath]   // plugin/scripts/mcp-server.cjs
};

Cursor 是双轨集成:hooks 负责被动记录,MCP server 让 Cursor 可以主动查询历史记忆。


五、多 Profile 隔离机制

5.1 两个环境变量控制一切

1
2
CLAUDE_MEM_DATA_DIR    — 数据根目录(默认 ~/.claude-mem)
CLAUDE_MEM_WORKER_PORT — Worker HTTP 端口(默认 37700 + uid%100)

5.2 路径派生:Single Source of Truth

src/shared/paths.ts 的核心设计是:DATA_DIR 在模块加载时计算一次,此后所有路径都从它派生,不存在任何硬编码的 ~/.claude-mem 子路径。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// paths.ts:18-40 — resolveDataDir() 三层优先级
export function resolveDataDir(): string {
  // 1. 环境变量(最高优先级)
  if (process.env.CLAUDE_MEM_DATA_DIR) return process.env.CLAUDE_MEM_DATA_DIR;

  // 2. 默认路径下的 settings.json(持久化配置)
  const settingsPath = join(homedir(), '.claude-mem', 'settings.json');
  try {
    if (existsSync(settingsPath)) {
      const raw = JSON.parse(readFileSync(settingsPath, 'utf-8'));
      const settings = raw.env ?? raw;
      if (settings.CLAUDE_MEM_DATA_DIR) return settings.CLAUDE_MEM_DATA_DIR;
    }
  } catch { /* 文件缺失或损坏,回退默认值 */ }

  // 3. 硬编码默认值
  return join(homedir(), '.claude-mem');
}

export const DATA_DIR = resolveDataDir();  // 模块级常量,加载时固定

// 所有子路径全部从 DATA_DIR 派生:
export const DB_PATH = join(DATA_DIR, 'claude-mem.db');
export const LOGS_DIR = join(DATA_DIR, 'logs');
export const paths = {
  workerPid:         () => join(DATA_DIR, 'worker.pid'),
  settings:          () => join(DATA_DIR, 'settings.json'),
  database:          () => join(DATA_DIR, 'claude-mem.db'),
  chroma:            () => join(DATA_DIR, 'chroma'),
  transcriptsConfig: () => join(DATA_DIR, 'transcript-watch.json'),
  envFile:           () => join(DATA_DIR, '.env'),
  // ... 共 15 个路径
};

完整路径树

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$CLAUDE_MEM_DATA_DIR  (默认 ~/.claude-mem)
├── claude-mem.db          # SQLite 数据库
├── settings.json          # 用户配置
├── .env                   # 敏感 env(ANTHROPIC_API_KEY 等)
├── worker.pid             # Worker 进程 PID
├── chroma/                # 向量数据库(Chroma)
├── logs/                  # 日志文件
├── archives/              # 归档数据
├── corpora/               # 知识语料库
├── modes/                 # 运行模式配置
├── transcript-watch.json  # Transcript 监视配置
├── supervisor.json        # Supervisor 注册表
└── cursor-projects.json   # Cursor 项目注册表

💡 Tip 单一根路径的隔离完整性 改一个 CLAUDE_MEM_DATA_DIR 环境变量,所有子路径自动跟着变。不可能出现"数据库用了新路径,但 PID 文件还在旧路径"这种状态分裂。Profile 隔离的完整性由架构保证,而非规范要求。

5.3 多 Profile 使用方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 工作账号 profile
export CLAUDE_MEM_DATA_DIR="$HOME/.claude-mem-work"
export CLAUDE_MEM_WORKER_PORT=37801
npx claude-mem start

# 个人 profile(另一个终端)
export CLAUDE_MEM_DATA_DIR="$HOME/.claude-mem-personal"
export CLAUDE_MEM_WORKER_PORT=37802
npx claude-mem start
# 两个 Worker 完全隔离,各自有独立的 DB、Chroma、日志

六、端口计算:37700 + (uid % 100)

代码位置src/shared/SettingsDefaultsManager.ts:73

1
2
CLAUDE_MEM_WORKER_PORT: String(37700 + ((process.getuid?.() ?? 77) % 100)),
//                                       ↑ Windows 无 getuid,fallback 到 77 → 端口 37777

OpenCode 插件里的复制实现(opencode-plugin/index.ts:74-82):

1
2
3
4
5
6
function resolveWorkerPort(): string {
  const fromEnv = process.env.CLAUDE_MEM_WORKER_PORT;
  if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535) return String(parsed);
  const uid = typeof process.getuid === 'function' ? process.getuid() : 77;
  return String(37700 + (uid % 100));  // 完全相同的逻辑
}

为什么用 uid 而不是随机端口?

方案 优点 缺点
随机端口 完全无冲突 每次重启端口变化,hooks 脚本里写死的端口失效
固定端口 (如 8080) 简单 多用户机器必然冲突
37700 + (uid % 100) 同一用户端口稳定;不同用户大概率不同 uid 差 100 的用户会碰撞(极罕见)

💡 Tip uid % 100 的工程价值 hooks.json 里的 bash 命令通过 SettingsDefaultsManager.get('CLAUDE_MEM_WORKER_PORT') 读端口,而不是硬编码。但 Worker 启动是按 uid 计算的,所以同一用户在不同 shell、不同会话里总是找到同一个端口——这是稳定性的保证。

⚠️ Warning uid % 100 的碰撞风险 理论上 uid=1000 和 uid=1100 的用户会碰撞到同一端口(37800)。同机多 Profile 场景应该同时设 CLAUDE_MEM_WORKER_PORT 手动避免。另外,OpenCode 插件和 SettingsDefaultsManager 各自实现了相同的逻辑(DRY 问题),未来修改默认起始端口需要同步两处。


七、Setup Hook 的"缓存依赖检测"机制

7.1 version-check.js 的职责定位

plugin/scripts/version-check.js 作为 Setup 生命周期 hook,在每个 Claude Code session 启动时触发。但它的 timeout 只有 300ms(hooks.json 中配置),所以只做一件事:检测版本是否匹配,不执行安装。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// version-check.js:53-69
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
const markerPath = join(ROOT, '.install-version');

if (!existsSync(markerPath)) {
  emitUpgradeHint('claude-mem: runtime not yet set up - run: npx claude-mem@latest install');
  process.exit(0);  // ← 永远 exit 0,从不阻塞 session
}
const markerVersion = readInstallMarkerVersion(markerPath);
if (markerVersion !== pkg.version) {
  emitUpgradeHint(`claude-mem: upgraded to v${pkg.version} - run: npx claude-mem@latest install`);
}
process.exit(0);

两种提示场景

  1. .install-version 不存在 → 从未安装 runtime,提示用户运行 install
  2. marker 版本 ≠ package.json 版本 → 插件代码已更新但 runtime 未重装,提示升级

7.2 .install-version Marker 文件(setup-runtime.ts:262-287)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 安装完成后写入(setup-runtime.ts:262-274)
export function writeInstallMarker(targetDir, version, bunVersion, uvVersion) {
  const payload: MarkerSchema = {
    version,       // "13.1.0"
    bun: bunVersion, // "1.1.42"  ← 关键:记录安装时的 Bun 版本
    uv: uvVersion,
    installedAt: new Date().toISOString(),
  };
  writeFileSync(join(targetDir, '.install-version'), JSON.stringify(payload));
}

// 判断是否需要重新安装(setup-runtime.ts:277-287)
export function isInstallCurrent(targetDir: string, expectedVersion: string): boolean {
  if (!existsSync(join(targetDir, 'node_modules'))) return false;  // node_modules 被删了
  const marker = readInstallMarker(targetDir);
  if (!marker) return false;
  if (marker.version !== expectedVersion) return false;            // 版本变了

  // 关键检查:Bun 版本变了也要重装
  const currentBun = getBunVersion();
  if (currentBun && marker.bun && currentBun !== marker.bun) return false;
  return true;
}

三重检查逻辑

  1. node_modules/ 必须存在(防止被手动删除)
  2. marker 版本 = 期望版本
  3. 安装时的 Bun 版本 = 现在的 Bun 版本 → Bun 升级后强制重装(防止 native bindings 不兼容)

💡 Tip Marker File 幂等安装模式 这是经典的"安装状态持久化"技巧:

  1. 安装完成后写入包含版本+工具链版本+时间戳的 marker 文件
  2. 下次安装前先比对 marker,匹配则跳过耗时的 bun install(可能 30s+)
  3. 不仅检查 plugin 版本,还检查 Bun 版本——这防止了 Bun 升级后 native addon 失效的隐患 结果:npx claude-mem install 可以安全地反复运行,只在真正需要时才慢。

⚠️ Warning 职责分离:检测 vs 执行 Setup hook 只用 node(不用 Bun),因为它运行在 Bun 可能还未安装 的时机。这是鸡蛋问题:不能用 Bun 来检测 Bun 是否安装好。所以 version-check.js 是纯 Node.js 脚本,只做轻量的文件检查;真正的 bun install 在用户手动运行 npx claude-mem install 时才发生。


八、hooks.json 的动态路径发现机制

Claude Code 的每个 hook 命令都包含一段路径解析前缀(plugin/hooks/hooks.json 中):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 简化版本,原版更复杂(处理 Cygwin/WSL 路径转换)
_C="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
_P=$({
  # 优先查 cache 目录(按时间倒序,取最新版本)
  ls -dt "$_C/plugins/cache/thedotmack/claude-mem"/[0-9]*/ 2>/dev/null
  # 降级到 marketplace 目录
  printf '%s\n' "$_C/plugins/marketplaces/thedotmack/plugin"
} | while IFS= read -r _R; do
  _R="${_R%/}"
  [ -d "$_R/plugin/scripts" ] && _Q="$_R/plugin" || _Q="$_R"
  [ -f "$_Q/scripts/bun-runner.js" ] && { printf '%s\n' "$_Q"; break; }
done)
node "$_P/scripts/bun-runner.js" "$_P/scripts/worker-service.cjs" hook claude-code observation

路径查找优先级(高→低):

  1. $CLAUDE_PLUGIN_ROOT(环境变量覆盖,开发调试用)
  2. ~/.claude/plugins/cache/thedotmack/claude-mem/<最新版本>/
  3. ~/.claude/plugins/marketplaces/thedotmack/plugin/

💡 Tip cache 优先的意义 当用户升级 claude-mem 后(npx claude-mem install 把新版写入 cache),下次 hook 触发时自动用新版本,无需重启 Claude Code。marketplace 目录作为降级方案(可能是旧版本),保证向后兼容。

Setup hook 的特殊性:Setup hook 使用 node version-check.js(而非 bun worker-service.cjs),且只在 Claude Code 自己的插件路径下工作,Cursor 等 IDE 没有 Setup 生命周期。


九、设置系统的三层优先级

SettingsDefaultsManager.ts 实现了完整的配置层级(get 方法,第 137-139 行):

1
2
3
4
5
6
7
// 实际读取:env var > settings.json > 代码默认值
static get(key: keyof SettingsDefaults): string {
  return process.env[key] ?? this.DEFAULTS[key];
}

// loadFromFile() 在文件加载后再调用 applyEnvOverrides()
// 确保 env var 始终覆盖文件配置

优先级链(高→低)

1
2
3
1. process.env[KEY]                  # Shell 级,每次进程生效
2. ~/.claude-mem/settings.json       # 用户持久配置(UI 可写)
3. SettingsDefaultsManager.DEFAULTS  # 代码硬编码默认值

settings.json 的 Schema 迁移(loadFromFile:181-192):旧版把所有配置包在 env: {} 嵌套对象里,新版是扁平结构。加载时自动检测并迁移,迁移后立即写回文件。


十、总结:设计模式与可借鉴思路

模式 代码体现 可借鉴点
文件即事实 existsSync(plugin.json) 判断已安装 不依赖注册表,简单可靠
Marker 文件缓存 .install-version 跳过 bun install 幂等安装,避免重复耗时操作
单一路径根 DATA_DIR 派生所有子路径 隔离=改一个变量,不会状态分裂
uid 派生端口 37700 + uid%100 多用户自动隔离,单用户端口稳定
不阻塞用户 version-check.js 永远 exit 0 检测与执行分离,启动不因插件失败而卡住
白名单 cpSync allowedTopLevelEntries 列表 避免意外复制敏感文件或大文件
裸标志路由 --provider 直接触发 install 零摩擦 CI 安装体验

参考文件速查

文件 行数 核心内容
src/npx-cli/index.ts 184 命令路由,裸标志→install
src/npx-cli/commands/install.ts 1371 完整安装流程,runInstallCommand
src/npx-cli/commands/ide-detection.ts 129 12 种 IDE 检测策略
src/npx-cli/install/setup-runtime.ts 288 Bun/uv 安装,.install-version marker
src/services/integrations/CursorHooksInstaller.ts 588 Cursor hooks.json + MDC 写入
src/integrations/opencode-plugin/index.ts 297 OpenCode 插件,复制端口计算逻辑
src/shared/SettingsDefaultsManager.ts 207 三层优先级配置,uid%100 端口默认值
src/shared/paths.ts 149 DATA_DIR 派生所有路径
plugin/scripts/version-check.js 69 Setup hook,只检测不安装
plugin/hooks/hooks.json Claude Code 6 个生命周期 hook 的 bash 命令