diff --git a/integrations/claude_code/README.md b/integrations/claude_code/README.md index ad7f2fcd..e8cd0661 100644 --- a/integrations/claude_code/README.md +++ b/integrations/claude_code/README.md @@ -15,7 +15,7 @@ Start ReMe → Install the ReMe plugin → Use memory in Claude Code ## Requirements -- Python 3.11+ available as `python3` in the host's environment; the plugin uses only the standard library. +- Python 3.11+ available as `python3` in the host's environment, with FastMCP >= 3.1 (included with ReMe). - A current Claude Code version supporting plugin command hooks, `async`, and Stop `last_assistant_message`. - A running ReMe HTTP service exposing `search`, `auto_memory`, and `health_check`, with MCP enabled. - A working model configuration on the ReMe server for memory extraction. Default BM25 recall does not need embeddings. @@ -58,7 +58,10 @@ Set `auto_memory` to `false` for recall-only use. The default service address works immediately after plugin installation. Create a configuration file only when you want to change the defaults below. -Optional settings live at `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme/config.json`. Start from the bundled example: +Edit the user configuration file directly to change addresses and behavior. Plugin upgrades preserve +this file, and only overrides need to be saved. + +Settings live at `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme/config.json`. For a new file, copy the bundled example; edit an existing file in place: ```bash mkdir -p "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme" @@ -67,22 +70,44 @@ cp integrations/claude_code/reme/config.example.json "${CLAUDE_CONFIG_DIR:-$HOME | Option | Default | Meaning | | --- | --- | --- | +| `mcp_url` | `http://127.0.0.1:2333/mcp` | MCP address; removing the trailing `/mcp` gives the default HTTP base for hooks | +| `api_url` | Empty string | Optional HTTP Job API base; required for custom MCP paths | | `auto_recall` | `true` | Search before a user prompt | | `auto_memory` | `true` | Queue and submit completed turns; false also disables retries | | `recall_limit` | `5` | Search result limit | | `recall_min_score` | `0` | Minimum recall score | | `recall_timeout` | `5` | Foreground HTTP timeout in seconds, at most 10 | -| `request_timeout` | `600` | Background HTTP timeout in seconds, at most 600 | +| `request_timeout` | `600` | Memory-write and MCP request timeout in seconds, at most 600 | | `memory_interval` | `5` | Completed turns per batch; use 1 for immediate per-turn writes | | `shutdown_timeout` | `2` | Total best-effort exit drain budget in seconds, at most 2 | | `context_max_chars` | `8000` | Maximum recalled payload characters | | `timezone` | `Asia/Shanghai` | Daily batch timezone; match the ReMe workspace | -Unknown fields and invalid values fail validation. Changes apply on the next hook invocation. -The plugin's `.mcp.json` is the single endpoint setting: both MCP and hooks use its `reme.url`, -which must end in `/mcp`. For a different port or protected proxy, edit that source file **before -installation**, then refresh/reinstall the plugin. Do not edit a versioned installation cache. -There is no separate `REME_HOST`/`REME_PORT` hook override. +For example, use another port and disable automatic recording: + +```json +{ + "mcp_url": "http://127.0.0.1:2444/mcp", + "auto_memory": false, + "auto_recall": true, + "memory_interval": 1 +} +``` + +Both MCP and hooks read addresses from this user configuration. Hooks pick up edits on their next +invocation. After changing an address or the MCP timeout, reconnect MCP or restart the host so the +existing MCP process also reads the new settings. No source edits, cache edits, or reinstall are needed. +For a custom MCP path such as `https://memory.example.com/tools`, also set `api_url` to the HTTP Job +API base, for example `https://memory.example.com/reme`. + +Unknown fields and invalid values fail validation; hooks skip work and log `hook_failed` instead of +falling back to another service. Disabling `auto_memory` stops automatic capture and retries while +preserving pending batches; explicit MCP tools remain available. Changing the service address never +sends old pending conversations to the new service. Remove a field to restore its default. + +The plugin uses a host-managed stdio process to forward MCP. Start the host with a `python3` +environment containing FastMCP. If ReMe runs on another machine or in another Python environment, +run `python3 -m pip install "fastmcp>=3.1"` in the host environment. Hooks use only the standard library. ## Lifecycle and failure behavior diff --git a/integrations/claude_code/README_ZH.md b/integrations/claude_code/README_ZH.md index 2597610b..f405ff0c 100644 --- a/integrations/claude_code/README_ZH.md +++ b/integrations/claude_code/README_ZH.md @@ -14,7 +14,7 @@ ## 环境要求 -- 宿主环境中有 Python 3.11+,可通过 `python3` 执行;插件仅使用标准库。 +- 宿主环境中有 Python 3.11+,可通过 `python3` 执行,并已安装 FastMCP >= 3.1(ReMe 自带此依赖)。 - 当前 Claude Code 版本支持插件、`UserPromptSubmit`、`Stop`、原生异步命令 Hook 和 `last_assistant_message`。 - 已启动 ReMe HTTP 服务,开放 `search`、`auto_memory`、`health_check`,并启用 MCP。 - ReMe 服务端有可用于自动记忆提取的模型配置。默认 BM25 召回不要求 Embedding。 @@ -53,7 +53,9 @@ reme start workspace_dir=/absolute/path/to/workspace service.backend=http 使用默认服务地址时,安装插件后即可使用。需要调整下列默认行为时,再创建配置文件。 -可选配置位于 `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme/config.json`。从仓库根目录复制示例: +直接编辑用户配置文件即可调整地址和行为,升级插件会保留配置。文件中只需写入要覆盖的选项。 + +配置位于 `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme/config.json`。首次创建时,可从仓库根目录复制示例;已有文件请直接编辑: ```bash mkdir -p "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/reme" @@ -62,21 +64,42 @@ cp integrations/claude_code/reme/config.example.json "${CLAUDE_CONFIG_DIR:-$HOME | 配置项 | 默认值 | 作用 | | --- | --- | --- | +| `mcp_url` | `http://127.0.0.1:2333/mcp` | ReMe MCP 地址;默认去掉末尾 `/mcp` 得到 Hook 的 HTTP 地址 | +| `api_url` | 空字符串 | 可选 HTTP Job API 基址;MCP 使用自定义路径时需显式填写 | | `auto_recall` | `true` | 用户提交消息时自动检索 | | `auto_memory` | `true` | 自动捕获与提交;关闭后也不重试已有队列 | | `recall_limit` | `5` | 检索结果上限 | | `recall_min_score` | `0` | 自动召回最低分数 | | `recall_timeout` | `5` | 前台 HTTP 超时秒数,最大 10 | -| `request_timeout` | `600` | 后台 HTTP 超时秒数,最大 600 | +| `request_timeout` | `600` | 写入与 MCP 请求超时秒数,最大 600 | | `memory_interval` | `5` | 每批完成的对话轮数;设为 1 则逐轮提交 | | `shutdown_timeout` | `2` | 退出时尽力刷新队列的总秒数,最大 2 | | `context_max_chars` | `8000` | 召回正文字符上限 | | `timezone` | `Asia/Shanghai` | 批次按此时区分日,应与 ReMe workspace 一致 | -未知字段或无效值会使配置校验失败;配置修改在下一次 Hook 调用生效。 -插件 `.mcp.json` 中的 `reme.url` 是 MCP 和 Hook 共用的唯一服务地址,必须以 `/mcp` 结尾。 -自定义端口或代理时,在安装前修改源码中的此文件,再刷新或重新安装插件,不要修改版本化安装缓存。 -不再提供单独的 `REME_HOST` / `REME_PORT` Hook 地址覆盖。 +例如,改用另一个端口并关闭自动记录: + +```json +{ + "mcp_url": "http://127.0.0.1:2444/mcp", + "auto_memory": false, + "auto_recall": true, + "memory_interval": 1 +} +``` + +MCP 和 Hook 从同一份用户配置读取地址。Hook 在下一次调用时读取修改;更改地址或 MCP 超时后, +请重新连接 MCP 或重启宿主,确保现有 MCP 进程也使用新配置。无需修改插件源码、安装缓存或重装插件。 +使用自定义 MCP 路径时,例如 `https://memory.example.com/tools`,同时设置 +`api_url` 为 HTTP Job API 基址,例如 `https://memory.example.com/reme`。 + +未知字段或无效值会使配置校验失败,Hook 会跳过并记录 `hook_failed`,不会回退到其他服务。 +关闭 `auto_memory` 会停止自动捕获和重试,保留已有队列;显式 MCP 工具仍可使用。 +修改服务地址不会把旧地址下的待提交对话发送到新服务。移除配置项即可恢复该项默认值。 + +插件通过宿主管理的 stdio 连接进程转发 MCP,请确保启动宿主时的 `python3` 环境包含 FastMCP。 +如果 ReMe 服务运行在另一台机器或另一个 Python 环境,在宿主环境执行 +`python3 -m pip install "fastmcp>=3.1"`。Hook 本身仅使用标准库。 ## 生命周期与失败行为 diff --git a/integrations/claude_code/reme/.mcp.json b/integrations/claude_code/reme/.mcp.json index b5d271ee..79bd3890 100644 --- a/integrations/claude_code/reme/.mcp.json +++ b/integrations/claude_code/reme/.mcp.json @@ -1,8 +1,10 @@ { "mcpServers": { "reme": { - "type": "http", - "url": "http://127.0.0.1:2333/mcp" + "command": "python3", + "args": [ + "${CLAUDE_PLUGIN_ROOT}/hooks/mcp_bridge.py" + ] } } } diff --git a/integrations/claude_code/reme/config.example.json b/integrations/claude_code/reme/config.example.json index 311ad726..7e154eb1 100644 --- a/integrations/claude_code/reme/config.example.json +++ b/integrations/claude_code/reme/config.example.json @@ -1,4 +1,6 @@ { + "mcp_url": "http://127.0.0.1:2333/mcp", + "api_url": "", "auto_recall": true, "auto_memory": true, "recall_limit": 5, diff --git a/integrations/claude_code/reme/hooks/auto_memory.py b/integrations/claude_code/reme/hooks/auto_memory.py index 496a716f..bf9269b8 100755 --- a/integrations/claude_code/reme/hooks/auto_memory.py +++ b/integrations/claude_code/reme/hooks/auto_memory.py @@ -22,6 +22,8 @@ HOST = "claude-code" HOME_ENV = "CLAUDE_CONFIG_DIR" HOME_DEFAULT = "~/.claude" DEFAULTS = { + "mcp_url": "http://127.0.0.1:2333/mcp", + "api_url": "", "auto_recall": True, "auto_memory": True, "recall_limit": 5, @@ -41,9 +43,14 @@ def data_dir() -> Path: def load_config() -> dict: - """Read host-local settings; resolve HTTP from the bundled MCP URL.""" + """Read the same persistent settings used by the MCP bridge and lifecycle hooks.""" path = data_dir() / "config.json" values = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {} + return validate_config(values) + + +def validate_config(values: dict) -> dict: + """Validate user overrides before either transport can use them.""" if not isinstance(values, dict) or values.keys() - DEFAULTS.keys(): raise ValueError("Unknown ReMe configuration fields") config = {**DEFAULTS, **values} @@ -52,6 +59,9 @@ def load_config() -> dict: if isinstance(default, bool): if not isinstance(value, bool): raise ValueError(f"{key} must be a boolean") + elif isinstance(default, str): + if not isinstance(value, str): + raise ValueError(f"{key} must be a string") elif isinstance(default, (int, float)): if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value): raise ValueError(f"{key} must be a finite number") @@ -61,20 +71,33 @@ def load_config() -> dict: raise ValueError(f"{key} must be an integer") if config["recall_timeout"] > 10 or config["request_timeout"] > 600 or config["shutdown_timeout"] > 2: raise ValueError("Timeout exceeds the lifecycle hook budget") - ZoneInfo(config["timezone"]) - mcp = json.loads((Path(__file__).resolve().parents[1] / ".mcp.json").read_text(encoding="utf-8")) - url = mcp["mcpServers"]["reme"]["url"].rstrip("/") + try: + ZoneInfo(config["timezone"]) + except (KeyError, ValueError) as exc: + raise ValueError("timezone must be an IANA timezone") from exc + config["mcp_url"] = validate_url(config["mcp_url"]) + if config["api_url"]: + config["endpoint"] = validate_url(config["api_url"]) + elif config["mcp_url"].endswith("/mcp"): + config["endpoint"] = config["mcp_url"][:-4] + else: + raise ValueError("Set api_url explicitly when mcp_url does not end in /mcp") + return config + + +def validate_url(url: str) -> str: + """Accept HTTP endpoints without embedded credentials or ambiguous URL suffixes.""" + url = url.rstrip("/") parsed = urlsplit(url) if ( parsed.scheme not in {"http", "https"} or not parsed.hostname or any((parsed.username is not None, parsed.password is not None, parsed.query, parsed.fragment)) - or not parsed.path.endswith("/mcp") + or any(character.isspace() for character in url) ): - raise ValueError("MCP URL must be an absolute HTTP(S) URL ending in /mcp") + raise ValueError("ReMe URLs must be absolute HTTP(S) URLs without credentials, query, or fragment") _ = parsed.port - config["endpoint"] = url[:-4] - return config + return url def call(config: dict, action: str, payload: dict, timeout: float) -> dict: diff --git a/integrations/claude_code/reme/hooks/mcp_bridge.py b/integrations/claude_code/reme/hooks/mcp_bridge.py new file mode 100644 index 00000000..666f5d6a --- /dev/null +++ b/integrations/claude_code/reme/hooks/mcp_bridge.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Host-managed stdio connection to the user's independently running ReMe service.""" + +from auto_memory import load_config + + +def main() -> None: + """Resolve persistent settings on connection; leave MCP transport handling to FastMCP.""" + config = load_config() + try: + from fastmcp.server import create_proxy + from fastmcp.server.providers.proxy import ProxyClient + except ImportError as exc: + raise SystemExit( + "ReMe MCP requires fastmcp>=3.1 in the host's python3 environment (included in reme-ai).", + ) from exc + upstream = ProxyClient(config["mcp_url"], timeout=config["request_timeout"]) + proxy = create_proxy(upstream, name="ReMe") + proxy.run(transport="stdio", show_banner=False) + + +if __name__ == "__main__": + main() diff --git a/integrations/claude_code/reme/skills/reme-memory/SKILL.md b/integrations/claude_code/reme/skills/reme-memory/SKILL.md index 448854e3..0328849e 100644 --- a/integrations/claude_code/reme/skills/reme-memory/SKILL.md +++ b/integrations/claude_code/reme/skills/reme-memory/SKILL.md @@ -27,8 +27,10 @@ mean the plugin is disabled, the MCP connection failed, or the server's job allo Check the plugin and `/mcp` status before concluding the service is stopped. The default service is started with `reme start workspace_dir=/absolute/path/to/workspace service.backend=http`. -The plugin's `.mcp.json` is the endpoint source for both MCP and automatic hooks. Hook settings and -content-free logs live in the host's `reme/` directory. Hooks batch five completed turns by default; +The host's `reme/config.json` supplies `mcp_url` and hook settings to both the MCP bridge and +automatic hooks. Users can edit this file directly. Reconnect MCP after changing its address; +hook changes apply on the next invocation. Content-free logs live in the host's `reme/` directory. +Hooks batch five completed turns by default; short batches flush at session boundaries, subject to a bounded shutdown budget. Failed batches remain for a later attempt. Do not manually submit the same conversation just because a background write has not finished. For an explicit user request to store a fact, use `auto_memory` with only diff --git a/integrations/codex/README.md b/integrations/codex/README.md index 9a98932f..5514cb6f 100644 --- a/integrations/codex/README.md +++ b/integrations/codex/README.md @@ -15,7 +15,7 @@ Start ReMe → Install the ReMe plugin → Use memory in Codex ## Requirements -- Python 3.11+ available as `python3` in the host's environment; the plugin uses only the standard library. +- Python 3.11+ available as `python3` in the host's environment, with FastMCP >= 3.1 (included with ReMe). - Codex CLI 0.145.0 or a compatible client supporting local plugins, command hooks, and hook trust review. - A running ReMe HTTP service exposing `search`, `auto_memory`, and `health_check`, with MCP enabled. - A working model configuration on the ReMe server for memory extraction. Default BM25 recall does not need embeddings. @@ -59,7 +59,10 @@ Set `auto_memory` to `false` for recall-only use. The default service address works immediately after plugin installation. Create a configuration file only when you want to change the defaults below. -Optional settings live at `${CODEX_HOME:-$HOME/.codex}/reme/config.json`. Start from the bundled example: +Edit the user configuration file directly to change addresses and behavior. Plugin upgrades preserve +this file, and only overrides need to be saved. + +Settings live at `${CODEX_HOME:-$HOME/.codex}/reme/config.json`. For a new file, copy the bundled example; edit an existing file in place: ```bash mkdir -p "${CODEX_HOME:-$HOME/.codex}/reme" @@ -68,22 +71,44 @@ cp integrations/codex/plugins/reme/config.example.json "${CODEX_HOME:-$HOME/.cod | Option | Default | Meaning | | --- | --- | --- | +| `mcp_url` | `http://127.0.0.1:2333/mcp` | MCP address; removing the trailing `/mcp` gives the default HTTP base for hooks | +| `api_url` | Empty string | Optional HTTP Job API base; required for custom MCP paths | | `auto_recall` | `true` | Search before a user prompt | | `auto_memory` | `true` | Queue and submit completed turns; false also disables retries | | `recall_limit` | `5` | Search result limit | | `recall_min_score` | `0` | Minimum recall score | | `recall_timeout` | `5` | Foreground HTTP timeout in seconds, at most 10 | -| `request_timeout` | `600` | Memory-write HTTP timeout in seconds, at most 600 | +| `request_timeout` | `600` | Memory-write and MCP request timeout in seconds, at most 600 | | `memory_interval` | `5` | Completed turns per batch; use 1 for immediate per-turn writes | | `shutdown_timeout` | `2` | Total best-effort exit drain budget in seconds, at most 2 | | `context_max_chars` | `8000` | Maximum recalled payload characters | | `timezone` | `Asia/Shanghai` | Daily batch timezone; match the ReMe workspace | -Unknown fields and invalid values fail validation. Changes apply on the next hook invocation. -The plugin's `.mcp.json` is the single endpoint setting: both MCP and hooks use its `reme.url`, -which must end in `/mcp`. For a different port or protected proxy, edit that source file **before -installation**, then refresh/reinstall the plugin. Do not edit a versioned installation cache. -There is no separate `REME_HOST`/`REME_PORT` hook override. +For example, use another port and disable automatic recording: + +```json +{ + "mcp_url": "http://127.0.0.1:2444/mcp", + "auto_memory": false, + "auto_recall": true, + "memory_interval": 1 +} +``` + +Both MCP and hooks read addresses from this user configuration. Hooks pick up edits on their next +invocation. After changing an address or the MCP timeout, reconnect MCP or restart the host so the +existing MCP process also reads the new settings. No source edits, cache edits, or reinstall are needed. +For a custom MCP path such as `https://memory.example.com/tools`, also set `api_url` to the HTTP Job +API base, for example `https://memory.example.com/reme`. + +Unknown fields and invalid values fail validation; hooks skip work and log `hook_failed` instead of +falling back to another service. Disabling `auto_memory` stops automatic capture and retries while +preserving pending batches; explicit MCP tools remain available. Changing the service address never +sends old pending conversations to the new service. Remove a field to restore its default. + +The plugin uses a host-managed stdio process to forward MCP. Start the host with a `python3` +environment containing FastMCP. If ReMe runs on another machine or in another Python environment, +run `python3 -m pip install "fastmcp>=3.1"` in the host environment. Hooks use only the standard library. ## Lifecycle and failure behavior diff --git a/integrations/codex/README_ZH.md b/integrations/codex/README_ZH.md index 872149ed..0923c2c5 100644 --- a/integrations/codex/README_ZH.md +++ b/integrations/codex/README_ZH.md @@ -14,7 +14,7 @@ ## 环境要求 -- 宿主环境中有 Python 3.11+,可通过 `python3` 执行;插件仅使用标准库。 +- 宿主环境中有 Python 3.11+,可通过 `python3` 执行,并已安装 FastMCP >= 3.1(ReMe 自带此依赖)。 - Codex CLI 0.145.0 或兼容版本,支持插件、`UserPromptSubmit`、`Stop`、同步命令 Hook 和 `last_assistant_message`。 - 已启动 ReMe HTTP 服务,开放 `search`、`auto_memory`、`health_check`,并启用 MCP。 - ReMe 服务端有可用于自动记忆提取的模型配置。默认 BM25 召回不要求 Embedding。 @@ -53,7 +53,9 @@ codex plugin add reme@reme-codex 使用默认服务地址时,安装插件后即可使用。需要调整下列默认行为时,再创建配置文件。 -可选配置位于 `${CODEX_HOME:-$HOME/.codex}/reme/config.json`。从仓库根目录复制示例: +直接编辑用户配置文件即可调整地址和行为,升级插件会保留配置。文件中只需写入要覆盖的选项。 + +配置位于 `${CODEX_HOME:-$HOME/.codex}/reme/config.json`。首次创建时,可从仓库根目录复制示例;已有文件请直接编辑: ```bash mkdir -p "${CODEX_HOME:-$HOME/.codex}/reme" @@ -62,21 +64,42 @@ cp integrations/codex/plugins/reme/config.example.json "${CODEX_HOME:-$HOME/.cod | 配置项 | 默认值 | 作用 | | --- | --- | --- | +| `mcp_url` | `http://127.0.0.1:2333/mcp` | ReMe MCP 地址;默认去掉末尾 `/mcp` 得到 Hook 的 HTTP 地址 | +| `api_url` | 空字符串 | 可选 HTTP Job API 基址;MCP 使用自定义路径时需显式填写 | | `auto_recall` | `true` | 用户提交消息时自动检索 | | `auto_memory` | `true` | 自动捕获与提交;关闭后也不重试已有队列 | | `recall_limit` | `5` | 检索结果上限 | | `recall_min_score` | `0` | 自动召回最低分数 | | `recall_timeout` | `5` | 前台 HTTP 超时秒数,最大 10 | -| `request_timeout` | `600` | 写入 HTTP 超时秒数,最大 600 | +| `request_timeout` | `600` | 写入与 MCP 请求超时秒数,最大 600 | | `memory_interval` | `5` | 每批完成的对话轮数;设为 1 则逐轮提交 | | `shutdown_timeout` | `2` | 退出时尽力刷新队列的总秒数,最大 2 | | `context_max_chars` | `8000` | 召回正文字符上限 | | `timezone` | `Asia/Shanghai` | 批次按此时区分日,应与 ReMe workspace 一致 | -未知字段或无效值会使配置校验失败;配置修改在下一次 Hook 调用生效。 -插件 `.mcp.json` 中的 `reme.url` 是 MCP 和 Hook 共用的唯一服务地址,必须以 `/mcp` 结尾。 -自定义端口或代理时,在安装前修改源码中的此文件,再刷新或重新安装插件,不要修改版本化安装缓存。 -不再提供单独的 `REME_HOST` / `REME_PORT` Hook 地址覆盖。 +例如,改用另一个端口并关闭自动记录: + +```json +{ + "mcp_url": "http://127.0.0.1:2444/mcp", + "auto_memory": false, + "auto_recall": true, + "memory_interval": 1 +} +``` + +MCP 和 Hook 从同一份用户配置读取地址。Hook 在下一次调用时读取修改;更改地址或 MCP 超时后, +请重新连接 MCP 或重启宿主,确保现有 MCP 进程也使用新配置。无需修改插件源码、安装缓存或重装插件。 +使用自定义 MCP 路径时,例如 `https://memory.example.com/tools`,同时设置 +`api_url` 为 HTTP Job API 基址,例如 `https://memory.example.com/reme`。 + +未知字段或无效值会使配置校验失败,Hook 会跳过并记录 `hook_failed`,不会回退到其他服务。 +关闭 `auto_memory` 会停止自动捕获和重试,保留已有队列;显式 MCP 工具仍可使用。 +修改服务地址不会把旧地址下的待提交对话发送到新服务。移除配置项即可恢复该项默认值。 + +插件通过宿主管理的 stdio 连接进程转发 MCP,请确保启动宿主时的 `python3` 环境包含 FastMCP。 +如果 ReMe 服务运行在另一台机器或另一个 Python 环境,在宿主环境执行 +`python3 -m pip install "fastmcp>=3.1"`。Hook 本身仅使用标准库。 ## 生命周期与失败行为 diff --git a/integrations/codex/plugins/reme/.codex-plugin/plugin.json b/integrations/codex/plugins/reme/.codex-plugin/plugin.json index e479d5ea..674dcdba 100644 --- a/integrations/codex/plugins/reme/.codex-plugin/plugin.json +++ b/integrations/codex/plugins/reme/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "reme", - "version": "0.1.0", + "version": "0.1.0+codex.20260928104620", "description": "Automatic recall, batched conversation recording, and explicit MCP memory tools for Codex.", "author": { "name": "EconML team of Alibaba Tongyi Lab", diff --git a/integrations/codex/plugins/reme/.mcp.json b/integrations/codex/plugins/reme/.mcp.json index b5d271ee..727f2753 100644 --- a/integrations/codex/plugins/reme/.mcp.json +++ b/integrations/codex/plugins/reme/.mcp.json @@ -1,8 +1,12 @@ { "mcpServers": { "reme": { - "type": "http", - "url": "http://127.0.0.1:2333/mcp" + "command": "python3", + "args": [ + "hooks/mcp_bridge.py" + ], + "cwd": ".", + "env_vars": ["CODEX_HOME"] } } } diff --git a/integrations/codex/plugins/reme/config.example.json b/integrations/codex/plugins/reme/config.example.json index 311ad726..7e154eb1 100644 --- a/integrations/codex/plugins/reme/config.example.json +++ b/integrations/codex/plugins/reme/config.example.json @@ -1,4 +1,6 @@ { + "mcp_url": "http://127.0.0.1:2333/mcp", + "api_url": "", "auto_recall": true, "auto_memory": true, "recall_limit": 5, diff --git a/integrations/codex/plugins/reme/hooks/auto_memory.py b/integrations/codex/plugins/reme/hooks/auto_memory.py index c5d6c80d..cef8bc0f 100644 --- a/integrations/codex/plugins/reme/hooks/auto_memory.py +++ b/integrations/codex/plugins/reme/hooks/auto_memory.py @@ -22,6 +22,8 @@ HOST = "codex" HOME_ENV = "CODEX_HOME" HOME_DEFAULT = "~/.codex" DEFAULTS = { + "mcp_url": "http://127.0.0.1:2333/mcp", + "api_url": "", "auto_recall": True, "auto_memory": True, "recall_limit": 5, @@ -41,9 +43,14 @@ def data_dir() -> Path: def load_config() -> dict: - """Read host-local settings; resolve HTTP from the bundled MCP URL.""" + """Read the same persistent settings used by the MCP bridge and lifecycle hooks.""" path = data_dir() / "config.json" values = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {} + return validate_config(values) + + +def validate_config(values: dict) -> dict: + """Validate user overrides before either transport can use them.""" if not isinstance(values, dict) or values.keys() - DEFAULTS.keys(): raise ValueError("Unknown ReMe configuration fields") config = {**DEFAULTS, **values} @@ -52,6 +59,9 @@ def load_config() -> dict: if isinstance(default, bool): if not isinstance(value, bool): raise ValueError(f"{key} must be a boolean") + elif isinstance(default, str): + if not isinstance(value, str): + raise ValueError(f"{key} must be a string") elif isinstance(default, (int, float)): if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value): raise ValueError(f"{key} must be a finite number") @@ -61,20 +71,33 @@ def load_config() -> dict: raise ValueError(f"{key} must be an integer") if config["recall_timeout"] > 10 or config["request_timeout"] > 600 or config["shutdown_timeout"] > 2: raise ValueError("Timeout exceeds the lifecycle hook budget") - ZoneInfo(config["timezone"]) - mcp = json.loads((Path(__file__).resolve().parents[1] / ".mcp.json").read_text(encoding="utf-8")) - url = mcp["mcpServers"]["reme"]["url"].rstrip("/") + try: + ZoneInfo(config["timezone"]) + except (KeyError, ValueError) as exc: + raise ValueError("timezone must be an IANA timezone") from exc + config["mcp_url"] = validate_url(config["mcp_url"]) + if config["api_url"]: + config["endpoint"] = validate_url(config["api_url"]) + elif config["mcp_url"].endswith("/mcp"): + config["endpoint"] = config["mcp_url"][:-4] + else: + raise ValueError("Set api_url explicitly when mcp_url does not end in /mcp") + return config + + +def validate_url(url: str) -> str: + """Accept HTTP endpoints without embedded credentials or ambiguous URL suffixes.""" + url = url.rstrip("/") parsed = urlsplit(url) if ( parsed.scheme not in {"http", "https"} or not parsed.hostname or any((parsed.username is not None, parsed.password is not None, parsed.query, parsed.fragment)) - or not parsed.path.endswith("/mcp") + or any(character.isspace() for character in url) ): - raise ValueError("MCP URL must be an absolute HTTP(S) URL ending in /mcp") + raise ValueError("ReMe URLs must be absolute HTTP(S) URLs without credentials, query, or fragment") _ = parsed.port - config["endpoint"] = url[:-4] - return config + return url def call(config: dict, action: str, payload: dict, timeout: float) -> dict: diff --git a/integrations/codex/plugins/reme/hooks/mcp_bridge.py b/integrations/codex/plugins/reme/hooks/mcp_bridge.py new file mode 100644 index 00000000..666f5d6a --- /dev/null +++ b/integrations/codex/plugins/reme/hooks/mcp_bridge.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +"""Host-managed stdio connection to the user's independently running ReMe service.""" + +from auto_memory import load_config + + +def main() -> None: + """Resolve persistent settings on connection; leave MCP transport handling to FastMCP.""" + config = load_config() + try: + from fastmcp.server import create_proxy + from fastmcp.server.providers.proxy import ProxyClient + except ImportError as exc: + raise SystemExit( + "ReMe MCP requires fastmcp>=3.1 in the host's python3 environment (included in reme-ai).", + ) from exc + upstream = ProxyClient(config["mcp_url"], timeout=config["request_timeout"]) + proxy = create_proxy(upstream, name="ReMe") + proxy.run(transport="stdio", show_banner=False) + + +if __name__ == "__main__": + main() diff --git a/integrations/codex/plugins/reme/skills/reme-memory/SKILL.md b/integrations/codex/plugins/reme/skills/reme-memory/SKILL.md index 50bbb39c..c10f9290 100644 --- a/integrations/codex/plugins/reme/skills/reme-memory/SKILL.md +++ b/integrations/codex/plugins/reme/skills/reme-memory/SKILL.md @@ -27,8 +27,10 @@ mean the plugin is disabled, the MCP connection failed, or the server's job allo Check the plugin and `/mcp` status before concluding the service is stopped. The default service is started with `reme start workspace_dir=/absolute/path/to/workspace service.backend=http`. -The plugin's `.mcp.json` is the endpoint source for both MCP and automatic hooks. Hook settings and -content-free logs live in the host's `reme/` directory. Hooks batch five completed turns by default; +The host's `reme/config.json` supplies `mcp_url` and hook settings to both the MCP bridge and +automatic hooks. Users can edit this file directly. Reconnect MCP after changing its address; +hook changes apply on the next invocation. Content-free logs live in the host's `reme/` directory. +Hooks batch five completed turns by default; short batches flush at session boundaries, subject to a bounded shutdown budget. Failed batches remain for a later attempt. Do not manually submit the same conversation just because a queued write has not finished. For an explicit user request to store a fact, use `auto_memory` with only diff --git a/tests/unit/test_coding_agent_plugins.py b/tests/unit/test_coding_agent_plugins.py index 7e2a03ff..a8fe8808 100644 --- a/tests/unit/test_coding_agent_plugins.py +++ b/tests/unit/test_coding_agent_plugins.py @@ -70,6 +70,15 @@ def test_native_manifests_resolve_within_their_own_plugin(): assert manifest["name"] == root.name == "reme" assert (root / ".mcp.json").is_file() assert (root / "skills/reme-memory/SKILL.md").is_file() + mcp = json.loads((root / ".mcp.json").read_text())["mcpServers"]["reme"] + assert mcp["command"] == "python3" + if host == "claude-code": + assert mcp["args"] == ["${CLAUDE_PLUGIN_ROOT}/hooks/mcp_bridge.py"] + else: + assert mcp["args"] == ["hooks/mcp_bridge.py"] + assert mcp["cwd"] == "." + assert "CODEX_HOME" in mcp["env_vars"] + assert (root / "hooks/mcp_bridge.py").is_file() hooks = json.loads((root / "hooks/hooks.json").read_text())["hooks"] assert "SubagentStop" not in hooks for event, groups in hooks.items(): @@ -100,6 +109,18 @@ def test_defaults_and_mcp_endpoint_agree(adapter): {"recall_limit": True}, {"memory_interval": 1.5}, {"shutdown_timeout": 3}, + {"timezone": False}, + {"timezone": "Unknown/Timezone"}, + {"mcp_url": False}, + {"mcp_url": "file:///tmp/mcp"}, + {"mcp_url": "http://name:secret@example.com/mcp"}, + {"mcp_url": "http://example.com/mcp?token=secret"}, + {"mcp_url": "http://example.com/mcp#fragment"}, + {"mcp_url": "http://example.com:invalid/mcp"}, + {"mcp_url": "http://example.com/other"}, + {"mcp_url": "http://example.com/\nmcp"}, + {"api_url": 12}, + {"api_url": "relative/path"}, ], ) def test_invalid_config_fails_explicitly(adapter, values): @@ -108,12 +129,60 @@ def test_invalid_config_fails_explicitly(adapter, values): adapter.load_config() -def test_custom_endpoint_has_one_source(adapter, tmp_path, monkeypatch): - root = tmp_path / "plugin" - root.mkdir() - (root / ".mcp.json").write_text(json.dumps({"mcpServers": {"reme": {"url": "https://example.com/reme/mcp"}}})) - monkeypatch.setattr(adapter, "__file__", str(root / "hooks/auto_memory.py")) - assert adapter.load_config()["endpoint"] == "https://example.com/reme" +def test_custom_endpoint_has_one_source(adapter): + adapter.write_json(adapter.data_dir() / "config.json", {"mcp_url": "https://example.com/reme/mcp/"}) + config = adapter.load_config() + assert config["mcp_url"] == "https://example.com/reme/mcp" + assert config["endpoint"] == "https://example.com/reme" + + +def test_custom_mcp_path_and_http_prefix(adapter): + adapter.write_json( + adapter.data_dir() / "config.json", + { + "mcp_url": "https://example.com/tools/reme", + "api_url": "https://example.com/jobs/reme/", + }, + ) + assert adapter.load_config()["endpoint"] == "https://example.com/jobs/reme" + + +def test_config_edit_takes_effect_on_next_hook(adapter, monkeypatch): + call = Mock(return_value={"success": True, "answer": "daily/fact.md"}) + monkeypatch.setattr(adapter, "call", call) + adapter.write_json(adapter.data_dir() / "config.json", {"auto_recall": False, "auto_memory": False}) + assert adapter.handle_event({"hook_event_name": "UserPromptSubmit", "prompt": "fact"}) == {} + adapter.handle_event({"hook_event_name": "SessionStart"}) + call.assert_not_called() + adapter.write_json( + adapter.data_dir() / "config.json", + {"auto_recall": True, "mcp_url": "http://127.0.0.1:2444/mcp"}, + ) + assert adapter.handle_event({"hook_event_name": "UserPromptSubmit", "prompt": "fact"}) + assert call.call_args.args[0]["endpoint"] == "http://127.0.0.1:2444" + + +def test_mcp_bridge_uses_persistent_configuration(adapter, monkeypatch): + import fastmcp.server + import fastmcp.server.providers.proxy + + adapter.write_json( + adapter.data_dir() / "config.json", + { + "mcp_url": "http://127.0.0.1:2444/mcp", + "request_timeout": 42, + }, + ) + monkeypatch.setitem(sys.modules, "auto_memory", adapter) + spec = importlib.util.spec_from_file_location("reme_test_bridge", Path(adapter.__file__).with_name("mcp_bridge.py")) + bridge = importlib.util.module_from_spec(spec) + spec.loader.exec_module(bridge) + client, proxy = Mock(), Mock() + monkeypatch.setattr(fastmcp.server.providers.proxy, "ProxyClient", client) + monkeypatch.setattr(fastmcp.server, "create_proxy", proxy) + bridge.main() + client.assert_called_once_with("http://127.0.0.1:2444/mcp", timeout=42) + proxy.return_value.run.assert_called_once_with(transport="stdio", show_banner=False) def test_recall_is_bounded_and_cannot_close_evidence_wrapper(adapter, monkeypatch):