@openviking/openclaw-plugin 2026.7.11 → 2026.8.6
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/INSTALL-ZH.md +39 -0
- package/INSTALL.md +39 -0
- package/README.md +5 -0
- package/auto-recall.ts +5 -4
- package/config.ts +145 -6
- package/context-engine.ts +0 -1
- package/dist/auto-recall.js +4 -4
- package/dist/config.js +101 -4
- package/dist/context-engine.js +0 -1
- package/dist/index.js +13 -4
- package/dist/plugin/openviking-archive-tools.js +1 -2
- package/dist/plugin/openviking-command-definitions.js +3 -3
- package/dist/plugin/openviking-experience-tools.js +105 -0
- package/dist/plugin/openviking-import-tools.js +2 -2
- package/dist/plugin/openviking-memory-recall-tools.js +4 -4
- package/dist/plugin/openviking-memory-tools.js +11 -6
- package/dist/plugin/openviking-query-runtime.js +1 -1
- package/dist/plugin/openviking-query-tools.js +4 -4
- package/dist/plugin/openviking-recall-trace-runtime.js +3 -3
- package/dist/plugin/openviking-session-routing-runtime.js +9 -2
- package/dist/plugin/openviking-tool-result-tools.js +3 -3
- package/dist/routing/identity-routing.js +27 -0
- package/dist/routing/memory-uri.js +1 -0
- package/dist/services/context-lifecycle-service.js +21 -16
- package/dist/services/context-message-adapter.js +2 -9
- package/index.ts +13 -3
- package/install-manifest.json +1 -0
- package/openclaw.plugin.json +46 -3
- package/package.json +1 -1
- package/plugin/command-registration.ts +2 -0
- package/plugin/openviking-archive-tools.ts +1 -4
- package/plugin/openviking-command-definitions.ts +4 -3
- package/plugin/openviking-experience-tools.ts +160 -0
- package/plugin/openviking-import-tools.ts +4 -2
- package/plugin/openviking-memory-recall-tools.ts +6 -4
- package/plugin/openviking-memory-tools.ts +18 -10
- package/plugin/openviking-query-runtime.ts +1 -1
- package/plugin/openviking-query-tools.ts +6 -4
- package/plugin/openviking-recall-trace-runtime.ts +4 -3
- package/plugin/openviking-recall-trace-tools.ts +2 -0
- package/plugin/openviking-session-routing-runtime.ts +15 -1
- package/plugin/openviking-tool-result-tools.ts +0 -6
- package/registries/openviking-tools.ts +6 -0
- package/routing/identity-routing.ts +41 -0
- package/routing/memory-uri.ts +1 -0
- package/services/context-lifecycle-service.ts +27 -18
- package/services/context-message-adapter.ts +2 -9
- package/skills/ov-experience-memory/SKILL.md +156 -0
package/INSTALL-ZH.md
CHANGED
|
@@ -203,6 +203,45 @@ openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --forc
|
|
|
203
203
|
openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role assistant --peer-prefix <PREFIX> --json
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
+
#### 无法执行 CLI 时直接配置文件
|
|
207
|
+
|
|
208
|
+
如果容器内无法执行 `openclaw` CLI,可以把以下字段合并到 OpenClaw 实际读取的配置文件。设置了 `OPENCLAW_CONFIG_PATH` 时使用该路径;否则通常是 `$OPENCLAW_STATE_DIR/openclaw.json`(默认 `~/.openclaw/openclaw.json`)。
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"plugins": {
|
|
213
|
+
"entries": {
|
|
214
|
+
"openviking": {
|
|
215
|
+
"enabled": true,
|
|
216
|
+
"config": {
|
|
217
|
+
"mode": "remote",
|
|
218
|
+
"baseUrl": "http://openviking:1933",
|
|
219
|
+
"apiKey": "<API_KEY>",
|
|
220
|
+
"peer_role": "assistant"
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
"slots": {
|
|
225
|
+
"contextEngine": "openviking"
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- 插件必须已经安装;编辑前请备份配置,并把以上字段合并到现有 `plugins` 配置。
|
|
232
|
+
- 如果配置中已有 `plugins.allow`,把 `openviking` 追加进去;如果没有,不要仅为本插件新建 allowlist。
|
|
233
|
+
- `contextEngine` 是独占 slot;若已有其他 context engine,只有确认替换后再修改该字段。使用 root API key 时,还需在 `config` 中设置 `accountId` 和 `userId`。
|
|
234
|
+
- 容器连接其他服务时,`baseUrl` 应使用容器内可访问的服务地址,而不是 `127.0.0.1`。
|
|
235
|
+
- 推荐使用 `SecretRef` 对象作为 `apiKey`(而不是明文字符串),避免密钥直接落盘写入 `openclaw.json`。支持的形式与 OpenClaw 核心中 LLM/TTS/MCP 等 provider 配置使用的标准 `SecretRef` 一致:
|
|
236
|
+
|
|
237
|
+
| 类型 | 示例 | 说明 |
|
|
238
|
+
| --- | --- | --- |
|
|
239
|
+
| `env` | `{ "source": "env", "id": "OPENVIKING_API_KEY" }` | 启动时读取同名环境变量。 |
|
|
240
|
+
| `file` | `{ "source": "file", "id": "/etc/secrets/openviking.key" }` | 以 UTF-8 读取并去除首尾空白;`~` 可展开,适配 Kubernetes `secretKeyRef` 卷挂载、0600 权限文件。 |
|
|
241
|
+
| `exec` | `{ "source": "exec", "provider": "op", "id": "op://vault/openviking/credential" }` | 运行 `<provider> <id>`,去除 stdout 首尾空白(1Password `op`、Vault、gopass 等均符合此形态)。 |
|
|
242
|
+
|
|
243
|
+
仍可使用纯字符串形式(含 `${ENV_VAR}` 插值)作为向后兼容路径;此时请限制文件权限或通过受控 Secret 卷提供,并在修改后重启 Gateway、容器或 Pod。
|
|
244
|
+
|
|
206
245
|
### 3. 重启 OpenClaw Gateway
|
|
207
246
|
|
|
208
247
|
```bash
|
package/INSTALL.md
CHANGED
|
@@ -147,6 +147,45 @@ If you want assistant messages to carry a prefixed `peer_id` and data-plane reca
|
|
|
147
147
|
openclaw openviking setup --base-url <OPENVIKING_URL> --api-key <API_KEY> --peer-role assistant --peer-prefix <PREFIX> --json
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
#### Configure The File Directly When The CLI Is Unavailable
|
|
151
|
+
|
|
152
|
+
If the `openclaw` CLI cannot run inside the container, merge the following fields into the config file that OpenClaw actually reads. Use `OPENCLAW_CONFIG_PATH` when it is set; otherwise the file is usually `$OPENCLAW_STATE_DIR/openclaw.json` (default: `~/.openclaw/openclaw.json`).
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"plugins": {
|
|
157
|
+
"entries": {
|
|
158
|
+
"openviking": {
|
|
159
|
+
"enabled": true,
|
|
160
|
+
"config": {
|
|
161
|
+
"mode": "remote",
|
|
162
|
+
"baseUrl": "http://openviking:1933",
|
|
163
|
+
"apiKey": "<API_KEY>",
|
|
164
|
+
"peer_role": "assistant"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"slots": {
|
|
169
|
+
"contextEngine": "openviking"
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- The plugin must already be installed. Back up the file and merge these fields into the existing `plugins` config.
|
|
176
|
+
- If `plugins.allow` already exists, append `openviking`; otherwise, do not create an allowlist only for this plugin.
|
|
177
|
+
- `contextEngine` is an exclusive slot. If another context engine is configured, change it only after confirming the replacement. Root API keys also require `accountId` and `userId` in `config`.
|
|
178
|
+
- When connecting to another service from a container, use a `baseUrl` that is reachable from the container rather than `127.0.0.1`.
|
|
179
|
+
- Prefer a `SecretRef` for `apiKey` instead of a plaintext string so the key is never stored inside `openclaw.json`. Supported shapes match OpenClaw's standard `SecretRef` used by LLM/TTS/MCP provider configs:
|
|
180
|
+
|
|
181
|
+
| Shape | Example | Notes |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `env` | `{ "source": "env", "id": "OPENVIKING_API_KEY" }` | Reads the named env var at plugin load time. |
|
|
184
|
+
| `file` | `{ "source": "file", "id": "/etc/secrets/openviking.key" }` | Reads UTF-8, trims whitespace. `~` is expanded; ideal for Kubernetes `secretKeyRef` volumes and 0600-managed files. |
|
|
185
|
+
| `exec` | `{ "source": "exec", "provider": "op", "id": "op://vault/openviking/credential" }` | Runs `<provider> <id>` and trims stdout (1Password `op`, Vault, gopass, etc. all match this shape). |
|
|
186
|
+
|
|
187
|
+
A plain `string` (including `${ENV_VAR}` interpolation) is still accepted, but only as a backward-compatibility path; in that case, restrict file permissions or provide the file through a managed Secret volume, then restart the Gateway, container, or Pod.
|
|
188
|
+
|
|
150
189
|
### 3. Restart OpenClaw Gateway
|
|
151
190
|
|
|
152
191
|
```bash
|
package/README.md
CHANGED
|
@@ -46,6 +46,8 @@ Once installed, the plugin provides these agent tools:
|
|
|
46
46
|
| `ov_read` | Read the full original content of one exact OpenViking URI |
|
|
47
47
|
| `ov_multi_read` | Read the full original content of multiple OpenViking URIs |
|
|
48
48
|
| `ov_list` | List OpenViking directories after search to inspect sibling chunks and overview files |
|
|
49
|
+
| `search_experience` | Search reusable execution experiences for the current OpenViking user |
|
|
50
|
+
| `read_experience` | Read one exact Experience returned by `search_experience` |
|
|
49
51
|
| `openviking_tool_result_read` | Restore the full original content of an externalized tool result |
|
|
50
52
|
| `openviking_tool_result_search` | Search inside an externalized tool result by keyword |
|
|
51
53
|
| `openviking_tool_result_list` | List externalized tool results in the current session |
|
|
@@ -248,6 +250,8 @@ Beyond automatic behavior, the plugin exposes these tools directly:
|
|
|
248
250
|
- `ov_read`: read one exact `viking://` URI returned by `ov_search` or `ov_list`
|
|
249
251
|
- `ov_multi_read`: read multiple exact `viking://` URIs, useful for an overview plus sibling chunks
|
|
250
252
|
- `ov_list`: list a hit's parent directory after `ov_search` to recover sibling chunks, `.overview.md`, and related split-document context
|
|
253
|
+
- `search_experience`: search reusable execution experiences for the current OpenViking user
|
|
254
|
+
- `read_experience`: read one exact Experience returned by `search_experience`
|
|
251
255
|
|
|
252
256
|
They serve different roles:
|
|
253
257
|
|
|
@@ -261,6 +265,7 @@ They serve different roles:
|
|
|
261
265
|
- `ov_read` turns a ranked hit into original evidence before answering precise documentation, codebase, configuration, or procedural questions
|
|
262
266
|
- `ov_multi_read` reads overview and sibling chunks together when a split document needs more context than a single hit
|
|
263
267
|
- `ov_list` complements `ov_search` when a ranked hit is only one chunk of a larger procedure or document
|
|
268
|
+
- `search_experience` and `read_experience` provide the exact tool-call contract used by the bundled Experience Memory skill and usage reporting
|
|
264
269
|
|
|
265
270
|
`ov_archive_expand` is especially important because `assemble()` normally returns archive summaries and indexes, not the full raw transcript.
|
|
266
271
|
|
package/auto-recall.ts
CHANGED
|
@@ -313,6 +313,7 @@ export async function buildAutoRecallContext(params: {
|
|
|
313
313
|
queryConfig?: EffectiveQueryConfig;
|
|
314
314
|
client: OpenVikingClient;
|
|
315
315
|
agentId: string;
|
|
316
|
+
actorPeerId?: string;
|
|
316
317
|
queryText: string;
|
|
317
318
|
logger: Logger;
|
|
318
319
|
verbose?: (message: string) => void;
|
|
@@ -324,14 +325,14 @@ export async function buildAutoRecallContext(params: {
|
|
|
324
325
|
queryTruncated?: boolean;
|
|
325
326
|
resourceTypes?: RecallResourceType[];
|
|
326
327
|
}): Promise<{ block?: string; memoryCount: number; estimatedTokens: number }> {
|
|
327
|
-
const { cfg, client, agentId, queryText, logger, verbose } = params;
|
|
328
|
+
const { cfg, client, agentId, actorPeerId, queryText, logger, verbose } = params;
|
|
328
329
|
const queryConfig = params.queryConfig;
|
|
329
330
|
|
|
330
331
|
if (!cfg.autoRecall || queryText.length < 5) {
|
|
331
332
|
return { memoryCount: 0, estimatedTokens: 0 };
|
|
332
333
|
}
|
|
333
334
|
|
|
334
|
-
const precheck = await quickRecallPrecheck(client,
|
|
335
|
+
const precheck = await quickRecallPrecheck(client, actorPeerId);
|
|
335
336
|
if (!precheck.ok) {
|
|
336
337
|
verbose?.(`openviking: skipping auto-recall because precheck failed (${precheck.reason})`);
|
|
337
338
|
return { memoryCount: 0, estimatedTokens: 0 };
|
|
@@ -369,7 +370,7 @@ export async function buildAutoRecallContext(params: {
|
|
|
369
370
|
limit: candidateLimit,
|
|
370
371
|
scoreThreshold: 0,
|
|
371
372
|
contextType: search.contextType,
|
|
372
|
-
actorPeerId
|
|
373
|
+
actorPeerId,
|
|
373
374
|
});
|
|
374
375
|
return {
|
|
375
376
|
resourceType: search.resourceType,
|
|
@@ -484,7 +485,7 @@ export async function buildAutoRecallContext(params: {
|
|
|
484
485
|
|
|
485
486
|
const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(
|
|
486
487
|
memories,
|
|
487
|
-
(uri) => client.read(uri,
|
|
488
|
+
(uri) => client.read(uri, actorPeerId),
|
|
488
489
|
{
|
|
489
490
|
recallPreferAbstract,
|
|
490
491
|
recallMaxInjectedChars: maxInjectedChars,
|
package/config.ts
CHANGED
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
|
|
3
4
|
import { getEnv } from "./runtime-utils.js";
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Subset of OpenClaw's standard `SecretRef` shape supported by the OpenViking
|
|
8
|
+
* plugin. The full runtime SDK also recognises `source: "exec"` with a
|
|
9
|
+
* provider plugin, but the plugin self-hosts its own env/file resolution so
|
|
10
|
+
* users on pluginSdkVersion < 2026.6.x (no host-level secret-resolver) still
|
|
11
|
+
* get vault-free secret management.
|
|
12
|
+
*
|
|
13
|
+
* source: "env" -> `process.env[id]` (same as the bare `${ENV}` string
|
|
14
|
+
* interpolation, but declarative and visible to UI hints).
|
|
15
|
+
* source: "file" -> contents of `id` path (``~`` expansion supported),
|
|
16
|
+
* leading/trailing whitespace trimmed — convenient for
|
|
17
|
+
* Kubernetes `secretKeyRef` mount volumes and
|
|
18
|
+
* file-permission-only deployments.
|
|
19
|
+
* source: "exec" -> delegate to host `openclaw` secret resolver when the
|
|
20
|
+
* runtime exposes it; today the plugin performs a
|
|
21
|
+
* best-effort `provider + id` CLI exec call so that
|
|
22
|
+
* 1Password / Vault / gopass users can share OpenViking
|
|
23
|
+
* credentials with their other providers.
|
|
24
|
+
*/
|
|
25
|
+
export type OpenVikingSecretRef =
|
|
26
|
+
| { source: "env"; id: string }
|
|
27
|
+
| { source: "file"; id: string }
|
|
28
|
+
| { source: "exec"; provider: string; id: string };
|
|
29
|
+
|
|
5
30
|
export type MemoryOpenVikingConfig = {
|
|
6
31
|
mode?: "remote";
|
|
7
32
|
baseUrl?: string;
|
|
8
33
|
peer_role?: "none" | "assistant" | "person";
|
|
9
34
|
peer_prefix?: string;
|
|
10
|
-
apiKey?: string;
|
|
35
|
+
apiKey?: string | OpenVikingSecretRef;
|
|
11
36
|
/** Optional HTTP headers merged into every OpenViking request. */
|
|
12
37
|
headers?: Record<string, string>;
|
|
13
38
|
/** Advanced option. Only needed when explicitly sending tenant identity headers. With a user key the server derives identity from the key. */
|
|
@@ -102,8 +127,10 @@ export type MemoryOpenVikingConfig = {
|
|
|
102
127
|
|
|
103
128
|
/** Runtime config after memoryOpenVikingConfigSchema.parse() has applied defaults. */
|
|
104
129
|
export type ParsedMemoryOpenVikingConfig = Required<
|
|
105
|
-
Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes">
|
|
130
|
+
Omit<MemoryOpenVikingConfig, "agentExperience" | "recallTargetTypes" | "apiKey">
|
|
106
131
|
> & {
|
|
132
|
+
/** parse() resolves SecretRef values, so the runtime shape is always a plain string. */
|
|
133
|
+
apiKey: string;
|
|
107
134
|
agentExperience: Required<NonNullable<MemoryOpenVikingConfig["agentExperience"]>>;
|
|
108
135
|
recallTargetTypes: Array<"resource" | "user" | "agent">;
|
|
109
136
|
};
|
|
@@ -143,6 +170,8 @@ export const OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES = [
|
|
|
143
170
|
"ov_read",
|
|
144
171
|
"ov_multi_read",
|
|
145
172
|
"ov_list",
|
|
173
|
+
"search_experience",
|
|
174
|
+
"read_experience",
|
|
146
175
|
"memory_recall",
|
|
147
176
|
"ov_recall_trace",
|
|
148
177
|
"memory_store",
|
|
@@ -163,6 +192,7 @@ export const OPENVIKING_TOOL_GROUPS: Record<string, readonly OpenVikingToolName[
|
|
|
163
192
|
default: OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES,
|
|
164
193
|
memory: ["memory_recall", "memory_store", "memory_forget"],
|
|
165
194
|
resource_query: ["ov_search", "ov_read", "ov_multi_read", "ov_list"],
|
|
195
|
+
experience: ["search_experience", "read_experience"],
|
|
166
196
|
import: ["add_resource", "add_skill"],
|
|
167
197
|
recall_trace: ["ov_recall_trace"],
|
|
168
198
|
archive: ["ov_archive_search", "ov_archive_expand"],
|
|
@@ -188,6 +218,105 @@ function resolvePeerPrefix(configured: unknown): string {
|
|
|
188
218
|
return DEFAULT_PEER_PREFIX;
|
|
189
219
|
}
|
|
190
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Resolve an {@link OpenVikingSecretRef} to the actual secret string. Plain
|
|
223
|
+
* strings pass through untouched so callers can compose this with the legacy
|
|
224
|
+
* `resolveEnvVars` pipeline without branching.
|
|
225
|
+
*
|
|
226
|
+
* Intentional behaviours:
|
|
227
|
+
* * `env`: uses `getEnv` (process.env but exposed for vitest overrides). An
|
|
228
|
+
* unset / empty env var is an error — the user asked for a named secret
|
|
229
|
+
* so silent empty-string fallback would mask misconfigurations.
|
|
230
|
+
* * `file`: ~ expanded, then read as UTF-8, then leading/trailing whitespace
|
|
231
|
+
* stripped (de-facto standard for files written with `echo xxx > key`).
|
|
232
|
+
* Missing / unreadable files propagate the original node error with an
|
|
233
|
+
* error prefix that names the OpenViking field, so the user knows which
|
|
234
|
+
* secretRef failed to load.
|
|
235
|
+
* * `exec`: shells out `<provider> <id>`. Providers commonly expose this
|
|
236
|
+
* shape (1Password CLI, gopass, HashiCorp Vault), which matches how
|
|
237
|
+
* OpenClaw's own `@transmitt0r/openclaw-plugin-onepassword` provider
|
|
238
|
+
* resolves `{source:"exec", provider:"onepassword", id:"..."}`. Exec is
|
|
239
|
+
* best-effort; failures surface with an error prefix so users can
|
|
240
|
+
* distinguish `provider not installed` from `provider configured but id
|
|
241
|
+
* missing`.
|
|
242
|
+
*/
|
|
243
|
+
function resolveSecret(
|
|
244
|
+
value: string | OpenVikingSecretRef | undefined | null,
|
|
245
|
+
label: string,
|
|
246
|
+
): string {
|
|
247
|
+
if (value === undefined || value === null) return "";
|
|
248
|
+
if (typeof value === "string") return value;
|
|
249
|
+
if (!value || typeof value !== "object") {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`OpenViking ${label} must be a plain string or a SecretRef object ` +
|
|
252
|
+
`({source:"env"|"file"|"exec", id})`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
const obj = value as Record<string, unknown>;
|
|
256
|
+
if (typeof obj.id !== "string" || !obj.id) {
|
|
257
|
+
throw new Error(`OpenViking ${label} SecretRef requires a non-empty string "id"`);
|
|
258
|
+
}
|
|
259
|
+
const id = obj.id;
|
|
260
|
+
switch (obj.source) {
|
|
261
|
+
case "env": {
|
|
262
|
+
const envValue = getEnv(id);
|
|
263
|
+
if (!envValue) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`OpenViking ${label} SecretRef env source: environment variable ${id} is not set or empty`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return envValue;
|
|
269
|
+
}
|
|
270
|
+
case "file": {
|
|
271
|
+
const resolvedPath = expandHomeDir(id);
|
|
272
|
+
try {
|
|
273
|
+
const contents = readFileSync(resolvedPath, "utf8");
|
|
274
|
+
return contents.trim();
|
|
275
|
+
} catch (err) {
|
|
276
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
277
|
+
throw new Error(
|
|
278
|
+
`OpenViking ${label} SecretRef file source: cannot read ${resolvedPath} (${msg})`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
case "exec": {
|
|
283
|
+
const provider = typeof (obj as Record<string, unknown>).provider === "string"
|
|
284
|
+
? (obj as Record<string, unknown>).provider as string
|
|
285
|
+
: "";
|
|
286
|
+
if (!provider) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`OpenViking ${label} SecretRef exec source requires a "provider" field naming the secret-manager CLI on PATH`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
// Avoid top-level `import "node:child_process"` — this branch is rare
|
|
293
|
+
// and keeping the require lazy makes startup marginally faster for
|
|
294
|
+
// the 95% of users who pick env/file.
|
|
295
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
296
|
+
const { execFileSync } = require("node:child_process") as typeof import("node:child_process");
|
|
297
|
+
const out = execFileSync(provider, [id], {
|
|
298
|
+
encoding: "utf8",
|
|
299
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
300
|
+
timeout: 15_000,
|
|
301
|
+
env: process.env,
|
|
302
|
+
}) as string;
|
|
303
|
+
return out.trim();
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
306
|
+
throw new Error(
|
|
307
|
+
`OpenViking ${label} SecretRef exec source: provider "${provider}" failed for id "${id}" (${msg})`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
default:
|
|
312
|
+
throw new Error(
|
|
313
|
+
`OpenViking ${label} SecretRef has unknown source "${String(
|
|
314
|
+
(obj as Record<string, unknown>).source,
|
|
315
|
+
)}". Supported: "env" | "file" | "exec".`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
191
320
|
function resolvePeerRole(configured: unknown) {
|
|
192
321
|
if (typeof configured === "string") {
|
|
193
322
|
const role = configured.trim().toLowerCase();
|
|
@@ -465,7 +594,14 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
465
594
|
const peerPrefix = resolvePeerPrefix(cfg.peer_prefix);
|
|
466
595
|
const rawBaseUrl = typeof cfg.baseUrl === "string" ? cfg.baseUrl : resolveDefaultBaseUrl();
|
|
467
596
|
const resolvedBaseUrl = resolveEnvVars(rawBaseUrl).replace(/\/+$/, "");
|
|
468
|
-
|
|
597
|
+
// Support plain string, SecretRef object, and OPENVIKING_API_KEY fallback.
|
|
598
|
+
// A user writing a SecretRef has explicitly opted out of the bare env
|
|
599
|
+
// interpolation path, so the fallback kicks in only when nothing is
|
|
600
|
+
// configured at all (matches existing behaviour).
|
|
601
|
+
const rawApiKey: string | OpenVikingSecretRef | undefined =
|
|
602
|
+
cfg.apiKey !== undefined && cfg.apiKey !== null
|
|
603
|
+
? (cfg.apiKey as string | OpenVikingSecretRef)
|
|
604
|
+
: getEnv("OPENVIKING_API_KEY") || undefined;
|
|
469
605
|
const captureMode = cfg.captureMode;
|
|
470
606
|
if (
|
|
471
607
|
typeof captureMode !== "undefined" &&
|
|
@@ -508,7 +644,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
508
644
|
baseUrl: resolvedBaseUrl,
|
|
509
645
|
peer_role: peerRole,
|
|
510
646
|
peer_prefix: peerPrefix,
|
|
511
|
-
apiKey: rawApiKey ? resolveEnvVars(rawApiKey) : "",
|
|
647
|
+
apiKey: rawApiKey ? resolveEnvVars(resolveSecret(rawApiKey, "config.apiKey")) : "",
|
|
512
648
|
headers: toStringRecord(cfg.headers, "openviking config headers"),
|
|
513
649
|
accountId,
|
|
514
650
|
userId,
|
|
@@ -678,7 +814,10 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
678
814
|
label: "OpenViking API Key",
|
|
679
815
|
sensitive: true,
|
|
680
816
|
placeholder: "${OPENVIKING_API_KEY}",
|
|
681
|
-
help:
|
|
817
|
+
help:
|
|
818
|
+
"Optional API key for OpenViking server. Accepts a plain string, " +
|
|
819
|
+
"${ENV_VAR} interpolation, or a SecretRef object ({source: env/file/exec, id}). " +
|
|
820
|
+
"Prefer the SecretRef shapes so the key never sits as plaintext in openclaw.json.",
|
|
682
821
|
},
|
|
683
822
|
headers: {
|
|
684
823
|
label: "Headers",
|
|
@@ -836,7 +975,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
836
975
|
enabledTools: {
|
|
837
976
|
label: "Enabled Tools",
|
|
838
977
|
placeholder: "default",
|
|
839
|
-
help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
|
|
978
|
+
help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, experience, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
|
|
840
979
|
advanced: true,
|
|
841
980
|
},
|
|
842
981
|
disabledTools: {
|
package/context-engine.ts
CHANGED
package/dist/auto-recall.js
CHANGED
|
@@ -200,12 +200,12 @@ export function shouldRecallAgentExperience(input) {
|
|
|
200
200
|
return { recall: false, score, reason: score < 0 ? "non_execution" : "below_threshold" };
|
|
201
201
|
}
|
|
202
202
|
export async function buildAutoRecallContext(params) {
|
|
203
|
-
const { cfg, client, agentId, queryText, logger, verbose } = params;
|
|
203
|
+
const { cfg, client, agentId, actorPeerId, queryText, logger, verbose } = params;
|
|
204
204
|
const queryConfig = params.queryConfig;
|
|
205
205
|
if (!cfg.autoRecall || queryText.length < 5) {
|
|
206
206
|
return { memoryCount: 0, estimatedTokens: 0 };
|
|
207
207
|
}
|
|
208
|
-
const precheck = await quickRecallPrecheck(client,
|
|
208
|
+
const precheck = await quickRecallPrecheck(client, actorPeerId);
|
|
209
209
|
if (!precheck.ok) {
|
|
210
210
|
verbose?.(`openviking: skipping auto-recall because precheck failed (${precheck.reason})`);
|
|
211
211
|
return { memoryCount: 0, estimatedTokens: 0 };
|
|
@@ -236,7 +236,7 @@ export async function buildAutoRecallContext(params) {
|
|
|
236
236
|
limit: candidateLimit,
|
|
237
237
|
scoreThreshold: 0,
|
|
238
238
|
contextType: search.contextType,
|
|
239
|
-
actorPeerId
|
|
239
|
+
actorPeerId,
|
|
240
240
|
});
|
|
241
241
|
return {
|
|
242
242
|
resourceType: search.resourceType,
|
|
@@ -343,7 +343,7 @@ export async function buildAutoRecallContext(params) {
|
|
|
343
343
|
await recordTrace([], 0, 0);
|
|
344
344
|
return { memoryCount: 0, estimatedTokens: 0 };
|
|
345
345
|
}
|
|
346
|
-
const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(memories, (uri) => client.read(uri,
|
|
346
|
+
const { lines: memoryLines, estimatedTokens } = await buildMemoryLinesWithBudget(memories, (uri) => client.read(uri, actorPeerId), {
|
|
347
347
|
recallPreferAbstract,
|
|
348
348
|
recallMaxInjectedChars: maxInjectedChars,
|
|
349
349
|
includeUri: true,
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { getEnv } from "./runtime-utils.js";
|
|
3
4
|
const DEFAULT_BASE_URL = "http://127.0.0.1:1933";
|
|
4
5
|
const DEFAULT_TARGET_URI = "viking://user/memories";
|
|
@@ -34,6 +35,8 @@ export const OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES = [
|
|
|
34
35
|
"ov_read",
|
|
35
36
|
"ov_multi_read",
|
|
36
37
|
"ov_list",
|
|
38
|
+
"search_experience",
|
|
39
|
+
"read_experience",
|
|
37
40
|
"memory_recall",
|
|
38
41
|
"ov_recall_trace",
|
|
39
42
|
"memory_store",
|
|
@@ -53,6 +56,7 @@ export const OPENVIKING_TOOL_GROUPS = {
|
|
|
53
56
|
default: OPENVIKING_DEFAULT_ENABLED_TOOL_NAMES,
|
|
54
57
|
memory: ["memory_recall", "memory_store", "memory_forget"],
|
|
55
58
|
resource_query: ["ov_search", "ov_read", "ov_multi_read", "ov_list"],
|
|
59
|
+
experience: ["search_experience", "read_experience"],
|
|
56
60
|
import: ["add_resource", "add_skill"],
|
|
57
61
|
recall_trace: ["ov_recall_trace"],
|
|
58
62
|
archive: ["ov_archive_search", "ov_archive_expand"],
|
|
@@ -76,6 +80,91 @@ function resolvePeerPrefix(configured) {
|
|
|
76
80
|
}
|
|
77
81
|
return DEFAULT_PEER_PREFIX;
|
|
78
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve an {@link OpenVikingSecretRef} to the actual secret string. Plain
|
|
85
|
+
* strings pass through untouched so callers can compose this with the legacy
|
|
86
|
+
* `resolveEnvVars` pipeline without branching.
|
|
87
|
+
*
|
|
88
|
+
* Intentional behaviours:
|
|
89
|
+
* * `env`: uses `getEnv` (process.env but exposed for vitest overrides). An
|
|
90
|
+
* unset / empty env var is an error — the user asked for a named secret
|
|
91
|
+
* so silent empty-string fallback would mask misconfigurations.
|
|
92
|
+
* * `file`: ~ expanded, then read as UTF-8, then leading/trailing whitespace
|
|
93
|
+
* stripped (de-facto standard for files written with `echo xxx > key`).
|
|
94
|
+
* Missing / unreadable files propagate the original node error with an
|
|
95
|
+
* error prefix that names the OpenViking field, so the user knows which
|
|
96
|
+
* secretRef failed to load.
|
|
97
|
+
* * `exec`: shells out `<provider> <id>`. Providers commonly expose this
|
|
98
|
+
* shape (1Password CLI, gopass, HashiCorp Vault), which matches how
|
|
99
|
+
* OpenClaw's own `@transmitt0r/openclaw-plugin-onepassword` provider
|
|
100
|
+
* resolves `{source:"exec", provider:"onepassword", id:"..."}`. Exec is
|
|
101
|
+
* best-effort; failures surface with an error prefix so users can
|
|
102
|
+
* distinguish `provider not installed` from `provider configured but id
|
|
103
|
+
* missing`.
|
|
104
|
+
*/
|
|
105
|
+
function resolveSecret(value, label) {
|
|
106
|
+
if (value === undefined || value === null)
|
|
107
|
+
return "";
|
|
108
|
+
if (typeof value === "string")
|
|
109
|
+
return value;
|
|
110
|
+
if (!value || typeof value !== "object") {
|
|
111
|
+
throw new Error(`OpenViking ${label} must be a plain string or a SecretRef object ` +
|
|
112
|
+
`({source:"env"|"file"|"exec", id})`);
|
|
113
|
+
}
|
|
114
|
+
const obj = value;
|
|
115
|
+
if (typeof obj.id !== "string" || !obj.id) {
|
|
116
|
+
throw new Error(`OpenViking ${label} SecretRef requires a non-empty string "id"`);
|
|
117
|
+
}
|
|
118
|
+
const id = obj.id;
|
|
119
|
+
switch (obj.source) {
|
|
120
|
+
case "env": {
|
|
121
|
+
const envValue = getEnv(id);
|
|
122
|
+
if (!envValue) {
|
|
123
|
+
throw new Error(`OpenViking ${label} SecretRef env source: environment variable ${id} is not set or empty`);
|
|
124
|
+
}
|
|
125
|
+
return envValue;
|
|
126
|
+
}
|
|
127
|
+
case "file": {
|
|
128
|
+
const resolvedPath = expandHomeDir(id);
|
|
129
|
+
try {
|
|
130
|
+
const contents = readFileSync(resolvedPath, "utf8");
|
|
131
|
+
return contents.trim();
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
135
|
+
throw new Error(`OpenViking ${label} SecretRef file source: cannot read ${resolvedPath} (${msg})`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
case "exec": {
|
|
139
|
+
const provider = typeof obj.provider === "string"
|
|
140
|
+
? obj.provider
|
|
141
|
+
: "";
|
|
142
|
+
if (!provider) {
|
|
143
|
+
throw new Error(`OpenViking ${label} SecretRef exec source requires a "provider" field naming the secret-manager CLI on PATH`);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
// Avoid top-level `import "node:child_process"` — this branch is rare
|
|
147
|
+
// and keeping the require lazy makes startup marginally faster for
|
|
148
|
+
// the 95% of users who pick env/file.
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
150
|
+
const { execFileSync } = require("node:child_process");
|
|
151
|
+
const out = execFileSync(provider, [id], {
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
154
|
+
timeout: 15_000,
|
|
155
|
+
env: process.env,
|
|
156
|
+
});
|
|
157
|
+
return out.trim();
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
161
|
+
throw new Error(`OpenViking ${label} SecretRef exec source: provider "${provider}" failed for id "${id}" (${msg})`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
default:
|
|
165
|
+
throw new Error(`OpenViking ${label} SecretRef has unknown source "${String(obj.source)}". Supported: "env" | "file" | "exec".`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
79
168
|
function resolvePeerRole(configured) {
|
|
80
169
|
if (typeof configured === "string") {
|
|
81
170
|
const role = configured.trim().toLowerCase();
|
|
@@ -320,7 +409,13 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
320
409
|
const peerPrefix = resolvePeerPrefix(cfg.peer_prefix);
|
|
321
410
|
const rawBaseUrl = typeof cfg.baseUrl === "string" ? cfg.baseUrl : resolveDefaultBaseUrl();
|
|
322
411
|
const resolvedBaseUrl = resolveEnvVars(rawBaseUrl).replace(/\/+$/, "");
|
|
323
|
-
|
|
412
|
+
// Support plain string, SecretRef object, and OPENVIKING_API_KEY fallback.
|
|
413
|
+
// A user writing a SecretRef has explicitly opted out of the bare env
|
|
414
|
+
// interpolation path, so the fallback kicks in only when nothing is
|
|
415
|
+
// configured at all (matches existing behaviour).
|
|
416
|
+
const rawApiKey = cfg.apiKey !== undefined && cfg.apiKey !== null
|
|
417
|
+
? cfg.apiKey
|
|
418
|
+
: getEnv("OPENVIKING_API_KEY") || undefined;
|
|
324
419
|
const captureMode = cfg.captureMode;
|
|
325
420
|
if (typeof captureMode !== "undefined" &&
|
|
326
421
|
captureMode !== "semantic" &&
|
|
@@ -342,7 +437,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
342
437
|
baseUrl: resolvedBaseUrl,
|
|
343
438
|
peer_role: peerRole,
|
|
344
439
|
peer_prefix: peerPrefix,
|
|
345
|
-
apiKey: rawApiKey ? resolveEnvVars(rawApiKey) : "",
|
|
440
|
+
apiKey: rawApiKey ? resolveEnvVars(resolveSecret(rawApiKey, "config.apiKey")) : "",
|
|
346
441
|
headers: toStringRecord(cfg.headers, "openviking config headers"),
|
|
347
442
|
accountId,
|
|
348
443
|
userId,
|
|
@@ -423,7 +518,9 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
423
518
|
label: "OpenViking API Key",
|
|
424
519
|
sensitive: true,
|
|
425
520
|
placeholder: "${OPENVIKING_API_KEY}",
|
|
426
|
-
help: "Optional API key for OpenViking server"
|
|
521
|
+
help: "Optional API key for OpenViking server. Accepts a plain string, " +
|
|
522
|
+
"${ENV_VAR} interpolation, or a SecretRef object ({source: env/file/exec, id}). " +
|
|
523
|
+
"Prefer the SecretRef shapes so the key never sits as plaintext in openclaw.json.",
|
|
427
524
|
},
|
|
428
525
|
headers: {
|
|
429
526
|
label: "Headers",
|
|
@@ -579,7 +676,7 @@ export const memoryOpenVikingConfigSchema = {
|
|
|
579
676
|
enabledTools: {
|
|
580
677
|
label: "Enabled Tools",
|
|
581
678
|
placeholder: "default",
|
|
582
|
-
help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
|
|
679
|
+
help: "Agent-visible tool allowlist. Accepts tool names or groups: default, all, memory, resource_query, experience, import, recall_trace, archive, tool_result. add_resource also requires enableAddResourceTool=true.",
|
|
583
680
|
advanced: true,
|
|
584
681
|
},
|
|
585
682
|
disabledTools: {
|
package/dist/context-engine.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { registerOpenVikingCommands } from "./plugin/command-registration.js";
|
|
|
15
15
|
import { registerRecallTraceRoutes as registerRecallTraceRouteAdapters } from "./plugin/recall-trace-routes.js";
|
|
16
16
|
import { createOpenVikingService } from "./plugin/openviking-services.js";
|
|
17
17
|
import { registerOpenVikingArchiveTools } from "./plugin/openviking-archive-tools.js";
|
|
18
|
+
import { registerOpenVikingExperienceTools } from "./plugin/openviking-experience-tools.js";
|
|
18
19
|
import { createOpenVikingImportRuntime } from "./plugin/openviking-import-runtime.js";
|
|
19
20
|
import { registerOpenVikingImportTools } from "./plugin/openviking-import-tools.js";
|
|
20
21
|
import { registerOpenVikingLifecycleHooks } from "./plugin/openviking-lifecycle-hooks.js";
|
|
@@ -25,8 +26,8 @@ import { registerOpenVikingRecallTraceTools } from "./plugin/openviking-recall-t
|
|
|
25
26
|
import { createOpenVikingRuntimeState } from "./plugin/openviking-runtime-state.js";
|
|
26
27
|
import { createOpenVikingSessionRoutingRuntime } from "./plugin/openviking-session-routing-runtime.js";
|
|
27
28
|
import { registerOpenVikingToolResultTools } from "./plugin/openviking-tool-result-tools.js";
|
|
28
|
-
import { boundTraceQuery, createMemoryStoreTempSessionId, createTraceId,
|
|
29
|
-
import { formatMessageFaithful
|
|
29
|
+
import { boundTraceQuery, createMemoryStoreTempSessionId, createTraceId, inferRecallResourceType, makeBypassedToolResult, previewText, } from "./plugin/openviking-runtime-utils.js";
|
|
30
|
+
import { formatMessageFaithful } from "./services/context-message-adapter.js";
|
|
30
31
|
import { clampScore, postProcessMemories, pickMemoriesForInjection, } from "./memory-ranking.js";
|
|
31
32
|
import { createMemoryOpenVikingContextEngine, } from "./context-engine.js";
|
|
32
33
|
import { openClawSessionRefToOvStorageId, openClawSessionToOvStorageId, } from "./routing/identity-routing.js";
|
|
@@ -80,6 +81,7 @@ const contextEnginePlugin = {
|
|
|
80
81
|
logger: api.logger,
|
|
81
82
|
});
|
|
82
83
|
const { rememberSessionAgentId, resolveAgentId, resolvePluginSessionRouting, toQueryConfigContext, } = createOpenVikingSessionRoutingRuntime({
|
|
84
|
+
peerRole: cfg.peer_role,
|
|
83
85
|
peerPrefix: cfg.peer_prefix,
|
|
84
86
|
logFindRequests: cfg.logFindRequests,
|
|
85
87
|
logger: api.logger,
|
|
@@ -135,6 +137,14 @@ const contextEnginePlugin = {
|
|
|
135
137
|
isBypassedSession,
|
|
136
138
|
makeBypassedToolResult,
|
|
137
139
|
});
|
|
140
|
+
registerOpenVikingExperienceTools({
|
|
141
|
+
registerTool: registerOpenVikingTool,
|
|
142
|
+
getClient,
|
|
143
|
+
resolvePluginSessionRouting,
|
|
144
|
+
isBypassedSession,
|
|
145
|
+
makeBypassedToolResult,
|
|
146
|
+
userId: cfg.userId,
|
|
147
|
+
});
|
|
138
148
|
registerOpenVikingMemoryRecallTools({
|
|
139
149
|
registerTool: registerOpenVikingTool,
|
|
140
150
|
getClient,
|
|
@@ -182,8 +192,7 @@ const contextEnginePlugin = {
|
|
|
182
192
|
getClient,
|
|
183
193
|
normalizeSessionId: openClawSessionRefToOvStorageId,
|
|
184
194
|
createTempSessionId: createMemoryStoreTempSessionId,
|
|
185
|
-
|
|
186
|
-
toRoleId,
|
|
195
|
+
peerRole: cfg.peer_role,
|
|
187
196
|
resolvePluginSessionRouting,
|
|
188
197
|
isBypassedSession,
|
|
189
198
|
makeBypassedToolResult,
|
|
@@ -55,7 +55,6 @@ export function registerOpenVikingArchiveTools(deps) {
|
|
|
55
55
|
const result = await client.grepSessionArchives(ovSessionId, escapedQuery, {
|
|
56
56
|
archiveId,
|
|
57
57
|
caseInsensitive: true,
|
|
58
|
-
agentId,
|
|
59
58
|
});
|
|
60
59
|
const traceResults = (result.matches ?? []).slice(0, deps.traceRecallMaxResultsPerSearch).map((match) => ({
|
|
61
60
|
uri: match.uri,
|
|
@@ -175,7 +174,7 @@ export function registerOpenVikingArchiveTools(deps) {
|
|
|
175
174
|
}
|
|
176
175
|
try {
|
|
177
176
|
const client = await deps.getClient();
|
|
178
|
-
const detail = await client.getSessionArchive(session.ovSessionId, archiveId
|
|
177
|
+
const detail = await client.getSessionArchive(session.ovSessionId, archiveId);
|
|
179
178
|
const header = [
|
|
180
179
|
`## ${detail.archive_id}`,
|
|
181
180
|
detail.abstract ? `**Summary**: ${detail.abstract}` : "",
|