@poprobertdaniel/openclaw-memory 0.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/LICENSE +21 -0
- package/README.md +410 -0
- package/dist/chunk-CRPEAZ44.cjs +1881 -0
- package/dist/chunk-CRPEAZ44.cjs.map +1 -0
- package/dist/chunk-JNWCMHOB.js +309 -0
- package/dist/chunk-JNWCMHOB.js.map +1 -0
- package/dist/chunk-JSQBXYDM.js +1881 -0
- package/dist/chunk-JSQBXYDM.js.map +1 -0
- package/dist/chunk-NHFPLDZK.js +236 -0
- package/dist/chunk-NHFPLDZK.js.map +1 -0
- package/dist/chunk-NMUPGLJW.cjs +752 -0
- package/dist/chunk-NMUPGLJW.cjs.map +1 -0
- package/dist/chunk-RFLG2CCR.js +752 -0
- package/dist/chunk-RFLG2CCR.js.map +1 -0
- package/dist/chunk-VXULEX3A.cjs +236 -0
- package/dist/chunk-VXULEX3A.cjs.map +1 -0
- package/dist/chunk-ZY2C2CJQ.cjs +309 -0
- package/dist/chunk-ZY2C2CJQ.cjs.map +1 -0
- package/dist/cli/index.cjs +764 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +764 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.cjs +48 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +790 -0
- package/dist/index.d.ts +790 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/memory-service-6WDMF6KX.cjs +9 -0
- package/dist/memory-service-6WDMF6KX.cjs.map +1 -0
- package/dist/memory-service-GKEG6J2D.js +9 -0
- package/dist/memory-service-GKEG6J2D.js.map +1 -0
- package/dist/server-BTbRv-yX.d.cts +1199 -0
- package/dist/server-BTbRv-yX.d.ts +1199 -0
- package/dist/server.cjs +9 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +9 -0
- package/dist/server.js.map +1 -0
- package/docker/full.yml +45 -0
- package/docker/standard.yml +26 -0
- package/package.json +109 -0
- package/skill/SKILL.md +139 -0
- package/templates/.env.example +42 -0
- package/templates/docker-compose.full.yml +45 -0
- package/templates/docker-compose.standard.yml +26 -0
- package/templates/openclaw-memory.config.ts +49 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getDataDir,
|
|
4
|
+
getDefaultSqlitePath,
|
|
5
|
+
getPidFilePath,
|
|
6
|
+
loadConfig
|
|
7
|
+
} from "../chunk-JNWCMHOB.js";
|
|
8
|
+
|
|
9
|
+
// src/cli/index.ts
|
|
10
|
+
import { Command as Command9 } from "commander";
|
|
11
|
+
|
|
12
|
+
// src/cli/commands/init.ts
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
import fs2 from "fs";
|
|
15
|
+
import path from "path";
|
|
16
|
+
|
|
17
|
+
// src/cli/utils.ts
|
|
18
|
+
import pc from "picocolors";
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
function info(msg) {
|
|
21
|
+
console.log(pc.blue("\u2139") + " " + msg);
|
|
22
|
+
}
|
|
23
|
+
function success(msg) {
|
|
24
|
+
console.log(pc.green("\u2713") + " " + msg);
|
|
25
|
+
}
|
|
26
|
+
function warn(msg) {
|
|
27
|
+
console.log(pc.yellow("\u26A0") + " " + msg);
|
|
28
|
+
}
|
|
29
|
+
function error(msg) {
|
|
30
|
+
console.error(pc.red("\u2717") + " " + msg);
|
|
31
|
+
}
|
|
32
|
+
function header(title) {
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(pc.bold(` \u{1F9E0} OpenClaw Memory \u2014 ${title}`));
|
|
35
|
+
console.log();
|
|
36
|
+
}
|
|
37
|
+
function bullet(label, value, status) {
|
|
38
|
+
const dot = status === "ok" ? pc.green("\u25CF") : status === "error" ? pc.red("\u25CF") : status === "degraded" ? pc.yellow("\u25CF") : status === "disabled" ? pc.dim("\u25CB") : " ";
|
|
39
|
+
console.log(` ${dot} ${pc.bold(label)} ${value}`);
|
|
40
|
+
}
|
|
41
|
+
function getServerPid() {
|
|
42
|
+
const pidPath = getPidFilePath();
|
|
43
|
+
if (!fs.existsSync(pidPath)) return null;
|
|
44
|
+
try {
|
|
45
|
+
const pid = parseInt(fs.readFileSync(pidPath, "utf-8").trim(), 10);
|
|
46
|
+
if (isNaN(pid)) return null;
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return pid;
|
|
50
|
+
} catch {
|
|
51
|
+
fs.unlinkSync(pidPath);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function isServerRunning(baseUrl = "http://localhost:7777") {
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`${baseUrl}/api/health`, {
|
|
61
|
+
signal: AbortSignal.timeout(2e3)
|
|
62
|
+
});
|
|
63
|
+
return res.ok;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function getBaseUrl(port) {
|
|
69
|
+
return `http://localhost:${port || 7777}`;
|
|
70
|
+
}
|
|
71
|
+
async function apiGet(baseUrl, path3, token) {
|
|
72
|
+
const headers = {};
|
|
73
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
74
|
+
const res = await fetch(`${baseUrl}${path3}`, { headers });
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const text = await res.text();
|
|
77
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
78
|
+
}
|
|
79
|
+
return res.json();
|
|
80
|
+
}
|
|
81
|
+
async function apiPost(baseUrl, path3, body, token) {
|
|
82
|
+
const headers = { "Content-Type": "application/json" };
|
|
83
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
84
|
+
const res = await fetch(`${baseUrl}${path3}`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers,
|
|
87
|
+
body: JSON.stringify(body)
|
|
88
|
+
});
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
const text = await res.text();
|
|
91
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
92
|
+
}
|
|
93
|
+
return res.json();
|
|
94
|
+
}
|
|
95
|
+
function output(data, format = "json") {
|
|
96
|
+
if (format === "text") {
|
|
97
|
+
console.log(JSON.stringify(data, null, 2));
|
|
98
|
+
} else {
|
|
99
|
+
console.log(JSON.stringify(data));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function readStdin() {
|
|
103
|
+
if (process.stdin.isTTY) return "";
|
|
104
|
+
const chunks = [];
|
|
105
|
+
for await (const chunk of process.stdin) {
|
|
106
|
+
chunks.push(Buffer.from(chunk));
|
|
107
|
+
}
|
|
108
|
+
return Buffer.concat(chunks).toString("utf-8").trim();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/cli/commands/init.ts
|
|
112
|
+
function initCommand() {
|
|
113
|
+
return new Command("init").description("Interactive setup wizard").option("--tier <tier>", "Tier: lite, standard, or full").option("--non-interactive", "Skip prompts, use defaults + flags").action(async (opts) => {
|
|
114
|
+
header("Setup Wizard");
|
|
115
|
+
const tier = opts.tier || "lite";
|
|
116
|
+
const dataDir = getDataDir();
|
|
117
|
+
const sqlitePath = getDefaultSqlitePath();
|
|
118
|
+
fs2.mkdirSync(dataDir, { recursive: true });
|
|
119
|
+
info(`Data directory: ${dataDir}`);
|
|
120
|
+
let qdrantAvailable = false;
|
|
121
|
+
let ageAvailable = false;
|
|
122
|
+
if (tier !== "lite") {
|
|
123
|
+
info("Checking Qdrant connectivity...");
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch("http://localhost:6333/collections", {
|
|
126
|
+
signal: AbortSignal.timeout(2e3)
|
|
127
|
+
});
|
|
128
|
+
qdrantAvailable = res.ok;
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
if (qdrantAvailable) {
|
|
132
|
+
success("Qdrant is reachable at http://localhost:6333");
|
|
133
|
+
} else {
|
|
134
|
+
warn("Qdrant not reachable at http://localhost:6333");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (tier === "full") {
|
|
138
|
+
info("Checking PostgreSQL/AGE connectivity...");
|
|
139
|
+
try {
|
|
140
|
+
const net = await import("net");
|
|
141
|
+
ageAvailable = await new Promise((resolve) => {
|
|
142
|
+
const socket = new net.Socket();
|
|
143
|
+
socket.setTimeout(2e3);
|
|
144
|
+
socket.connect(5432, "localhost", () => {
|
|
145
|
+
socket.destroy();
|
|
146
|
+
resolve(true);
|
|
147
|
+
});
|
|
148
|
+
socket.on("error", () => resolve(false));
|
|
149
|
+
socket.on("timeout", () => {
|
|
150
|
+
socket.destroy();
|
|
151
|
+
resolve(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
if (ageAvailable) {
|
|
157
|
+
success("PostgreSQL is reachable at localhost:5432");
|
|
158
|
+
} else {
|
|
159
|
+
warn("PostgreSQL not reachable at localhost:5432");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
let effectiveTier = tier;
|
|
163
|
+
if (tier === "full" && !ageAvailable) {
|
|
164
|
+
effectiveTier = qdrantAvailable ? "standard" : "lite";
|
|
165
|
+
warn(`Downgrading to ${effectiveTier} tier (missing dependencies)`);
|
|
166
|
+
} else if (tier === "standard" && !qdrantAvailable) {
|
|
167
|
+
effectiveTier = "lite";
|
|
168
|
+
warn("Downgrading to lite tier (Qdrant not available)");
|
|
169
|
+
}
|
|
170
|
+
const configContent = generateConfig(effectiveTier, sqlitePath);
|
|
171
|
+
const configPath = path.join(process.cwd(), "openclaw-memory.config.ts");
|
|
172
|
+
fs2.writeFileSync(configPath, configContent, "utf-8");
|
|
173
|
+
success(`Config written to ${configPath}`);
|
|
174
|
+
const envExample = generateEnvExample(effectiveTier);
|
|
175
|
+
const envPath = path.join(process.cwd(), ".env.example");
|
|
176
|
+
fs2.writeFileSync(envPath, envExample, "utf-8");
|
|
177
|
+
success(`Environment template written to ${envPath}`);
|
|
178
|
+
console.log();
|
|
179
|
+
info("Next steps:");
|
|
180
|
+
console.log(" openclaw-memory start # Start the server");
|
|
181
|
+
console.log(" openclaw-memory status # Check all layers");
|
|
182
|
+
console.log();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function generateConfig(tier, sqlitePath) {
|
|
186
|
+
const lines = [
|
|
187
|
+
`import { defineConfig } from 'openclaw-memory';`,
|
|
188
|
+
``,
|
|
189
|
+
`export default defineConfig({`,
|
|
190
|
+
` tier: '${tier}',`,
|
|
191
|
+
` port: 7777,`,
|
|
192
|
+
` auth: {`,
|
|
193
|
+
` token: process.env.MEMORY_AUTH_TOKEN || 'change-me',`,
|
|
194
|
+
` },`,
|
|
195
|
+
` sqlite: {`,
|
|
196
|
+
` path: '${sqlitePath}',`,
|
|
197
|
+
` },`
|
|
198
|
+
];
|
|
199
|
+
if (tier === "standard" || tier === "full") {
|
|
200
|
+
lines.push(` qdrant: {`);
|
|
201
|
+
lines.push(` url: process.env.QDRANT_URL || 'http://localhost:6333',`);
|
|
202
|
+
lines.push(` collection: 'openclaw_memories',`);
|
|
203
|
+
lines.push(` },`);
|
|
204
|
+
lines.push(` embedding: {`);
|
|
205
|
+
lines.push(` apiKey: process.env.OPENAI_API_KEY || '',`);
|
|
206
|
+
lines.push(` model: 'text-embedding-3-small',`);
|
|
207
|
+
lines.push(` dimensions: 1536,`);
|
|
208
|
+
lines.push(` },`);
|
|
209
|
+
lines.push(` extraction: {`);
|
|
210
|
+
lines.push(` apiKey: process.env.OPENAI_API_KEY || '',`);
|
|
211
|
+
lines.push(` model: 'gpt-4o-mini',`);
|
|
212
|
+
lines.push(` enabled: true,`);
|
|
213
|
+
lines.push(` },`);
|
|
214
|
+
}
|
|
215
|
+
if (tier === "full") {
|
|
216
|
+
lines.push(` age: {`);
|
|
217
|
+
lines.push(` host: process.env.PGHOST || 'localhost',`);
|
|
218
|
+
lines.push(` port: parseInt(process.env.PGPORT || '5432', 10),`);
|
|
219
|
+
lines.push(` user: process.env.PGUSER || 'openclaw',`);
|
|
220
|
+
lines.push(` password: process.env.PGPASSWORD || '',`);
|
|
221
|
+
lines.push(` database: process.env.PGDATABASE || 'agent_memory',`);
|
|
222
|
+
lines.push(` graph: 'agent_memory',`);
|
|
223
|
+
lines.push(` },`);
|
|
224
|
+
}
|
|
225
|
+
lines.push(`});`);
|
|
226
|
+
lines.push(``);
|
|
227
|
+
return lines.join("\n");
|
|
228
|
+
}
|
|
229
|
+
function generateEnvExample(tier) {
|
|
230
|
+
const lines = [
|
|
231
|
+
`# openclaw-memory environment variables`,
|
|
232
|
+
``,
|
|
233
|
+
`# Server`,
|
|
234
|
+
`# OPENCLAW_MEMORY_PORT=7777`,
|
|
235
|
+
`# OPENCLAW_MEMORY_HOST=0.0.0.0`,
|
|
236
|
+
``,
|
|
237
|
+
`# Authentication`,
|
|
238
|
+
`MEMORY_AUTH_TOKEN=change-me-to-a-secure-token`,
|
|
239
|
+
``,
|
|
240
|
+
`# SQLite (always required)`,
|
|
241
|
+
`# SQLITE_PATH=~/.openclaw-memory/memory.sqlite`,
|
|
242
|
+
``
|
|
243
|
+
];
|
|
244
|
+
if (tier === "standard" || tier === "full") {
|
|
245
|
+
lines.push(`# Qdrant (Standard/Full tier)`);
|
|
246
|
+
lines.push(`QDRANT_URL=http://localhost:6333`);
|
|
247
|
+
lines.push(``);
|
|
248
|
+
lines.push(`# Embedding provider`);
|
|
249
|
+
lines.push(`OPENAI_API_KEY=sk-your-key-here`);
|
|
250
|
+
lines.push(`# EMBEDDING_MODEL=text-embedding-3-small`);
|
|
251
|
+
lines.push(`# EMBEDDING_BASE_URL=https://api.openai.com/v1`);
|
|
252
|
+
lines.push(``);
|
|
253
|
+
lines.push(`# Entity extraction`);
|
|
254
|
+
lines.push(`# EXTRACTION_MODEL=gpt-4o-mini`);
|
|
255
|
+
lines.push(``);
|
|
256
|
+
}
|
|
257
|
+
if (tier === "full") {
|
|
258
|
+
lines.push(`# PostgreSQL + Apache AGE (Full tier)`);
|
|
259
|
+
lines.push(`PGHOST=localhost`);
|
|
260
|
+
lines.push(`PGPORT=5432`);
|
|
261
|
+
lines.push(`PGUSER=openclaw`);
|
|
262
|
+
lines.push(`PGPASSWORD=your-password`);
|
|
263
|
+
lines.push(`PGDATABASE=agent_memory`);
|
|
264
|
+
lines.push(`# AGE_GRAPH=agent_memory`);
|
|
265
|
+
lines.push(``);
|
|
266
|
+
}
|
|
267
|
+
return lines.join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/cli/commands/start.ts
|
|
271
|
+
import { Command as Command2 } from "commander";
|
|
272
|
+
import fs3 from "fs";
|
|
273
|
+
import { spawn } from "child_process";
|
|
274
|
+
function startCommand() {
|
|
275
|
+
return new Command2("start").description("Start the HTTP server").option("-p, --port <port>", "Server port").option("--bg", "Run in background (daemon mode)").option("--config <path>", "Path to config file").action(async (opts) => {
|
|
276
|
+
if (opts.bg) {
|
|
277
|
+
await startBackground(opts);
|
|
278
|
+
} else {
|
|
279
|
+
await startForeground(opts);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async function startForeground(opts) {
|
|
284
|
+
if (opts.port) {
|
|
285
|
+
process.env.OPENCLAW_MEMORY_PORT = opts.port;
|
|
286
|
+
process.env.PORT = opts.port;
|
|
287
|
+
}
|
|
288
|
+
const { createServer } = await import("../server.js");
|
|
289
|
+
const { app, config } = await createServer(opts.config);
|
|
290
|
+
const port = opts.port ? parseInt(opts.port, 10) : config.port;
|
|
291
|
+
app.listen(port);
|
|
292
|
+
console.log(`[server] Listening on http://0.0.0.0:${port}`);
|
|
293
|
+
const dataDir = getDataDir();
|
|
294
|
+
fs3.mkdirSync(dataDir, { recursive: true });
|
|
295
|
+
fs3.writeFileSync(getPidFilePath(), String(process.pid), "utf-8");
|
|
296
|
+
const cleanup = () => {
|
|
297
|
+
try {
|
|
298
|
+
fs3.unlinkSync(getPidFilePath());
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
process.exit(0);
|
|
302
|
+
};
|
|
303
|
+
process.on("SIGINT", cleanup);
|
|
304
|
+
process.on("SIGTERM", cleanup);
|
|
305
|
+
}
|
|
306
|
+
async function startBackground(opts) {
|
|
307
|
+
header("Starting Server (background)");
|
|
308
|
+
const dataDir = getDataDir();
|
|
309
|
+
fs3.mkdirSync(dataDir, { recursive: true });
|
|
310
|
+
const args = ["run", "src/server.ts"];
|
|
311
|
+
const env = { ...process.env };
|
|
312
|
+
if (opts.port) {
|
|
313
|
+
env.OPENCLAW_MEMORY_PORT = opts.port;
|
|
314
|
+
env.PORT = opts.port;
|
|
315
|
+
}
|
|
316
|
+
const runtime = typeof Bun !== "undefined" ? "bun" : "node";
|
|
317
|
+
const execPath = runtime === "bun" ? "bun" : process.execPath;
|
|
318
|
+
const execArgs = runtime === "bun" ? args : ["--import", "tsx", ...args];
|
|
319
|
+
const child = spawn(execPath, execArgs, {
|
|
320
|
+
env,
|
|
321
|
+
detached: true,
|
|
322
|
+
stdio: "ignore",
|
|
323
|
+
cwd: process.cwd()
|
|
324
|
+
});
|
|
325
|
+
child.unref();
|
|
326
|
+
if (child.pid) {
|
|
327
|
+
fs3.writeFileSync(getPidFilePath(), String(child.pid), "utf-8");
|
|
328
|
+
success(`Server started in background (PID: ${child.pid})`);
|
|
329
|
+
info(`PID file: ${getPidFilePath()}`);
|
|
330
|
+
info(`Stop with: openclaw-memory stop`);
|
|
331
|
+
} else {
|
|
332
|
+
error("Failed to start server");
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/cli/commands/stop.ts
|
|
338
|
+
import { Command as Command3 } from "commander";
|
|
339
|
+
import fs4 from "fs";
|
|
340
|
+
function stopCommand() {
|
|
341
|
+
return new Command3("stop").description("Stop the running server").action(async () => {
|
|
342
|
+
header("Stopping Server");
|
|
343
|
+
const pidPath = getPidFilePath();
|
|
344
|
+
if (!fs4.existsSync(pidPath)) {
|
|
345
|
+
warn("No PID file found \u2014 server may not be running");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const pidStr = fs4.readFileSync(pidPath, "utf-8").trim();
|
|
349
|
+
const pid = parseInt(pidStr, 10);
|
|
350
|
+
if (isNaN(pid)) {
|
|
351
|
+
error(`Invalid PID in ${pidPath}: ${pidStr}`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
process.kill(pid, "SIGTERM");
|
|
356
|
+
success(`Sent SIGTERM to PID ${pid}`);
|
|
357
|
+
let alive = true;
|
|
358
|
+
for (let i = 0; i < 30; i++) {
|
|
359
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
360
|
+
try {
|
|
361
|
+
process.kill(pid, 0);
|
|
362
|
+
} catch {
|
|
363
|
+
alive = false;
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (alive) {
|
|
368
|
+
warn("Process still running after 3s, sending SIGKILL...");
|
|
369
|
+
try {
|
|
370
|
+
process.kill(pid, "SIGKILL");
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
success("Server stopped");
|
|
375
|
+
} catch (err) {
|
|
376
|
+
if (err.code === "ESRCH") {
|
|
377
|
+
warn(`Process ${pid} not found \u2014 may have already stopped`);
|
|
378
|
+
} else {
|
|
379
|
+
error(`Failed to stop process ${pid}: ${err}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
fs4.unlinkSync(pidPath);
|
|
384
|
+
} catch {
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/cli/commands/status.ts
|
|
390
|
+
import { Command as Command4 } from "commander";
|
|
391
|
+
function statusCommand() {
|
|
392
|
+
return new Command4("status").description("Show server and layer health status").option("-p, --port <port>", "Server port to check").option("--config <path>", "Path to config file").action(async (opts) => {
|
|
393
|
+
header("Status");
|
|
394
|
+
let config;
|
|
395
|
+
try {
|
|
396
|
+
config = await loadConfig(opts.config);
|
|
397
|
+
} catch {
|
|
398
|
+
config = null;
|
|
399
|
+
}
|
|
400
|
+
const port = opts.port ? parseInt(opts.port, 10) : config?.port || 7777;
|
|
401
|
+
const baseUrl = getBaseUrl(port);
|
|
402
|
+
const pid = getServerPid();
|
|
403
|
+
const running = await isServerRunning(baseUrl);
|
|
404
|
+
if (running && pid) {
|
|
405
|
+
bullet("Server", `Running (PID ${pid}, port ${port})`, "ok");
|
|
406
|
+
} else if (running) {
|
|
407
|
+
bullet("Server", `Running (port ${port})`, "ok");
|
|
408
|
+
} else if (pid) {
|
|
409
|
+
bullet("Server", `PID file exists (${pid}) but not responding`, "error");
|
|
410
|
+
} else {
|
|
411
|
+
bullet("Server", "Not running", "disabled");
|
|
412
|
+
}
|
|
413
|
+
if (config) {
|
|
414
|
+
bullet("Tier", config.tier, void 0);
|
|
415
|
+
}
|
|
416
|
+
console.log();
|
|
417
|
+
if (running) {
|
|
418
|
+
try {
|
|
419
|
+
const health = await apiGet(baseUrl, "/api/health", config?.auth?.token);
|
|
420
|
+
info("Layers:");
|
|
421
|
+
const sqliteStatus = health.sqlite === "ok" ? "ok" : "error";
|
|
422
|
+
bullet("L1 SQLite", String(health.sqlite), sqliteStatus);
|
|
423
|
+
if (health.qdrant !== "disabled") {
|
|
424
|
+
const qdrantStatus = health.qdrant === "ok" ? "ok" : "error";
|
|
425
|
+
bullet("L2 Qdrant", String(health.qdrant), qdrantStatus);
|
|
426
|
+
} else {
|
|
427
|
+
bullet("L2 Qdrant", "disabled", "disabled");
|
|
428
|
+
}
|
|
429
|
+
if (health.age !== "disabled") {
|
|
430
|
+
const ageStatus = health.age === "ok" ? "ok" : "error";
|
|
431
|
+
bullet("L3 AGE", String(health.age), ageStatus);
|
|
432
|
+
} else {
|
|
433
|
+
bullet("L3 AGE", "disabled", "disabled");
|
|
434
|
+
}
|
|
435
|
+
if (health.uptime !== void 0) {
|
|
436
|
+
const uptime = formatUptime(Number(health.uptime));
|
|
437
|
+
console.log();
|
|
438
|
+
info(`Uptime: ${uptime}`);
|
|
439
|
+
}
|
|
440
|
+
} catch (error2) {
|
|
441
|
+
info("Could not fetch health status from server");
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
if (config) {
|
|
445
|
+
info("Server not running. Checking layers directly...");
|
|
446
|
+
console.log();
|
|
447
|
+
bullet("L1 SQLite", config.sqlite.path, "ok");
|
|
448
|
+
if (config.qdrant) {
|
|
449
|
+
try {
|
|
450
|
+
const res = await fetch(`${config.qdrant.url}/collections`, {
|
|
451
|
+
signal: AbortSignal.timeout(2e3)
|
|
452
|
+
});
|
|
453
|
+
bullet("L2 Qdrant", config.qdrant.url, res.ok ? "ok" : "error");
|
|
454
|
+
} catch {
|
|
455
|
+
bullet("L2 Qdrant", `${config.qdrant.url} (unreachable)`, "error");
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
bullet("L2 Qdrant", "not configured", "disabled");
|
|
459
|
+
}
|
|
460
|
+
if (config.age) {
|
|
461
|
+
try {
|
|
462
|
+
const net = await import("net");
|
|
463
|
+
const reachable = await new Promise((resolve) => {
|
|
464
|
+
const socket = new net.Socket();
|
|
465
|
+
socket.setTimeout(2e3);
|
|
466
|
+
socket.connect(config.age.port, config.age.host, () => {
|
|
467
|
+
socket.destroy();
|
|
468
|
+
resolve(true);
|
|
469
|
+
});
|
|
470
|
+
socket.on("error", () => resolve(false));
|
|
471
|
+
socket.on("timeout", () => {
|
|
472
|
+
socket.destroy();
|
|
473
|
+
resolve(false);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
bullet("L3 AGE", `${config.age.host}:${config.age.port}`, reachable ? "ok" : "error");
|
|
477
|
+
} catch {
|
|
478
|
+
bullet("L3 AGE", "unreachable", "error");
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
bullet("L3 AGE", "not configured", "disabled");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
console.log();
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
function formatUptime(seconds) {
|
|
489
|
+
const h = Math.floor(seconds / 3600);
|
|
490
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
491
|
+
const s = seconds % 60;
|
|
492
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
493
|
+
if (m > 0) return `${m}m ${s}s`;
|
|
494
|
+
return `${s}s`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/cli/commands/store.ts
|
|
498
|
+
import { Command as Command5 } from "commander";
|
|
499
|
+
function storeCommand() {
|
|
500
|
+
return new Command5("store").description("Store a new memory").argument("[content]", "Memory content (or pipe via stdin)").requiredOption("--agent <id>", "Agent ID").requiredOption("--scope <scope>", "Memory scope (user, agent, global, project, session)").option("--subject <id>", "Subject ID").option("--tags <tags>", "Comma-separated tags").option("--source <source>", "Memory source", "explicit").option("--no-extract", "Skip entity extraction").option("--format <fmt>", "Output format (json, text)", "json").option("--config <path>", "Path to config file").action(async (contentArg, opts) => {
|
|
501
|
+
const content = contentArg || await readStdin();
|
|
502
|
+
if (!content) {
|
|
503
|
+
console.error("Error: content is required (pass as argument or pipe via stdin)");
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
const config = await loadConfig(opts.config);
|
|
507
|
+
const baseUrl = getBaseUrl(config.port);
|
|
508
|
+
const serverUp = await isServerRunning(baseUrl);
|
|
509
|
+
const body = {
|
|
510
|
+
agent_id: opts.agent,
|
|
511
|
+
scope: opts.scope,
|
|
512
|
+
subject_id: opts.subject || null,
|
|
513
|
+
content,
|
|
514
|
+
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : [],
|
|
515
|
+
source: opts.source,
|
|
516
|
+
extract_entities: opts.extract !== false
|
|
517
|
+
};
|
|
518
|
+
if (serverUp) {
|
|
519
|
+
const result = await apiPost(baseUrl, "/api/memories", body, config.auth.token);
|
|
520
|
+
output(result, opts.format);
|
|
521
|
+
} else {
|
|
522
|
+
const { MemoryService } = await import("../memory-service-GKEG6J2D.js");
|
|
523
|
+
const service = new MemoryService();
|
|
524
|
+
await service.init();
|
|
525
|
+
try {
|
|
526
|
+
const result = await service.store({
|
|
527
|
+
agentId: opts.agent,
|
|
528
|
+
scope: opts.scope,
|
|
529
|
+
subjectId: opts.subject || null,
|
|
530
|
+
content,
|
|
531
|
+
tags: body.tags,
|
|
532
|
+
source: opts.source,
|
|
533
|
+
extractEntities: opts.extract !== false
|
|
534
|
+
});
|
|
535
|
+
output(result, opts.format);
|
|
536
|
+
} finally {
|
|
537
|
+
await service.close();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/cli/commands/search.ts
|
|
544
|
+
import { Command as Command6 } from "commander";
|
|
545
|
+
function searchCommand() {
|
|
546
|
+
const cmd = new Command6("search").description("Search memories");
|
|
547
|
+
cmd.argument("<query>", "Search query").requiredOption("--agent <id>", "Agent ID").option("--limit <n>", "Max results", "10").option("--strategy <s>", "Search strategy (auto, semantic, fulltext, graph, all)", "auto").option("--scopes <scopes>", "Comma-separated scopes").option("--subject <id>", "Subject ID filter").option("--cross-agent", "Search across all agents").option("--no-graph", "Exclude graph results").option("--recall", "Format output for LLM context injection").option("--format <fmt>", "Output format (json, text)", "json").option("--config <path>", "Path to config file").action(async (query, opts) => {
|
|
548
|
+
const config = await loadConfig(opts.config);
|
|
549
|
+
const baseUrl = getBaseUrl(config.port);
|
|
550
|
+
const serverUp = await isServerRunning(baseUrl);
|
|
551
|
+
const body = {
|
|
552
|
+
agent_id: opts.agent,
|
|
553
|
+
query,
|
|
554
|
+
limit: parseInt(opts.limit, 10),
|
|
555
|
+
strategy: opts.strategy,
|
|
556
|
+
scopes: opts.scopes ? opts.scopes.split(",") : void 0,
|
|
557
|
+
subject_id: opts.subject || null,
|
|
558
|
+
cross_agent: opts.crossAgent || false,
|
|
559
|
+
include_graph: opts.graph !== false
|
|
560
|
+
};
|
|
561
|
+
let result;
|
|
562
|
+
if (serverUp) {
|
|
563
|
+
result = await apiPost(baseUrl, "/api/search", body, config.auth.token);
|
|
564
|
+
} else {
|
|
565
|
+
const { MemoryService } = await import("../memory-service-GKEG6J2D.js");
|
|
566
|
+
const service = new MemoryService();
|
|
567
|
+
await service.init();
|
|
568
|
+
try {
|
|
569
|
+
result = await service.search({
|
|
570
|
+
agentId: opts.agent,
|
|
571
|
+
query,
|
|
572
|
+
limit: parseInt(opts.limit, 10),
|
|
573
|
+
strategy: opts.strategy,
|
|
574
|
+
scopes: opts.scopes ? opts.scopes.split(",") : void 0,
|
|
575
|
+
subjectId: opts.subject || null,
|
|
576
|
+
crossAgent: opts.crossAgent || false,
|
|
577
|
+
includeGraph: opts.graph !== false
|
|
578
|
+
});
|
|
579
|
+
} finally {
|
|
580
|
+
await service.close();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (opts.recall) {
|
|
584
|
+
const data = result;
|
|
585
|
+
if (data.results && data.results.length > 0) {
|
|
586
|
+
console.log("## Relevant Memories");
|
|
587
|
+
for (const r of data.results) {
|
|
588
|
+
const date = new Date(r.memory.created_at).toLocaleDateString();
|
|
589
|
+
console.log(`- ${r.memory.content} (${r.memory.scope}, ${date})`);
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
console.log("No relevant memories found.");
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
output(result, opts.format);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
return cmd;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/cli/commands/migrate.ts
|
|
602
|
+
import { Command as Command7 } from "commander";
|
|
603
|
+
function migrateCommand() {
|
|
604
|
+
return new Command7("migrate").description("Import memories from markdown files").requiredOption("--paths <paths>", "Comma-separated file paths").requiredOption("--agent <id>", "Agent ID").option("--dry-run", "Preview without writing").option("--format <fmt>", "Output format (json, text)", "json").option("--config <path>", "Path to config file").action(async (opts) => {
|
|
605
|
+
const config = await loadConfig(opts.config);
|
|
606
|
+
const baseUrl = getBaseUrl(config.port);
|
|
607
|
+
const serverUp = await isServerRunning(baseUrl);
|
|
608
|
+
const paths = opts.paths.split(",").map((p) => p.trim());
|
|
609
|
+
if (opts.dryRun) {
|
|
610
|
+
header("Migration (dry run)");
|
|
611
|
+
}
|
|
612
|
+
const body = {
|
|
613
|
+
markdown_paths: paths,
|
|
614
|
+
agent_id: opts.agent,
|
|
615
|
+
dry_run: opts.dryRun || false
|
|
616
|
+
};
|
|
617
|
+
let result;
|
|
618
|
+
if (serverUp) {
|
|
619
|
+
result = await apiPost(baseUrl, "/api/admin/migrate-markdown", body, config.auth.token);
|
|
620
|
+
} else {
|
|
621
|
+
const { MemoryService } = await import("../memory-service-GKEG6J2D.js");
|
|
622
|
+
const service = new MemoryService();
|
|
623
|
+
await service.init();
|
|
624
|
+
try {
|
|
625
|
+
const migrated = await service.migrateMarkdown(paths, opts.agent);
|
|
626
|
+
result = migrated;
|
|
627
|
+
} finally {
|
|
628
|
+
await service.close();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (opts.format === "text") {
|
|
632
|
+
const data = result;
|
|
633
|
+
if (data.migrated !== void 0) {
|
|
634
|
+
success(`Migrated ${data.migrated} memories`);
|
|
635
|
+
}
|
|
636
|
+
if (data.skipped) {
|
|
637
|
+
info(`Skipped ${data.skipped} sections`);
|
|
638
|
+
}
|
|
639
|
+
if (data.errors && data.errors.length > 0) {
|
|
640
|
+
for (const err of data.errors) {
|
|
641
|
+
console.error(` \u2717 ${err}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
output(result, opts.format);
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/cli/commands/infra.ts
|
|
651
|
+
import { Command as Command8 } from "commander";
|
|
652
|
+
import { execSync } from "child_process";
|
|
653
|
+
import fs5 from "fs";
|
|
654
|
+
import path2 from "path";
|
|
655
|
+
import { fileURLToPath } from "url";
|
|
656
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
657
|
+
var __dirname = path2.dirname(__filename);
|
|
658
|
+
function infraCommand() {
|
|
659
|
+
const infra = new Command8("infra").description("Manage Docker infrastructure");
|
|
660
|
+
infra.command("up").description("Start Docker containers for the configured tier").option("--tier <tier>", "Override tier (standard, full)").action(async (opts) => {
|
|
661
|
+
header("Infrastructure Up");
|
|
662
|
+
let tier = opts.tier;
|
|
663
|
+
if (!tier) {
|
|
664
|
+
try {
|
|
665
|
+
const config = await loadConfig();
|
|
666
|
+
tier = config.tier;
|
|
667
|
+
} catch {
|
|
668
|
+
tier = "standard";
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (tier === "lite") {
|
|
672
|
+
info("Lite tier uses only SQLite \u2014 no Docker infrastructure needed.");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const templateFile = tier === "full" ? "full.yml" : "standard.yml";
|
|
676
|
+
const templatePath = findTemplate(templateFile);
|
|
677
|
+
if (!templatePath) {
|
|
678
|
+
error(`Template not found: ${templateFile}`);
|
|
679
|
+
error("Expected in ./docker/ or ./templates/ directory");
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
const dataDir = getDataDir();
|
|
683
|
+
fs5.mkdirSync(dataDir, { recursive: true });
|
|
684
|
+
const targetPath = path2.join(dataDir, "docker-compose.yml");
|
|
685
|
+
fs5.copyFileSync(templatePath, targetPath);
|
|
686
|
+
info(`Using template: ${templatePath}`);
|
|
687
|
+
try {
|
|
688
|
+
execSync(`docker compose -f ${targetPath} up -d`, {
|
|
689
|
+
stdio: "inherit",
|
|
690
|
+
cwd: dataDir
|
|
691
|
+
});
|
|
692
|
+
success("Docker containers started");
|
|
693
|
+
} catch (error2) {
|
|
694
|
+
error("Failed to start Docker containers");
|
|
695
|
+
error("Make sure Docker is installed and running");
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
infra.command("down").description("Stop Docker containers").action(async () => {
|
|
700
|
+
header("Infrastructure Down");
|
|
701
|
+
const dataDir = getDataDir();
|
|
702
|
+
const composePath = path2.join(dataDir, "docker-compose.yml");
|
|
703
|
+
if (!fs5.existsSync(composePath)) {
|
|
704
|
+
warn("No docker-compose.yml found in data directory");
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
execSync(`docker compose -f ${composePath} down`, {
|
|
709
|
+
stdio: "inherit",
|
|
710
|
+
cwd: dataDir
|
|
711
|
+
});
|
|
712
|
+
success("Docker containers stopped");
|
|
713
|
+
} catch (error2) {
|
|
714
|
+
error("Failed to stop Docker containers");
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
infra.command("status").description("Show Docker container status").action(async () => {
|
|
719
|
+
header("Infrastructure Status");
|
|
720
|
+
const dataDir = getDataDir();
|
|
721
|
+
const composePath = path2.join(dataDir, "docker-compose.yml");
|
|
722
|
+
if (!fs5.existsSync(composePath)) {
|
|
723
|
+
info("No docker-compose.yml found \u2014 infrastructure not set up");
|
|
724
|
+
info("Run: openclaw-memory infra up");
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
execSync(`docker compose -f ${composePath} ps`, {
|
|
729
|
+
stdio: "inherit",
|
|
730
|
+
cwd: dataDir
|
|
731
|
+
});
|
|
732
|
+
} catch {
|
|
733
|
+
error("Failed to get container status");
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
return infra;
|
|
737
|
+
}
|
|
738
|
+
function findTemplate(filename) {
|
|
739
|
+
const searchPaths = [
|
|
740
|
+
path2.join(process.cwd(), "docker", filename),
|
|
741
|
+
path2.join(process.cwd(), "templates", filename),
|
|
742
|
+
// Look in the package's installed location
|
|
743
|
+
path2.join(__dirname, "../../docker", filename),
|
|
744
|
+
path2.join(__dirname, "../../templates", filename)
|
|
745
|
+
];
|
|
746
|
+
for (const p of searchPaths) {
|
|
747
|
+
if (fs5.existsSync(p)) return p;
|
|
748
|
+
}
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/cli/index.ts
|
|
753
|
+
var program = new Command9();
|
|
754
|
+
program.name("openclaw-memory").description("Triple-layer memory system for AI agents \u2014 SQLite + Qdrant + Postgres/AGE").version("0.1.0");
|
|
755
|
+
program.addCommand(initCommand());
|
|
756
|
+
program.addCommand(startCommand());
|
|
757
|
+
program.addCommand(stopCommand());
|
|
758
|
+
program.addCommand(statusCommand());
|
|
759
|
+
program.addCommand(storeCommand());
|
|
760
|
+
program.addCommand(searchCommand());
|
|
761
|
+
program.addCommand(migrateCommand());
|
|
762
|
+
program.addCommand(infraCommand());
|
|
763
|
+
program.parse();
|
|
764
|
+
//# sourceMappingURL=index.js.map
|