@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 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
+ }