diff --git a/.gitignore b/.gitignore index c563eb5..e690067 100644 --- a/.gitignore +++ b/.gitignore @@ -31,5 +31,8 @@ node_modules/ data/ mex-dsh-plugin/ +# WorkBuddy 项目数据(记忆日志等,含隐私) +.workbuddy/ + # 内部改造计划文档(含个人决策信息,不入库) docs/opensource-refactor.md \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md index 3c253f5..d83eb19 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -3,7 +3,7 @@ > 版本:v1.2(待用户审查) > 日期:2026-08-06 > 依据:`docs/requirements.md`(最终需求)、三份竞品评估报告(已归档至 `docs/archive/research/`) -> 状态说明:本文档记录顶层架构决策及其理由,作为后续开发的依据。ADR 部分(含 ADR-12)已与用户确认 ✅;§3.5-3.7、§4.3、§5.2、§6.4、§7、§9、§10 为补齐的 gap,待审查。 +> 状态说明:本文档记录顶层架构决策及其理由,作为后续开发的依据。ADR 部分已与用户确认 ✅(ADR-12 已于 2026-08-20 修订:skill 承载读写、会话结束 hook 留待扩展);§3.5-3.7、§4.3、§5.2、§6.4、§7、§9、§10 为补齐的 gap,待审查。 ## 1. 产品定位与设计目标 @@ -209,21 +209,21 @@ mex add --topic finance --content "今天基金跌了 3%,有点焦虑" **理由**:个人量级(年约千条)下 FTS5 检索无压力(保留原结论);兼顾"隐私与数字遗忘"需求——用户能设定某些敏感对话到期自动销毁。`--expires` 只接受绝对日期/时间(本地转 UTC),不支持 `24h` 等相对时长(用户决定)。 -### ADR-12:extract 触发与 agent 集成 —— hook 兜底写、skill 管读与即时写 ✅ +### ADR-12:extract 触发与 agent 集成 —— skill 承载读写,hook 留待扩展 ✅(2026-08-20 修订) -**决策**:hook 与 skill 都采用,读写分工: +**决策**:以 skill 说明书承载 agent 侧的读写引导,`mex integrate ` 生成 skill(写入官方 `skills//SKILL.md` 发现路径)+ README: -- **写入主路径(hook 兜底)**:在 agent 软件(OpenCode / Claude Code)配置"会话结束"hook,自动执行 `mex extract --file <会话文件>`(后台异步)。hook 是 agent 软件的确定性事件规则,必然触发、不经过大模型判断,保证不漏记。 -- **写入即时路径(skill 引导)**:skill 说明书告诉 agent——当用户明确说"记住这个"时,立即执行 `mex extract "<用户原话>"` 将该内容传入,重要信息即时入库,不等会话结束。 -- **读取路径(skill 引导)**:skill 教 agent 在对话开始执行 `mex profile` 注入画像(自动模式)、需要细节时执行 `mex search`(手动模式)。 +- **写入即时路径(skill 引导)**:skill 告诉 agent——用户明确说"记住这个"时立即执行 `mex extract "<用户原话>"`;简单明确的单条事实也可用 `mex add`。重要信息即时入库,不等会话结束。 +- **读取路径(skill 引导)**:skill 教 agent 在对话开始执行 `mex profile` 注入画像(自动模式)、涉及生活/情绪/近期状态时执行 `mex search --since`(时敏召回)、需要细节时执行 `mex search --topic/--keyword`(手动模式)。 - **手动命令是地基**:`mex extract --file ` / `mex extract "<文本>"` 随时可用。 -- **集成落地**:`mex integrate --scope project|global` 生成 hook 配置与 skill 文件,**由用户指定生成到项目级目录(仅该项目生效)还是全局目录(所有项目生效)**。 +- **集成落地**:`mex integrate --scope project|global` 生成 skill 说明书 + README,**由用户指定生成到项目级目录(仅该项目生效)还是全局目录(所有项目生效)**。 +- **会话结束 hook 暂缓**:早期方案以"hook 兜底写"为主路径,但 OpenCode 官方配置 schema 无 hooks 字段(原生不支持 Claude Code 风格的生命周期 hook),第一版**不生成 hook 配置**,写路径由 skill 引导即时写承担。hook 能力(第三方插件桥接 / 包装脚本聚合会话)留待后续扩展。 **理由**: -- hook 与 skill 不是二选一的竞争关系:hook 是确定性规则(必然触发但不理解内容),适合"写入兜底";skill 是大模型自主判断(理解内容但可能漏),适合"读取调用"与"即时写入"这种需要理解对话语义的场景。两者组合互补短板。 +- skill 是 Claude Code / OpenCode 均支持的官方扩展点,承载读写引导无平台差异;hook 在 OpenCode 上无官方支持,作为主路径不成立。 +- skill 引导即时写依赖大模型自主判断(可能漏记),hook 兜底可弥补此短板——这正是 hook 留待后续扩展的动机(本 ADR 记录在案)。 - "每轮对话后实时抽取"明确不做:LLM 延迟打断对话节奏,成本无谓放大。 -- hook 全量触发的 token 浪费可忽略:增量抽取(§8.3)保证重复触发只处理新增内容;纯 coding 会话返回空结果(§7.2 契约),一次调用约几分钱。 - scope 参数的意义:用户可把工作项目(不需要记忆系统)与个性化对话场景分开——项目级配置只在该项目目录生效,全局配置对所有会话生效。 ## 3. 数据模型 @@ -565,7 +565,7 @@ src/mex/ | `mex import ` | 从导出文件恢复,或从 v1 markdown 抽取迁移 | `--mode restore\|extract` `--on-conflict skip\|overwrite` | | `mex config llm` | 交互式配置 LLM 供应商与模型(TTY 引导选择预设;非 TTY 需参数) | `--provider openai\|deepseek\|ollama\|moonshot\|qwen\|custom` `--base-url` `--api-key` `--model` | | `mex config show` | 查看当前 LLM 配置(key 打码) | `--json` | -| `mex integrate ` | 生成 agent 的 hook 配置与 skill 文件(ADR-12) | `--scope project\|global`(默认 global) | +| `mex integrate ` | 生成 agent 的 skill 说明书与 README(ADR-12) | `--scope project\|global`(默认 global) | | `mex stats` | 统计:画像内/画像外条目数、待审查数、库大小、LLM 累计用量 | | 斜杠命令入口不属于 mex 本身:在 agent 侧配置 skill 文件调用 `mex profile` 即可。 @@ -742,15 +742,18 @@ system 消息按序拼接 8 部分,外加 user 消息(对话文本)。** ## 8. Agent 集成 -meX 不修改 agent 软件本身,通过它们公开的 hook 机制与 skill 机制对接(ADR-12)。 +meX 不修改 agent 软件本身,通过 agent 软件公开的 skill 机制引导其读写记忆(ADR-12); +会话结束 hook(OpenCode 官方不支持)留待后续扩展。 ### 8.1 触发时机总览 | 时机 | 机制 | 说明 | |---|---|---| -| 会话结束 | hook 自动触发 | agent 软件执行 `mex extract --file <会话文件> --from claude|opencode`,后台异步,不阻塞开新会话 | -| 用户明确说"记住这个" | skill 引导 agent 即时执行 | `mex extract "<用户原话>"`,重要信息即时入库 | +| 用户明确说"记住这个" | skill 引导 agent 即时执行 | `mex extract "<用户原话>"`(或 `mex add` 写单条事实),重要信息即时入库 | +| 对话开始 | skill 引导 agent 加载背景 | `mex profile`,把画像注入上下文 | +| 需要细节 / 近期状态 | skill 引导 agent 检索 | `mex search --topic`(定向)/ `mex search --since`(时敏召回) | | 任意时刻 | 手动 `mex extract --file ` 或 `mex extract "<文本>"` | 地基,永远可用 | +| 会话结束自动抽取 | ❌ 暂缓(hook) | OpenCode 官方配置无 hooks 字段,第一版不生成 hook 配置,留待后续扩展(ADR-12) | | 每轮对话后 | ❌ 明确不做 | LLM 延迟打断对话节奏,成本无谓放大 | ### 8.2 会话解析适配器 @@ -767,7 +770,7 @@ meX 不修改 agent 软件本身,通过它们公开的 hook 机制与 skill `extraction_state` 表(§3.1)记录每个会话文件"已处理到哪个位置": -- 对同一会话文件重复触发 extract 时(hook 多次触发、手动补抽),只处理 `last_position` 之后的新增内容——不重复抽取、不重复写入、不重复扣费。 +- 对同一会话文件重复触发 extract 时(重复触发、手动补抽),只处理 `last_position` 之后的新增内容——不重复抽取、不重复写入、不重复扣费。 - 处理位置与记忆写入在同一事务中更新(§6.1 第 7 步),保证"处理成功才记录"。 - 边界情况:若文件比上次记录的位置还短(被重建 / 截断),视为新文件从头处理。 @@ -782,20 +785,22 @@ mex integrate opencode --scope project # 生成到 ./.opencode/ - `--scope project`(项目级):配置生成到当前项目目录,只对该目录下的 agent 会话生效。 - `--scope global`(全局,**默认**):生成到用户级配置目录,对所有项目的会话生效。 -- 生成内容:hook 配置(会话结束执行 extract)+ skill 说明书文件。 +- 生成内容:skill 说明书(写入官方 `skills//SKILL.md` 发现路径)+ README(对接步骤)。 + 不再生成 hook 配置——OpenCode 官方配置 schema 无 hooks 字段,会话结束自动抽取留待后续扩展(ADR-12)。 - 执行后输出实际写入的文件路径清单,便于检查;重复执行覆盖旧文件(幂等)。 典型用法:全局开启 = 所有会话都接入记忆系统;项目级 = 只给特定项目接入(如把纯工作代码项目排除在外)。 ### 8.5 skill 说明书内容要点 -生成的 skill 文件教 agent 五件事: +生成的 skill 文件教 agent 读写记忆(含写作原则): 1. **对话开始**:执行 `mex profile`,把输出作为用户背景注入上下文(自动模式)。 2. **对话涉及用户生活、情绪或近期状态时**:先执行 `mex search --since 7d` 拉取近期记录(含画像外记录与画像槽位),感知用户最近发生了什么(时敏性召回;天数默认 7,可配置)。 3. **需要细节时**:执行 `mex search --topic <领域> [--keyword ...]`(手动模式)。 -4. **用户明确说"记住这个"**:立即把用户原话经 `mex extract "<用户原话>"` 写入。 -5. **不要主动、频繁调用 extract**:写路径由会话结束 hook 兜底,避免重复花费。 +4. **用户明确说"记住这个"**:立即把用户原话经 `mex extract "<用户原话>"` 写入;简单明确的单条事实也可用 `mex add`。 +5. **不要主动、频繁调用写命令**:除非用户明确要求或说出"记住这个",否则不主动调用 `mex extract` / `mex add`——写路径按需触发,避免重复花费 LLM 成本或写入噪音。 +6. **写作原则**:具体(保留名称/数字/日期/角色/原因)、独立(不读上下文可理解,禁止"本项目/它/那个"等指代)、有用、不重复;时间写具体日期(YYYY-MM-DD);稳定事实写画像槽位(topic + sub_topic)、临时状态写画像外记录(省略 sub_topic);人名/项目名用全称。 ## 9. 本地存储布局 @@ -808,7 +813,7 @@ mex integrate opencode --scope project # 生成到 ./.opencode/ 数据目录默认为 `~/.mex`,可用环境变量 `MEX_HOME` 覆盖(测试隔离、多实例场景使用)。 -**安装方式**:项目为可安装的 Python 包,`pip install -e .`(开发模式)或 `uv sync` 安装后 `mex` 命令进入 PATH。integrate 生成的 hook/skill 均依赖 PATH 中的 `mex`,故安装是集成的前置条件。 +**安装方式**:项目为可安装的 Python 包,`pip install -e .`(开发模式)或 `uv sync` 安装后 `mex` 命令进入 PATH。integrate 生成的 skill 依赖 PATH 中的 `mex`,故安装是集成的前置条件。 **config.yaml 完整配置项**: diff --git a/src/mex/cli/integrate.py b/src/mex/cli/integrate.py index 73d2216..57c2de6 100644 --- a/src/mex/cli/integrate.py +++ b/src/mex/cli/integrate.py @@ -1,4 +1,4 @@ -"""``mex integrate`` 命令:生成 agent hook 配置与 skill 说明书(M8)。 +"""``mex integrate`` 命令:生成 agent 的 skill 说明书与 README(M8)。 用法: mex integrate claude|opencode|dsh [--scope project|global] [--json] @@ -18,14 +18,14 @@ _SCOPES = ("project", "global") -@app.command("integrate", help="Generate agent integration files (hook config + skill guide + README)") +@app.command("integrate", help="Generate agent integration files (skill guide + README)") @run def integrate_cmd( agent: str = typer.Argument(..., help="Target agent: claude / opencode / dsh"), scope: str = typer.Option("global", "--scope", help="project (current dir) / global (user dir, default)"), json_output: bool = typer.Option(False, "--json", help="Output as JSON"), ) -> None: - """生成 agent 集成文件(hook 配置 + skill 说明书 + README)。""" + """生成 agent 集成文件(skill 说明书 + README)。""" if agent not in AGENT_TARGETS and agent != "dsh": raise UserError(f"Unknown agent '{agent}'. Supported agents: {_SUPPORTED_AGENTS}") if scope not in _SCOPES: @@ -48,18 +48,16 @@ def _print_human(result: IntegrationResult) -> None: def _next_steps(agent: AgentName) -> str: """下一步说明:按 agent 类型给出不同的安装指引。""" if agent == "claude": - hook = "~/.claude/settings.json, project ./.claude/settings.json" - skill = "~/.claude/skills/mex/SKILL.md, project ./.claude/skills/mex/SKILL.md" + skill = "~/.claude/skills/mex/SKILL.md (global), ./.claude/skills/mex/SKILL.md (project)" return ( - f"Next steps: paste the config in hooks.md into the \"hooks\" field of {agent}'s config " - f"({hook} for global); put the content of skill.md into the skill file ({skill} for global)." + f"Next steps: the skill is ready at {skill}. " + "Start a new session for it to take effect, then ask the agent to run `mex profile` to verify." ) if agent == "opencode": - hook = "~/.config/opencode/opencode.json, project ./.opencode/opencode.json" - skill = "~/.config/opencode/skill/mex/SKILL.md, project ./.opencode/skill/mex/SKILL.md" + skill = "~/.config/opencode/skills/mex/SKILL.md (global), ./.opencode/skills/mex/SKILL.md (project)" return ( - f"Next steps: paste the config in hooks.md into the \"hooks\" field of {agent}'s config " - f"({hook} for global); put the content of skill.md into the skill file ({skill} for global)." + f"Next steps: the skill is ready at {skill}. " + "Start a new session for it to take effect, then ask the agent to run `mex profile` to verify." ) # dsh return ( diff --git a/src/mex/integration/claude/README.md b/src/mex/integration/claude/README.md index 1ef3d8c..c30c8f3 100644 --- a/src/mex/integration/claude/README.md +++ b/src/mex/integration/claude/README.md @@ -2,14 +2,16 @@ > 由 `mex integrate claude` 生成,生成目录:`{{AGENT_DIR}}` -本目录包含 meX 与 Claude Code 对接所需的 3 个文件: +本目录包含 meX 与 Claude Code 对接所需的文件: | 文件 | 用途 | |---|---| -| `hooks.md` | hook 配置说明 + JSON 片段(会话结束自动抽取记忆) | -| `skill.md` | skill 说明书(教 Claude 何时读取 / 写入记忆) | +| `skills/mex/SKILL.md` | skill 说明书(教 agent 何时读取 / 写入记忆) | | `README.md` | 本文件(对接步骤) | +> 说明:本集成以 skill 承载记忆读写(读:`mex profile` / `mex search`;写:用户说"记住这个" +> 时 `mex extract` 即时写入)。会话结束 hook 自动抽取能力统一留待后续扩展,暂不生成 hook 配置。 + ## 前置条件 1. meX 已初始化:`mex init`(数据目录 `{{MEX_HOME}}`)。 @@ -17,34 +19,24 @@ ## 对接步骤 -### 1. 配置 hook(会话结束自动抽取) - -打开 `hooks.md`,把其中的 JSON 片段粘贴到 Claude Code 的 `settings.json`: - -- 全局(推荐,所有项目生效):`~/.claude/settings.json` 的 `"hooks"` 字段 -- 项目级(仅当前项目):`./.claude/settings.json` 的 `"hooks"` 字段 - -注意把 JSON 中的 `<会话文件路径>` 替换为实际会话文件路径 -(典型位置:`~/.claude/projects/<项目名>/<会话id>.jsonl`)。 - -### 2. 安装 skill(教 Claude 用记忆) +### 1. 安装 skill(教 Claude 用记忆) -把 `skill.md` 的内容放入 skill 文件: +skill 已生成到本目录 `skills/mex/SKILL.md`。Claude Code 从以下位置自动发现 skill: -- 全局:`~/.claude/skills/mex/SKILL.md` -- 项目级:`./.claude/skills/mex/SKILL.md` +- 全局(推荐,所有项目生效):`{{AGENT_DIR}}/skills/mex/SKILL.md`(即 `~/.claude/skills/mex/SKILL.md`) +- 项目级(仅当前项目):`./.claude/skills/mex/SKILL.md` -即新建 `mex` 目录并把 `skill.md` 复制为其中的 `SKILL.md`。 +如果生成目录与上述位置一致,直接**重新打开一个 Claude Code 会话**即可生效; +若不一致,把 `skills/mex/` 目录复制到对应位置。 -### 3. 验证 +### 2. 验证 -1. 重新打开一个 Claude Code 会话(让新 hook / skill 生效)。 +1. 重新打开一个 Claude Code 会话(让新 skill 生效)。 2. 让 Claude 执行 `mex profile`,确认能输出用户画像。 -3. 对 Claude 说"记住这个 我喜欢喝美式咖啡",确认它执行 - `mex extract "我喜欢喝美式咖啡"`。 -4. 结束会话,确认没有报错;运行 `mex list` 能看到新记忆。 +3. 对 Claude 说"记住这个:我喜欢喝美式咖啡",确认它执行 `mex extract "我喜欢喝美式咖啡"`。 +4. 运行 `mex list` 能看到新记忆。 ## 数据与回滚 - 数据目录:`{{MEX_HOME}}`(数据库 `{{DB_PATH}}`,可用 `mex export` 备份)。 -- 想取消集成:删掉 settings.json 中粘贴的 hook 片段与 skill 目录即可,不影响既有记忆。 +- 想取消集成:删掉 skill 目录(`skills/mex/`)即可,不影响既有记忆。 diff --git a/src/mex/integration/claude/hooks.md b/src/mex/integration/claude/hooks.md deleted file mode 100644 index dba5c85..0000000 --- a/src/mex/integration/claude/hooks.md +++ /dev/null @@ -1,39 +0,0 @@ -# meX × Claude Code:会话结束自动抽取(hook 配置) - -> 由 `mex integrate claude` 生成,数据目录:`{{MEX_HOME}}` - -把下面的 JSON 片段合并到 Claude Code 的 hook 配置中,会话结束时会自动把 -本次对话交给 meX 抽取个人记忆(异步执行,不阻塞你开新会话)。 - -## 配置位置 - -- 全局(所有项目生效):`~/.claude/settings.json` 的 `"hooks"` 字段 -- 项目级(仅当前项目):`./.claude/settings.json` 的 `"hooks"` 字段 - -## JSON 片段 - -```json -{ - "hooks": { - "SessionEnd": [ - { - "matcher": "", - "hooks": [ - { "type": "command", "command": "mex extract --file <会话文件路径> --from claude" } - ] - } - ] - } -} -``` - -## 使用说明 - -1. 把 `<会话文件路径>` 替换为实际会话文件路径。Claude Code 的会话存储为 - JSONL,典型位置:`~/.claude/projects/<项目名>/<会话id>.jsonl`。 -2. 确保 `mex` 命令在 PATH 中(项目 `pip install -e .` 或 `uv sync` 安装后生效)。 -3. 记忆写入 meX 数据库:`{{DB_PATH}}`(可用环境变量 `MEX_HOME` 覆盖数据目录)。 -4. 同一会话文件重复触发 extract 会自动增量处理(只抽取新增内容,不重复扣费)。 - -> 注:hook 事件名与配置结构以 Claude Code 官方文档为准;本模板给出当前主流写法, -> 如与你使用的版本不符,请按官方文档调整事件名。 diff --git a/src/mex/integration/claude/skill.md b/src/mex/integration/claude/skill.md index 2212fcd..3860fb2 100644 --- a/src/mex/integration/claude/skill.md +++ b/src/mex/integration/claude/skill.md @@ -1,11 +1,17 @@ -# meX skill:个人记忆助手(Claude Code) +--- +name: mex +description: >- + meX 个人记忆系统的使用指南。当对话涉及用户的个人背景(工作、生活、健康、情绪、偏好、家庭)、 + 用户说"记住这个"或类似表述、或需要回忆用户之前说过的事情时使用。通过 mex CLI 读写本地记忆库。 +--- -> 由 `mex integrate claude` 生成。安装方法见同目录 README.md。 -> 数据目录:`{{MEX_HOME}}` +# meX 个人记忆助手(Claude Code skill) -你(Claude)是 meX 个人记忆系统在 Claude Code 中的使用指南。meX 是一个本地 -优先的个人记忆库,帮你记住用户的画像(schema 字段定义的画像槽位)与画像外 -记录(临时/近期状态,按时间索引)。按以下规则在对话中使用: +> 由 `mex integrate claude` 生成。数据目录:`{{MEX_HOME}}` + +你是 meX 个人记忆系统在 Claude Code 中的使用指南。meX 是本地优先的个人记忆库,帮你记住用户的 +画像(schema.yaml 声明的画像槽位,形如 `topic.sub_topic`)与画像外记录(临时/近期状态, +按时间索引)。按以下规则在对话中使用。 ## 1. 对话开始时:加载用户背景 @@ -20,7 +26,7 @@ mex search --since <7 天前的日期,格式 YYYY-MM-DD> ``` -拉取最近 7 天的记忆(含不常驻画像快照的画像外记录),感知用户最近发生了什么(时敏性召回)。 +拉取最近 7 天的记忆(含画像外记录),感知用户最近发生了什么。 ## 3. 需要细节时:定向检索 @@ -31,24 +37,44 @@ mex search --topic <领域> [--keyword ...] ``` `--topic` 用画像领域(如 work/health/finance),`--keyword` 做内容关键词过滤。 +不确定有哪些领域时,先 `mex profile` 或 `mex list` 查看。 -## 4. 用户明确说"记住这个":即时写入 +## 4. 用户明确说"记住这个"时:即时写入 -当用户明确说"记住这个"(或类似表述)时,立即把用户原话写入 meX: +用户明确说"记住这个"(或类似表述)时,把用户原话交给 meX 抽取: ``` mex extract "<用户原话>" ``` -重要信息即时入库,不要等会话结束。 +`mex extract` 内部调用 LLM 抽取,会自动把信息归类到画像槽位或画像外记录,无需你手动分类。 + +简单、明确的单条事实也可以直接用 `mex add`: + +``` +mex add --topic <领域> --sub-topic <字段> --content "<事实>" +``` + +(省略 `--sub-topic` 时作为画像外记录写入。) + +## 5. 写作原则(写记忆前必须遵守) + +meX 是跨项目记忆系统,写下的记忆会在任何项目、任何时间被长期读取,必须脱离当前会话上下文自包含: + +- **具体**:保留名称、数字、日期、角色、原因;不写模糊概括。 +- **独立**:不读本次对话上下文也能理解;禁止"本项目/它/那个/这家/那边"等指代,写具体项目名、公司名、人名。 +- **有用**:只记影响未来对话、决策或行动的信息,不记过程流水。 +- **不重复**:与已有记忆相同的内容不再写入(不确定时先 `mex search` 查一下)。 +- **时间**:用具体日期(YYYY-MM-DD),禁止"最近/昨天/下周/前几天"等相对表述。 +- **画像 vs 画像外**:稳定事实(姓名、公司、职位、学历、家庭、偏好)写入画像槽位(topic + sub_topic);临时状态(今天完成的事、情绪、计划、近况)作为画像外记录(省略 sub_topic)。 +- **全称**:人名/项目名用全称,首次出现时必要时附简短说明(如"meX(本地记忆系统)")。 -## 5. 不要主动频繁调用 extract +## 6. 不要主动频繁调用写命令 -除非用户明确要求或说出"记住这个",否则不要调用 `mex extract` 相关的写操作—— -写路径由会话结束 hook 兜底,频繁调用会重复花费 LLM 成本。 +除非用户明确要求或说出"记住这个",否则不要主动调用 `mex extract` / `mex add`—— +写路径按需触发,频繁调用会重复花费 LLM 成本或写入噪音。 ## 其他 - 任何命令报错时,参考 `mex --help` 查看用法。 -- 数据目录:`{{MEX_HOME}}`(数据库 `{{DB_PATH}}`,环境变量 `MEX_HOME` 可覆盖, - 全局配置 `config.yaml` 在数据目录内)。 +- 数据目录:`{{MEX_HOME}}`(数据库 `{{DB_PATH}}`,环境变量 `MEX_HOME` 可覆盖)。 diff --git a/src/mex/integration/opencode/README.md b/src/mex/integration/opencode/README.md index 822f47d..aa1939c 100644 --- a/src/mex/integration/opencode/README.md +++ b/src/mex/integration/opencode/README.md @@ -2,14 +2,17 @@ > 由 `mex integrate opencode` 生成,生成目录:`{{AGENT_DIR}}` -本目录包含 meX 与 OpenCode 对接所需的 3 个文件: +本目录包含 meX 与 OpenCode 对接所需的文件: | 文件 | 用途 | |---|---| -| `hooks.md` | hook 配置说明 + JSON 片段(会话结束自动抽取记忆) | -| `skill.md` | skill 说明书(教 agent 何时读取 / 写入记忆) | +| `skills/mex/SKILL.md` | skill 说明书(教 agent 何时读取 / 写入记忆) | | `README.md` | 本文件(对接步骤) | +> 说明:OpenCode 目前不支持 Claude Code 风格的"会话结束 hook"(官方配置没有 hooks 字段), +> 因此本集成不再生成 hook 配置——记忆写入由 skill 引导 agent 在对话中即时完成 +> (用户说"记住这个"时执行 `mex extract`)。hook 自动抽取能力留待后续扩展。 + ## 前置条件 1. meX 已初始化:`mex init`(数据目录 `{{MEX_HOME}}`)。 @@ -17,34 +20,24 @@ ## 对接步骤 -### 1. 配置 hook(会话结束自动抽取) - -打开 `hooks.md`,把其中的 JSON 片段粘贴到 OpenCode 的 `opencode.json`: - -- 全局(推荐,所有项目生效):`~/.config/opencode/opencode.json` 的 `"hooks"` 字段 -- 项目级(仅当前项目):`./.opencode/opencode.json` 的 `"hooks"` 字段 - -注意把 JSON 中的 `<会话文件路径>` 替换为实际会话文件路径 -(OpenCode 会话存储在 `~/.local/share/opencode/` 下,按项目分目录)。 - -### 2. 安装 skill(教 agent 用记忆) +### 1. 安装 skill(教 agent 用记忆) -把 `skill.md` 的内容放入 skill 文件: +skill 已生成到本目录 `skills/mex/SKILL.md`。OpenCode 从以下位置自动发现 skill: -- 全局:`~/.config/opencode/skill/mex/SKILL.md` -- 项目级:`./.opencode/skill/mex/SKILL.md` +- 全局(推荐,所有项目生效):`{{AGENT_DIR}}/skills/mex/SKILL.md`(即 `~/.config/opencode/skills/mex/SKILL.md`) +- 项目级(仅当前项目):`./.opencode/skills/mex/SKILL.md` -即新建 `mex` 目录并把 `skill.md` 复制为其中的 `SKILL.md`。 +如果生成目录与上述位置一致,直接**重新打开一个 OpenCode 会话**即可生效; +若不一致,把 `skills/mex/` 目录复制到对应位置。 -### 3. 验证 +### 2. 验证 -1. 重新打开一个 OpenCode 会话(让新 hook / skill 生效)。 +1. 重新打开一个 OpenCode 会话(让新 skill 生效)。 2. 让 agent 执行 `mex profile`,确认能输出用户画像。 -3. 对 agent 说"记住这个 我喜欢喝美式咖啡",确认它执行 - `mex extract "我喜欢喝美式咖啡"`。 -4. 结束会话,确认没有报错;运行 `mex list` 能看到新记忆。 +3. 对 agent 说"记住这个:我喜欢喝美式咖啡",确认它执行 `mex extract "我喜欢喝美式咖啡"`。 +4. 运行 `mex list` 能看到新记忆。 ## 数据与回滚 - 数据目录:`{{MEX_HOME}}`(数据库 `{{DB_PATH}}`,可用 `mex export` 备份)。 -- 想取消集成:删掉 opencode.json 中粘贴的 hook 片段与 skill 目录即可,不影响既有记忆。 +- 想取消集成:删掉 skill 目录(`skills/mex/`)即可,不影响既有记忆。 diff --git a/src/mex/integration/opencode/hooks.md b/src/mex/integration/opencode/hooks.md deleted file mode 100644 index 0f3281c..0000000 --- a/src/mex/integration/opencode/hooks.md +++ /dev/null @@ -1,40 +0,0 @@ -# meX × OpenCode:会话结束自动抽取(hook 配置) - -> 由 `mex integrate opencode` 生成,数据目录:`{{MEX_HOME}}` - -把下面的 JSON 片段合并到 OpenCode 的 hook 配置中,会话结束时会自动把 -本次对话交给 meX 抽取个人记忆(异步执行,不阻塞你开新会话)。 - -## 配置位置 - -- 全局(所有项目生效):`~/.config/opencode/opencode.json` 的 `"hooks"` 字段 -- 项目级(仅当前项目):`./.opencode/opencode.json` 的 `"hooks"` 字段 - -## JSON 片段 - -```json -{ - "$schema": "https://opencode.ai/config.json", - "hooks": { - "SessionEnd": [ - { - "matcher": "mex", - "hooks": [ - { "type": "command", "command": "mex extract --file <会话文件路径> --from opencode" } - ] - } - ] - } -} -``` - -## 使用说明 - -1. 把 `<会话文件路径>` 替换为实际会话文件路径。OpenCode 的会话存储在 - `~/.local/share/opencode/` 下(按项目分目录的 JSON 文件)。 -2. 确保 `mex` 命令在 PATH 中(项目 `pip install -e .` 或 `uv sync` 安装后生效)。 -3. 记忆写入 meX 数据库:`{{DB_PATH}}`(可用环境变量 `MEX_HOME` 覆盖数据目录)。 -4. 同一会话文件重复触发 extract 会自动增量处理(只抽取新增内容,不重复扣费)。 - -> 注:hook 事件名与配置结构以 OpenCode 官方文档为准;本模板给出当前主流写法, -> 如与你使用的版本不符,请按官方文档调整事件名。 diff --git a/src/mex/integration/opencode/skill.md b/src/mex/integration/opencode/skill.md index b86d198..7116acd 100644 --- a/src/mex/integration/opencode/skill.md +++ b/src/mex/integration/opencode/skill.md @@ -1,11 +1,17 @@ -# meX skill:个人记忆助手(OpenCode) +--- +name: mex +description: >- + meX 个人记忆系统的使用指南。当对话涉及用户的个人背景(工作、生活、健康、情绪、偏好、家庭)、 + 用户说"记住这个"或类似表述、或需要回忆用户之前说过的事情时使用。通过 mex CLI 读写本地记忆库。 +--- -> 由 `mex integrate opencode` 生成。安装方法见同目录 README.md。 -> 数据目录:`{{MEX_HOME}}` +# meX 个人记忆助手(OpenCode skill) -你(agent)是 meX 个人记忆系统在 OpenCode 中的使用指南。meX 是一个本地 -优先的个人记忆库,帮你记住用户的画像(schema 字段定义的画像槽位)与画像外 -记录(临时/近期状态,按时间索引)。按以下规则在对话中使用: +> 由 `mex integrate opencode` 生成。数据目录:`{{MEX_HOME}}` + +你是 meX 个人记忆系统在 OpenCode 中的使用指南。meX 是本地优先的个人记忆库,帮你记住用户的 +画像(schema.yaml 声明的画像槽位,形如 `topic.sub_topic`)与画像外记录(临时/近期状态, +按时间索引)。按以下规则在对话中使用。 ## 1. 对话开始时:加载用户背景 @@ -20,7 +26,7 @@ mex search --since <7 天前的日期,格式 YYYY-MM-DD> ``` -拉取最近 7 天的记忆(含不常驻画像快照的画像外记录),感知用户最近发生了什么(时敏性召回)。 +拉取最近 7 天的记忆(含画像外记录),感知用户最近发生了什么。 ## 3. 需要细节时:定向检索 @@ -31,24 +37,44 @@ mex search --topic <领域> [--keyword ...] ``` `--topic` 用画像领域(如 work/health/finance),`--keyword` 做内容关键词过滤。 +不确定有哪些领域时,先 `mex profile` 或 `mex list` 查看。 -## 4. 用户明确说"记住这个":即时写入 +## 4. 用户明确说"记住这个"时:即时写入 -当用户明确说"记住这个"(或类似表述)时,立即把用户原话写入 meX: +用户明确说"记住这个"(或类似表述)时,把用户原话交给 meX 抽取: ``` mex extract "<用户原话>" ``` -重要信息即时入库,不要等会话结束。 +`mex extract` 内部调用 LLM 抽取,会自动把信息归类到画像槽位或画像外记录,无需你手动分类。 + +简单、明确的单条事实也可以直接用 `mex add`: + +``` +mex add --topic <领域> --sub-topic <字段> --content "<事实>" +``` + +(省略 `--sub-topic` 时作为画像外记录写入。) + +## 5. 写作原则(写记忆前必须遵守) + +meX 是跨项目记忆系统,写下的记忆会在任何项目、任何时间被长期读取,必须脱离当前会话上下文自包含: + +- **具体**:保留名称、数字、日期、角色、原因;不写模糊概括。 +- **独立**:不读本次对话上下文也能理解;禁止"本项目/它/那个/这家/那边"等指代,写具体项目名、公司名、人名。 +- **有用**:只记影响未来对话、决策或行动的信息,不记过程流水。 +- **不重复**:与已有记忆相同的内容不再写入(不确定时先 `mex search` 查一下)。 +- **时间**:用具体日期(YYYY-MM-DD),禁止"最近/昨天/下周/前几天"等相对表述。 +- **画像 vs 画像外**:稳定事实(姓名、公司、职位、学历、家庭、偏好)写入画像槽位(topic + sub_topic);临时状态(今天完成的事、情绪、计划、近况)作为画像外记录(省略 sub_topic)。 +- **全称**:人名/项目名用全称,首次出现时必要时附简短说明(如"meX(本地记忆系统)")。 -## 5. 不要主动频繁调用 extract +## 6. 不要主动频繁调用写命令 -除非用户明确要求或说出"记住这个",否则不要调用 `mex extract` 相关的写操作—— -写路径由会话结束 hook 兜底,频繁调用会重复花费 LLM 成本。 +除非用户明确要求或说出"记住这个",否则不要主动调用 `mex extract` / `mex add`—— +写路径按需触发,频繁调用会重复花费 LLM 成本或写入噪音。 ## 其他 - 任何命令报错时,参考 `mex --help` 查看用法。 -- 数据目录:`{{MEX_HOME}}`(数据库 `{{DB_PATH}}`,环境变量 `MEX_HOME` 可覆盖, - 全局配置 `config.yaml` 在数据目录内)。 +- 数据目录:`{{MEX_HOME}}`(数据库 `{{DB_PATH}}`,环境变量 `MEX_HOME` 可覆盖)。 diff --git a/src/mex/services/integrate.py b/src/mex/services/integrate.py index c22673d..8f905fc 100644 --- a/src/mex/services/integrate.py +++ b/src/mex/services/integrate.py @@ -1,8 +1,10 @@ -"""M8 agent 集成服务:生成 hook 配置与 skill 说明书文件(架构文档 §8、ADR-12)。 +"""M8 agent 集成服务:生成 skill 说明书与 README 文件(架构文档 §8、ADR-12)。 仅生成文件、不执行任何外部命令;幂等(重复执行覆盖旧文件,不报错)。 -claude / opencode 产出 hook.md + skill.md + README.md(写入 agent 配置目录); +claude / opencode 产出 skill 说明书(写入官方 skills/mex/SKILL.md 发现路径)+ README.md +(写入 agent 配置目录)。不再生成 hooks.md——OpenCode 原生不支持 SessionEnd hook +(官方配置 schema 无 hooks 字段),写路径由 skill 引导 agent 即时写承担,hook 能力留待扩展; dsh 产出标准 DSH bundle 目录(index.js + package.json + cordis.patch.yml + README.md)。 """ @@ -22,9 +24,15 @@ AgentName = Literal["claude", "opencode", "dsh"] Scope = Literal["project", "global"] -FILE_NAMES = ("hooks.md", "skill.md", "README.md") +# claude/opencode 的生成物布局:(模板文件名, 目标相对路径)。 +# skill 说明书写入官方 skills//SKILL.md(skills 为复数,Claude Code / OpenCode 的 +# 官方发现路径),README 写配置目录根。hooks.md 不再生成(见模块 docstring)。 +SKILL_FILE_LAYOUT = ( + ("skill.md", "skills/mex/SKILL.md"), + ("README.md", "README.md"), +) -# DSH bundle 的组成文件(与 FILE_NAMES 不同的独立清单)。 +# DSH bundle 的组成文件(独立于 SKILL_FILE_LAYOUT 的清单)。 DSH_BUNDLE_FILES = ("index.js", "package.json", "cordis.patch.yml", "README.md") @@ -76,12 +84,23 @@ def integrate(agent: AgentName, scope: Scope) -> IntegrationResult: def _generate_skill_files(agent: AgentName, base_dir: Path) -> list[str]: - """渲染 claude/opencode 的 hook/skill/README 三件套写入 base_dir。""" + """渲染 claude/opencode 的 skill 说明书 + README 写入 base_dir。 + + skill.md 落到 ``base_dir/skills/mex/SKILL.md``(官方发现路径),README 落根目录。 + + Args: + agent: 目标 agent(claude / opencode)。 + base_dir: agent 配置根目录。 + + Returns: + 写入的文件绝对路径清单。 + """ mex_home = get_mex_home() written: list[str] = [] - for name in FILE_NAMES: - target = base_dir / name - content = _render_template(agent, name, base_dir, mex_home) + for template_name, rel_path in SKILL_FILE_LAYOUT: + target = base_dir / rel_path + content = _render_template(agent, template_name, base_dir, mex_home) + target.parent.mkdir(parents=True, exist_ok=True) target.write_text(content, encoding="utf-8") written.append(str(target)) logger.info("已生成 {}:{}", agent, target) @@ -134,7 +153,7 @@ def _render_template(agent: AgentName, name: str, base_dir: Path, mex_home: str) Args: agent: 目标 agent(决定模板子目录)。 - name: 模板文件名(hooks.md / skill.md / README.md)。 + name: 模板文件名(skill.md / README.md)。 base_dir: 目标根目录(渲染进 {{AGENT_DIR}})。 mex_home: 数据目录(渲染进 {{MEX_HOME}} / {{DB_PATH}})。 diff --git a/tests/cli/test_integrate.py b/tests/cli/test_integrate.py index d7b85d8..e95fc06 100644 --- a/tests/cli/test_integrate.py +++ b/tests/cli/test_integrate.py @@ -51,12 +51,12 @@ def test_success_outputs_paths(self, mex_home, tmp_path, monkeypatch, agent, sco result = runner.invoke(app, ["integrate", agent, "--scope", scope]) assert result.exit_code == 0, result.stderr - assert "hooks.md" in result.stdout and "skill.md" in result.stdout + assert "SKILL.md" in result.stdout and "README.md" in result.stdout assert "Next steps" in result.stdout target = expected_dir(tmp_path, agent, scope) - for name in ("hooks.md", "skill.md", "README.md"): - assert (target / name).exists() - assert str(target / name) in result.stdout + for rel in ("skills/mex/SKILL.md", "README.md"): + assert (target / rel).exists() + assert str(target / rel) in result.stdout def test_default_scope_is_global(self, mex_home, tmp_path, monkeypatch): fake_home(tmp_path, monkeypatch) @@ -64,7 +64,7 @@ def test_default_scope_is_global(self, mex_home, tmp_path, monkeypatch): result = runner.invoke(app, ["integrate", "claude"]) assert result.exit_code == 0, result.stderr - assert (tmp_path / ".claude" / "hooks.md").exists() + assert (tmp_path / ".claude" / "skills" / "mex" / "SKILL.md").exists() def test_unknown_agent_exit_1(self, mex_home, tmp_path, monkeypatch): fake_home(tmp_path, monkeypatch) @@ -92,7 +92,7 @@ def test_json_output_structure(self, mex_home, tmp_path, monkeypatch): data = json.loads(result.stdout) assert data["agent"] == "claude" assert data["scope"] == "global" - assert len(data["written_files"]) == 3 + assert len(data["written_files"]) == 2 assert all(p.startswith(str(tmp_path / ".claude")) for p in data["written_files"]) def test_project_scope_in_cwd(self, mex_home, tmp_path, monkeypatch): @@ -101,7 +101,7 @@ def test_project_scope_in_cwd(self, mex_home, tmp_path, monkeypatch): result = runner.invoke(app, ["integrate", "opencode", "--scope", "project"]) assert result.exit_code == 0, result.stderr - assert (tmp_path / ".opencode" / "hooks.md").exists() + assert (tmp_path / ".opencode" / "skills" / "mex" / "SKILL.md").exists() assert str(tmp_path) in result.stdout def test_not_initialized_exit_1(self, tmp_path, monkeypatch): diff --git a/tests/services/test_integrate.py b/tests/services/test_integrate.py index 3650afa..d02b032 100644 --- a/tests/services/test_integrate.py +++ b/tests/services/test_integrate.py @@ -38,18 +38,21 @@ def target_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, agent: str, scop class TestIntegrate: + # claude/opencode 生成物:skill 说明书(官方 skills/mex/SKILL.md 发现路径)+ README。 + GENERATED_FILES = ("skills/mex/SKILL.md", "README.md") + @pytest.mark.parametrize("agent", AGENTS) @pytest.mark.parametrize("scope", SCOPES) - def test_generates_three_files(self, mex_home, tmp_path, monkeypatch, agent, scope): + def test_generates_skill_and_readme(self, mex_home, tmp_path, monkeypatch, agent, scope): expected = target_dir(tmp_path, monkeypatch, agent, scope) result = integrate(agent, scope) assert result.agent == agent and result.scope == scope - assert len(result.written_files) == 3 - for name in ("hooks.md", "skill.md", "README.md"): - assert (expected / name).exists() - assert str(expected / name) in result.written_files + assert len(result.written_files) == 2 + for rel in self.GENERATED_FILES: + assert (expected / rel).exists() + assert str(expected / rel) in result.written_files @pytest.mark.parametrize("agent", AGENTS) @pytest.mark.parametrize("scope", SCOPES) @@ -58,9 +61,9 @@ def test_placeholders_replaced(self, mex_home, tmp_path, monkeypatch, agent, sco integrate(agent, scope) - for name in ("hooks.md", "skill.md", "README.md"): - content = (expected / name).read_text(encoding="utf-8") - assert "{{" not in content, f"{name} 仍含未替换占位符" + for rel in self.GENERATED_FILES: + content = (expected / rel).read_text(encoding="utf-8") + assert "{{" not in content, f"{rel} 仍含未替换占位符" assert str(mex_home) in content assert "mex.db" in content @@ -72,9 +75,9 @@ def test_idempotent(self, mex_home, tmp_path, monkeypatch, agent): second = integrate(agent, "project") assert first.written_files == second.written_files - for name in ("hooks.md", "skill.md", "README.md"): - assert (expected / name).exists() - assert "{{" not in (expected / "skill.md").read_text(encoding="utf-8") + for rel in self.GENERATED_FILES: + assert (expected / rel).exists() + assert "{{" not in (expected / "skills" / "mex" / "SKILL.md").read_text(encoding="utf-8") def test_not_initialized_raises(self, tmp_path, monkeypatch): monkeypatch.setenv("MEX_HOME", str(tmp_path))