Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7d30ed7
feat(gateway): add optional Bearer token authentication
YOMXXX May 15, 2026
73f63e1
feat(plugin): add Claude Code plugin adapter
YOMXXX May 15, 2026
517a377
docs: changelog entry for Claude Code plugin
YOMXXX May 15, 2026
30e4b4e
feat(plugin): add Codex CLI compatibility
YOMXXX May 15, 2026
2e4405a
fix(plugin): use object author in cc manifest
YOMXXX May 15, 2026
ff0a5d5
refactor(plugin): spawn Gateway via npx tdai-memory-gateway bin
YOMXXX May 15, 2026
b7473e6
fix(plugin): recall e2e fixes from real-device cc TUI validation
YOMXXX May 15, 2026
feeb611
fix(plugin): 加固 Gateway Bearer 鉴权与 daemon 进程安全
YOMXXX May 15, 2026
c7f0da9
fix(plugin): Stop hook 改增量发送 + 持久化 cursor,修 L0 重复写入
YOMXXX May 15, 2026
606a48c
fix(plugin): 修订 CJK 停用词集 + L0 文件 streaming 读取 + mtime 排序
YOMXXX May 15, 2026
1f781c8
fix(plugin): daemon 引入 spawn lock + 状态原子写 + 跨平台路径与 cwd 显式化
YOMXXX May 15, 2026
1a2e1fe
fix(plugin): GatewayClient 失败追加 hook.log + memory-status 输出日志路径
YOMXXX May 15, 2026
2dbca67
test: 补全鉴权矩阵、Stop cursor 增量与 ccPid 校验
YOMXXX May 15, 2026
d1bf429
docs: 同步本轮安全加固与 hook 行为到 CHANGELOG + README
YOMXXX May 15, 2026
babcabc
chore: gitignore 忽略本地 CLAUDE.md
YOMXXX May 15, 2026
088edc2
fix(plugin): 补全 .codex-plugin manifest 的 hooks 引用并标注上游已知限制
YOMXXX May 15, 2026
33e3796
docs: 把 Codex 适配状态从"已就绪"降级为"三层 blocker 部分阻塞"
YOMXXX May 15, 2026
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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,12 @@ test-offload-sessions.sh
# npm pack / release tarballs (never commit packaged outputs)
*.tgz
*.tar.gz

# Local development notes (contributor-only, not shipped)
docs/superpowers/

# Per-developer Claude Code project instructions (contributor-only)
CLAUDE.md

# Plugin build output
claude-code-plugin/dist/
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,48 @@

---

## [Unreleased]

### 📦 新功能

