mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-13 04:01:35 +00:00
feat(integrations): configure plugin connections from user settings
This commit is contained in:
parent
2ce7b25d94
commit
8691778a44
16 changed files with 332 additions and 61 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 本身仅使用标准库。
|
||||
|
||||
## 生命周期与失败行为
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"mcp_url": "http://127.0.0.1:2333/mcp",
|
||||
"api_url": "",
|
||||
"auto_recall": true,
|
||||
"auto_memory": true,
|
||||
"recall_limit": 5,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
23
integrations/claude_code/reme/hooks/mcp_bridge.py
Normal file
23
integrations/claude_code/reme/hooks/mcp_bridge.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 本身仅使用标准库。
|
||||
|
||||
## 生命周期与失败行为
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"mcp_url": "http://127.0.0.1:2333/mcp",
|
||||
"api_url": "",
|
||||
"auto_recall": true,
|
||||
"auto_memory": true,
|
||||
"recall_limit": 5,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
23
integrations/codex/plugins/reme/hooks/mcp_bridge.py
Normal file
23
integrations/codex/plugins/reme/hooks/mcp_bridge.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue