@cybermem/cli 0.9.12 → 0.13.3

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.
Files changed (56) hide show
  1. package/dist/commands/install.js +430 -0
  2. package/dist/commands/reset.js +18 -2
  3. package/dist/commands/uninstall.js +145 -0
  4. package/dist/commands/upgrade.js +91 -52
  5. package/dist/index.js +18 -5
  6. package/dist/templates/ansible/playbooks/deploy-cybermem.yml +201 -24
  7. package/dist/templates/ansible/playbooks/reset-db.yml +44 -0
  8. package/dist/templates/auth-sidecar/Dockerfile +2 -10
  9. package/dist/templates/auth-sidecar/package.json +1 -3
  10. package/dist/templates/auth-sidecar/server.js +149 -110
  11. package/dist/templates/charts/cybermem/.helmignore +13 -0
  12. package/dist/templates/charts/cybermem/templates/dashboard-deployment.yaml +31 -7
  13. package/dist/templates/charts/cybermem/templates/dashboard-service.yaml +4 -4
  14. package/dist/templates/charts/cybermem/templates/openmemory-deployment.yaml +14 -8
  15. package/dist/templates/charts/cybermem/templates/openmemory-service.yaml +3 -3
  16. package/dist/templates/charts/cybermem/templates/secret.yaml +9 -0
  17. package/dist/templates/charts/cybermem/templates/traefik-config.yaml +67 -0
  18. package/dist/templates/charts/cybermem/templates/traefik-deployment.yaml +53 -0
  19. package/dist/templates/charts/cybermem/templates/traefik-service.yaml +17 -0
  20. package/dist/templates/charts/cybermem/values-vps.yaml +8 -4
  21. package/dist/templates/charts/cybermem/values.yaml +17 -9
  22. package/dist/templates/docker-compose.yml +103 -78
  23. package/dist/templates/monitoring/log_exporter/exporter.py +22 -29
  24. package/dist/templates/monitoring/traefik/traefik.yml +1 -4
  25. package/package.json +9 -3
  26. package/templates/ansible/playbooks/deploy-cybermem.yml +201 -24
  27. package/templates/ansible/playbooks/reset-db.yml +44 -0
  28. package/templates/auth-sidecar/Dockerfile +2 -10
  29. package/templates/auth-sidecar/package.json +1 -3
  30. package/templates/auth-sidecar/server.js +149 -110
  31. package/templates/charts/cybermem/.helmignore +13 -0
  32. package/templates/charts/cybermem/templates/dashboard-deployment.yaml +31 -7
  33. package/templates/charts/cybermem/templates/dashboard-service.yaml +4 -4
  34. package/templates/charts/cybermem/templates/openmemory-deployment.yaml +14 -8
  35. package/templates/charts/cybermem/templates/openmemory-service.yaml +3 -3
  36. package/templates/charts/cybermem/templates/secret.yaml +9 -0
  37. package/templates/charts/cybermem/templates/traefik-config.yaml +67 -0
  38. package/templates/charts/cybermem/templates/traefik-deployment.yaml +53 -0
  39. package/templates/charts/cybermem/templates/traefik-service.yaml +17 -0
  40. package/templates/charts/cybermem/values-vps.yaml +8 -4
  41. package/templates/charts/cybermem/values.yaml +17 -9
  42. package/templates/docker-compose.yml +103 -78
  43. package/templates/monitoring/log_exporter/exporter.py +22 -29
  44. package/templates/monitoring/traefik/traefik.yml +1 -4
  45. package/dist/commands/__tests__/backup.test.js +0 -75
  46. package/dist/commands/__tests__/restore.test.js +0 -70
  47. package/dist/commands/deploy.js +0 -239
  48. package/dist/commands/init.js +0 -362
  49. package/dist/commands/login.js +0 -165
  50. package/dist/templates/envs/local.example +0 -27
  51. package/dist/templates/envs/rpi.example +0 -27
  52. package/dist/templates/envs/vps.example +0 -25
  53. package/dist/templates/monitoring/instructions_injector/Dockerfile +0 -15
  54. package/dist/templates/monitoring/instructions_injector/injector.py +0 -137
  55. package/dist/templates/monitoring/instructions_injector/requirements.txt +0 -3
  56. package/dist/templates/openmemory/Dockerfile +0 -19
