@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
@@ -1,239 +0,0 @@
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.deploy = deploy;
7
- const chalk_1 = __importDefault(require("chalk"));
8
- const crypto_1 = __importDefault(require("crypto"));
9
- const execa_1 = __importDefault(require("execa"));
10
- const fs_1 = __importDefault(require("fs"));
11
- const inquirer_1 = __importDefault(require("inquirer"));
12
- const os_1 = __importDefault(require("os"));
13
- const path_1 = __importDefault(require("path"));
14
- async function deploy(options) {
15
- // Determine target from flags
16
- let target = 'local';
17
- if (options.rpi)
18
- target = 'rpi';
19
- if (options.vps)
20
- target = 'vps';
21
- const useTailscale = options.remoteAccess;
22
- console.log(chalk_1.default.blue(`Deploying CyberMem (${target})...`));
23
- try {
24
- // Resolve Template Directory (Support both Dev and Prod)
25
- let templateDir = path_1.default.resolve(__dirname, '../../templates');
26
- if (!fs_1.default.existsSync(templateDir)) {
27
- templateDir = path_1.default.resolve(__dirname, '../../../templates');
28
- }
29
- if (!fs_1.default.existsSync(templateDir)) {
30
- templateDir = path_1.default.resolve(process.cwd(), 'packages/cli/templates');
31
- }
32
- if (!fs_1.default.existsSync(templateDir)) {
33
- // Fallback for different build structures
34
- templateDir = path_1.default.resolve(__dirname, '../templates');
35
- }
36
- if (!fs_1.default.existsSync(templateDir)) {
37
- throw new Error(`Templates not found at ${templateDir}. Please ensure package is built correctly.`);
38
- }
39
- if (target === 'local') {
40
- const composeFile = path_1.default.join(templateDir, 'docker-compose.yml');
41
- const internalEnvExample = path_1.default.join(templateDir, 'envs/local.example');
42
- if (!fs_1.default.existsSync(composeFile)) {
43
- console.error(chalk_1.default.red(`Internal Error: Template not found at ${composeFile}`));
44
- process.exit(1);
45
- }
46
- // Home Directory Config
47
- const homeDir = os_1.default.homedir();
48
- const configDir = path_1.default.join(homeDir, '.cybermem');
49
- const envFile = path_1.default.join(configDir, '.env');
50
- const dataDir = path_1.default.join(configDir, 'data');
51
- // 1. Ensure ~/.cybermem exists
52
- if (!fs_1.default.existsSync(configDir)) {
53
- fs_1.default.mkdirSync(configDir, { recursive: true });
54
- fs_1.default.mkdirSync(dataDir, { recursive: true });
55
- }
56
- // 2. Local Mode: Simplified setup without mandatory API key
57
- if (!fs_1.default.existsSync(envFile)) {
58
- console.log(chalk_1.default.yellow(`Initializing local configuration in ${configDir}...`));
59
- const envContent = fs_1.default.readFileSync(internalEnvExample, 'utf-8');
60
- fs_1.default.writeFileSync(envFile, envContent);
61
- console.log(chalk_1.default.green(`Created .env at ${envFile}`));
62
- }
63
- console.log(chalk_1.default.blue('Starting CyberMem services in Local Mode...'));
64
- await (0, execa_1.default)('docker-compose', [
65
- '-f', composeFile,
66
- '--env-file', envFile,
67
- '--project-name', 'cybermem',
68
- 'up', '-d', '--remove-orphans'
69
- ], {
70
- stdio: 'inherit',
71
- env: {
72
- ...process.env,
73
- DATA_DIR: dataDir,
74
- CYBERMEM_ENV_PATH: envFile,
75
- OM_API_KEY: ''
76
- }
77
- });
78
- console.log(chalk_1.default.green('\n🎉 CyberMem Installed!'));
79
- console.log('');
80
- console.log(chalk_1.default.bold('Next Steps:'));
81
- console.log(` 1. Open ${chalk_1.default.underline('http://localhost:3000/client-connect')} to connect your MCP clients`);
82
- console.log(` 2. Default password: ${chalk_1.default.bold('admin')} (you'll be prompted to change it)`);
83
- console.log('');
84
- console.log(chalk_1.default.dim('Local mode is active: No API key required for connections from this laptop.'));
85
- }
86
- else if (target === 'rpi') {
87
- const composeFile = path_1.default.join(templateDir, 'docker-compose.yml');
88
- const internalEnvExample = path_1.default.join(templateDir, 'envs/rpi.example');
89
- const answers = await inquirer_1.default.prompt([
90
- {
91
- type: 'input',
92
- name: 'host',
93
- message: 'Enter SSH Host (e.g. pi@raspberrypi.local):',
94
- validate: (input) => input.includes('@') ? true : 'Format must be user@host'
95
- }
96
- ]);
97
- const sshHost = answers.host;
98
- console.log(chalk_1.default.blue(`Remote deploying to ${sshHost}...`));
99
- // 1. Create remote directory
100
- await (0, execa_1.default)('ssh', [sshHost, 'mkdir -p ~/.cybermem/data']);
101
- // 1.5 Check and fix Docker architecture (64-bit kernel with 32-bit Docker)
102
- console.log(chalk_1.default.blue('Checking Docker architecture...'));
103
- try {
104
- const { stdout: kernelArch } = await (0, execa_1.default)('ssh', [sshHost, 'uname -m']);
105
- const { stdout: dockerArch } = await (0, execa_1.default)('ssh', [sshHost, 'docker version --format "{{.Server.Arch}}" 2>/dev/null || echo "unknown"']);
106
- if (kernelArch.trim() === 'aarch64' && dockerArch.trim() !== 'arm64') {
107
- console.log(chalk_1.default.yellow(`⚠️ Docker is ${dockerArch.trim()}, kernel is aarch64. Installing arm64 Docker...`));
108
- const installCmd = `
109
- sudo systemctl stop docker docker.socket 2>/dev/null || true
110
- curl -fsSL https://download.docker.com/linux/static/stable/aarch64/docker-27.5.1.tgz -o /tmp/docker.tgz
111
- sudo tar -xzf /tmp/docker.tgz -C /usr/local/bin --strip-components=1
112
- sudo /usr/local/bin/dockerd &
113
- sleep 5
114
- docker version --format "{{.Server.Arch}}"
115
- `;
116
- const { stdout } = await (0, execa_1.default)('ssh', [sshHost, installCmd], { shell: true });
117
- if (stdout.includes('arm64')) {
118
- console.log(chalk_1.default.green('✅ Docker arm64 installed successfully'));
119
- }
120
- else {
121
- console.log(chalk_1.default.yellow('⚠️ Docker arm64 install may need manual verification'));
122
- }
123
- }
124
- else if (dockerArch.trim() === 'arm64') {
125
- console.log(chalk_1.default.green(`✅ Docker is already arm64`));
126
- }
127
- else {
128
- console.log(chalk_1.default.gray(`Docker arch: ${dockerArch.trim()}, kernel: ${kernelArch.trim()}`));
129
- }
130
- }
131
- catch (e) {
132
- console.log(chalk_1.default.yellow(`⚠️ Docker arch check skipped: ${e.message}`));
133
- }
134
- // 2. Initial Env Setup (if missing)
135
- try {
136
- await (0, execa_1.default)('ssh', [sshHost, '[ -f ~/.cybermem/.env ]']);
137
- console.log(chalk_1.default.gray('Remote .env exists, skipping generation.'));
138
- }
139
- catch (e) {
140
- console.log(chalk_1.default.yellow('Generating remote .env...'));
141
- let envContent = fs_1.default.readFileSync(internalEnvExample, 'utf-8');
142
- const newKey = `sk-${crypto_1.default.randomBytes(16).toString('hex')}`;
143
- // Replace OM_API_KEY with generated key
144
- if (envContent.includes('OM_API_KEY=')) {
145
- envContent = envContent.replace(/OM_API_KEY=.*/, `OM_API_KEY=${newKey}`);
146
- }
147
- else {
148
- envContent += `\nOM_API_KEY=${newKey}\n`;
149
- }
150
- const tempEnv = path_1.default.join(os_1.default.tmpdir(), 'cybermem-rpi.env');
151
- fs_1.default.writeFileSync(tempEnv, envContent);
152
- await (0, execa_1.default)('scp', [tempEnv, `${sshHost}:~/.cybermem/.env`]);
153
- fs_1.default.unlinkSync(tempEnv);
154
- }
155
- // 3. Copy Docker Compose
156
- console.log(chalk_1.default.blue('Uploading templates...'));
157
- await (0, execa_1.default)('scp', [composeFile, `${sshHost}:~/.cybermem/docker-compose.yml`]);
158
- // 4. Run Docker Compose Remotely
159
- console.log(chalk_1.default.blue('Starting services on RPi...'));
160
- // DOCKER_DEFAULT_PLATFORM=linux/arm64 forces arm64 images on RPi with 64-bit kernel but 32-bit Docker
161
- const remoteCmd = `
162
- export CYBERMEM_ENV_PATH=~/.cybermem/.env
163
- export DATA_DIR=~/.cybermem/data
164
- export DOCKER_DEFAULT_PLATFORM=linux/arm64
165
- docker-compose -f ~/.cybermem/docker-compose.yml up -d --remove-orphans
166
- `;
167
- await (0, execa_1.default)('ssh', [sshHost, remoteCmd], { stdio: 'inherit' });
168
- console.log(chalk_1.default.green('\n✅ RPi deployment successful!'));
169
- const hostIp = sshHost.split('@')[1];
170
- console.log(chalk_1.default.bold('Access Points (LAN):'));
171
- console.log(` - Dashboard: ${chalk_1.default.underline(`http://${hostIp}:3000`)} (admin/admin)`);
172
- console.log(` - OpenMemory: ${chalk_1.default.underline(`http://${hostIp}:8080`)}`);
173
- // Tailscale Funnel setup
174
- if (useTailscale) {
175
- console.log(chalk_1.default.blue('\n🔗 Setting up Remote Access (Tailscale Funnel)...'));
176
- try {
177
- try {
178
- await (0, execa_1.default)('ssh', [sshHost, 'which tailscale']);
179
- }
180
- catch (e) {
181
- console.log(chalk_1.default.yellow(' Tailscale not found. Installing...'));
182
- await (0, execa_1.default)('ssh', [sshHost, 'curl -fsSL https://tailscale.com/install.sh | sh'], { stdio: 'inherit' });
183
- }
184
- console.log(chalk_1.default.blue(' Ensuring Tailscale is up...'));
185
- try {
186
- await (0, execa_1.default)('ssh', [sshHost, 'tailscale status']);
187
- }
188
- catch (e) {
189
- console.log(chalk_1.default.yellow(' ⚠️ Tailscale authentication required. Please follow the prompts:'));
190
- await (0, execa_1.default)('ssh', [sshHost, 'sudo tailscale up'], { stdio: 'inherit' });
191
- }
192
- console.log(chalk_1.default.blue(' Configuring HTTPS Funnel (requires sudo access)...'));
193
- console.log(chalk_1.default.gray(' You may be prompted for your RPi password.'));
194
- await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale serve reset'], { stdio: 'inherit' }).catch(() => { });
195
- await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale serve --bg --set-path /cybermem http://127.0.0.1:8626'], { stdio: 'inherit' });
196
- await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale serve --bg http://127.0.0.1:3000'], { stdio: 'inherit' });
197
- await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale funnel --bg 443'], { stdio: 'inherit' });
198
- const { stdout } = await (0, execa_1.default)('ssh', [sshHost, "tailscale status --json | jq -r '.Self.DNSName' | sed 's/\\.$//'"]);
199
- const dnsName = stdout.trim();
200
- console.log(chalk_1.default.green('\n🌐 Remote Access Active (HTTPS):'));
201
- console.log(` - Dashboard: ${chalk_1.default.underline(`https://${dnsName}/`)}`);
202
- console.log(` - MCP API: ${chalk_1.default.underline(`https://${dnsName}/cybermem/mcp`)}`);
203
- }
204
- catch (e) {
205
- console.log(chalk_1.default.red('\n❌ Remote Access setup failed:'));
206
- console.error(e);
207
- console.log(chalk_1.default.gray('Manual setup: curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up'));
208
- }
209
- }
210
- else {
211
- console.log(chalk_1.default.gray('\n💡 For remote access, re-run with: npx @cybermem/cli --rpi --remote-access'));
212
- }
213
- }
214
- else if (target === 'vps') {
215
- console.log(chalk_1.default.yellow('VPS deployment is similar to RPi.'));
216
- console.log(chalk_1.default.blue('\n📋 VPS Deployment Steps:'));
217
- console.log('1. Run: npx @cybermem/cli --rpi pi@your-vps-ip');
218
- console.log('2. For HTTPS, choose one of:');
219
- console.log(chalk_1.default.gray(' a) Tailscale Funnel: --remote-access flag'));
220
- console.log(chalk_1.default.gray(' b) Caddy (recommended for public VPS):'));
221
- console.log(chalk_1.default.gray(' - Install Caddy: sudo apt install caddy'));
222
- console.log(chalk_1.default.gray(' - Configure /etc/caddy/Caddyfile:'));
223
- console.log(chalk_1.default.cyan(`
224
- cybermem.yourdomain.com {
225
- reverse_proxy localhost:3000
226
- }
227
- api.cybermem.yourdomain.com {
228
- reverse_proxy localhost:8080
229
- }
230
- `));
231
- console.log(chalk_1.default.gray(' - Restart: sudo systemctl restart caddy'));
232
- console.log(chalk_1.default.green('\n📚 Full docs: https://docs.cybermem.dev#https'));
233
- }
234
- }
235
- catch (error) {
236
- console.error(chalk_1.default.red('Deployment failed:'), error);
237
- process.exit(1);
238
- }
239
- }
@@ -1,362 +0,0 @@
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.init = init;
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
- // Hash token using PBKDF2 (built-in, no bcrypt dependency)
48
- async function hashToken(token) {
49
- return new Promise((resolve, reject) => {
50
- // Use a fixed salt prefix for deterministic validation
51
- const salt = crypto_1.default
52
- .createHash("sha256")
53
- .update("cybermem-salt-v1")
54
- .digest("hex")
55
- .slice(0, 16);
56
- crypto_1.default.pbkdf2(token, salt, 100000, 64, "sha512", (err, key) => {
57
- if (err)
58
- reject(err);
59
- else
60
- resolve(key.toString("hex"));
61
- });
62
- });
63
- }
64
- async function init(options) {
65
- // Determine target from flags
66
- let target = "local";
67
- if (options.rpi)
68
- target = "rpi";
69
- if (options.vps)
70
- target = "vps";
71
- const useTailscale = options.remoteAccess;
72
- console.log(chalk_1.default.blue(`Initializing CyberMem (${target})...`));
73
- try {
74
- // Resolve Template Directory (Support both Dev and Prod)
75
- let templateDir = path_1.default.resolve(__dirname, "../../templates");
76
- if (!fs_1.default.existsSync(templateDir)) {
77
- templateDir = path_1.default.resolve(__dirname, "../../../templates");
78
- }
79
- if (!fs_1.default.existsSync(templateDir)) {
80
- templateDir = path_1.default.resolve(process.cwd(), "packages/cli/templates");
81
- }
82
- if (!fs_1.default.existsSync(templateDir)) {
83
- // Fallback for different build structures
84
- templateDir = path_1.default.resolve(__dirname, "../templates");
85
- }
86
- if (!fs_1.default.existsSync(templateDir)) {
87
- throw new Error(`Templates not found at ${templateDir}. Please ensure package is built correctly.`);
88
- }
89
- if (target === "local") {
90
- const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
91
- if (!fs_1.default.existsSync(composeFile)) {
92
- console.error(chalk_1.default.red(`Internal Error: Template not found at ${composeFile}`));
93
- process.exit(1);
94
- }
95
- // Home Directory Config
96
- const homeDir = os_1.default.homedir();
97
- const configDir = path_1.default.join(homeDir, ".cybermem");
98
- const envFile = path_1.default.join(configDir, ".env");
99
- const dataDir = path_1.default.join(configDir, "data");
100
- // 1. Ensure ~/.cybermem exists
101
- if (!fs_1.default.existsSync(configDir)) {
102
- fs_1.default.mkdirSync(configDir, { recursive: true });
103
- fs_1.default.mkdirSync(dataDir, { recursive: true });
104
- }
105
- // 2. Local Mode
106
- if (!fs_1.default.existsSync(envFile)) {
107
- console.log(chalk_1.default.yellow(`Initializing local configuration in ${configDir}...`));
108
- const templateEnv = path_1.default.join(templateDir, "envs/local.env");
109
- const envContent = fs_1.default.readFileSync(templateEnv, "utf-8");
110
- fs_1.default.writeFileSync(envFile, envContent);
111
- console.log(chalk_1.default.green(`Created .env at ${envFile}`));
112
- }
113
- console.log(chalk_1.default.blue("Starting CyberMem services in Local Mode..."));
114
- await (0, execa_1.default)("docker-compose", [
115
- "-f",
116
- composeFile,
117
- "--env-file",
118
- envFile,
119
- "--project-name",
120
- "cybermem",
121
- "up",
122
- "-d",
123
- "--remove-orphans",
124
- ], {
125
- stdio: "inherit",
126
- env: {
127
- ...process.env,
128
- DATA_DIR: dataDir,
129
- CYBERMEM_ENV_PATH: envFile,
130
- OM_API_KEY: "",
131
- },
132
- });
133
- // Generate access token and store hash in SQLite
134
- const accessToken = `sk-${crypto_1.default.randomBytes(24).toString("base64url")}`;
135
- const bcryptHash = await hashToken(accessToken);
136
- const dbPath = path_1.default.join(dataDir, "openmemory.sqlite");
137
- // Wait for SQLite DB to be created by MCP server
138
- console.log(chalk_1.default.blue("Initializing access token..."));
139
- await new Promise((resolve) => setTimeout(resolve, 3000));
140
- // Check if token already exists
141
- try {
142
- const sqlite3 = await Promise.resolve().then(() => __importStar(require("sqlite3")));
143
- const db = new sqlite3.default.Database(dbPath);
144
- // Create table if not exists (in case MCP hasn't run yet)
145
- db.run(`CREATE TABLE IF NOT EXISTS access_keys (
146
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
147
- key_hash TEXT NOT NULL,
148
- name TEXT DEFAULT 'default',
149
- user_id TEXT DEFAULT 'default',
150
- created_at TEXT DEFAULT (datetime('now')),
151
- last_used_at TEXT,
152
- is_active INTEGER DEFAULT 1
153
- );`);
154
- // Check for existing key
155
- db.get("SELECT COUNT(*) as count FROM access_keys WHERE is_active = 1", [], (err, row) => {
156
- if (err || (row && row.count > 0)) {
157
- db.close();
158
- // Token exists, don't regenerate
159
- console.log(chalk_1.default.gray("Access token already configured."));
160
- printSuccessMessage(false);
161
- return;
162
- }
163
- // Insert new key
164
- db.run("INSERT INTO access_keys (id, key_hash, name, user_id) VALUES (?, ?, ?, ?)", [
165
- crypto_1.default.randomBytes(8).toString("hex"),
166
- bcryptHash,
167
- "default",
168
- "default",
169
- ], (err) => {
170
- db.close();
171
- if (err) {
172
- console.warn(chalk_1.default.yellow("Could not store access token: " + err.message));
173
- printSuccessMessage(false);
174
- }
175
- else {
176
- printSuccessMessage(true, accessToken);
177
- }
178
- });
179
- });
180
- }
181
- catch (e) {
182
- console.warn(chalk_1.default.yellow("Could not initialize access token: " + e.message));
183
- console.log(chalk_1.default.gray("You can generate a token from the Dashboard Settings."));
184
- printSuccessMessage(false);
185
- }
186
- function printSuccessMessage(showToken, token) {
187
- console.log(chalk_1.default.green("\n🎉 CyberMem Installed!"));
188
- console.log("");
189
- if (showToken && token) {
190
- console.log(chalk_1.default.bold("⚡ Your Access Token (save this!):"));
191
- console.log(chalk_1.default.cyan.bold(` ${token}`));
192
- console.log("");
193
- console.log(chalk_1.default.gray(" Use this token to connect MCP clients from other devices."));
194
- console.log(chalk_1.default.gray(" You can regenerate it from Dashboard Settings."));
195
- console.log("");
196
- }
197
- console.log(chalk_1.default.bold("Next Steps:"));
198
- console.log(` 1. Open ${chalk_1.default.underline("http://localhost:3000/client-setup")} to connect your MCP clients`);
199
- console.log(` 2. Local access is auto-authenticated (no token needed on localhost)`);
200
- console.log("");
201
- console.log(chalk_1.default.dim("Local mode is active: No auth required for connections from this device."));
202
- }
203
- }
204
- else if (target === "rpi" || target === "vps") {
205
- const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
206
- const answers = await inquirer_1.default.prompt([
207
- {
208
- type: "input",
209
- name: "host",
210
- message: "Enter SSH Host (e.g. pi@raspberrypi.local):",
211
- validate: (input) => input.includes("@") ? true : "Format must be user@host",
212
- },
213
- ]);
214
- const sshHost = answers.host;
215
- console.log(chalk_1.default.blue(`Remote deploying to ${sshHost}...`));
216
- // 1. Create remote directory
217
- await (0, execa_1.default)("ssh", [sshHost, "mkdir -p ~/.cybermem/data"]);
218
- // 1.5 Check and fix Docker architecture (64-bit kernel with 32-bit Docker)
219
- console.log(chalk_1.default.blue("Checking Docker architecture..."));
220
- try {
221
- const { stdout: kernelArch } = await (0, execa_1.default)("ssh", [
222
- sshHost,
223
- "uname -m",
224
- ]);
225
- const { stdout: dockerArch } = await (0, execa_1.default)("ssh", [
226
- sshHost,
227
- 'docker version --format "{{.Server.Arch}}" 2>/dev/null || echo "unknown"',
228
- ]);
229
- if (kernelArch.trim() === "aarch64" && dockerArch.trim() !== "arm64") {
230
- console.log(chalk_1.default.yellow(`⚠️ Docker is ${dockerArch.trim()}, kernel is aarch64. Installing arm64 Docker...`));
231
- const installCmd = `
232
- sudo systemctl stop docker docker.socket 2>/dev/null || true
233
- curl -fsSL https://download.docker.com/linux/static/stable/aarch64/docker-27.5.1.tgz -o /tmp/docker.tgz
234
- sudo tar -xzf /tmp/docker.tgz -C /usr/local/bin --strip-components=1
235
- sudo /usr/local/bin/dockerd &
236
- sleep 5
237
- docker version --format "{{.Server.Arch}}"
238
- `;
239
- const { stdout } = await (0, execa_1.default)("ssh", [sshHost, installCmd], {
240
- shell: true,
241
- });
242
- if (stdout.includes("arm64")) {
243
- console.log(chalk_1.default.green("✅ Docker arm64 installed successfully"));
244
- }
245
- else {
246
- console.log(chalk_1.default.yellow("⚠️ Docker arm64 install may need manual verification"));
247
- }
248
- }
249
- else if (dockerArch.trim() === "arm64") {
250
- console.log(chalk_1.default.green(`✅ Docker is already arm64`));
251
- }
252
- else {
253
- console.log(chalk_1.default.gray(`Docker arch: ${dockerArch.trim()}, kernel: ${kernelArch.trim()}`));
254
- }
255
- }
256
- catch (e) {
257
- console.log(chalk_1.default.yellow(`⚠️ Docker arch check skipped: ${e.message}`));
258
- }
259
- // 2. Initial Env Setup (if missing)
260
- try {
261
- await (0, execa_1.default)("ssh", [sshHost, "[ -f ~/.cybermem/.env ]"]);
262
- console.log(chalk_1.default.gray("Remote .env exists, skipping generation."));
263
- }
264
- catch (e) {
265
- console.log(chalk_1.default.yellow("Generating remote configuration..."));
266
- let templateName = "rpi.env";
267
- if (target === "vps")
268
- templateName = "vps.env";
269
- else if (useTailscale)
270
- templateName = "rpi-tailscale.env";
271
- const templateEnv = path_1.default.join(templateDir, "envs", templateName);
272
- let envContent = fs_1.default.readFileSync(templateEnv, "utf-8");
273
- const newKey = `cm-${crypto_1.default.randomBytes(16).toString("hex")}`;
274
- // Replace OM_API_KEY with generated key
275
- if (envContent.includes("OM_API_KEY=")) {
276
- envContent = envContent.replace(/OM_API_KEY=.*/, `OM_API_KEY=${newKey}`);
277
- }
278
- const tempEnv = path_1.default.join(os_1.default.tmpdir(), "cybermem-remote.env");
279
- fs_1.default.writeFileSync(tempEnv, envContent);
280
- await (0, execa_1.default)("scp", [tempEnv, `${sshHost}:~/.cybermem/.env`]);
281
- fs_1.default.unlinkSync(tempEnv);
282
- console.log(chalk_1.default.green(`✅ Security configuration generated (${templateName}).`));
283
- }
284
- // 3. Copy Docker Compose
285
- console.log(chalk_1.default.blue("Uploading templates..."));
286
- await (0, execa_1.default)("scp", [
287
- composeFile,
288
- `${sshHost}:~/.cybermem/docker-compose.yml`,
289
- ]);
290
- // 4. Run Docker Compose Remotely
291
- console.log(chalk_1.default.blue("Starting services on RPi..."));
292
- // DOCKER_DEFAULT_PLATFORM=linux/arm64 forces arm64 images on RPi with 64-bit kernel but 32-bit Docker
293
- const remoteCmd = `
294
- export CYBERMEM_ENV_PATH=~/.cybermem/.env
295
- export DATA_DIR=~/.cybermem/data
296
- export DOCKER_DEFAULT_PLATFORM=linux/arm64
297
- docker-compose -f ~/.cybermem/docker-compose.yml up -d --remove-orphans
298
- `;
299
- await (0, execa_1.default)("ssh", [sshHost, remoteCmd], { stdio: "inherit" });
300
- console.log(chalk_1.default.green("\n✅ RPi deployment successful!"));
301
- const hostIp = sshHost.split("@")[1];
302
- console.log(chalk_1.default.bold("Access Points (LAN):"));
303
- console.log(` - Dashboard: ${chalk_1.default.underline(`http://${hostIp}:3000`)} (admin/admin)`);
304
- console.log(` - OpenMemory: ${chalk_1.default.underline(`http://${hostIp}:8080`)}`);
305
- // Tailscale Funnel setup
306
- if (useTailscale) {
307
- console.log(chalk_1.default.blue("\n🔗 Setting up Remote Access (Tailscale Funnel)..."));
308
- try {
309
- try {
310
- await (0, execa_1.default)("ssh", [sshHost, "which tailscale"]);
311
- }
312
- catch (e) {
313
- console.log(chalk_1.default.yellow(" Tailscale not found. Installing..."));
314
- await (0, execa_1.default)("ssh", [sshHost, "curl -fsSL https://tailscale.com/install.sh | sh"], { stdio: "inherit" });
315
- }
316
- console.log(chalk_1.default.blue(" Ensuring Tailscale is up..."));
317
- try {
318
- await (0, execa_1.default)("ssh", [sshHost, "tailscale status"]);
319
- }
320
- catch (e) {
321
- console.log(chalk_1.default.yellow(" ⚠️ Tailscale authentication required. Please follow the prompts:"));
322
- await (0, execa_1.default)("ssh", [sshHost, "sudo tailscale up"], {
323
- stdio: "inherit",
324
- });
325
- }
326
- console.log(chalk_1.default.blue(" Configuring HTTPS Funnel (requires sudo access)..."));
327
- console.log(chalk_1.default.gray(" You may be prompted for your RPi password."));
328
- await (0, execa_1.default)("ssh", ["-t", sshHost, "sudo tailscale serve reset"], {
329
- stdio: "inherit",
330
- }).catch(() => { });
331
- await (0, execa_1.default)("ssh", [
332
- "-t",
333
- sshHost,
334
- "sudo tailscale serve --bg --set-path /cybermem http://127.0.0.1:8626",
335
- ], { stdio: "inherit" });
336
- await (0, execa_1.default)("ssh", ["-t", sshHost, "sudo tailscale serve --bg http://127.0.0.1:3000"], { stdio: "inherit" });
337
- await (0, execa_1.default)("ssh", ["-t", sshHost, "sudo tailscale funnel --bg 443"], { stdio: "inherit" });
338
- const { stdout } = await (0, execa_1.default)("ssh", [
339
- sshHost,
340
- "tailscale status --json | jq -r '.Self.DNSName' | sed 's/\\.$//'",
341
- ]);
342
- const dnsName = stdout.trim();
343
- console.log(chalk_1.default.green("\n🌐 Remote Access Active (HTTPS):"));
344
- console.log(` - Dashboard: ${chalk_1.default.underline(`https://${dnsName}/`)}`);
345
- console.log(` - MCP API: ${chalk_1.default.underline(`https://${dnsName}/cybermem/mcp`)}`);
346
- }
347
- catch (e) {
348
- console.log(chalk_1.default.red("\n❌ Remote Access setup failed:"));
349
- console.error(e);
350
- console.log(chalk_1.default.gray("Manual setup: curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up"));
351
- }
352
- }
353
- else {
354
- console.log(chalk_1.default.gray("\n💡 For remote access, re-run with: npx @cybermem/cli --rpi --remote-access"));
355
- }
356
- }
357
- }
358
- catch (error) {
359
- console.error(chalk_1.default.red("Deployment failed:"), error);
360
- process.exit(1);
361
- }
362
- }