@aliyun-es/agentic-context 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +46 -16
- package/adapters/hermes/__init__.py +29 -38
- package/adapters/hermes/plugin.yaml +1 -1
- package/adapters/openclaw/index.js +3 -22
- package/package.json +1 -1
- package/runtime/client.js +8 -62
- package/runtime/endpoints.js +8 -0
- package/runtime/errors.js +31 -0
- package/runtime/mcp.js +39 -5
- package/runtime/recall.js +5 -11
- package/runtime/skill-client.js +36 -0
- package/runtime/version.js +1 -1
- package/runtime/write-cli.js +23 -0
- package/src/installer.js +1 -1
- package/src/payload.js +17 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-11-05
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Codex、Claude Code、Qoder、OpenClaw、Hermes 的 Memory 自动采集统一调用 MCP `memory_add`;自动检索继续调用 MCP `hybrid_search`。用户身份使用现有连接 Header,保留会话消息、采集来源和 session 元信息。
|
|
12
|
+
- 统一解析 MCP 工具结果;HTTP 成功但工具返回 `isError`,或没有有效 `event_id` 时,不能记录为采集提交成功或推进去重状态。错误日志隐藏凭证和本次提交的消息内容。
|
|
13
|
+
- 插件内部的每次 MCP HTTP 请求生成独立 `x-request-id`,满足下游记忆写入的追踪要求;不把固定请求 ID 写入宿主连接配置。
|
|
14
|
+
- Hermes 通过安装时定位的 Node.js 调用共用 MCP 采集运行时,保留原有落盘队列和发送状态。
|
|
15
|
+
- OpenClaw 的可选自动 Skill 提取独立保留现有 REST 调用;现有 MCP 未提供会话抽取工具,本次不改变服务端及抽取逻辑,也不新增文件上传能力。
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
- 移除 OpenClaw 的 `openclaw eke` 和 `openclaw agentic-context` 手动业务子命令,包括 `search`、`get`、`update`、`forget`、`event`。依赖这些命令的脚本需改为使用 MCP 工具。npm 包的 `eke` / `agentic-context` 安装器别名及 `install`、`status`、`uninstall` 保持不变。
|
|
20
|
+
|
|
21
|
+
### Upgrade notes
|
|
22
|
+
|
|
23
|
+
- 本版包含上述命令移除,因此使用 `0.2.0` 版本号。覆盖安装保留已有连接、知识库、采集开关、时间边界、游标和失败记录;升级前结束活动任务,升级后重载宿主并按要求重新审查 Hook。
|
|
24
|
+
- Memory 自动采集只在收到 `event_id` 后视为提交被接收,异步抽取结果仍需通过 `memory_event_status_get` 确认。采集关闭时不上传,既有失败记录不会因本次升级自动重放。
|
|
25
|
+
|
|
26
|
+
## [0.1.1] - 2026-11-05
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- MCP 请求统一使用 `/v1/context/mcp`;Memory/Skill REST 请求统一使用 v3/v4 的 `/openapi/workspaces/{workspace}/context/...`,覆盖全部 Agent、自动检索、自动采集及 Hermes Provider,不再回退旧 URL。
|
|
31
|
+
- 已有配置中的 `serviceId` 继续保留,但不再用于构造请求 URL;身份、认证、请求体、自动采集开关和本地采集状态保持原有语义。
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- 新版状态检查识别 0.1.0 的运行时文件集合,避免将正常旧安装误报为缺失新文件。
|
|
36
|
+
- Windows Codex 本地插件 Hook 使用安装时定位的 Node.js,通过宿主 cmd 直接执行,移除插件主动启动的 `powershell.exe -EncodedCommand`;复用现有进程启动依赖的路径转义。其他 Agent 和 macOS/Linux 启动方式保持不变。
|
|
37
|
+
|
|
38
|
+
### Documentation
|
|
39
|
+
|
|
40
|
+
- 补充不依赖 PowerShell 的 Windows 安装命令、错误 786 的处理及验收要求,区分 MCP 成功、Hook 信任和实际自动执行。
|
|
41
|
+
|
|
42
|
+
### Upgrade notes
|
|
43
|
+
|
|
44
|
+
- 服务端须支持 `context` 路由。使用本版 `install` 覆盖升级,再按宿主要求重新加载和审查 Hook;无需卸载,原连接配置、采集时间边界、游标及待处理记录继续保留。
|
|
45
|
+
- 已验证官方 0.1.0 安装包生成的五类宿主配置可升级;Windows 原生 Hook 执行和真实会话自动检索、采集仍需在 Windows 验收,不能用 macOS 上的测试通过代替。去除 PowerShell 依赖不会修改组织对 cmd、Node 或 Hook 的执行策略。
|
|
46
|
+
- 已知限制:Windows Codex 0.144.0 及采用相同旧执行器的版本存在 Hook 命令引号兼容问题,本版暂未处理;0.145.0 起的执行器已调整此传参方式。
|
|
47
|
+
|
|
7
48
|
## [0.1.0] - 2026-11-04
|
|
8
49
|
|
|
9
50
|
首个使用 `@aliyun-es/agentic-context` npm 包名的版本。新包采用独立版本序列,从 `0.1.0` 开始;原 `@aliyun-es/eke@0.1.1` 的受管理安装仍可通过新包升级,版本数字不代表对旧包的降级。
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
将阿里云 Elasticsearch 企业上下文引擎接入 Agent,可赋予 Agent 强大的云上 agent memory, skill, knowledge base, agentic wiki 等核心能力。现已支持 Codex、OpenClaw、Hermes、Claude Code 和 Qoder 等主流 Agent。
|
|
4
4
|
|
|
5
|
-
本文的安装、验证和升级命令固定使用 `@aliyun-es/agentic-context@0.
|
|
5
|
+
本文的安装、验证和升级命令固定使用 `@aliyun-es/agentic-context@0.2.0`。新 npm 包采用独立版本序列,从 `0.1.0` 开始;此前由 `@aliyun-es/eke@0.1.1` 创建的受管理安装仍可按下文升级。
|
|
6
6
|
|
|
7
7
|
## 前置条件:在阿里云开通 Elasticsearch 企业上下文引擎
|
|
8
8
|
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
|
|
73
73
|
同一业务用户跨 Agent 共享 Memory 时,使用相同的 endpoint、workspace 和 `user_id`。不同用户不要共用同一个 `user_id`。`user_id` 和 `--kb` 不作为授权凭证;使用前需确认 API Key 具有对应的服务访问权限。
|
|
74
74
|
|
|
75
|
-
**Memory
|
|
75
|
+
**Memory 自动采集在首次安装时默认开启,更新时保留原设置**:插件生效后,会通过 MCP `memory_add` 将新的用户和助手对话文本提交到所配置的企业上下文引擎服务,不补传历史会话,不自动对正文做通用敏感信息脱敏。如需关闭,使用 `--no-auto-capture`。关闭自动采集不禁止 Agent 根据明确请求调用 Memory 写入工具。该安装配置面向单个业务用户,不能用一个固定 `user_id` 代表多用户应用中的所有人。
|
|
76
76
|
|
|
77
77
|
**自动检索**:Hook 或 Provider 启用后,插件会将当前请求发送到 `hybrid_search`,将召回内容补充到 Agent 上下文。未配置 `--kb` 时仅检索 Memory 和 Skill;配置后同时检索指定知识库,支持多个知识库。检索失败不阻断任务。`--no-auto-capture` 只关闭自动采集,不关闭自动检索。
|
|
78
78
|
|
|
@@ -92,9 +92,11 @@ node -p "JSON.stringify({platform:process.platform,arch:process.arch,node:proces
|
|
|
92
92
|
|
|
93
93
|
要求 **Node.js 22+**,且 npm/npx 可用。已有符合要求的环境直接复用;需要新装时可使用 [Node.js 24 LTS](https://nodejs.org/en/download)。安装器适配 macOS、Linux 和 Windows,但目标 Agent 自身也须支持对应系统。
|
|
94
94
|
|
|
95
|
+
Windows 安装器不依赖 PowerShell。在 **命令提示符(cmd.exe)** 中将本文安装命令的 `npx` 写为 `npx.cmd`,其余参数保持一致;检查 npm 时使用 `npm.cmd --version`。从 `0.1.1` 起,Windows Codex 的三个本地插件 Hook 也直接通过宿主的 cmd 调用安装时定位的 Node.js,不再启动 PowerShell。
|
|
96
|
+
|
|
95
97
|
#### Node.js 不存在、版本过低或终端找不到它
|
|
96
98
|
|
|
97
|
-
先定位已有程序,避免重复安装:macOS/Linux 执行 `command -v node npm npx`,Windows
|
|
99
|
+
先定位已有程序,避免重复安装:macOS/Linux 执行 `command -v node npm npx`,Windows 命令提示符执行 `where node`、`where npm.cmd`、`where npx.cmd`。如果程序已安装但未进入 PATH,将已确认的安装目录加入**当前终端**的 PATH,再检查版本。
|
|
98
100
|
|
|
99
101
|
按当前环境选择一种处理方式,不同时安装多套 Node.js:
|
|
100
102
|
|
|
@@ -155,7 +157,7 @@ PATH 中找不到时,继续检查 `npm prefix -g` 返回的目录:macOS/Linu
|
|
|
155
157
|
将 `<agent>` 替换成上表中的一个名称,将其余占位符替换为真实接入信息,只执行这一条安装命令:
|
|
156
158
|
|
|
157
159
|
```bash
|
|
158
|
-
npx --yes @aliyun-es/agentic-context@0.
|
|
160
|
+
npx --yes @aliyun-es/agentic-context@0.2.0 install <agent> --endpoint "<服务地址>" --workspace "<workspace名称>" --user_id "<业务用户ID>" --api_key "<API Key>" --json
|
|
159
161
|
```
|
|
160
162
|
|
|
161
163
|
无需全局安装 npm 包。`--yes` 仅跳过 npx 下载包的确认,不跳过 Agent 的信任和工具授权。
|
|
@@ -183,9 +185,9 @@ npm view @aliyun-es/agentic-context version --registry=https://registry.npmjs.or
|
|
|
183
185
|
|
|
184
186
|
成功后,可将这两个 registry 参数放在原 `npx` 命令的包名之前重试。`@aliyun-es` 的专用 registry 配置可能覆盖默认 registry,因此两者都要检查。
|
|
185
187
|
|
|
186
|
-
缓存无写入权限时,在原 `npx` 命令的包名之前加入一个新缓存路径:macOS/Linux 使用 `--cache "$HOME/.cache/agentic-context-npm"`,Windows
|
|
188
|
+
缓存无写入权限时,在原 `npx` 命令的包名之前加入一个新缓存路径:macOS/Linux 使用 `--cache "$HOME/.cache/agentic-context-npm"`,Windows 命令提示符使用 `--cache "%LOCALAPPDATA%\agentic-context-npm-cache"`。如果错误发生在 Agent 配置目录而不是 npm 缓存,应修正当前账号和配置路径,不能靠换缓存解决。
|
|
187
189
|
|
|
188
|
-
企业网络必须使用代理时,复用企业批准的 `HTTP_PROXY`、`HTTPS_PROXY` 和 `NO_PROXY`。对 Node.js 24 的服务请求,macOS/Linux 执行 `export NODE_USE_ENV_PROXY=1`,
|
|
190
|
+
企业网络必须使用代理时,复用企业批准的 `HTTP_PROXY`、`HTTPS_PROXY` 和 `NO_PROXY`。对 Node.js 24 的服务请求,macOS/Linux 执行 `export NODE_USE_ENV_PROXY=1`,Windows 命令提示符执行 `set "NODE_USE_ENV_PROXY=1"`;旧版本不能假设仅设置代理地址就会生效。企业 CA 使用 `NODE_EXTRA_CA_CERTS` 指向获批准的 PEM 文件。代理和 CA 配置也须进入实际 Agent 的启动环境,不能只配置安装终端。具体行为见 [Node.js 企业网络配置](https://github.com/nodejs/learn/blob/main/pages/http/enterprise-network-configuration.md)。
|
|
189
191
|
|
|
190
192
|
#### 服务连接失败
|
|
191
193
|
|
|
@@ -213,6 +215,24 @@ npm view @aliyun-es/agentic-context version --registry=https://registry.npmjs.or
|
|
|
213
215
|
|
|
214
216
|
若 Hooks 未显示,检查 Agentic Context 插件是否启用;若组织策略限制非托管 Hooks,由管理员处理,不使用绕过信任的启动参数。自定义目录须通过 `CODEX_HOME` 指向安装使用的配置目录。参见 [Codex Hooks 信任说明](https://learn.chatgpt.com/docs/hooks#review-and-trust-hooks)。
|
|
215
217
|
|
|
218
|
+
##### Windows 管理员策略阻止 Hook(错误 786)
|
|
219
|
+
|
|
220
|
+
Windows 错误 `786 (0x312)` 对应 `ERROR_ACCESS_DISABLED_NO_SAFER_UI_BY_POLICY`,表示管理员策略限制访问。若报错指向 `powershell.exe -EncodedCommand`,只能确认该次启动被阻止;不能仅凭错误码判定是 PowerShell 整体被禁用,还是某条进程或命令规则拒绝了本次调用。参见 [Microsoft 系统错误码 500–999](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--500-999-)。
|
|
221
|
+
|
|
222
|
+
`0.1.0` 的 Windows Codex Hook 会额外启动 PowerShell。`0.1.1` 已去除这一依赖,执行链为 **Codex → cmd.exe → 安装时定位的 node.exe → hook.js**;Node 使用绝对路径,不要求 Desktop 的 PATH 包含 Node。升级时在命令提示符执行 `npx.cmd --yes --registry=https://registry.npmjs.org @aliyun-es/agentic-context@0.2.0 install codex --json`,保留原账号、配置目录和安装参数;随后重新加载插件,按 Codex 提示审查这三个已修改的 Hook。若原来使用自定义配置目录,命令须继续传入同一 `--config-dir`。
|
|
223
|
+
|
|
224
|
+
分别记录以下三层结果,不将前两层通过写成自动功能已验证:
|
|
225
|
+
|
|
226
|
+
| 验证层 | 通过所需证据 |
|
|
227
|
+
| --- | --- |
|
|
228
|
+
| MCP 连接与工具调用 | Codex 实际发现并成功调用工具;这不依赖本地 Hook 成功启动 |
|
|
229
|
+
| Codex Hook 审查 | `SessionStart`、`UserPromptSubmit`、`Stop` 已受信任且启用;这不代表 Windows 允许启动其进程 |
|
|
230
|
+
| 宿主触发与业务结果 | 真实会话触发自动检索并注入上下文;自动采集还须在用户同意后,用无敏感测试对话确认提交、异步处理完成和结果可检索 |
|
|
231
|
+
|
|
232
|
+
去掉插件主动启动的 PowerShell 不会修改管理员策略。若新版执行链中的 `cmd.exe` 或 `node.exe` 仍被组织策略限制,再由管理员按组织流程处理;保留完整启动错误、被阻止程序及规则,去除凭证和对话正文。不修改注册表、安全策略或使用 `ExecutionPolicy Bypass`。
|
|
233
|
+
|
|
234
|
+
升级后仍需完成第三层验证。在取得宿主执行和业务结果证据前,报告“自动检索/自动采集待端到端验证”,不要用手动运行检索或采集代码代替。参见 [Codex 插件 Hook 的更新与信任规则](https://learn.chatgpt.com/docs/hooks#plugin-bundled-hooks)。本次启动方式调整仅适用于 Windows Codex;其他 Agent 的 Hook 启动方式保持不变。
|
|
235
|
+
|
|
216
236
|
#### Claude Code
|
|
217
237
|
|
|
218
238
|
1. 检查 `agentic-context@aliyun-agentic-context` 的启用状态;未启用时,通过 shell 执行 `claude plugin enable agentic-context@aliyun-agentic-context`,使用安装时的配置目录和作用域。
|
|
@@ -286,7 +306,7 @@ New Qoder 的远程 MCP 地址必须使用 **HTTPS**。安装时将 `--endpoint`
|
|
|
286
306
|
使用相同的 Agent 名称和配置目录检查连接;`status` 的 `nextSteps` 可再次查看加载指引:
|
|
287
307
|
|
|
288
308
|
```bash
|
|
289
|
-
npx --yes @aliyun-es/agentic-context@0.
|
|
309
|
+
npx --yes @aliyun-es/agentic-context@0.2.0 status <agent> --check --json
|
|
290
310
|
```
|
|
291
311
|
|
|
292
312
|
结果需要同时满足以下条件,其他字段略:
|
|
@@ -328,21 +348,31 @@ Hermes 先将新对话保存到 `.agentic-context/pending-captures/`,再后台
|
|
|
328
348
|
|
|
329
349
|
### 5. 调整配置、更新和卸载
|
|
330
350
|
|
|
331
|
-
增加知识库、修改连接信息或更新插件,都重新执行 `install`,无需先卸载。已有安装可以省略连接参数,只传需要修改的选项。`install` 会将指定版本的文件安装到 Agent,仅下载 npm 包不会更新已安装插件。升级到后续版本时,将命令中的 `0.
|
|
351
|
+
增加知识库、修改连接信息或更新插件,都重新执行 `install`,无需先卸载。已有安装可以省略连接参数,只传需要修改的选项。`install` 会将指定版本的文件安装到 Agent,仅下载 npm 包不会更新已安装插件。升级到后续版本时,将命令中的 `0.2.0` 替换为已确认的目标版本。
|
|
332
352
|
|
|
333
353
|
**更新时保留未显式指定的配置**:不传 `--kb` 保留已有默认知识库;传入 `--kb` 时使用本次完整列表,清空使用 `--clear-kbs`。自动采集和 Skill 提取同样保留原开关,使用 `--auto-capture` / `--no-auto-capture`、`--auto-skill` / `--no-auto-skill` 显式调整。修改后重新加载并验证。
|
|
334
354
|
|
|
335
355
|
`--kb` 指定自动检索的知识库,也作为 Agent 的默认查询指引,不是权限白名单。配置多个知识库时,自动检索分别调用 `hybrid_search` 并合并结果;没有默认知识库时,也可以在查询中指定知识库名称。
|
|
336
356
|
|
|
357
|
+
#### `context` URL 升级
|
|
358
|
+
|
|
359
|
+
从 `0.1.1` 起,请求使用 `context` 路径。`0.2.0` 中,五种 Agent 的自动检索和 Memory 自动采集统一通过 `/v1/context/mcp` 调用 `hybrid_search` 和 `memory_add`;Agent 主动执行的 Memory、Skill、知识库工具也使用 MCP。
|
|
360
|
+
|
|
361
|
+
仅 OpenClaw 的可选自动 Skill 提取继续调用 `/v3/openapi/workspaces/{workspace}/context/skills/add`,因为现有 MCP 工具尚未提供从会话异步抽取 Skill 的等价能力。自动 Skill 提取默认关闭,更新时保留原开关;本次不修改服务端,也不新增文件上传能力。服务端须支持上述路径;连接失败时保留错误,不自动回退旧路径。
|
|
362
|
+
|
|
363
|
+
已安装 `0.1.x` 的客户需重新执行目标版本的 `install` 完成覆盖升级,并按宿主要求重新加载插件;仅下载新版 npm 包不会更新已安装的运行时和 MCP 配置。省略连接参数会保留原 endpoint、workspace、用户身份、API Key、知识库列表及采集开关;采集进度与待处理记录继续沿用。旧配置中的 `serviceId` 不再决定请求路径。
|
|
364
|
+
|
|
365
|
+
URL 升级不改变 Windows 管理员策略,也不代表自动检索或采集已通过端到端验证。
|
|
366
|
+
|
|
337
367
|
#### 从旧版安装升级
|
|
338
368
|
|
|
339
369
|
新版安装器兼容此前由 `@aliyun-es/eke` 创建的受管理安装。升级前结束正在执行的任务并退出或停止对应 Agent,避免旧进程在升级期间继续上传或修改采集进度。**不需要先卸载旧版**,也不要手工删除状态目录。
|
|
340
370
|
|
|
341
|
-
|
|
371
|
+
在原账号和原配置目录执行以下命令;`agentic-context@0.2.0` 可直接升级 `agentic-context@0.1.0`、`agentic-context@0.1.1` 或旧包 `eke@0.1.1` 的受管理安装,无需先安装其他过渡版本:
|
|
342
372
|
|
|
343
373
|
```bash
|
|
344
|
-
npx --yes @aliyun-es/agentic-context@0.
|
|
345
|
-
npx --yes @aliyun-es/agentic-context@0.
|
|
374
|
+
npx --yes @aliyun-es/agentic-context@0.2.0 status <agent> --json
|
|
375
|
+
npx --yes @aliyun-es/agentic-context@0.2.0 install <agent> --json
|
|
346
376
|
```
|
|
347
377
|
|
|
348
378
|
自定义配置目录时,两条命令均追加原来的 `--config-dir "<Agent配置根目录>"`。省略连接参数会复用原 endpoint、workspace、user ID 和 API Key;省略可选参数会保留知识库列表、自动采集和 Skill 提取开关。
|
|
@@ -351,7 +381,7 @@ npx --yes @aliyun-es/agentic-context@0.1.0 install <agent> --json
|
|
|
351
381
|
|
|
352
382
|
```bash
|
|
353
383
|
npm uninstall -g @aliyun-es/eke
|
|
354
|
-
npm install -g @aliyun-es/agentic-context@0.
|
|
384
|
+
npm install -g @aliyun-es/agentic-context@0.2.0
|
|
355
385
|
agentic-context install <agent> --json
|
|
356
386
|
```
|
|
357
387
|
|
|
@@ -359,20 +389,20 @@ agentic-context install <agent> --json
|
|
|
359
389
|
|
|
360
390
|
旧 npm 包的弃用或下架将单独执行;已有插件通过新包升级,原配置和本地状态仍保留。下架不会删除本地已经安装或缓存的旧安装器,`npx` 升级也不会替换原全局 `eke` 可执行文件。升级后统一使用新包管理插件;旧 `0.1.1` 安装器不认识新插件标识,不能用于管理或降级已经迁移的安装。需要继续使用全局 `eke` 命令时,先按上面的步骤换成新 npm 包提供的兼容入口。
|
|
361
391
|
|
|
362
|
-
|
|
392
|
+
对于仍使用旧 `eke` 标识的安装,升级会移除旧插件注册、MCP 入口和本插件拥有的旧 Hook、Skill,再注册 `agentic-context`;不会同时保留两套自动采集。旧 EKE 安装的 `.eke` 状态目录继续使用,保存既有连接、安装时间边界、采集游标及失败或待发送记录;已有 `agentic-context@0.1.x` 安装沿用自己的 `.agentic-context` 或 `.eke` 目录,新安装创建 `.agentic-context`。这是旧安装的存储兼容,不是另一个产品或插件。
|
|
363
393
|
|
|
364
|
-
如果发现配置被外部修改、目标标识已被其他安装占用,安装器停止并保留相关配置。升级中途失败时,根据错误处理后,用相同账号、目录和 `install` 命令重试,不清空状态目录。旧版尚未升级也可以通过新命令执行 `status` 或 `uninstall
|
|
394
|
+
如果发现配置被外部修改、目标标识已被其他安装占用,安装器停止并保留相关配置。升级中途失败时,根据错误处理后,用相同账号、目录和 `install` 命令重试,不清空状态目录。旧版尚未升级也可以通过新命令执行 `status` 或 `uninstall`。旧 `eke` 标识迁移不支持 `--prepare`,以免生成新文件却未完成注册切换。已有 `agentic-context` 标识的覆盖升级不属于该限制。
|
|
365
395
|
|
|
366
396
|
升级完成后,重新启动或重载 Agent,按 `nextSteps` 审查并授权新的 Hook 定义,再验证工具调用、自动检索及原有采集设置。插件改名不会继承宿主对旧 Hook 的信任。
|
|
367
397
|
|
|
368
|
-
OpenClaw
|
|
398
|
+
**`0.2.0` 移除了 OpenClaw 的手动业务子命令**:`openclaw eke` 和 `openclaw agentic-context` 下的 `search`、`get`、`update`、`forget`、`event` 均不再提供。依赖这些命令的脚本需改用对应 MCP 工具;自动检索、Memory 自动采集和可选自动 Skill 提取继续由宿主生命周期事件触发。npm 包的 `agentic-context` 与 `eke` 安装器入口仍保留,`install`、`status`、`uninstall` 的用法不变。
|
|
369
399
|
|
|
370
400
|
#### 卸载
|
|
371
401
|
|
|
372
402
|
卸载命令:
|
|
373
403
|
|
|
374
404
|
```bash
|
|
375
|
-
npx --yes @aliyun-es/agentic-context@0.
|
|
405
|
+
npx --yes @aliyun-es/agentic-context@0.2.0 uninstall <agent> --json
|
|
376
406
|
```
|
|
377
407
|
|
|
378
408
|
返回 `UNINSTALLED` 后重载或重启 Agent;只移除本地 Agentic Context 配置与文件,不删除服务端 Memory、知识库或 Skill。若返回 `MANUAL_CLEANUP_REQUIRED`,只检查结果指出的被外部修改的配置,不删除整个 Agent 配置目录。
|
|
@@ -8,9 +8,6 @@ import threading
|
|
|
8
8
|
import time
|
|
9
9
|
from uuid import uuid4
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from urllib.error import HTTPError
|
|
12
|
-
from urllib.parse import quote
|
|
13
|
-
from urllib.request import Request, build_opener, HTTPRedirectHandler
|
|
14
11
|
|
|
15
12
|
from agent.memory_provider import MemoryProvider, spawn_context_thread
|
|
16
13
|
|
|
@@ -20,7 +17,7 @@ log = logging.getLogger(__name__)
|
|
|
20
17
|
class ServiceError(RuntimeError):
|
|
21
18
|
def __init__(self, status, data, private_values):
|
|
22
19
|
detail = data.get('error') if isinstance(data.get('error'), dict) else {}
|
|
23
|
-
message = data.get('message') or detail.get('message') or 'Agentic Context request failed'
|
|
20
|
+
message = data.get('message') or detail.get('message') or data.get('error') or 'Agentic Context request failed'
|
|
24
21
|
if not isinstance(message, str):
|
|
25
22
|
message = 'Agentic Context request failed'
|
|
26
23
|
for value in private_values:
|
|
@@ -44,46 +41,38 @@ def error_summary(error, private_values=()):
|
|
|
44
41
|
return json.dumps({'error': message[:1024], 'type': type(error).__name__, 'code': code})
|
|
45
42
|
|
|
46
43
|
|
|
47
|
-
class NoRedirect(HTTPRedirectHandler):
|
|
48
|
-
def redirect_request(self, *args, **kwargs):
|
|
49
|
-
return None
|
|
50
|
-
|
|
51
|
-
|
|
52
44
|
class MemoryClient:
|
|
53
|
-
|
|
45
|
+
"""Use the installed Node MCP transport for automatic memory submission."""
|
|
46
|
+
def __init__(self, config, command):
|
|
54
47
|
self.config = config
|
|
55
|
-
self.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
with self.opener.open(request, timeout=10) as response:
|
|
68
|
-
status = response.status
|
|
69
|
-
result = json.load(response)
|
|
70
|
-
except HTTPError as error:
|
|
48
|
+
self.command = command
|
|
49
|
+
|
|
50
|
+
def add(self, messages, session_id):
|
|
51
|
+
private_values = [self.config.get('apiKey')]
|
|
52
|
+
private_values.extend(item.get('content') for item in messages)
|
|
53
|
+
response = subprocess.run(
|
|
54
|
+
self.command,
|
|
55
|
+
input=json.dumps({'messages': messages, 'metadata': {'source': 'hermes', 'session_id': session_id},
|
|
56
|
+
'expectedConnection': {key: self.config[key] for key in ('endpoint', 'workspace', 'userId')}}),
|
|
57
|
+
capture_output=True, text=True, encoding='utf-8',
|
|
58
|
+
timeout=self.config.get('timeoutMs', 10000) / 1000 + 5)
|
|
59
|
+
if response.returncode:
|
|
71
60
|
try:
|
|
72
|
-
detail = json.loads(
|
|
61
|
+
detail = json.loads(response.stderr)
|
|
73
62
|
except (ValueError, UnicodeDecodeError):
|
|
74
63
|
detail = {}
|
|
75
64
|
if not isinstance(detail, dict):
|
|
76
65
|
detail = {}
|
|
77
|
-
if error.
|
|
78
|
-
detail
|
|
79
|
-
raise ServiceError(
|
|
80
|
-
if
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
66
|
+
if not detail.get('error') and not detail.get('message'):
|
|
67
|
+
detail['message'] = 'Agentic Context capture helper exited with code ' + str(response.returncode)
|
|
68
|
+
raise ServiceError(detail.get('status'), detail, private_values)
|
|
69
|
+
if response.stderr:
|
|
70
|
+
log.warning('Agentic Context capture helper: %s',
|
|
71
|
+
error_summary(RuntimeError(response.stderr), private_values))
|
|
72
|
+
result = json.loads(response.stdout)
|
|
73
|
+
if not isinstance(result, dict):
|
|
74
|
+
raise ValueError('Invalid memory capture helper response')
|
|
75
|
+
return result
|
|
87
76
|
|
|
88
77
|
|
|
89
78
|
def state_directory(home):
|
|
@@ -123,7 +112,9 @@ class AgenticContextProvider(MemoryProvider):
|
|
|
123
112
|
for field in ('endpoint', 'workspace', 'userId', 'apiKey'):
|
|
124
113
|
if not self.config.get(field):
|
|
125
114
|
raise ValueError('Missing Agentic Context setting: ' + field)
|
|
126
|
-
self.client = MemoryClient(self.config
|
|
115
|
+
self.client = MemoryClient(self.config, [self.recall_command[0],
|
|
116
|
+
str(Path(self.recall_command[1]).with_name('write-cli.js')),
|
|
117
|
+
self.recall_command[2]])
|
|
127
118
|
self.pending_directory = state / 'pending-captures'
|
|
128
119
|
self.session_id = session_id
|
|
129
120
|
self.closed = False
|
|
@@ -2,11 +2,10 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { MemoryClient, contextText, errorSummary } from './runtime/client.js';
|
|
4
4
|
import { latestTurn } from './runtime/capture.js';
|
|
5
|
-
import {
|
|
5
|
+
import { recallContext } from './runtime/recall.js';
|
|
6
|
+
import { SkillClient } from './runtime/skill-client.js';
|
|
6
7
|
import { submitCapture } from './runtime/submission.js';
|
|
7
8
|
|
|
8
|
-
const CLI_NAMES = ['agentic-context', 'eke'];
|
|
9
|
-
|
|
10
9
|
export default {
|
|
11
10
|
id: 'agentic-context', name: 'Agentic Context',
|
|
12
11
|
register(api) {
|
|
@@ -75,7 +74,7 @@ export default {
|
|
|
75
74
|
skillPending.add(key);
|
|
76
75
|
try {
|
|
77
76
|
await submitCapture(config, { kind: 'skill', sessionId: session, messages },
|
|
78
|
-
() => new
|
|
77
|
+
() => new SkillClient(config).add(messages), warn);
|
|
79
78
|
const remaining = (skillTurns.get(key) || []).slice(messages.length);
|
|
80
79
|
if (remaining.length) skillTurns.set(key, remaining);
|
|
81
80
|
else skillTurns.delete(key);
|
|
@@ -83,23 +82,5 @@ export default {
|
|
|
83
82
|
}
|
|
84
83
|
} catch (error) { api.logger.warn(errorSummary(error)); }
|
|
85
84
|
});
|
|
86
|
-
api.registerCli(({ program }) => {
|
|
87
|
-
const execute = action => async (...args) => {
|
|
88
|
-
let config;
|
|
89
|
-
try { config = await load(); console.log(JSON.stringify(await action(new MemoryClient(config), ...args), null, 2)); }
|
|
90
|
-
catch (error) { console.error(errorSummary(error, [config?.apiKey, ...args])); process.exitCode = 1; }
|
|
91
|
-
};
|
|
92
|
-
// Keep the original host command callable by existing scripts after upgrade.
|
|
93
|
-
for (const name of CLI_NAMES) {
|
|
94
|
-
const command = program.command(name).description('Agentic Context memory and skill operations');
|
|
95
|
-
command.command('search <query>').action(execute((client, query) => name === 'eke'
|
|
96
|
-
? client.request('POST', 'search', { query, user_id: client.config.userId, size: 5, enhancements: { skill: { enable: true } } }, 'v3')
|
|
97
|
-
: hybridSearch(client.config, query)));
|
|
98
|
-
command.command('get <id>').action(execute((client, id) => id.startsWith('s-') ? client.getSkill(id) : client.get(id)));
|
|
99
|
-
command.command('update <id> <text>').action(execute((client, id, text) => client.update(id, text)));
|
|
100
|
-
command.command('forget <id>').action(execute((client, id) => id.startsWith('s-') ? client.deleteSkill(id) : client.delete(id)));
|
|
101
|
-
command.command('event <id>').action(execute((client, id) => client.event(id)));
|
|
102
|
-
}
|
|
103
|
-
}, { descriptors: CLI_NAMES.map(name => ({ name, description: 'Agentic Context memory and skill operations', hasSubcommands: true })) });
|
|
104
85
|
},
|
|
105
86
|
};
|
package/package.json
CHANGED
package/runtime/client.js
CHANGED
|
@@ -1,70 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function requestHeaders(config) {
|
|
8
|
-
return { Authorization: `Bearer ${config.apiKey}`, 'eke-user-id': config.userId };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function redactErrorValue(value, privateValues) {
|
|
12
|
-
if (typeof value !== 'string') return value;
|
|
13
|
-
for (const secret of privateValues.filter(item => typeof item === 'string' && item.length)) value = value.replaceAll(secret, '[REDACTED]');
|
|
14
|
-
return value.replace(/Bearer\s+[^\s"']+/gi, 'Bearer [REDACTED]').slice(0, 1024);
|
|
15
|
-
}
|
|
1
|
+
import { callMcpTool } from './mcp.js';
|
|
2
|
+
import { ServiceError } from './errors.js';
|
|
3
|
+
export { ServiceError, requestHeaders, redactErrorValue, errorSummary } from './errors.js';
|
|
16
4
|
|
|
17
5
|
export class MemoryClient {
|
|
18
6
|
constructor(config, fetcher = globalThis.fetch) { this.config = config; this.fetcher = fetcher; }
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const privateValues = [this.config.apiKey, body?.query, body?.text,
|
|
24
|
-
...(Array.isArray(body?.messages) ? body.messages.map(item => item.content) : [])];
|
|
25
|
-
let response;
|
|
26
|
-
try {
|
|
27
|
-
response = await this.fetcher(`${this.prefix(version)}/${route}`, {
|
|
28
|
-
method, redirect: 'error', signal: AbortSignal.timeout(this.config.timeoutMs || 10000),
|
|
29
|
-
headers: { ...requestHeaders(this.config), 'Content-Type': 'application/json' },
|
|
30
|
-
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
31
|
-
});
|
|
32
|
-
} catch (error) {
|
|
33
|
-
throw new ServiceError(redactErrorValue(`${method} ${route}: ${error.message}`, privateValues), {
|
|
34
|
-
code: error.code || error.cause?.code,
|
|
35
|
-
cause: error.cause && { message: redactErrorValue(error.cause.message, privateValues), code: error.cause.code },
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
if (response.status === 204) return {};
|
|
39
|
-
let data;
|
|
40
|
-
try { data = await response.json(); } catch { throw new ServiceError('Service returned a non-JSON response', { status: response.status }); }
|
|
41
|
-
if (!response.ok || data.error || (data.code && !['200', '0', 'OK', 'Success'].includes(String(data.code)))) {
|
|
42
|
-
let message = data.message || data.error?.message || 'Agentic Context request failed';
|
|
43
|
-
if (typeof message !== 'string') message = 'Agentic Context request failed';
|
|
44
|
-
throw new ServiceError(redactErrorValue(message, privateValues), {
|
|
45
|
-
status: response.status, code: data.code || data.error?.code,
|
|
46
|
-
requestId: data.request_id || data.requestId || response.headers.get('x-request-id'),
|
|
47
|
-
});
|
|
7
|
+
async add(messages, metadata = {}) {
|
|
8
|
+
const result = await callMcpTool(this.config, 'memory_add', { messages, infer: true, metadata }, this.fetcher);
|
|
9
|
+
if (typeof result?.event_id !== 'string' || !result.event_id.trim()) {
|
|
10
|
+
throw new ServiceError('memory_add did not confirm acceptance with an event_id');
|
|
48
11
|
}
|
|
49
|
-
return
|
|
12
|
+
return result;
|
|
50
13
|
}
|
|
51
|
-
// Direct REST APIs still require these fields; values come from config, never the model.
|
|
52
|
-
add(messages, metadata = {}) { return this.request('POST', 'memories/add', { messages, user_id: this.config.userId, infer: true, metadata }); }
|
|
53
|
-
get(id) { return this.request('GET', `memories/${encodeURIComponent(id)}`); }
|
|
54
|
-
update(id, text) { return this.request('PUT', `memories/${encodeURIComponent(id)}`, { text }); }
|
|
55
|
-
delete(id) { return this.request('DELETE', `memories/${encodeURIComponent(id)}`); }
|
|
56
|
-
event(id) { return this.request('GET', `events/${encodeURIComponent(id)}`); }
|
|
57
|
-
addSkill(messages) { return this.request('POST', 'skills/add', { messages, user_id: this.config.userId }, 'v3'); }
|
|
58
|
-
getSkill(id) { return this.request('GET', `skills/${encodeURIComponent(id)}`, undefined, 'v3'); }
|
|
59
|
-
deleteSkill(id) { return this.request('DELETE', `skills/${encodeURIComponent(id)}`, undefined, 'v3'); }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function errorSummary(error, privateValues = []) {
|
|
63
|
-
// JSON parser messages can quote configuration or conversation content.
|
|
64
|
-
const message = error instanceof SyntaxError ? 'Invalid JSON input' : error.message;
|
|
65
|
-
return JSON.stringify({ error: message, type: error.name, status: error.status, code: error.code, requestId: error.requestId,
|
|
66
|
-
cause: error.cause && { message: error.cause instanceof SyntaxError ? 'Invalid JSON input' : error.cause.message, code: error.cause.code },
|
|
67
|
-
}, (_key, value) => redactErrorValue(value, privateValues));
|
|
68
14
|
}
|
|
69
15
|
|
|
70
16
|
export function contextText(config) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// serviceId may remain in older connection files; it never selects a URL path.
|
|
2
|
+
export function mcpUrl(config) {
|
|
3
|
+
return `${config.endpoint}/v1/context/mcp`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function skillExtractionUrl(config) {
|
|
7
|
+
return `${config.endpoint}/v3/openapi/workspaces/${encodeURIComponent(config.workspace)}/context/skills/add`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class ServiceError extends Error {
|
|
2
|
+
constructor(message, { status, code, requestId, cause } = {}) {
|
|
3
|
+
super(message, { cause }); this.name = 'ServiceError'; this.status = status; this.code = code; this.requestId = requestId;
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function requestHeaders(config) {
|
|
8
|
+
return { Authorization: `Bearer ${config.apiKey}`, 'eke-user-id': config.userId };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function redactErrorValue(value, privateValues) {
|
|
12
|
+
if (typeof value !== 'string') return value;
|
|
13
|
+
for (const secret of privateValues.filter(item => typeof item === 'string' && item.length)) value = value.replaceAll(secret, '[REDACTED]');
|
|
14
|
+
return value.replace(/Bearer\s+[^\s"']+/gi, 'Bearer [REDACTED]').slice(0, 1024);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function errorSummary(error, privateValues = []) {
|
|
18
|
+
// JSON parser messages can quote configuration or conversation content.
|
|
19
|
+
const message = error instanceof SyntaxError ? 'Invalid JSON input' : error.message;
|
|
20
|
+
return JSON.stringify({ error: message, type: error.name, status: error.status, code: error.code, requestId: error.requestId,
|
|
21
|
+
cause: error.cause && { message: error.cause instanceof SyntaxError ? 'Invalid JSON input' : error.cause.message, code: error.cause.code },
|
|
22
|
+
}, (_key, value) => redactErrorValue(value, privateValues));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Tool arguments may contain nested message content, metadata, or corrections.
|
|
26
|
+
export function privateStrings(value) {
|
|
27
|
+
if (typeof value === 'string') return [value];
|
|
28
|
+
if (Array.isArray(value)) return value.flatMap(privateStrings);
|
|
29
|
+
if (value && typeof value === 'object') return Object.values(value).flatMap(privateStrings);
|
|
30
|
+
return [];
|
|
31
|
+
}
|
package/runtime/mcp.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { ServiceError, requestHeaders, redactErrorValue, privateStrings } from './errors.js';
|
|
3
|
+
import { mcpUrl } from './endpoints.js';
|
|
2
4
|
import { VERSION } from './version.js';
|
|
3
5
|
import { createParser } from 'eventsource-parser';
|
|
4
6
|
|
|
@@ -37,19 +39,21 @@ export async function withMcp(config, action, fetcher = globalThis.fetch) {
|
|
|
37
39
|
let session;
|
|
38
40
|
let protocol = PROTOCOL;
|
|
39
41
|
let id = 0;
|
|
40
|
-
const url =
|
|
42
|
+
const url = mcpUrl(config);
|
|
43
|
+
// Generated for each HTTP request; never persist a trace ID in host config.
|
|
44
|
+
const headers = () => ({ ...requestHeaders(config), 'x-request-id': randomUUID() });
|
|
41
45
|
// One deadline covers initialization and all calls, not a fresh timeout per KB.
|
|
42
46
|
const signal = AbortSignal.timeout(config.timeoutMs || 10000);
|
|
43
47
|
const rpc = async (method, params, notification = false) => {
|
|
44
48
|
const requestId = notification ? undefined : ++id;
|
|
45
49
|
const response = await fetcher(url, {
|
|
46
50
|
method: 'POST', redirect: 'error', signal,
|
|
47
|
-
headers: { ...
|
|
51
|
+
headers: { ...headers(), 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream',
|
|
48
52
|
...(method === 'initialize' ? {} : { 'MCP-Protocol-Version': protocol }), ...(session ? { 'Mcp-Session-Id': session } : {}) },
|
|
49
53
|
body: JSON.stringify({ jsonrpc: '2.0', ...(notification ? {} : { id: requestId }), method, ...(params === undefined ? {} : { params }) }),
|
|
50
54
|
});
|
|
51
55
|
session = response.headers.get('mcp-session-id') || session;
|
|
52
|
-
const privateValues = [config.apiKey, params?.arguments
|
|
56
|
+
const privateValues = [config.apiKey, ...privateStrings(params?.arguments)];
|
|
53
57
|
if (!response.ok) {
|
|
54
58
|
throw responseError(response, await readErrorBody(response), `MCP ${method} failed`, privateValues);
|
|
55
59
|
}
|
|
@@ -95,9 +99,39 @@ export async function withMcp(config, action, fetcher = globalThis.fetch) {
|
|
|
95
99
|
} finally {
|
|
96
100
|
if (session) {
|
|
97
101
|
try {
|
|
98
|
-
const response = await fetcher(url, { method: 'DELETE', redirect: 'error', headers: { ...
|
|
102
|
+
const response = await fetcher(url, { method: 'DELETE', redirect: 'error', headers: { ...headers(), 'Mcp-Session-Id': session, 'MCP-Protocol-Version': protocol }, signal: AbortSignal.timeout(3000) });
|
|
99
103
|
await response.body?.cancel();
|
|
100
104
|
} catch { /* Session cleanup is best effort, never a business data delete. */ }
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
}
|
|
108
|
+
|
|
109
|
+
// Both recall and capture accept the standard structured or JSON-text tool result.
|
|
110
|
+
export function toolResult(response, name) {
|
|
111
|
+
let result = response?.structuredContent;
|
|
112
|
+
const text = response?.content?.find(item => item.type === 'text')?.text;
|
|
113
|
+
if (result === undefined && text) {
|
|
114
|
+
try { result = JSON.parse(text); } catch { /* Never expose malformed result content. */ }
|
|
115
|
+
}
|
|
116
|
+
if (response?.isError) {
|
|
117
|
+
const detail = result?.error ?? result;
|
|
118
|
+
throw new ServiceError(typeof detail?.message === 'string' ? detail.message : text || `${name} failed`, {
|
|
119
|
+
code: detail?.code, requestId: detail?.request_id || detail?.requestId,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (result === undefined || result === null) throw new ServiceError(`Invalid ${name} result`);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function callMcpTool(config, name, args, fetcher = globalThis.fetch) {
|
|
127
|
+
const privateValues = [config.apiKey, ...privateStrings(args)];
|
|
128
|
+
try {
|
|
129
|
+
return await withMcp(config, async rpc => toolResult(await rpc('tools/call', { name, arguments: args }), name), fetcher);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const clean = value => redactErrorValue(value, privateValues);
|
|
132
|
+
throw new ServiceError(error instanceof SyntaxError ? `Invalid ${name} response` : clean(`${name}: ${error.message}`), {
|
|
133
|
+
status: error.status, code: clean(error.code || error.cause?.code), requestId: clean(error.requestId),
|
|
134
|
+
cause: error.cause && { message: error.cause instanceof SyntaxError ? 'Invalid JSON input' : clean(error.cause.message), code: clean(error.cause.code) },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
package/runtime/recall.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { withMcp } from './mcp.js';
|
|
1
|
+
import { errorSummary } from './errors.js';
|
|
2
|
+
import { withMcp, toolResult } from './mcp.js';
|
|
3
3
|
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
const detail = response.structuredContent;
|
|
7
|
-
throw new ServiceError(detail?.message || response.content?.find(item => item.type === 'text')?.text || 'hybrid_search failed', {
|
|
8
|
-
code: detail?.code, requestId: detail?.request_id || detail?.requestId,
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
const result = response.structuredContent ?? JSON.parse(response.content?.find(item => item.type === 'text')?.text || 'null');
|
|
4
|
+
function recallResult(response) {
|
|
5
|
+
const result = toolResult(response, 'hybrid_search');
|
|
12
6
|
if (!result || !Array.isArray(result.memories) || !Array.isArray(result.skills)
|
|
13
7
|
|| (result.kb_docs !== undefined && !Array.isArray(result.kb_docs))) throw new Error('Invalid hybrid_search result');
|
|
14
8
|
return result;
|
|
@@ -31,7 +25,7 @@ export async function hybridSearch(config, query, fetcher = globalThis.fetch) {
|
|
|
31
25
|
const responses = await Promise.all(scopes.map(async kb => {
|
|
32
26
|
try {
|
|
33
27
|
const response = await rpc('tools/call', { name: 'hybrid_search', arguments: { query, top_k: 5, ...(kb ? { kb_id: kb } : {}) } });
|
|
34
|
-
return { kb, result:
|
|
28
|
+
return { kb, result: recallResult(response) };
|
|
35
29
|
} catch (error) { return { kb, error }; }
|
|
36
30
|
}));
|
|
37
31
|
const successful = responses.filter(item => item.result);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { skillExtractionUrl } from './endpoints.js';
|
|
2
|
+
import { ServiceError, requestHeaders, redactErrorValue, privateStrings } from './errors.js';
|
|
3
|
+
|
|
4
|
+
// Temporary REST exception: the current MCP server has no conversation extraction tool.
|
|
5
|
+
export class SkillClient {
|
|
6
|
+
constructor(config, fetcher = globalThis.fetch) { this.config = config; this.fetcher = fetcher; }
|
|
7
|
+
async add(messages) {
|
|
8
|
+
const body = { messages, user_id: this.config.userId };
|
|
9
|
+
const privateValues = [this.config.apiKey, ...privateStrings(body)];
|
|
10
|
+
let response;
|
|
11
|
+
try {
|
|
12
|
+
response = await this.fetcher(skillExtractionUrl(this.config), {
|
|
13
|
+
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(this.config.timeoutMs || 10000),
|
|
14
|
+
headers: { ...requestHeaders(this.config), 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify(body),
|
|
16
|
+
});
|
|
17
|
+
} catch (error) {
|
|
18
|
+
throw new ServiceError(redactErrorValue(`Skill extraction: ${error.message}`, privateValues), {
|
|
19
|
+
code: error.code || error.cause?.code,
|
|
20
|
+
cause: error.cause && { message: redactErrorValue(error.cause.message, privateValues), code: error.cause.code },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (response.status === 204) return {};
|
|
24
|
+
let data;
|
|
25
|
+
try { data = await response.json(); } catch { throw new ServiceError('Service returned a non-JSON response', { status: response.status }); }
|
|
26
|
+
if (!response.ok || data.error || (data.code && !['200', '0', 'OK', 'Success'].includes(String(data.code)))) {
|
|
27
|
+
let message = data.message || data.error?.message || 'Agentic Context request failed';
|
|
28
|
+
if (typeof message !== 'string') message = 'Agentic Context request failed';
|
|
29
|
+
throw new ServiceError(redactErrorValue(message, privateValues), {
|
|
30
|
+
status: response.status, code: data.code || data.error?.code,
|
|
31
|
+
requestId: data.request_id || data.requestId || response.headers.get('x-request-id'),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return data.result ?? data;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/runtime/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.2.0';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { MemoryClient } from './client.js';
|
|
3
|
+
import { errorSummary, privateStrings } from './errors.js';
|
|
4
|
+
|
|
5
|
+
// Hermes passes conversation data on stdin, never in process arguments.
|
|
6
|
+
let config, input;
|
|
7
|
+
try {
|
|
8
|
+
config = JSON.parse(await fs.readFile(process.argv[2], 'utf8'));
|
|
9
|
+
let body = '';
|
|
10
|
+
for await (const chunk of process.stdin) body += chunk;
|
|
11
|
+
input = JSON.parse(body);
|
|
12
|
+
if (!config.autoCapture) throw new Error('Automatic capture is disabled');
|
|
13
|
+
if (!['endpoint', 'workspace', 'userId'].every(key => typeof input.expectedConnection?.[key] === 'string'
|
|
14
|
+
&& input.expectedConnection[key] === config[key])) {
|
|
15
|
+
throw new Error('Capture connection changed; restart Hermes before submitting queued conversations');
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(input.messages) || !input.messages.length) throw new Error('Capture messages must be a non-empty array');
|
|
18
|
+
const result = await new MemoryClient(config).add(input.messages, input.metadata);
|
|
19
|
+
process.stdout.write(JSON.stringify(result));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
process.stderr.write(`${errorSummary(error, [config?.apiKey, ...privateStrings(input)])}\n`);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
}
|
package/src/installer.js
CHANGED
|
@@ -362,7 +362,7 @@ export async function status(agent, options = {}, dependencies = {}) {
|
|
|
362
362
|
const config = await readJson(paths.config);
|
|
363
363
|
const drift = [];
|
|
364
364
|
if (!config) drift.push(paths.config);
|
|
365
|
-
for (const file of requiredPayloadFiles(agent, paths)) {
|
|
365
|
+
for (const file of requiredPayloadFiles(agent, paths, receipt.version)) {
|
|
366
366
|
try {
|
|
367
367
|
if (!(await fs.stat(file)).isFile()) drift.push(file);
|
|
368
368
|
else await fs.access(file, fs.constants.R_OK);
|
package/src/payload.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import
|
|
4
|
+
import cmdEscape from 'cross-spawn/lib/util/escape.js';
|
|
5
|
+
import { NAME, MARKETPLACE, VERSION, platformPath } from './config.js';
|
|
5
6
|
import { writePrivate } from './files.js';
|
|
6
7
|
import { contextText, requestHeaders } from '../runtime/client.js';
|
|
8
|
+
import { mcpUrl } from '../runtime/endpoints.js';
|
|
7
9
|
|
|
8
10
|
export const PACKAGE_ROOT = fileURLToPath(new URL('../', import.meta.url));
|
|
9
11
|
export function shellQuote(value) {
|
|
@@ -14,6 +16,14 @@ export function hookConfig(paths, agent, { platform = process.platform, execPath
|
|
|
14
16
|
let invocation;
|
|
15
17
|
if (platform === 'win32' && agent === 'claude-code') {
|
|
16
18
|
invocation = { command: execPath, args };
|
|
19
|
+
} else if (platform === 'win32' && agent === 'codex') {
|
|
20
|
+
// Codex runs local plugin hooks through COMSPEC /C. Reuse our process
|
|
21
|
+
// launcher's cmd escaping and the detected Node binary; no extra shell.
|
|
22
|
+
// Unlike path metacharacters, line breaks cannot be escaped by cmdEscape.
|
|
23
|
+
if ([execPath, ...args].some(value => /[\0\r\n]/.test(value))) {
|
|
24
|
+
throw new Error('Windows Codex hook paths must not contain NUL or line breaks');
|
|
25
|
+
}
|
|
26
|
+
invocation = { command: [cmdEscape.command(platformPath(platform).normalize(execPath)), ...args.map(value => cmdEscape.argument(value))].join(' ') };
|
|
17
27
|
} else if (platform === 'win32') {
|
|
18
28
|
// A fixed PowerShell launcher works from cmd, PowerShell and Git Bash without re-parsing paths.
|
|
19
29
|
const script = `[Console]::InputEncoding = [Console]::OutputEncoding = $OutputEncoding = [System.Text.UTF8Encoding]::new($false); & ${[execPath, ...args].map(value => `'${value.replaceAll("'", "''")}'`).join(' ')}; exit $LASTEXITCODE`;
|
|
@@ -25,7 +35,7 @@ export function hookConfig(paths, agent, { platform = process.platform, execPath
|
|
|
25
35
|
return { hooks: Object.fromEntries(events.map(event => [event, [{ hooks: [{ type: 'command', ...invocation, timeout: 15 }] }]])) };
|
|
26
36
|
}
|
|
27
37
|
export function mcpServer(config, agent) {
|
|
28
|
-
const base = { url:
|
|
38
|
+
const base = { url: mcpUrl(config) };
|
|
29
39
|
const headers = { ...requestHeaders(config), Accept: 'application/json, text/event-stream' };
|
|
30
40
|
if (agent === 'codex') return { ...base, http_headers: headers };
|
|
31
41
|
if (agent === 'hermes') return { ...base, headers };
|
|
@@ -40,16 +50,17 @@ async function copyRuntime(destination) {
|
|
|
40
50
|
await fs.cp(parserRoot, path.join(destination, 'node_modules/eventsource-parser'), { recursive: true });
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
export function requiredPayloadFiles(agent, paths) {
|
|
53
|
+
export function requiredPayloadFiles(agent, paths, installedVersion = VERSION) {
|
|
44
54
|
const skillName = paths.skillName || NAME;
|
|
45
55
|
const runtime = agent === 'openclaw' ? path.join(paths.plugin, 'runtime') : paths.runtime;
|
|
46
56
|
// Legacy releases predate automatic recall and the submission helper. Check
|
|
47
57
|
// their original entry points, not files introduced by a newer release.
|
|
58
|
+
const mcpCapture = !['0.1.0', '0.1.1'].includes(installedVersion);
|
|
48
59
|
const shared = paths.legacy ? (agent === 'hermes' ? [] : agent === 'openclaw' ? ['client.js'] : ['package.json', 'client.js']) : [
|
|
49
|
-
'package.json', 'client.js', 'mcp.js', 'recall.js', 'version.js',
|
|
60
|
+
'package.json', 'client.js', ...(installedVersion === '0.1.0' ? [] : ['endpoints.js']), ...(mcpCapture ? ['errors.js'] : []), 'mcp.js', 'recall.js', 'version.js',
|
|
50
61
|
'node_modules/eventsource-parser/package.json', 'node_modules/eventsource-parser/dist/index.js'];
|
|
51
|
-
const entrypoints = agent === 'hermes' ? (paths.legacy ? [] : ['recall-cli.js'])
|
|
52
|
-
: ['capture.js', ...(agent === 'openclaw' ? (paths.legacy ? [] : ['submission.js']) : ['hook.js'])];
|
|
62
|
+
const entrypoints = agent === 'hermes' ? (paths.legacy ? [] : ['recall-cli.js', ...(mcpCapture ? ['write-cli.js'] : [])])
|
|
63
|
+
: ['capture.js', ...(agent === 'openclaw' ? (paths.legacy ? [] : ['submission.js', ...(mcpCapture ? ['skill-client.js'] : [])]) : ['hook.js'])];
|
|
53
64
|
const files = [...shared, ...entrypoints].map(file => path.join(runtime, file));
|
|
54
65
|
files.push(path.join(paths.plugin, 'skills', skillName, 'SKILL.md'));
|
|
55
66
|
if (['codex', 'claude-code'].includes(agent)) {
|
|
@@ -103,7 +114,6 @@ export async function preparePayload(agent, paths, config) {
|
|
|
103
114
|
await writePrivate(path.join(paths.plugin, 'openclaw.plugin.json'), {
|
|
104
115
|
id: name, name: 'Agentic Context', version: VERSION, skills: ['./skills'],
|
|
105
116
|
activation: { onStartup: true },
|
|
106
|
-
cliCommands: [name, LEGACY_NAME].map(command => ({ name: command, description: 'Agentic Context memory and skill operations', hasSubcommands: true })),
|
|
107
117
|
// OpenClaw validates config during install, before the explicit plugin entry is written.
|
|
108
118
|
configSchema: { type: 'object', properties: { configPath: { type: 'string', default: paths.config } }, required: ['configPath'], additionalProperties: false },
|
|
109
119
|
});
|