@@ -0,0 +1,430 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.install = install;
40
+ const chalk_1 = __importDefault(require("chalk"));
41
+ const crypto_1 = __importDefault(require("crypto"));
42
+ const execa_1 = __importDefault(require("execa"));
43
+ const fs_1 = __importDefault(require("fs"));
44
+ const inquirer_1 = __importDefault(require("inquirer"));
45
+ const os_1 = __importDefault(require("os"));
46
+ const path_1 = __importDefault(require("path"));
47
+ // Helper to handle and suggest fixes for common errors
48
+ function handleExecError(error, context) {
49
+ const stderr = error.stderr || "";
50
+ let suggestion = "";
51
+ if (stderr.includes("Permission denied") || stderr.includes("publickey")) {
52
+ suggestion =
53
+ "\nšŸ’” Next Step: Ensure your SSH keys are added to the remote host: `ssh-copy-id user@host`";
54
+ }
55
+ else if (stderr.includes("Connection timed out") ||
56
+ stderr.includes("Could not resolve host")) {
57
+ suggestion =
58
+ "\nšŸ’” Next Step: Check your network connection or the remote host's availability.";
59
+ }
60
+ else if (stderr.includes("ansible-playbook: command not found")) {
61
+ suggestion =
62
+ "\nšŸ’” Next Step: Install Ansible: `brew install ansible` (on macOS).";
63
+ }
64
+ else if (stderr.includes("docker-compose: command not found")) {
65
+ suggestion =
66
+ "\nšŸ’” Next Step: Install Docker and ensure docker-compose is in your PATH.";
67
+ }
68
+ else if (stderr.includes("port is already allocated")) {
69
+ suggestion =
70
+ "\nšŸ’” Next Step: Check for port conflicts (8626, 3000, 8080) and stop competing services.";
71
+ }
72
+ console.error(chalk_1.default.red(`\nāŒ ${context} failed:`), error.message);
73
+ if (suggestion)
74
+ console.log(chalk_1.default.yellow(suggestion));
75
+ process.exit(1);
76
+ }
77
+ // Hash token using PBKDF2 (built-in, no bcrypt dependency)
78
+ async function hashToken(token) {
79
+ return new Promise((resolve, reject) => {
80
+ // Use a fixed salt prefix for deterministic validation
81
+ const salt = crypto_1.default
82
+ .createHash("sha256")
83
+ .update("cybermem-salt-v1")
84
+ .digest("hex")
85
+ .slice(0, 16);
86
+ crypto_1.default.pbkdf2(token, salt, 100000, 64, "sha512", (err, key) => {
87
+ if (err)
88
+ reject(err);
89
+ else
90
+ resolve(key.toString("hex"));
91
+ });
92
+ });
93
+ }
94
+ async function install(options) {
95
+ // Determine target from flags
96
+ let target = "local";
97
+ if (options.rpi)
98
+ target = "rpi";
99
+ if (options.vps)
100
+ target = "vps";
101
+ const isStaging = !!options.staging;
102
+ const envType = isStaging ? "staging" : "prod";
103
+ const useTailscale = !!options.remoteAccess;
104
+ const networkType = target === "local" ? "" : useTailscale ? "ts" : "lan";
105
+ const envLabel = [target === "local" ? "localhost" : target, networkType, envType].filter(Boolean).join("-");
106
+ console.log(chalk_1.default.blue(`Initializing CyberMem (${envLabel})...`));
107
+ try {
108
+ // Resolve Template Directory (Support both Dev and Prod)
109
+ let templateDir = path_1.default.resolve(__dirname, "../../templates");
110
+ if (!fs_1.default.existsSync(templateDir)) {
111
+ templateDir = path_1.default.resolve(__dirname, "../../../templates");
112
+ }
113
+ if (!fs_1.default.existsSync(templateDir)) {
114
+ templateDir = path_1.default.resolve(process.cwd(), "packages/cli/templates");
115
+ }
116
+ if (!fs_1.default.existsSync(templateDir)) {
117
+ // Fallback for different build structures
118
+ templateDir = path_1.default.resolve(__dirname, "../templates");
119
+ }
120
+ if (!fs_1.default.existsSync(templateDir)) {
121
+ throw new Error(`Templates not found at ${templateDir}. Please ensure package is built correctly.`);
122
+ }
123
+ // Generate secure token for ALL deployment types
124
+ // Format: sk- + 32 chars (16 bytes hex)
125
+ const accessToken = `sk-${crypto_1.default.randomBytes(16).toString("hex")}`;
126
+ const tokenHash = await hashToken(accessToken); // PBKDF2 hash
127
+ const tokenId = crypto_1.default.randomBytes(8).toString("hex");
128
+ const tokenName = isStaging ? "staging-verifier" : "admin-cli";
129
+ if (target === "local") {
130
+ const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
131
+ if (!fs_1.default.existsSync(composeFile)) {
132
+ console.error(chalk_1.default.red(`Internal Error: Template not found at ${composeFile}`));
133
+ process.exit(1);
134
+ }
135
+ // Home Directory Config
136
+ const homeDir = os_1.default.homedir();
137
+ const configDir = path_1.default.join(homeDir, ".cybermem");
138
+ const envFile = path_1.default.join(configDir, ".env");
139
+ const dataDir = path_1.default.join(configDir, isStaging ? "data-staging" : "data");
140
+ // 1. Ensure ~/.cybermem exists
141
+ if (!fs_1.default.existsSync(configDir)) {
142
+ fs_1.default.mkdirSync(configDir, { recursive: true });
143
+ }
144
+ if (!fs_1.default.existsSync(dataDir)) {
145
+ fs_1.default.mkdirSync(dataDir, { recursive: true });
146
+ }
147
+ // 2. Local Mode
148
+ if (!fs_1.default.existsSync(envFile)) {
149
+ console.log(chalk_1.default.yellow(`Initializing local configuration in ${configDir}...`));
150
+ const templateEnv = path_1.default.join(templateDir, "envs/local.env");
151
+ const envContent = fs_1.default.readFileSync(templateEnv, "utf-8");
152
+ fs_1.default.writeFileSync(envFile, envContent);
153
+ console.log(chalk_1.default.green(`Created .env at ${envFile}`));
154
+ }
155
+ const dbPath = path_1.default.join(dataDir, "openmemory.sqlite");
156
+ const secretsDir = path_1.default.join(configDir, "secrets");
157
+ const secretPath = path_1.default.join(secretsDir, "om_api_key");
158
+ let localAccessToken = accessToken;
159
+ // 1.5 Load existing local secret if present (SSoT)
160
+ if (fs_1.default.existsSync(secretPath)) {
161
+ localAccessToken = fs_1.default.readFileSync(secretPath, "utf-8").trim();
162
+ console.log(chalk_1.default.gray(`Loaded existing SSoT token from ${secretPath}`));
163
+ }
164
+ // Initialize access token and store hash in SQLite (BEFORE starting containers to avoid race/lock)
165
+ console.log(chalk_1.default.blue("Initializing local access token..."));
166
+ // Check if token key already exists (idempotency)
167
+ try {
168
+ const sqlite3 = await Promise.resolve().then(() => __importStar(require("sqlite3")));
169
+ const db = new sqlite3.default.Database(dbPath);
170
+ // Promisify db.run and db.get for cleaner logic
171
+ const run = (sql, params = []) => new Promise((resolve, reject) => {
172
+ db.run(sql, params, (err) => (err ? reject(err) : resolve()));
173
+ });
174
+ const get = (sql, params = []) => new Promise((resolve, reject) => {
175
+ db.get(sql, params, (err, row) => err ? reject(err) : resolve(row));
176
+ });
177
+ // Check if table exists and has correct schema
178
+ const tableInfo = await new Promise((resolve, reject) => {
179
+ db.all("PRAGMA table_info(access_keys)", (err, rows) => {
180
+ if (err)
181
+ reject(err);
182
+ else
183
+ resolve(rows);
184
+ });
185
+ });
186
+ const hasIdColumn = tableInfo.some((col) => col.name === "id");
187
+ if (tableInfo.length > 0 && !hasIdColumn) {
188
+ console.warn(chalk_1.default.yellow("Detected malformed access_keys table. Recreating..."));
189
+ await run("DROP TABLE access_keys");
190
+ }
191
+ // Create table if not exists with hash matching current localAccessToken
192
+ const currentHash = await hashToken(localAccessToken);
193
+ await run(`CREATE TABLE IF NOT EXISTS access_keys (
194
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
195
+ key_hash TEXT NOT NULL,
196
+ name TEXT DEFAULT 'default',
197
+ user_id TEXT DEFAULT 'default',
198
+ created_at TEXT DEFAULT (datetime('now')),
199
+ last_used_at TEXT,
200
+ is_active INTEGER DEFAULT 1
201
+ );`);
202
+ const row = await get("SELECT COUNT(*) as count FROM access_keys WHERE is_active = 1");
203
+ if (row && row.count > 0) {
204
+ db.close();
205
+ console.log(chalk_1.default.gray("Access token configuration preserved."));
206
+ // proceed without showing token
207
+ }
208
+ else {
209
+ // Insert new key
210
+ await run("INSERT INTO access_keys (id, key_hash, name, user_id) VALUES (?, ?, ?, ?)", [tokenId, currentHash, tokenName, "admin"]);
211
+ db.close();
212
+ // Store token as Docker Secret (File)
213
+ if (!fs_1.default.existsSync(secretsDir))
214
+ fs_1.default.mkdirSync(secretsDir, { recursive: true });
215
+ fs_1.default.writeFileSync(secretPath, localAccessToken, {
216
+ encoding: "utf-8",
217
+ mode: 0o600,
218
+ });
219
+ }
220
+ }
221
+ catch (e) {
222
+ console.warn(chalk_1.default.yellow("Could not initialize access token: " + e.message));
223
+ }
224
+ console.log(chalk_1.default.blue("Starting CyberMem services in Local Mode..."));
225
+ try {
226
+ // Detect if we should use 'docker compose' (v2) or 'docker-compose' (v1)
227
+ let composeCmd = ["docker-compose"];
228
+ try {
229
+ await (0, execa_1.default)("docker", ["compose", "version"]);
230
+ composeCmd = ["docker", "compose"];
231
+ }
232
+ catch (e) {
233
+ // Fallback to v1 if v2 not found
234
+ }
235
+ await (0, execa_1.default)(composeCmd[0], [
236
+ ...composeCmd.slice(1),
237
+ "-f",
238
+ composeFile,
239
+ "--env-file",
240
+ envFile,
241
+ "--project-name",
242
+ "cybermem" + (isStaging ? "-staging" : ""),
243
+ "up",
244
+ "-d",
245
+ "--build",
246
+ "--remove-orphans",
247
+ ], {
248
+ stdio: "inherit",
249
+ env: {
250
+ ...process.env,
251
+ DATA_DIR: dataDir,
252
+ SECRETS_DIR: path_1.default.join(configDir, "secrets"), // Pass secrets dir context
253
+ CYBERMEM_ENV_PATH: envFile,
254
+ OM_API_KEY: "", // Legacy env var disabled
255
+ PROJECT_NAME: "cybermem" + (isStaging ? "-staging" : ""),
256
+ // Refined environment tagging
257
+ CYBERMEM_ENV: envType,
258
+ CYBERMEM_INSTANCE: target,
259
+ CYBERMEM_TAILSCALE: useTailscale ? "true" : "false",
260
+ // Port parameterization
261
+ TRAEFIK_PORT: isStaging ? "8625" : "8626",
262
+ DASHBOARD_PORT: isStaging ? "3001" : "3000",
263
+ },
264
+ });
265
+ }
266
+ catch (e) {
267
+ handleExecError(e, "Local deployment");
268
+ }
269
+ await printSuccessMessage(true, localAccessToken);
270
+ async function printSuccessMessage(showToken, token) {
271
+ console.log(chalk_1.default.green("\nšŸŽ‰ CyberMem Installed!"));
272
+ console.log("");
273
+ if (showToken && token) {
274
+ console.log(chalk_1.default.bold("⚔ Your Access Token (save this!):"));
275
+ console.log(chalk_1.default.cyan.bold(` ${token}`));
276
+ console.log("");
277
+ console.log(chalk_1.default.gray(" Use this token to connect MCP clients from other devices."));
278
+ console.log(chalk_1.default.gray(" You can regenerate it from Dashboard Settings."));
279
+ console.log("");
280
+ }
281
+ const entryPort = isStaging ? "8625" : "8626";
282
+ console.log(chalk_1.default.bold("Next Steps:"));
283
+ console.log(` 1. Open ${chalk_1.default.underline(`http://localhost:${entryPort}/client-setup`)} to connect your MCP clients`);
284
+ console.log(` 2. Local access is auto-authenticated (no token needed on localhost)`);
285
+ console.log("");
286
+ console.log(chalk_1.default.dim("Local mode is active: No auth required for connections from this device."));
287
+ }
288
+ }
289
+ else if (target === "rpi" || target === "vps") {
290
+ const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
291
+ // Use --host flag or prompt interactively
292
+ let sshHost;
293
+ if (options.host) {
294
+ sshHost = options.host;
295
+ if (!sshHost.includes("@")) {
296
+ throw new Error("--host format must be user@host (e.g. pi@raspberrypi.local)");
297
+ }
298
+ }
299
+ else {
300
+ const answers = await inquirer_1.default.prompt([
301
+ {
302
+ type: "input",
303
+ name: "host",
304
+ message: "Enter SSH Host (e.g. pi@raspberrypi.local):",
305
+ validate: (input) => input.includes("@") ? true : "Format must be user@host",
306
+ },
307
+ ]);
308
+ sshHost = answers.host;
309
+ }
310
+ console.log(chalk_1.default.blue(`Remote deploying to ${sshHost} via Ansible...`));
311
+ // 1. Check if ansible-playbook is available
312
+ try {
313
+ await (0, execa_1.default)("ansible-playbook", ["--version"]);
314
+ }
315
+ catch (e) {
316
+ throw new Error("ansible-playbook not found. Please install Ansible on your MacBook to use remote deployment.");
317
+ }
318
+ // 2. Parse sshHost (user@host)
319
+ const [sshUser, host] = sshHost.split("@");
320
+ if (!host) {
321
+ throw new Error("Invalid SSH Host format. Use user@host");
322
+ }
323
+ // 3. Build images locally from source
324
+ console.log(chalk_1.default.blue("Building Docker images locally..."));
325
+ let composeCmd = ["docker-compose"];
326
+ try {
327
+ await (0, execa_1.default)("docker", ["compose", "version"]);
328
+ composeCmd = ["docker", "compose"];
329
+ }
330
+ catch (e) {
331
+ // Fallback to v1 if v2 not found
332
+ }
333
+ try {
334
+ await (0, execa_1.default)(composeCmd[0], [
335
+ ...composeCmd.slice(1),
336
+ "-f",
337
+ composeFile,
338
+ "build",
339
+ ], {
340
+ stdio: "inherit",
341
+ env: {
342
+ ...process.env,
343
+ CYBERMEM_ENV: envType,
344
+ PROJECT_NAME: isStaging ? "cybermem-staging" : "cybermem",
345
+ TRAEFIK_PORT: isStaging ? "8625" : "8626",
346
+ },
347
+ });
348
+ }
349
+ catch (e) {
350
+ handleExecError(e, "Local image build");
351
+ }
352
+ // 4. Transfer built images to remote host
353
+ console.log(chalk_1.default.blue(`Transferring images to ${host}...`));
354
+ // Get list of built images from compose file (filter cybermem-* only)
355
+ try {
356
+ const { stdout: allImages } = await (0, execa_1.default)(composeCmd[0], [...composeCmd.slice(1), "-f", composeFile, "config", "--images"]);
357
+ const builtImages = allImages
358
+ .trim()
359
+ .split("\n")
360
+ .filter((img) => img.includes("cybermem-"));
361
+ if (builtImages.length === 0) {
362
+ throw new Error("No cybermem images found after build");
363
+ }
364
+ console.log(chalk_1.default.gray(` Transferring ${builtImages.length} images...`));
365
+ await (0, execa_1.default)("bash", [
366
+ "-c",
367
+ `docker save ${builtImages.join(" ")} | ssh -o StrictHostKeyChecking=no ${sshHost} docker load`,
368
+ ], { stdio: "inherit" });
369
+ console.log(chalk_1.default.green(" āœ… Images transferred"));
370
+ }
371
+ catch (e) {
372
+ handleExecError(e, "Image transfer");
373
+ }
374
+ // 5. Resolve Ansible Paths
375
+ const playbookPath = path_1.default.join(templateDir, "ansible/playbooks/deploy-cybermem.yml");
376
+ const ansibleDir = path_1.default.join(templateDir, "ansible");
377
+ if (!fs_1.default.existsSync(playbookPath)) {
378
+ throw new Error(`Ansible playbook not found at ${playbookPath}`);
379
+ }
380
+ // 6. Run Ansible Playbook (skip pull — images already loaded)
381
+ console.log(chalk_1.default.blue("Deploying via Ansible..."));
382
+ const inventory = `${host},`;
383
+ try {
384
+ await (0, execa_1.default)("ansible-playbook", [
385
+ "-i",
386
+ inventory,
387
+ "-u",
388
+ sshUser,
389
+ playbookPath,
390
+ "--extra-vars",
391
+ `ansible_ssh_extra_args='-o StrictHostKeyChecking=no'`,
392
+ "--extra-vars",
393
+ `skip_pull=true`,
394
+ "--extra-vars",
395
+ `auth_token_hash=${tokenHash}`,
396
+ "--extra-vars",
397
+ `auth_token_id=${tokenId}`,
398
+ "--extra-vars",
399
+ `auth_token_name=${tokenName}`,
400
+ "--extra-vars",
401
+ `auth_token_value=${accessToken}`,
402
+ "--extra-vars",
403
+ `cybermem_env=${envType}`,
404
+ "--extra-vars",
405
+ `TRAEFIK_PORT=${isStaging ? "8625" : "8626"}`,
406
+ "--extra-vars",
407
+ `CYBERMEM_TAILSCALE=${useTailscale}`,
408
+ "--extra-vars",
409
+ `PROJECT_NAME=${isStaging ? "cybermem-staging" : "cybermem"}`,
410
+ ], {
411
+ stdio: "inherit",
412
+ cwd: ansibleDir,
413
+ });
414
+ }
415
+ catch (e) {
416
+ handleExecError(e, "Remote deployment");
417
+ }
418
+ const entryPort = isStaging ? "8625" : "8626";
419
+ console.log(chalk_1.default.green("\nāœ… Remote deployment successful via Ansible!"));
420
+ console.log(chalk_1.default.bold("⚔ Your Initial Access Token:"));
421
+ console.log(chalk_1.default.cyan.bold(` ${accessToken}`));
422
+ console.log("");
423
+ console.log(chalk_1.default.bold(`Dashboard: http://${host}:${entryPort}`));
424
+ }
425
+ }
426
+ catch (error) {
427
+ console.error(chalk_1.default.red("Deployment failed:"), error);
428
+ process.exit(1);
429
+ }
430
+ }
@@ -7,7 +7,8 @@ exports.reset = reset;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const child_process_1 = require("child_process");
9
9
  const ora_1 = __importDefault(require("ora"));
10
- async function reset() {
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
+ async function reset(options) {
11
12
  // āš ļø PRODUCTION PROTECTION - Never wipe RPi database
12
13
  const cyberMemEnv = process.env.CYBERMEM_ENV || "";
13
14
  const hostname = process.env.HOSTNAME || "";
@@ -17,9 +18,24 @@ async function reset() {
17
18
  console.error(chalk_1.default.gray(" Use npx @cybermem/cli backup first, then manually clear if needed."));
18
19
  process.exit(1);
19
20
  }
21
+ // Confirmation Prompt
22
+ if (!options.force) {
23
+ const { confirm } = await inquirer_1.default.prompt([
24
+ {
25
+ type: "confirm",
26
+ name: "confirm",
27
+ message: "āš ļø WARNING: This will corrupt all memories. Are you sure?",
28
+ default: false,
29
+ },
30
+ ]);
31
+ if (!confirm) {
32
+ console.log(chalk_1.default.gray("Reset cancelled."));
33
+ return;
34
+ }
35
+ }
20
36
  const spinner = (0, ora_1.default)("Resetting CyberMem database...").start();
21
37
  try {
22
- const containerName = "cybermem-mcp";
38
+ const containerName = process.env.MCP_CONTAINER_NAME || "cybermem-mcp-server-1";
23
39
  // Check if container exists
24
40
  try {
25
41
  (0, child_process_1.execSync)(`docker inspect ${containerName}`, { stdio: "pipe" });
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.uninstall = uninstall;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const execa_1 = __importDefault(require("execa"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const path_1 = __importDefault(require("path"));
13
+ // Helper to handle and suggest fixes for common errors
14
+ function handleExecError(error, context) {
15
+ const stderr = error.stderr || "";
16
+ let suggestion = "";
17
+ if (stderr.includes("Permission denied") || stderr.includes("publickey")) {
18
+ suggestion =
19
+ "\nšŸ’” Next Step: Ensure your SSH keys are added to the remote host: `ssh-copy-id user@host`";
20
+ }
21
+ else if (stderr.includes("docker-compose: command not found")) {
22
+ suggestion =
23
+ "\nšŸ’” Next Step: Install Docker and ensure docker-compose is in your PATH.";
24
+ }
25
+ console.error(chalk_1.default.red(`\nāŒ ${context} failed:`), error.message);
26
+ if (suggestion)
27
+ console.log(chalk_1.default.yellow(suggestion));
28
+ // process.exit(1); // Don't always exit on uninstall failure so we can try data wipe?
29
+ }
30
+ async function uninstall(options) {
31
+ let target = "local";
32
+ if (options.rpi)
33
+ target = "rpi";
34
+ if (options.vps)
35
+ target = "vps";
36
+ console.log(chalk_1.default.blue(`Uninstalling CyberMem (${target})...`));
37
+ try {
38
+ if (target === "local") {
39
+ const answers = await inquirer_1.default.prompt([
40
+ {
41
+ type: "confirm",
42
+ name: "confirm",
43
+ message: chalk_1.default.red("Are you sure you want to uninstall CyberMem? This will stop all services."),
44
+ default: false,
45
+ },
46
+ {
47
+ type: "confirm",
48
+ name: "wipeData",
49
+ message: chalk_1.default.yellow("Do you also want to wipe all data (~/.cybermem)?"),
50
+ default: false,
51
+ when: (a) => a.confirm,
52
+ },
53
+ ]);
54
+ if (!answers.confirm) {
55
+ console.log(chalk_1.default.gray("Aborted."));
56
+ return;
57
+ }
58
+ // Resolve Template Directory
59
+ let templateDir = path_1.default.resolve(__dirname, "../../templates");
60
+ if (!fs_1.default.existsSync(templateDir)) {
61
+ templateDir = path_1.default.resolve(__dirname, "../../../templates");
62
+ }
63
+ if (!fs_1.default.existsSync(templateDir)) {
64
+ templateDir = path_1.default.resolve(process.cwd(), "packages/cli/templates");
65
+ }
66
+ const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
67
+ const homeDir = os_1.default.homedir();
68
+ const configDir = path_1.default.join(homeDir, ".cybermem");
69
+ const envFile = path_1.default.join(configDir, ".env");
70
+ console.log(chalk_1.default.blue("Stopping services..."));
71
+ try {
72
+ await (0, execa_1.default)("docker-compose", [
73
+ "-f",
74
+ composeFile,
75
+ "--env-file",
76
+ envFile,
77
+ "--project-name",
78
+ "cybermem",
79
+ "down",
80
+ ], {
81
+ stdio: "inherit",
82
+ env: {
83
+ ...process.env,
84
+ DATA_DIR: path_1.default.join(configDir, "data"),
85
+ CYBERMEM_ENV_PATH: envFile,
86
+ OM_API_KEY: "",
87
+ },
88
+ });
89
+ }
90
+ catch (e) {
91
+ handleExecError(e, "Local uninstall");
92
+ }
93
+ if (answers.wipeData) {
94
+ console.log(chalk_1.default.yellow(`Wiping configuration at ${configDir}...`));
95
+ fs_1.default.rmSync(configDir, { recursive: true, force: true });
96
+ }
97
+ console.log(chalk_1.default.green("āœ… CyberMem uninstalled successfully."));
98
+ }
99
+ else if (target === "rpi" || target === "vps") {
100
+ let sshHost = options.host;
101
+ if (!sshHost) {
102
+ const answers = await inquirer_1.default.prompt([
103
+ {
104
+ type: "input",
105
+ name: "host",
106
+ message: "Enter SSH Host (e.g. pi@raspberrypi.local):",
107
+ validate: (input) => input.includes("@") ? true : "Format must be user@host",
108
+ },
109
+ ]);
110
+ sshHost = answers.host;
111
+ }
112
+ console.log(chalk_1.default.blue(`Uninstalling remote host ${sshHost} via Ansible...`));
113
+ // Resolve Ansible Paths
114
+ let templateDir = path_1.default.resolve(__dirname, "../../templates");
115
+ if (!fs_1.default.existsSync(templateDir)) {
116
+ templateDir = path_1.default.resolve(__dirname, "../../../templates");
117
+ }
118
+ if (!fs_1.default.existsSync(templateDir)) {
119
+ templateDir = path_1.default.resolve(process.cwd(), "packages/cli/templates");
120
+ }
121
+ const playbookPath = path_1.default.join(templateDir, "ansible/playbooks/deploy-cybermem.yml");
122
+ const ansibleDir = path_1.default.join(templateDir, "ansible");
123
+ const host = sshHost.split("@")[1];
124
+ const sshUser = sshHost.split("@")[0];
125
+ // We use state=absent via extra vars to trigger a teardown in the playbook
126
+ // (Assuming the playbook supports it or we add support)
127
+ // For now, let's use a simple remote command until we harden the playbook to support uninstall
128
+ const remoteCmd = `
129
+ cd ~/.cybermem && docker-compose down
130
+ rm -rf ~/.cybermem/docker-compose.yml
131
+ `;
132
+ try {
133
+ await (0, execa_1.default)("ssh", [sshHost, remoteCmd], { stdio: "inherit" });
134
+ }
135
+ catch (e) {
136
+ handleExecError(e, "Remote uninstall");
137
+ }
138
+ console.log(chalk_1.default.green("āœ… Remote uninstallation complete!"));
139
+ }
140
+ }
141
+ catch (error) {
142
+ console.error(chalk_1.default.red("Uninstall failed:"), error);
143
+ process.exit(1);
144
+ }
145
+ }