memorylake-openclaw 1.1.0 → 1.1.2
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 +55 -0
- package/lib/cli/register-cli.ts +2 -2
- package/lib/plugin-context.ts +2 -1
- package/lib/types.ts +1 -0
- package/lib/utils/builders.ts +3 -1
- package/lib/utils/config-parser.ts +14 -0
- package/package.json +2 -1
- package/skills/common/get-config.mjs +16 -4
- package/test/json5_config_smoke.test.mjs +104 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v1.1.0 — 2026-04-21
|
|
4
|
+
|
|
5
|
+
Major release introducing **model-driven memory recall**, **cross-platform archive upload**, and a full modular refactor of the plugin.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
#### Memory & retrieval
|
|
10
|
+
- **Model-driven recall** (#27): replaced server-side prefetch/inject with instructions that prompt the model to call `retrieve_context` itself — eliminates query-rewrite + search latency on every turn.
|
|
11
|
+
- **Renamed** `memory_search` → `retrieve_context`; the tool now searches **both memories and documents in a single call**, running provider searches in parallel via `Promise.allSettled`.
|
|
12
|
+
- **Memory conflicts** are surfaced in `retrieve_context` results (previously only in auto-recall path).
|
|
13
|
+
- **Per-turn `[MEMORYLAKE REMINDER]`** prepended to each user turn to keep retrieval enforcement alive in long conversations where the system prompt is far away.
|
|
14
|
+
|
|
15
|
+
#### Upload & documents
|
|
16
|
+
- New **`openclaw memorylake upload`** CLI command (`--agent`, `--project-id`).
|
|
17
|
+
- **Auto-detect files / archives / directories** — a single path can be a plain file, an archive, or a directory tree.
|
|
18
|
+
- **Cross-platform archive extraction via npm packages** (no external tools required):
|
|
19
|
+
- zip, tar, tar.gz, tgz — `adm-zip` + `tar`
|
|
20
|
+
- 7z, bz2, tar.bz2 — `7zip-min` (bundled 7za binaries)
|
|
21
|
+
- rar — `node-unrar-js` (WASM)
|
|
22
|
+
- xz, tar.xz, txz — `xz-decompress` (streaming, OOM-safe)
|
|
23
|
+
- New **`document_download`** tool: streams file to `{workspaceDir}/.memorylake/downloads/`, extracts filename from `Content-Disposition` (supports RFC 5987 / Unicode).
|
|
24
|
+
- Sliding-window concurrency pool: 10 uploads in flight, next file starts as soon as one finishes.
|
|
25
|
+
|
|
26
|
+
#### Config & install
|
|
27
|
+
- **Hot-reload global config** (#35): each tool/hook call re-reads `~/.openclaw/openclaw.json`, so `apiKey` / `projectId` / `host` / `topK` changes take effect without gateway restart.
|
|
28
|
+
- **Installer auto-sets `tools.profile = "full"`** (#34), prompting before overwriting a non-`full` value.
|
|
29
|
+
- **Installer `--host` / `-MLHost` / `MEMORYLAKE_HOST`** (#36) for overriding the MemoryLake host URL.
|
|
30
|
+
- Plugin version now included in memory metadata.
|
|
31
|
+
|
|
32
|
+
### Refactor
|
|
33
|
+
- Monolithic `index.ts` (~2190 lines) split into focused modules under `lib/`:
|
|
34
|
+
- `types.ts`, `config.ts`, `provider.ts`, `plugin-context.ts`, `core-bridge.ts`
|
|
35
|
+
- `tools/` (memory-tools, document-tools, search-tools)
|
|
36
|
+
- `hooks/` (auto-recall, auto-capture, auto-upload)
|
|
37
|
+
- `cli/`, `prompt/`, `helpers/`, `utils/`
|
|
38
|
+
- `index.ts` is now ~60 lines — plugin skeleton only.
|
|
39
|
+
|
|
40
|
+
### Removed
|
|
41
|
+
- `document_search` tool — redundant with `retrieve_context`.
|
|
42
|
+
- `memory_get` tool — rarely needed.
|
|
43
|
+
|
|
44
|
+
### Fixes
|
|
45
|
+
- Strip `[MEMORYLAKE REMINDER]` prefix before auto-capturing user messages so reminders don't leak into stored memories.
|
|
46
|
+
- `execSync` → `execFileSync` to prevent command injection via malicious filenames in archive extraction.
|
|
47
|
+
- Filter junk files (`.DS_Store`, `__MACOSX`, `Thumbs.db`, dotfiles) when collecting from archives/directories.
|
|
48
|
+
- Await `uploadMany` in archive handler so temp dir isn't deleted before uploads finish.
|
|
49
|
+
- Make mandatory `retrieve_context` language conditional on `autoRecall` being enabled.
|
|
50
|
+
- 500-file guard on directory collection to avoid accidental `node_modules` uploads.
|
|
51
|
+
- Plugin manifest: added `name`, `description`, `configSchema`; added `compat` / `build` fields for ClawHub publishing.
|
|
52
|
+
|
|
53
|
+
### Manifest
|
|
54
|
+
- `package.json`: `1.0.1` → `1.1.0`.
|
|
55
|
+
- Tag: [`v1.1.0`](https://github.com/memorylake-ai/memorylake-openclaw/releases/tag/v1.1.0).
|
package/lib/cli/register-cli.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import type { PluginContext } from "../plugin-context";
|
|
5
4
|
import type { MemoryLakeConfig, UploadFn } from "../types";
|
|
6
5
|
import { getProvider } from "../provider";
|
|
7
6
|
import { buildSearchOptions } from "../utils/builders";
|
|
7
|
+
import { readJson5ConfigFile } from "../utils/config-parser";
|
|
8
8
|
|
|
9
9
|
export function registerCli(pctx: PluginContext, cfg: MemoryLakeConfig): void {
|
|
10
10
|
const { api, resolveConfig } = pctx;
|
|
@@ -58,7 +58,7 @@ export function registerCli(pctx: PluginContext, cfg: MemoryLakeConfig): void {
|
|
|
58
58
|
if (opts.agent) {
|
|
59
59
|
try {
|
|
60
60
|
const openclawPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
61
|
-
const openclaw =
|
|
61
|
+
const openclaw = readJson5ConfigFile(openclawPath) as any;
|
|
62
62
|
const agents = openclaw?.agents;
|
|
63
63
|
const agentEntry = agents?.list?.find((a: any) => a.id === opts.agent);
|
|
64
64
|
const workspace = agentEntry?.workspace || agents?.defaults?.workspace;
|
package/lib/plugin-context.ts
CHANGED
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
5
|
import type { MemoryLakeConfig } from "./types";
|
|
6
6
|
import { ALLOWED_KEYS, memoryLakeConfigSchema } from "./config";
|
|
7
|
+
import { readJson5ConfigFile } from "./utils/config-parser";
|
|
7
8
|
|
|
8
9
|
const PLUGIN_ID = "memorylake-openclaw";
|
|
9
10
|
const GLOBAL_CONFIG_PATH = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
@@ -15,7 +16,7 @@ const GLOBAL_CONFIG_PATH = path.join(os.homedir(), ".openclaw", "openclaw.json")
|
|
|
15
16
|
*/
|
|
16
17
|
function readGlobalConfig(logger: OpenClawPluginApi["logger"]): MemoryLakeConfig | null {
|
|
17
18
|
try {
|
|
18
|
-
const raw =
|
|
19
|
+
const raw = readJson5ConfigFile(GLOBAL_CONFIG_PATH) as any;
|
|
19
20
|
const pluginCfg = raw?.plugins?.entries?.[PLUGIN_ID]?.config;
|
|
20
21
|
if (!pluginCfg) {
|
|
21
22
|
logger.info(`memorylake-openclaw: no plugin config found in global config (path: ${GLOBAL_CONFIG_PATH}, pluginId: ${PLUGIN_ID})`);
|
package/lib/types.ts
CHANGED
package/lib/utils/builders.ts
CHANGED
|
@@ -21,7 +21,9 @@ export function buildDocumentContext(
|
|
|
21
21
|
|
|
22
22
|
if (result.type === "table") {
|
|
23
23
|
const title = result.title || "Untitled Table";
|
|
24
|
-
|
|
24
|
+
const sheetLabel = result.semantic_sheet_name || result.sheet_name;
|
|
25
|
+
const sheetPart = sheetLabel ? `, sheet: ${sheetLabel}` : "";
|
|
26
|
+
parts.push(`### Table: ${title} (from ${source}${sheetPart}, doc_id: ${docId})`);
|
|
25
27
|
if (result.footnote) parts.push(`Note: ${result.footnote}`);
|
|
26
28
|
|
|
27
29
|
for (const innerTable of highlight?.inner_tables ?? []) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import JSON5 from "json5";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read and parse a JSON5-compatible config file.
|
|
6
|
+
*/
|
|
7
|
+
export function readJson5ConfigFile(filePath: string): unknown {
|
|
8
|
+
const source = fs.readFileSync(filePath, "utf-8");
|
|
9
|
+
try {
|
|
10
|
+
return JSON5.parse(source);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
throw new Error(`Failed to parse JSON5 config file "${filePath}": ${String(err)}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memorylake-openclaw",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MemoryLake memory backend for OpenClaw",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"7zip-min": "^3.0.1",
|
|
21
21
|
"adm-zip": "^0.5.17",
|
|
22
22
|
"got": "^14.0.0",
|
|
23
|
+
"json5": "^2.2.3",
|
|
23
24
|
"node-unrar-js": "^2.0.2",
|
|
24
25
|
"tar": "^7.5.13",
|
|
25
26
|
"xz-decompress": "^0.2.3"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { readFileSync, existsSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
+
import JSON5 from "json5";
|
|
6
7
|
|
|
7
8
|
// Parse --agent
|
|
8
9
|
const args = process.argv.slice(2);
|
|
@@ -15,7 +16,13 @@ const agentId = args[agentIdx + 1];
|
|
|
15
16
|
|
|
16
17
|
// Read global config
|
|
17
18
|
const openclawPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
18
|
-
|
|
19
|
+
let openclaw;
|
|
20
|
+
try {
|
|
21
|
+
openclaw = JSON5.parse(readFileSync(openclawPath, "utf-8"));
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error(`Error: failed to parse JSON5 config file "${openclawPath}": ${String(err)}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
19
26
|
const globalCfg = openclaw?.plugins?.entries?.["memorylake-openclaw"]?.config;
|
|
20
27
|
if (!globalCfg) {
|
|
21
28
|
console.error("Error: memorylake-openclaw plugin config not found");
|
|
@@ -35,9 +42,14 @@ if (!workspace) {
|
|
|
35
42
|
const merged = { ...globalCfg };
|
|
36
43
|
const localPath = join(workspace, ".memorylake", "config.json");
|
|
37
44
|
if (existsSync(localPath)) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
try {
|
|
46
|
+
const raw = JSON.parse(readFileSync(localPath, "utf-8"));
|
|
47
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
48
|
+
Object.assign(merged, raw);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(`Error: failed to parse workspace config at ${localPath}: ${String(err)}`);
|
|
52
|
+
process.exit(1);
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
55
|
merged.host = merged.host || "https://app.memorylake.ai";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const repoRoot = resolve(process.cwd());
|
|
9
|
+
const getConfigScript = join(repoRoot, "skills/common/get-config.mjs");
|
|
10
|
+
const pluginContextSource = join(repoRoot, "lib/plugin-context.ts");
|
|
11
|
+
const registerCliSource = join(repoRoot, "lib/cli/register-cli.ts");
|
|
12
|
+
|
|
13
|
+
function runGetConfig(homeDir, agentId = "a1") {
|
|
14
|
+
return spawnSync("node", [getConfigScript, "--agent", agentId], {
|
|
15
|
+
env: { ...process.env, HOME: homeDir },
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("json5 config smoke", () => {
|
|
21
|
+
it("accepts JSON5 openclaw.json in get-config.mjs", () => {
|
|
22
|
+
const root = mkdtempSync(join(tmpdir(), "ml-json5-ok-"));
|
|
23
|
+
const home = root;
|
|
24
|
+
const workspace = join(root, "workspace");
|
|
25
|
+
mkdirSync(join(home, ".openclaw"), { recursive: true });
|
|
26
|
+
mkdirSync(join(workspace, ".memorylake"), { recursive: true });
|
|
27
|
+
|
|
28
|
+
writeFileSync(
|
|
29
|
+
join(home, ".openclaw", "openclaw.json"),
|
|
30
|
+
`{
|
|
31
|
+
// allow comments
|
|
32
|
+
plugins: {
|
|
33
|
+
entries: {
|
|
34
|
+
"memorylake-openclaw": {
|
|
35
|
+
config: {
|
|
36
|
+
apiKey: "k",
|
|
37
|
+
projectId: "p",
|
|
38
|
+
host: "https://app.memorylake.ai",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
agents: {
|
|
44
|
+
list: [{ id: "a1", workspace: "${workspace.replaceAll("\\", "\\\\")}" }],
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
);
|
|
49
|
+
writeFileSync(join(workspace, ".memorylake", "config.json"), JSON.stringify({ topK: 5 }));
|
|
50
|
+
|
|
51
|
+
const result = runGetConfig(home);
|
|
52
|
+
assert.equal(result.status, 0, result.stderr);
|
|
53
|
+
const parsed = JSON.parse(result.stdout);
|
|
54
|
+
assert.equal(parsed.projectId, "p");
|
|
55
|
+
assert.equal(parsed.workspace, workspace);
|
|
56
|
+
assert.equal(parsed.topK, 5);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns clear non-zero error for malformed JSON5 global config", () => {
|
|
60
|
+
const root = mkdtempSync(join(tmpdir(), "ml-json5-bad-global-"));
|
|
61
|
+
const home = root;
|
|
62
|
+
mkdirSync(join(home, ".openclaw"), { recursive: true });
|
|
63
|
+
writeFileSync(join(home, ".openclaw", "openclaw.json"), "{ invalid json5 }");
|
|
64
|
+
|
|
65
|
+
const result = runGetConfig(home);
|
|
66
|
+
assert.notEqual(result.status, 0);
|
|
67
|
+
assert.match(result.stderr, /failed to parse JSON5 config file/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("keeps workspace override as strict JSON", () => {
|
|
71
|
+
const root = mkdtempSync(join(tmpdir(), "ml-json5-bad-local-"));
|
|
72
|
+
const home = root;
|
|
73
|
+
const workspace = join(root, "workspace");
|
|
74
|
+
mkdirSync(join(home, ".openclaw"), { recursive: true });
|
|
75
|
+
mkdirSync(join(workspace, ".memorylake"), { recursive: true });
|
|
76
|
+
|
|
77
|
+
writeFileSync(
|
|
78
|
+
join(home, ".openclaw", "openclaw.json"),
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
plugins: {
|
|
81
|
+
entries: {
|
|
82
|
+
"memorylake-openclaw": {
|
|
83
|
+
config: { apiKey: "k", projectId: "p", host: "https://app.memorylake.ai" },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
agents: { list: [{ id: "a1", workspace }] },
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
writeFileSync(join(workspace, ".memorylake", "config.json"), "{ trailing: 1, }");
|
|
91
|
+
|
|
92
|
+
const result = runGetConfig(home);
|
|
93
|
+
assert.notEqual(result.status, 0);
|
|
94
|
+
assert.match(result.stderr, /failed to parse workspace config/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("ensures plugin and CLI global config paths use shared JSON5 parser", () => {
|
|
98
|
+
const pluginContext = readFileSync(pluginContextSource, "utf8");
|
|
99
|
+
const registerCli = readFileSync(registerCliSource, "utf8");
|
|
100
|
+
|
|
101
|
+
assert.match(pluginContext, /readJson5ConfigFile\(GLOBAL_CONFIG_PATH\)/);
|
|
102
|
+
assert.match(registerCli, /readJson5ConfigFile\(openclawPath\)/);
|
|
103
|
+
});
|
|
104
|
+
});
|