- **Claude Code + Codex CLI 插件**(`claude-code-plugin/`):通过 Claude Code `/plugin install tdai-memory` 或 Codex CLI marketplace 一键启用,不修改用户 `~/.claude/settings.json` 或 `~/.codex/config.toml`。提供 3 个 hooks(`SessionStart` 异步预热、`UserPromptSubmit` 同步召回并通过 `additionalContext` 注入、`Stop` 异步捕获),3 个 slash skills(`/memory-search`、`/memory-status`、`/memory-clear-session`),以及一个总览 skill `tdai-memory`。Daemon 通过 `gateway-entry.ts` wrapper 绑定父进程生命周期。插件携带双 manifest(`.claude-plugin/plugin.json` 与 `.codex-plugin/plugin.json`),共享同一份 `hooks/hooks.json` 与 `skills/`。Claude Code(v2026.4+)是当前的一等宿主,端到端完整可用;Codex CLI(v0.130+)在 schema 层(hook 事件名、handler config 字段、`${CLAUDE_PLUGIN_ROOT}` 环境变量)已对齐,但当前部分阻塞——三层 blocker 详见 `claude-code-plugin/README.md`(discovery 层 [openai/codex#22078](https://github.com/openai/codex/issues/22078)、`async` 行为层 Codex 未实现、transcript 解析层只支持 cc 格式)。

### 🔧 兼容性 / 安全增强

- **Gateway 可选 Bearer Token 鉴权**:当设置 `TDAI_GATEWAY_TOKEN` 环境变量时,Gateway 要求所有非 OPTIONS 请求带 `Authorization: Bearer <token>`。未设置时行为不变,与 Hermes 完全向后兼容。Claude Code 插件每次 spawn daemon 时生成随机 256-bit token 写入权限 0600 文件。Bearer 字符串比较升级为 `crypto.timingSafeEqual`,Scheme 关键字按 RFC 6750 §2.1 大小写不敏感匹配(`Bearer`/`bearer`/`BEARER` 均可),401 响应携带 `WWW-Authenticate: Bearer realm="tdai-gateway"`。
- **Token 通过文件路径(`TDAI_TOKEN_PATH`)传递给 daemon 子进程**,不再注入到 `TDAI_GATEWAY_TOKEN` 环境变量。后者会随 execve() 写入子进程初始 environment block,使 token 暴露于 `/proc/<pid>/environ` 与 `ps -E`;改为文件传递后只剩 0o600 token 文件这一面,daemon 加载时还会校验文件 owner uid。
- **daemon 主机绑定加固**:cli.ts 启动时拒绝非 loopback 的 `TDAI_GATEWAY_HOST`,除非显式 `TDAI_GATEWAY_ALLOW_REMOTE=1` 打开开关;防止误把记忆端口曝露到 LAN/公网。
- **新增 `tdai-memory-gateway` bin**(`./dist/src/gateway/cli.mjs`):作为独立可执行 Gateway entry point,支持 `SIGTERM/SIGINT` 优雅关闭、可选父进程 PID liveness 探活(`TDAI_CC_PID` 环境变量,轮询间隔 15s)。供 Claude Code / Codex CLI 插件通过 `npx tdai-memory-gateway` 调用,无需把 npm 依赖打包进插件。
- **daemon 进程管理重写**:基于 `O_CREAT|O_EXCL` 的 `spawn.lock` 互斥,并发触发的 SessionStart / UserPromptSubmit / Stop hook 中只有一个会真正 spawn,其余复用结果,根本性解决双 daemon / 端口与 token 错配问题;`state.json` 改 tmp + rename 原子写;`ensureRunning` 复用旧 daemon 前校验 `state.ccPid` 与当前 cc 一致,避免跨用户/跨会话错用旧 daemon;spawn 时显式设置 `cwd` 与 `TDAI_DATA_DIR` 注入,避免数据目录受 hook 进程 cwd 漂移影响;token 文件权限校验在 Windows 上跳过 `0o077` 位检测(Node `fs` 在 Win 下返回固定 mode 会误报),改用 NTFS ACL。
- **`$ARGUMENTS` 命令注入面收敛**:cc 当前对 SKILL.md ``!`...` `` 块内的 `$ARGUMENTS` 执行字面 `replaceAll`,用户输入 `foo"; curl evil; "` 可注入到 shell(详见 anthropics/claude-code#16163)。重写 `memory-search/SKILL.md` 去掉 ``!`...` `` bash 块,改为引导 Claude 以 heredoc 通过 Bash 工具向 `hook.mjs search-stdin` 的 stdin 喂查询,用户输入不再经过 shell 词法解析。

### 🐛 修复

- **Stop hook 反复重写 L0**:之前每次 Stop 都向 `/capture` 全量发送最近 10 个 turn,而 Gateway 端 `originalUserMessageCount` 位置切片与 `afterTimestamp` 游标都缺失(`CaptureRequest` 不携带这两个字段),导致长会话前 N 个 turn 在每次 Stop 时反复写入 L0,污染 FTS5 与向量索引。改为基于 `$CLAUDE_PLUGIN_DATA/cursors/<sessionId>.json` 持久化的 `lastSentIndex` 取增量,首次发送以 50 turn 封顶,cursor 文件 tmp + rename 原子写。
- **CJK 召回退化**:底层 2-gram 停用词表此前包含 `我们/你们/他们/这个/那个/可以/有没/没有/就是/不是` 等普通双字实义词,"我们的部署方案" 被切成 `[们的, 的部, 部署, 署方, 方案]`、丢失 "我们" 锚点 token,中文查询召回受损。停用词表缩到真正低信息量的疑问/连接片段。
- **transcript 等待逻辑**:Stop hook 等待 cc 落盘从硬 sleep(800ms) 改为 `waitForTranscriptStable(2s)`:每 100ms 轮询 `stat().size`,连续两次相同字节数即视为 flush 完成;慢盘场景更稳。
- **L0 jsonl 直查内存压力**:`searchL0JsonlDirect` 从 `readFile` 整体加载改为 `readline + createReadStream` 流式扫描,避免长会话 jsonl 触发 OOM;文件遍历从字符串排序+reverse(依赖 `YYYY-MM-DD.jsonl` 命名)改为 mtime 倒序,对 cc UUID 命名也工作正常。
- **GatewayClient silent-failure 可观测**:所有 catch 块新增 `logPath` 失败追加,handleStatus 在 `/memory-status` 输出 `hook.log` / `daemon.log` 路径;daemon spawn 的 stdio stderr 重定向到 `daemon.log` 替代静默丢弃。
- **Codex CLI plugin 端 hooks 注册补全**:`.codex-plugin/plugin.json` 之前只声明了 `"skills": "./skills/"`,缺 `"hooks": "./hooks/hooks.json"` —— Codex CLI 与 Claude Code 不同,plugin-local hooks 不走"约定俗成路径",而是强制从 manifest 的 `hooks` 字段读取(见 `codex-rs/core-plugins/src/manifest.rs::RawPluginManifest`)。补上字段后 schema 层全部对齐:`SessionStart`/`UserPromptSubmit`/`Stop` 事件名、`command`/`timeout`/`statusMessage` handler 字段、`${CLAUDE_PLUGIN_ROOT}` 环境变量在 Codex 端都能解析(discovery.rs 注入了 `CLAUDE_PLUGIN_ROOT` backcompat alias,同时配 `PLUGIN_ROOT` 新名)。**注意 schema 层兼容 ≠ runtime 行为对齐**:Codex 解析 `async` 字段但实际硬编码为 sync 执行(`HookExecutionMode::Sync`,`core/src/hook_runtime.rs` 与 `hooks/src/engine/` 都没有消费 `r#async` 字段的代码),与 cc 的真异步行为不同;详见 README 中的 Codex 状态说明。

### ✅ 测试

- `auth.test.ts`:从 5 个 case 扩展到 14 个,覆盖鉴权对所有 POST 业务端点的矩阵、Bearer scheme 大小写、mangled Authorization 头、`WWW-Authenticate` 响应。
- `hook.test.ts`:新增 cursor 增量、无新 turn 跳过 captureTurn、`MAX_CAPTURE_TURNS=50` 边界 3 个 case,且把 stop describe 整体 stub `CLAUDE_PLUGIN_DATA` 到 mkdtemp 隔离 cursor 状态。
- `daemon.test.ts`:新增 `ensureRunning` 拒绝 ccPid 不匹配旧 state 的回归。

### 📚 文档

- `claude-code-plugin/README.md` 与 `README_CN.md`:安装、配置、数据布局、排障与安全模型完整说明,新增 `TDAI_TOKEN_PATH` / `TDAI_GATEWAY_ALLOW_REMOTE` / `TDAI_GATEWAY_CORS_ORIGIN` / Windows 兼容性说明。
- `claude-code-plugin/README.md` 与 `README_CN.md`:Codex CLI 安装段下重写 "Codex CLI 当前状态:部分阻塞" 小节,披露与 cc 对等之前的三层 blocker:
1. **Discovery 层(上游阻塞)**:`source_type = "local"` 安装受上游 [openai/codex#22078](https://github.com/openai/codex/issues/22078) 影响,manifest 解析正常但 `skills/` 与 `hooks/hooks.json` 在运行时被静默丢弃,hook 根本不会触发;
2. **`async` 行为层(Codex 不实现)**:Codex 解析 `async` 字段但 `HookRunSummary` 硬编码 `HookExecutionMode::Sync`,cc 端 `SessionStart`/`Stop` 上的 `async: true + timeout: 30` 在 Codex 修复 #22078 后会变成同步 30s 阻塞;计划用单独 `hooks/codex-hooks.json` 差异化 timeout(待办);
3. **Transcript 解析层(plugin 端未适配)**:Codex rollout jsonl schema `{timestamp, type, payload}` 与 cc transcript `{type, message, sessionId, parentUuid, …}` 完全不同,当前 `lib/transcript.ts` 仅解析 cc 格式,即使 Stop 在 Codex 上触发也会静默生成空 capture;Codex parser 是后续工作,等 #22078 修复后基于真实 Codex session 实现。

当前 Codex 上真正能用的部分:manifest 解析、`/plugin` 可见可切换、`lib/daemon.ts` 宿主无关 daemon spawn 与 cc 共用同一段代码。同时同步降级 README 与 CHANGELOG 顶部"双宿主对齐"的过度乐观表述。

---

## [0.3.4] - 2026-05-12

### 🐛 修复
Expand Down
11 changes: 11 additions & 0 deletions claude-code-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "tdai-memory",
"version": "0.1.0",
"description": "Long-term + symbolic short-term memory for Claude Code, powered by TencentDB Agent Memory.",
"homepage": "https://github.com/Tencent/TencentDB-Agent-Memory",
"license": "MIT",
"author": {
"name": "李冠辰",
"email": "liguanchen@xiaomi.com"
}
}
41 changes: 41 additions & 0 deletions claude-code-plugin/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "tdai-memory",
"version": "0.1.0",
"description": "Long-term + symbolic short-term memory for AI coding agents, powered by TencentDB Agent Memory.",
"author": {
"name": "李冠辰",
"email": "liguanchen@xiaomi.com"
},
"homepage": "https://github.com/Tencent/TencentDB-Agent-Memory",
"repository": "https://github.com/Tencent/TencentDB-Agent-Memory",
"license": "MIT",
"keywords": [
"memory",
"long-term-memory",
"short-term-memory",
"ai-memory",
"vector-search",
"sqlite-vec",
"persona",
"scene-extraction"
],
"skills": "./skills/",
"hooks": "./hooks/hooks.json",
"interface": {
"displayName": "TDAI Memory",
"shortDescription": "Long-term + short-term memory for AI coding agents",
"longDescription": "Adds long-term memory and symbolic short-term memory to Codex CLI: automatic recall before every prompt (relevant past memories injected via additionalContext), automatic capture after every turn (L0 conversation written, L1/L2/L3 atoms/scenarios/persona extracted in the background), and manual control via skills (memory-search, memory-status, memory-clear-session). Memory is partitioned per project (hash of cwd) by default. The daemon runs locally on 127.0.0.1 with a Bearer-token-protected HTTP API (file mode 0600).",
"developerName": "TencentDB Agent Memory contributors",
"category": "Productivity",
"capabilities": [
"Read",
"Write"
],
"brandColor": "#3B82F6",
"defaultPrompt": [
"Do you remember what we discussed about this project?",
"Search my memory for the migration plan we made last week",
"What were my preferences for the API design?"
]
}
}
125 changes: 125 additions & 0 deletions claude-code-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# TencentDB Agent Memory — Coding Agent Plugin

Long-term + symbolic short-term memory for [Claude Code](https://claude.com/claude-code) and [OpenAI Codex CLI](https://developers.openai.com/codex/cli), powered by [TencentDB Agent Memory](https://github.com/Tencent/TencentDB-Agent-Memory).

The plugin ships dual manifests (`.claude-plugin/plugin.json` and `.codex-plugin/plugin.json`) and reuses the same `hooks/hooks.json` and `skills/`. Claude Code (v2026.4+) and Codex CLI (v0.130+) share the hook protocol at the schema layer (event names, handler config fields, `${CLAUDE_PLUGIN_ROOT}` env var). **Claude Code is the first-class target today; Codex CLI is partially blocked** — see the [Codex CLI](#codex-cli) install section below for current status.

[中文版](./README_CN.md)

## What this gives you

- **Automatic recall** before every prompt — relevant past memories injected into context
- **Automatic capture** after every turn — L0 conversation written, L1/L2/L3 extracted in the background
- **Manual control** via slash skills: `/memory-search`, `/memory-status`, `/memory-clear-session`
- **Project-level isolation** by default (sessionKey = hash of cwd) — your `react-app` memories don't leak into your `golang-svc` work
- **Bearer-secured local daemon** — no plaintext localhost API

## Installation

### Prerequisite

Install the gateway runtime (the `tdai-memory-gateway` bin) globally — the plugin spawns the daemon via `npx tdai-memory-gateway`:

```bash
npm install -g @tencentdb-agent-memory/memory-tencentdb
```

This npm package contains the actual `TdaiGateway` (SQLite + sqlite-vec + LLM pipeline). The plugin itself is a thin shim that owns hooks, skills, and the per-session sessionKey — it does NOT bundle the heavy deps.

### Claude Code

```bash
/plugin install tdai-memory
```

### Codex CLI

```bash
codex plugin marketplace add <marketplace-url>
# then enable in the TUI: /plugin → toggle tdai-memory
```

(Once published to the Codex marketplace, this becomes a one-liner.)

> **Codex CLI status (≤ v0.130): partially blocked.** Three layered blockers separate the current Codex experience from Claude Code parity:
>
> 1. **Plugin discovery (upstream blocker).** `source_type = "local"` marketplace installs are affected by Codex issue [openai/codex#22078](https://github.com/openai/codex/issues/22078): the manifest parses, the plugin appears in `/plugin` and is toggleable, but the declared `skills/` and `hooks/hooks.json` are silently dropped at runtime. Hooks never fire on Codex today.
>
> 2. **`async` hook field is parsed but not honored.** Codex deserializes the `async` field on hook commands (`codex-rs/config/src/hook_config.rs::HookHandlerConfig::Command`), but no code in `core/src/hook_runtime.rs` or `hooks/src/engine/` consumes it — `HookRunSummary` is hardcoded to `HookExecutionMode::Sync`. Our `SessionStart` and `Stop` hooks declare `async: true, timeout: 30`. Once #22078 ships, this means a Codex session start will block synchronously on first-run daemon spawn, and every Stop will block on capture. Planned mitigation: a separate `hooks/codex-hooks.json` referenced from `.codex-plugin/plugin.json` with shorter timeouts.
>
> 3. **`lib/transcript.ts` only parses the Claude Code transcript format.** Codex records sessions to `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-*.jsonl` with shape `{timestamp, type: "session_meta" | …, payload: {…}}`, completely different from Claude Code's `{type, message, sessionId, parentUuid, …}`. Even if Stop fired on Codex, capture would silently produce empty turns. A Codex JSONL parser is planned once #22078 lets us validate end-to-end against a live Codex session.
>
> **What works today on Codex:** `.codex-plugin/plugin.json` is parsed correctly, the plugin is visible and toggleable in `/plugin`, and the shared daemon spawn / discovery logic in `lib/daemon.ts` is host-agnostic (same code path as Claude Code). Use Claude Code for actual memory functionality; track #22078 for the upstream fix.

---

No `~/.claude/settings.json` or `~/.codex/config.toml` mutation. The first time a session starts after installation, the plugin spawns the local daemon (via `npx tdai-memory-gateway`) on port 8421–8430 with a randomly generated Bearer token. State persists under `${CLAUDE_PLUGIN_DATA}`.

## Configuration

The plugin reads these optional environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `TDAI_SESSION_KEY` | `hash(cwd)` | Override the per-project memory partition |
| `TDAI_TOKEN_PATH` | auto-generated 0o600 file | Path to a file the daemon reads the Bearer token from (preferred over `TDAI_GATEWAY_TOKEN`; the env-var form puts the token into `/proc/<pid>/environ` and `ps -E`) |
| `TDAI_GATEWAY_TOKEN` | unset | Bearer token via env (fallback for the Hermes sidecar mode) |
| `TDAI_GATEWAY_HOST` | `127.0.0.1` | Daemon bind host. Non-loopback values are refused unless `TDAI_GATEWAY_ALLOW_REMOTE=1` is set, to avoid exposing the memory port to the LAN. |
| `TDAI_GATEWAY_ALLOW_REMOTE` | unset | Opt-in switch required to bind a non-loopback `TDAI_GATEWAY_HOST` |
| `TDAI_GATEWAY_CORS_ORIGIN` | unset | When set, enables CORS with the given Origin; the default disables CORS so cross-origin pages cannot probe the daemon's port. |
| `TDAI_GATEWAY_COMMAND` | `npx` | Override daemon spawn command (advanced; e.g. `node /path/to/cli.mjs` for development) |

Most users never need to set any of these. `TDAI_SESSION_KEY=shared-with-other-project` is the most common power-user override.

## Data location

- `${CLAUDE_PLUGIN_DATA}/state.json` — daemon PID + port (tmp+rename atomic)
- `${CLAUDE_PLUGIN_DATA}/token` — Bearer token (chmod 600, owner-uid checked)
- `${CLAUDE_PLUGIN_DATA}/spawn.lock` — O_CREAT|O_EXCL daemon-spawn mutex (stale after 60s)
- `${CLAUDE_PLUGIN_DATA}/cursors/<sessionId>.json` — per-cc-session `lastSentIndex` so Stop only POSTs new turns
- `${CLAUDE_PLUGIN_DATA}/memory-tdai/` — SQLite + sqlite-vec database, scene blocks, persona snapshots
- `${CLAUDE_PLUGIN_DATA}/hook.log` — hook diagnostic log (gateway-client failures, etc.)
- `${CLAUDE_PLUGIN_DATA}/daemon.log` — daemon stderr/stdout (cold-start crashes, etc.)

## How it works

```
User prompt → UserPromptSubmit hook → POST /recall → cc injects context
cc replies → Stop hook → POST /capture → L0 + L1/L2/L3 pipeline
Session end → daemon detects parent cc exit → graceful shutdown
```

All hook handlers fail silently (writing to `hook.log`) — memory is never on the critical path of your conversation.

## Troubleshooting

**`/memory-status` says "unreachable"**:
- Check `${CLAUDE_PLUGIN_DATA}/hook.log` (gateway-client request failures) and `${CLAUDE_PLUGIN_DATA}/daemon.log` (daemon cold-start crashes)
- Restart your cc session — the SessionStart hook re-probes and re-spawns the daemon

**Multiple cc terminals on the same project**:
- All terminals share one daemon. The first to launch spawns it; subsequent terminals discover and reuse it via `state.json`.

**Memory doesn't recall what I expect**:
- Run `/memory-search <topic>` directly to see what's stored
- Note that L1/L2/L3 extraction runs asynchronously — fresh conversations may need a few minutes before they appear in recall

## Security model

- The daemon listens only on `127.0.0.1` by default. Non-loopback `TDAI_GATEWAY_HOST` is refused unless `TDAI_GATEWAY_ALLOW_REMOTE=1` is also set.
- Every request requires `Authorization: Bearer <token>`. Comparison is timing-safe; the scheme keyword is RFC 6750 §2.1 case-insensitive; 401 responses include `WWW-Authenticate: Bearer realm="tdai-gateway"`.
- The token is generated freshly at each daemon spawn, written to `${CLAUDE_PLUGIN_DATA}/token` (chmod 600), and passed to the daemon child process **by file path** (`TDAI_TOKEN_PATH`) rather than as an env var, so the token does not surface via `/proc/<pid>/environ` or `ps -E`. Token-file owner is checked against the current uid on read.
- The `memory-search` skill passes the user query to the daemon over **stdin** via a heredoc, never as a shell argv element — this avoids the literal-`replaceAll` `$ARGUMENTS` injection surface in cc (anthropics/claude-code#16163).
- On Windows the 0o077 mode check is skipped (Node's `fs` returns fixed mode bits there); the OS-provided NTFS ACL on the token file is relied on instead.

## Building from source

```bash
pnpm install
pnpm build:cc-plugin
pnpm test:cc-plugin
```

## License

MIT — see [LICENSE](../LICENSE).
Loading