@byte5ai/palaia 1.1.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/README.md +137 -0
- package/index.ts +37 -0
- package/openclaw.plugin.json +51 -0
- package/package.json +35 -0
- package/src/config.ts +37 -0
- package/src/hooks.ts +98 -0
- package/src/runner.ts +218 -0
- package/src/tools.ts +220 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# @byte5ai/palaia
|
|
2
|
+
|
|
3
|
+
**Palaia memory backend for OpenClaw.**
|
|
4
|
+
|
|
5
|
+
Replace OpenClaw's built-in `memory-core` with Palaia — local, cloud-free, WAL-backed agent memory with tier routing and semantic search.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install Palaia (Python CLI)
|
|
11
|
+
pip install palaia
|
|
12
|
+
|
|
13
|
+
# Install the OpenClaw plugin
|
|
14
|
+
openclaw plugins install @byte5ai/palaia
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
Activate the plugin by setting the memory slot in your OpenClaw config:
|
|
20
|
+
|
|
21
|
+
```json5
|
|
22
|
+
// openclaw.config.json5
|
|
23
|
+
{
|
|
24
|
+
plugins: {
|
|
25
|
+
slots: { memory: "palaia" }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Restart the gateway after changing config:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
openclaw gateway restart
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Plugin Options
|
|
37
|
+
|
|
38
|
+
All options are optional — sensible defaults are used:
|
|
39
|
+
|
|
40
|
+
```json5
|
|
41
|
+
{
|
|
42
|
+
plugins: {
|
|
43
|
+
config: {
|
|
44
|
+
palaia: {
|
|
45
|
+
binaryPath: "/path/to/palaia", // default: auto-detect
|
|
46
|
+
workspace: "/path/to/workspace", // default: agent workspace
|
|
47
|
+
tier: "hot", // default: "hot" (hot|warm|all)
|
|
48
|
+
maxResults: 10, // default: 10
|
|
49
|
+
timeoutMs: 3000, // default: 3000
|
|
50
|
+
memoryInject: false, // default: false (inject HOT into context)
|
|
51
|
+
maxInjectedChars: 4000, // default: 4000
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Agent Tools
|
|
59
|
+
|
|
60
|
+
### `memory_search` (always available)
|
|
61
|
+
|
|
62
|
+
Semantically search Palaia memory:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
memory_search({ query: "deployment process", maxResults: 5, tier: "all" })
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `memory_get` (always available)
|
|
69
|
+
|
|
70
|
+
Read a specific memory entry:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
memory_get({ path: "abc-123-uuid", from: 1, lines: 50 })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `memory_write` (optional, opt-in)
|
|
77
|
+
|
|
78
|
+
Write new memory entries. Enable per-agent:
|
|
79
|
+
|
|
80
|
+
```json5
|
|
81
|
+
{
|
|
82
|
+
agents: {
|
|
83
|
+
list: [{
|
|
84
|
+
id: "main",
|
|
85
|
+
tools: { allow: ["memory_write"] }
|
|
86
|
+
}]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then agents can write:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
memory_write({ content: "Important finding", scope: "team", tags: ["project-x"] })
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Features
|
|
98
|
+
|
|
99
|
+
- **Zero breaking changes** — Drop-in replacement for `memory-core`
|
|
100
|
+
- **WAL-backed writes** — Crash-safe, recovers on startup
|
|
101
|
+
- **Tier routing** — HOT → WARM → COLD with automatic decay
|
|
102
|
+
- **Scope isolation** — private, team, shared:X, public
|
|
103
|
+
- **BM25 search** — Fast local search, no external API needed
|
|
104
|
+
- **HOT memory injection** — Opt-in: inject active memory into agent context
|
|
105
|
+
- **Auto binary detection** — Finds `palaia` in PATH, pipx, or venv
|
|
106
|
+
|
|
107
|
+
## Architecture
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
OpenClaw Agent
|
|
111
|
+
└─ @byte5ai/palaia (plugin)
|
|
112
|
+
└─ palaia CLI (subprocess, --json)
|
|
113
|
+
└─ .palaia/ (local storage)
|
|
114
|
+
├─ hot/ (active memory)
|
|
115
|
+
├─ warm/ (recent, less active)
|
|
116
|
+
├─ cold/ (archived)
|
|
117
|
+
├─ wal/ (write-ahead log)
|
|
118
|
+
└─ index/ (search index)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Clone the repo
|
|
125
|
+
git clone https://github.com/iret77/palaia.git
|
|
126
|
+
cd palaia/packages/openclaw-plugin
|
|
127
|
+
|
|
128
|
+
# Install deps
|
|
129
|
+
npm install
|
|
130
|
+
|
|
131
|
+
# Run tests
|
|
132
|
+
npx vitest run
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @byte5ai/palaia — Palaia Memory Backend for OpenClaw
|
|
3
|
+
*
|
|
4
|
+
* Plugin entry point. Loaded by OpenClaw via jiti (no build step needed).
|
|
5
|
+
*
|
|
6
|
+
* Registers:
|
|
7
|
+
* - memory_search: Semantic search over Palaia memory
|
|
8
|
+
* - memory_get: Read a specific memory entry
|
|
9
|
+
* - memory_write: Write new entries (optional, opt-in)
|
|
10
|
+
* - before_prompt_build: HOT memory injection (opt-in)
|
|
11
|
+
* - palaia-recovery: WAL replay on startup
|
|
12
|
+
*
|
|
13
|
+
* Activation:
|
|
14
|
+
* plugins: { slots: { memory: "palaia" } }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { resolveConfig, type PalaiaPluginConfig } from "./src/config.js";
|
|
18
|
+
import { registerTools } from "./src/tools.js";
|
|
19
|
+
import { registerHooks } from "./src/hooks.js";
|
|
20
|
+
|
|
21
|
+
export default function palaiaPlugin(api: any) {
|
|
22
|
+
const rawConfig = api.getConfig?.("palaia") as
|
|
23
|
+
| Partial<PalaiaPluginConfig>
|
|
24
|
+
| undefined;
|
|
25
|
+
const config = resolveConfig(rawConfig);
|
|
26
|
+
|
|
27
|
+
// If workspace not set, use agent workspace from context
|
|
28
|
+
if (!config.workspace && api.workspace) {
|
|
29
|
+
config.workspace = api.workspace;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Register agent tools (memory_search, memory_get, memory_write)
|
|
33
|
+
registerTools(api, config);
|
|
34
|
+
|
|
35
|
+
// Register lifecycle hooks (before_prompt_build, recovery service)
|
|
36
|
+
registerHooks(api, config);
|
|
37
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "palaia",
|
|
3
|
+
"name": "Palaia Memory",
|
|
4
|
+
"kind": "memory",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"binaryPath": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Path to palaia binary (default: auto-detect)"
|
|
11
|
+
},
|
|
12
|
+
"workspace": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Palaia workspace path (default: agent workspace)"
|
|
15
|
+
},
|
|
16
|
+
"tier": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"enum": ["hot", "warm", "all"],
|
|
19
|
+
"default": "hot"
|
|
20
|
+
},
|
|
21
|
+
"maxResults": {
|
|
22
|
+
"type": "number",
|
|
23
|
+
"default": 10
|
|
24
|
+
},
|
|
25
|
+
"timeoutMs": {
|
|
26
|
+
"type": "number",
|
|
27
|
+
"default": 3000
|
|
28
|
+
},
|
|
29
|
+
"memoryInject": {
|
|
30
|
+
"type": "boolean",
|
|
31
|
+
"default": false,
|
|
32
|
+
"description": "Inject HOT memory into agent context on prompt build"
|
|
33
|
+
},
|
|
34
|
+
"maxInjectedChars": {
|
|
35
|
+
"type": "number",
|
|
36
|
+
"default": 4000,
|
|
37
|
+
"description": "Max characters for injected memory context"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"uiHints": {
|
|
42
|
+
"binaryPath": {
|
|
43
|
+
"label": "Palaia Binary Path",
|
|
44
|
+
"placeholder": "auto-detect"
|
|
45
|
+
},
|
|
46
|
+
"workspace": {
|
|
47
|
+
"label": "Workspace Path",
|
|
48
|
+
"placeholder": "agent workspace"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@byte5ai/palaia",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Palaia memory backend for OpenClaw",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"openclaw": {
|
|
7
|
+
"extensions": ["./index.ts"]
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.ts",
|
|
11
|
+
"src/",
|
|
12
|
+
"openclaw.plugin.json",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"openclaw",
|
|
17
|
+
"openclaw-plugin",
|
|
18
|
+
"palaia",
|
|
19
|
+
"memory",
|
|
20
|
+
"agent-memory"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/iret77/palaia.git",
|
|
26
|
+
"directory": "packages/openclaw-plugin"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"openclaw": ">=2025.1.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@sinclair/typebox": "^0.32.0",
|
|
33
|
+
"vitest": "^1.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin configuration schema and defaults for @byte5ai/palaia.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface PalaiaPluginConfig {
|
|
6
|
+
/** Path to palaia binary (default: auto-detect) */
|
|
7
|
+
binaryPath?: string;
|
|
8
|
+
/** Palaia workspace path (default: agent workspace) */
|
|
9
|
+
workspace?: string;
|
|
10
|
+
/** Default tier filter: "hot" | "warm" | "all" */
|
|
11
|
+
tier: string;
|
|
12
|
+
/** Default max results for queries */
|
|
13
|
+
maxResults: number;
|
|
14
|
+
/** Timeout for CLI calls in milliseconds */
|
|
15
|
+
timeoutMs: number;
|
|
16
|
+
/** Inject HOT memory into agent context on prompt build */
|
|
17
|
+
memoryInject: boolean;
|
|
18
|
+
/** Max characters for injected memory context */
|
|
19
|
+
maxInjectedChars: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_CONFIG: PalaiaPluginConfig = {
|
|
23
|
+
tier: "hot",
|
|
24
|
+
maxResults: 10,
|
|
25
|
+
timeoutMs: 3000,
|
|
26
|
+
memoryInject: false,
|
|
27
|
+
maxInjectedChars: 4000,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Merge user config with defaults. Unknown keys are ignored.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveConfig(
|
|
34
|
+
userConfig: Partial<PalaiaPluginConfig> | undefined
|
|
35
|
+
): PalaiaPluginConfig {
|
|
36
|
+
return { ...DEFAULT_CONFIG, ...userConfig };
|
|
37
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle hooks for the Palaia OpenClaw plugin.
|
|
3
|
+
*
|
|
4
|
+
* - before_prompt_build: Injects HOT memory into agent context (opt-in).
|
|
5
|
+
* - palaia-recovery service: Replays WAL on startup.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { runJson, recover, type RunnerOpts } from "./runner.js";
|
|
9
|
+
import type { PalaiaPluginConfig } from "./config.js";
|
|
10
|
+
|
|
11
|
+
/** Shape returned by `palaia query --json` */
|
|
12
|
+
interface QueryResult {
|
|
13
|
+
results: Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
body?: string;
|
|
16
|
+
content?: string;
|
|
17
|
+
score: number;
|
|
18
|
+
tier: string;
|
|
19
|
+
scope: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build RunnerOpts from plugin config.
|
|
26
|
+
*/
|
|
27
|
+
function buildRunnerOpts(config: PalaiaPluginConfig): RunnerOpts {
|
|
28
|
+
return {
|
|
29
|
+
binaryPath: config.binaryPath,
|
|
30
|
+
workspace: config.workspace,
|
|
31
|
+
timeoutMs: config.timeoutMs,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Register lifecycle hooks on the plugin API.
|
|
37
|
+
*/
|
|
38
|
+
export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
39
|
+
const opts = buildRunnerOpts(config);
|
|
40
|
+
|
|
41
|
+
// ── before_prompt_build ────────────────────────────────────────
|
|
42
|
+
// Injects top HOT entries into agent system context.
|
|
43
|
+
// Only active when config.memoryInject === true (default: false).
|
|
44
|
+
if (config.memoryInject) {
|
|
45
|
+
api.on("before_prompt_build", async (event: any, ctx: any) => {
|
|
46
|
+
try {
|
|
47
|
+
const maxChars = config.maxInjectedChars || 4000;
|
|
48
|
+
const limit = Math.min(config.maxResults || 10, 20);
|
|
49
|
+
|
|
50
|
+
const result = await runJson<QueryResult>(
|
|
51
|
+
["list", "--tier", "hot", "--limit", String(limit)],
|
|
52
|
+
opts
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (!result || !Array.isArray(result.results) || result.results.length === 0) {
|
|
56
|
+
// Fallback: try query with empty string for recent entries
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const entries = result.results;
|
|
61
|
+
let text = "## Active Memory (Palaia)\n\n";
|
|
62
|
+
let chars = text.length;
|
|
63
|
+
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
const body = entry.content || entry.body || "";
|
|
66
|
+
const title = entry.title || "(untitled)";
|
|
67
|
+
const line = `**${title}** [${entry.scope}]\n${body}\n\n`;
|
|
68
|
+
|
|
69
|
+
if (chars + line.length > maxChars) break;
|
|
70
|
+
text += line;
|
|
71
|
+
chars += line.length;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (ctx.prependSystemContext) {
|
|
75
|
+
ctx.prependSystemContext(text);
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
// Non-fatal: if memory injection fails, agent continues without it
|
|
79
|
+
console.warn(`[palaia] Memory injection failed: ${error}`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Startup Recovery Service ───────────────────────────────────
|
|
85
|
+
// Replays pending WAL entries on plugin startup.
|
|
86
|
+
api.registerService({
|
|
87
|
+
id: "palaia-recovery",
|
|
88
|
+
start: async () => {
|
|
89
|
+
const result = await recover(opts);
|
|
90
|
+
if (result.replayed > 0) {
|
|
91
|
+
console.log(`[palaia] WAL recovery: replayed ${result.replayed} entries`);
|
|
92
|
+
}
|
|
93
|
+
if (result.errors > 0) {
|
|
94
|
+
console.warn(`[palaia] WAL recovery completed with ${result.errors} error(s)`);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
package/src/runner.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Palaia CLI subprocess runner.
|
|
3
|
+
*
|
|
4
|
+
* Executes palaia CLI commands, parses JSON output, handles binary detection
|
|
5
|
+
* and timeouts. This is the bridge between the OpenClaw plugin and the
|
|
6
|
+
* palaia Python CLI.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { access, constants } from "node:fs/promises";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
export interface RunnerOpts {
|
|
15
|
+
/** Working directory for palaia (sets PALAIA_ROOT context) */
|
|
16
|
+
workspace?: string;
|
|
17
|
+
/** Timeout in milliseconds */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
/** Override binary path */
|
|
20
|
+
binaryPath?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RunResult {
|
|
24
|
+
stdout: string;
|
|
25
|
+
stderr: string;
|
|
26
|
+
exitCode: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Cached binary path after first detection */
|
|
30
|
+
let cachedBinary: string | null = null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect the palaia binary location.
|
|
34
|
+
*
|
|
35
|
+
* Search order:
|
|
36
|
+
* 1. Explicit binaryPath from config
|
|
37
|
+
* 2. `palaia` in PATH
|
|
38
|
+
* 3. `~/.local/bin/palaia` (pipx default)
|
|
39
|
+
* 4. `python3 -m palaia` as fallback
|
|
40
|
+
* 5. Error with clear message
|
|
41
|
+
*/
|
|
42
|
+
export async function detectBinary(
|
|
43
|
+
explicitPath?: string
|
|
44
|
+
): Promise<string> {
|
|
45
|
+
if (explicitPath) {
|
|
46
|
+
try {
|
|
47
|
+
await access(explicitPath, constants.X_OK);
|
|
48
|
+
return explicitPath;
|
|
49
|
+
} catch {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Configured palaia binary not found or not executable: ${explicitPath}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (cachedBinary) return cachedBinary;
|
|
57
|
+
|
|
58
|
+
// Try `palaia` in PATH
|
|
59
|
+
try {
|
|
60
|
+
const result = await execCommand("palaia", ["--version"], {
|
|
61
|
+
timeoutMs: 5000,
|
|
62
|
+
});
|
|
63
|
+
if (result.exitCode === 0) {
|
|
64
|
+
cachedBinary = "palaia";
|
|
65
|
+
return "palaia";
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Not in PATH
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Try ~/.local/bin/palaia (pipx default)
|
|
72
|
+
const pipxPath = join(homedir(), ".local", "bin", "palaia");
|
|
73
|
+
try {
|
|
74
|
+
await access(pipxPath, constants.X_OK);
|
|
75
|
+
cachedBinary = pipxPath;
|
|
76
|
+
return pipxPath;
|
|
77
|
+
} catch {
|
|
78
|
+
// Not installed via pipx
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Try python3 -m palaia
|
|
82
|
+
try {
|
|
83
|
+
const result = await execCommand("python3", ["-m", "palaia", "--version"], {
|
|
84
|
+
timeoutMs: 5000,
|
|
85
|
+
});
|
|
86
|
+
if (result.exitCode === 0) {
|
|
87
|
+
cachedBinary = "python3";
|
|
88
|
+
return "python3"; // Will need `-m palaia` prefix
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Not available
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw new Error(
|
|
95
|
+
"Palaia binary not found. Install with: pip install palaia\n" +
|
|
96
|
+
"Or set binaryPath in plugin config."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if the detected binary requires `python3 -m palaia` invocation.
|
|
102
|
+
*/
|
|
103
|
+
function isPythonModule(binary: string): boolean {
|
|
104
|
+
return binary === "python3" || binary.endsWith("/python3");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Execute a raw command and return result.
|
|
109
|
+
*/
|
|
110
|
+
function execCommand(
|
|
111
|
+
cmd: string,
|
|
112
|
+
args: string[],
|
|
113
|
+
opts: { timeoutMs?: number; cwd?: string } = {}
|
|
114
|
+
): Promise<RunResult> {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const timeout = opts.timeoutMs || 10000;
|
|
117
|
+
const child = execFile(
|
|
118
|
+
cmd,
|
|
119
|
+
args,
|
|
120
|
+
{
|
|
121
|
+
timeout,
|
|
122
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
123
|
+
cwd: opts.cwd,
|
|
124
|
+
env: { ...process.env },
|
|
125
|
+
},
|
|
126
|
+
(error, stdout, stderr) => {
|
|
127
|
+
if (error && (error as any).killed) {
|
|
128
|
+
reject(
|
|
129
|
+
new Error(`Palaia command timed out after ${timeout}ms: ${cmd} ${args.join(" ")}`)
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
resolve({
|
|
134
|
+
stdout: stdout?.toString() || "",
|
|
135
|
+
stderr: stderr?.toString() || "",
|
|
136
|
+
exitCode: error ? (error as any).code || 1 : 0,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Run a palaia CLI command and return raw output.
|
|
145
|
+
*/
|
|
146
|
+
export async function run(
|
|
147
|
+
args: string[],
|
|
148
|
+
opts: RunnerOpts = {}
|
|
149
|
+
): Promise<string> {
|
|
150
|
+
const binary = await detectBinary(opts.binaryPath);
|
|
151
|
+
const timeoutMs = opts.timeoutMs || 3000;
|
|
152
|
+
|
|
153
|
+
let cmd: string;
|
|
154
|
+
let cmdArgs: string[];
|
|
155
|
+
|
|
156
|
+
if (isPythonModule(binary)) {
|
|
157
|
+
cmd = binary;
|
|
158
|
+
cmdArgs = ["-m", "palaia", ...args];
|
|
159
|
+
} else {
|
|
160
|
+
cmd = binary;
|
|
161
|
+
cmdArgs = [...args];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result = await execCommand(cmd, cmdArgs, {
|
|
165
|
+
timeoutMs,
|
|
166
|
+
cwd: opts.workspace,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (result.exitCode !== 0) {
|
|
170
|
+
const errMsg = result.stderr.trim() || result.stdout.trim();
|
|
171
|
+
throw new Error(`Palaia CLI error (exit ${result.exitCode}): ${errMsg}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result.stdout;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Run a palaia CLI command and parse JSON output.
|
|
179
|
+
*/
|
|
180
|
+
export async function runJson<T = unknown>(
|
|
181
|
+
args: string[],
|
|
182
|
+
opts: RunnerOpts = {}
|
|
183
|
+
): Promise<T> {
|
|
184
|
+
const stdout = await run([...args, "--json"], opts);
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(stdout) as T;
|
|
187
|
+
} catch {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Failed to parse palaia JSON output: ${stdout.slice(0, 200)}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Run WAL recovery on startup.
|
|
196
|
+
*/
|
|
197
|
+
export async function recover(opts: RunnerOpts = {}): Promise<{
|
|
198
|
+
replayed: number;
|
|
199
|
+
errors: number;
|
|
200
|
+
}> {
|
|
201
|
+
try {
|
|
202
|
+
return await runJson<{ replayed: number; errors: number }>(
|
|
203
|
+
["recover"],
|
|
204
|
+
{ ...opts, timeoutMs: opts.timeoutMs || 10000 }
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// Recovery failure is non-fatal — log and continue
|
|
208
|
+
console.warn(`[palaia] WAL recovery warning: ${error}`);
|
|
209
|
+
return { replayed: 0, errors: 1 };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Reset cached binary (for testing).
|
|
215
|
+
*/
|
|
216
|
+
export function resetCache(): void {
|
|
217
|
+
cachedBinary = null;
|
|
218
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tools: memory_search, memory_get, memory_write.
|
|
3
|
+
*
|
|
4
|
+
* These tools are the core of the Palaia OpenClaw integration.
|
|
5
|
+
* They shell out to the palaia CLI with --json and return results
|
|
6
|
+
* in the format OpenClaw agents expect.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
import { runJson, type RunnerOpts } from "./runner.js";
|
|
11
|
+
import type { PalaiaPluginConfig } from "./config.js";
|
|
12
|
+
|
|
13
|
+
/** Shape returned by `palaia query --json` */
|
|
14
|
+
interface QueryResult {
|
|
15
|
+
results: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
content?: string;
|
|
18
|
+
body?: string;
|
|
19
|
+
score: number;
|
|
20
|
+
tier: string;
|
|
21
|
+
scope: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
tags?: string[];
|
|
24
|
+
path?: string;
|
|
25
|
+
decay_score?: number;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Shape returned by `palaia get --json` */
|
|
30
|
+
interface GetResult {
|
|
31
|
+
id: string;
|
|
32
|
+
content: string;
|
|
33
|
+
meta: {
|
|
34
|
+
scope: string;
|
|
35
|
+
tier: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
tags?: string[];
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Shape returned by `palaia write --json` */
|
|
43
|
+
interface WriteResult {
|
|
44
|
+
id: string;
|
|
45
|
+
tier: string;
|
|
46
|
+
scope: string;
|
|
47
|
+
deduplicated: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build RunnerOpts from plugin config.
|
|
52
|
+
*/
|
|
53
|
+
function buildRunnerOpts(config: PalaiaPluginConfig): RunnerOpts {
|
|
54
|
+
return {
|
|
55
|
+
binaryPath: config.binaryPath,
|
|
56
|
+
workspace: config.workspace,
|
|
57
|
+
timeoutMs: config.timeoutMs,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Register all Palaia agent tools on the given plugin API.
|
|
63
|
+
*/
|
|
64
|
+
export function registerTools(api: any, config: PalaiaPluginConfig): void {
|
|
65
|
+
const opts = buildRunnerOpts(config);
|
|
66
|
+
|
|
67
|
+
// ── memory_search ──────────────────────────────────────────────
|
|
68
|
+
api.registerTool({
|
|
69
|
+
name: "memory_search",
|
|
70
|
+
description:
|
|
71
|
+
"Semantically search Palaia memory for relevant notes and context.",
|
|
72
|
+
parameters: Type.Object({
|
|
73
|
+
query: Type.String({ description: "Search query" }),
|
|
74
|
+
maxResults: Type.Optional(
|
|
75
|
+
Type.Number({ description: "Max results (default: 5)", default: 5 })
|
|
76
|
+
),
|
|
77
|
+
tier: Type.Optional(
|
|
78
|
+
Type.String({
|
|
79
|
+
description: "hot|warm|all (default: hot+warm)",
|
|
80
|
+
})
|
|
81
|
+
),
|
|
82
|
+
scope: Type.Optional(
|
|
83
|
+
Type.String({
|
|
84
|
+
description: "Filter by scope: private|team|shared:X|public",
|
|
85
|
+
})
|
|
86
|
+
),
|
|
87
|
+
}),
|
|
88
|
+
async execute(
|
|
89
|
+
_id: string,
|
|
90
|
+
params: {
|
|
91
|
+
query: string;
|
|
92
|
+
maxResults?: number;
|
|
93
|
+
tier?: string;
|
|
94
|
+
scope?: string;
|
|
95
|
+
}
|
|
96
|
+
) {
|
|
97
|
+
const limit = params.maxResults || config.maxResults || 5;
|
|
98
|
+
const args: string[] = ["query", params.query, "--limit", String(limit)];
|
|
99
|
+
|
|
100
|
+
// --all flag includes cold tier
|
|
101
|
+
if (params.tier === "all" || config.tier === "all") {
|
|
102
|
+
args.push("--all");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const result = await runJson<QueryResult>(args, opts);
|
|
106
|
+
|
|
107
|
+
// Format as memory_search compatible output
|
|
108
|
+
const snippets = (result.results || []).map((r) => {
|
|
109
|
+
const body = r.content || r.body || "";
|
|
110
|
+
const path = r.path || `${r.tier}/${r.id}.md`;
|
|
111
|
+
return {
|
|
112
|
+
text: body,
|
|
113
|
+
path,
|
|
114
|
+
score: r.score,
|
|
115
|
+
tier: r.tier,
|
|
116
|
+
scope: r.scope,
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Build text output compatible with memory-core format
|
|
121
|
+
const textParts = snippets.map(
|
|
122
|
+
(s) =>
|
|
123
|
+
`${s.text}\n— Source: ${s.path} (score: ${s.score}, tier: ${s.tier})`
|
|
124
|
+
);
|
|
125
|
+
const text =
|
|
126
|
+
textParts.length > 0
|
|
127
|
+
? textParts.join("\n\n")
|
|
128
|
+
: "No results found.";
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: "text" as const, text }],
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── memory_get ─────────────────────────────────────────────────
|
|
137
|
+
api.registerTool({
|
|
138
|
+
name: "memory_get",
|
|
139
|
+
description: "Read a specific Palaia memory entry by path or id.",
|
|
140
|
+
parameters: Type.Object({
|
|
141
|
+
path: Type.String({ description: "Memory path or UUID" }),
|
|
142
|
+
from: Type.Optional(
|
|
143
|
+
Type.Number({ description: "Start from line number (1-indexed)" })
|
|
144
|
+
),
|
|
145
|
+
lines: Type.Optional(
|
|
146
|
+
Type.Number({ description: "Number of lines to return" })
|
|
147
|
+
),
|
|
148
|
+
}),
|
|
149
|
+
async execute(
|
|
150
|
+
_id: string,
|
|
151
|
+
params: { path: string; from?: number; lines?: number }
|
|
152
|
+
) {
|
|
153
|
+
const args: string[] = ["get", params.path];
|
|
154
|
+
if (params.from != null) {
|
|
155
|
+
args.push("--from", String(params.from));
|
|
156
|
+
}
|
|
157
|
+
if (params.lines != null) {
|
|
158
|
+
args.push("--lines", String(params.lines));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = await runJson<GetResult>(args, opts);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: "text" as const,
|
|
167
|
+
text: result.content,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ── memory_write (optional, opt-in) ───────────────────────────
|
|
175
|
+
api.registerTool(
|
|
176
|
+
{
|
|
177
|
+
name: "memory_write",
|
|
178
|
+
description:
|
|
179
|
+
"Write a new memory entry to Palaia. WAL-backed, crash-safe.",
|
|
180
|
+
parameters: Type.Object({
|
|
181
|
+
content: Type.String({ description: "Memory content to write" }),
|
|
182
|
+
scope: Type.Optional(
|
|
183
|
+
Type.String({
|
|
184
|
+
description: "Scope: private|team|shared:X|public (default: team)",
|
|
185
|
+
default: "team",
|
|
186
|
+
})
|
|
187
|
+
),
|
|
188
|
+
tags: Type.Optional(
|
|
189
|
+
Type.Array(Type.String(), {
|
|
190
|
+
description: "Tags for categorization",
|
|
191
|
+
})
|
|
192
|
+
),
|
|
193
|
+
}),
|
|
194
|
+
async execute(
|
|
195
|
+
_id: string,
|
|
196
|
+
params: { content: string; scope?: string; tags?: string[] }
|
|
197
|
+
) {
|
|
198
|
+
const args: string[] = ["write", params.content];
|
|
199
|
+
if (params.scope) {
|
|
200
|
+
args.push("--scope", params.scope);
|
|
201
|
+
}
|
|
202
|
+
if (params.tags && params.tags.length > 0) {
|
|
203
|
+
args.push("--tags", params.tags.join(","));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result = await runJson<WriteResult>(args, opts);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
content: [
|
|
210
|
+
{
|
|
211
|
+
type: "text" as const,
|
|
212
|
+
text: `Memory written: ${result.id} (tier: ${result.tier}, scope: ${result.scope})`,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{ optional: true }
|
|
219
|
+
);
|
|
220
|
+
}
|