@cybermem/mcp 0.1.0 → 0.5.1

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 (68) hide show
  1. package/README.md +2 -46
  2. package/dist/commands/deploy.js +230 -0
  3. package/dist/commands/init.js +65 -0
  4. package/dist/index.js +198 -90
  5. package/dist/templates/ansible/inventory/hosts.ini +3 -0
  6. package/dist/templates/ansible/playbooks/deploy-cybermem.yml +71 -0
  7. package/dist/templates/ansible/playbooks/stop-cybermem.yml +17 -0
  8. package/dist/templates/charts/cybermem/Chart.yaml +6 -0
  9. package/dist/templates/charts/cybermem/templates/dashboard-deployment.yaml +29 -0
  10. package/dist/templates/charts/cybermem/templates/dashboard-service.yaml +20 -0
  11. package/dist/templates/charts/cybermem/templates/openmemory-deployment.yaml +40 -0
  12. package/dist/templates/charts/cybermem/templates/openmemory-pvc.yaml +10 -0
  13. package/dist/templates/charts/cybermem/templates/openmemory-service.yaml +13 -0
  14. package/dist/templates/charts/cybermem/values-vps.yaml +18 -0
  15. package/dist/templates/charts/cybermem/values.yaml +42 -0
  16. package/dist/templates/docker-compose.yml +219 -0
  17. package/dist/templates/envs/local.example +27 -0
  18. package/dist/templates/envs/rpi.example +27 -0
  19. package/dist/templates/envs/vps.example +25 -0
  20. package/dist/templates/monitoring/db_exporter/Dockerfile +19 -0
  21. package/dist/templates/monitoring/db_exporter/exporter.py +313 -0
  22. package/dist/templates/monitoring/db_exporter/requirements.txt +2 -0
  23. package/dist/templates/monitoring/grafana/dashboards/cybermem.json +1088 -0
  24. package/dist/templates/monitoring/grafana/provisioning/dashboards/default.yml +12 -0
  25. package/dist/templates/monitoring/grafana/provisioning/datasources/prometheus.yml +9 -0
  26. package/dist/templates/monitoring/log_exporter/Dockerfile +13 -0
  27. package/dist/templates/monitoring/log_exporter/exporter.py +274 -0
  28. package/dist/templates/monitoring/log_exporter/requirements.txt +1 -0
  29. package/dist/templates/monitoring/postgres_exporter/queries.yml +22 -0
  30. package/dist/templates/monitoring/prometheus/prometheus.yml +22 -0
  31. package/dist/templates/monitoring/traefik/traefik.yml +32 -0
  32. package/dist/templates/monitoring/vector/vector.toml/vector.yaml +77 -0
  33. package/dist/templates/monitoring/vector/vector.yaml +106 -0
  34. package/package.json +33 -10
  35. package/templates/ansible/inventory/hosts.ini +3 -0
  36. package/templates/ansible/playbooks/deploy-cybermem.yml +71 -0
  37. package/templates/ansible/playbooks/stop-cybermem.yml +17 -0
  38. package/templates/charts/cybermem/Chart.yaml +6 -0
  39. package/templates/charts/cybermem/templates/dashboard-deployment.yaml +29 -0
  40. package/templates/charts/cybermem/templates/dashboard-service.yaml +20 -0
  41. package/templates/charts/cybermem/templates/openmemory-deployment.yaml +40 -0
  42. package/templates/charts/cybermem/templates/openmemory-pvc.yaml +10 -0
  43. package/templates/charts/cybermem/templates/openmemory-service.yaml +13 -0
  44. package/templates/charts/cybermem/values-vps.yaml +18 -0
  45. package/templates/charts/cybermem/values.yaml +42 -0
  46. package/templates/docker-compose.yml +219 -0
  47. package/templates/envs/local.example +27 -0
  48. package/templates/envs/rpi.example +27 -0
  49. package/templates/envs/vps.example +25 -0
  50. package/templates/monitoring/db_exporter/Dockerfile +19 -0
  51. package/templates/monitoring/db_exporter/exporter.py +313 -0
  52. package/templates/monitoring/db_exporter/requirements.txt +2 -0
  53. package/templates/monitoring/grafana/dashboards/cybermem.json +1088 -0
  54. package/templates/monitoring/grafana/provisioning/dashboards/default.yml +12 -0
  55. package/templates/monitoring/grafana/provisioning/datasources/prometheus.yml +9 -0
  56. package/templates/monitoring/log_exporter/Dockerfile +13 -0
  57. package/templates/monitoring/log_exporter/exporter.py +274 -0
  58. package/templates/monitoring/log_exporter/requirements.txt +1 -0
  59. package/templates/monitoring/postgres_exporter/queries.yml +22 -0
  60. package/templates/monitoring/prometheus/prometheus.yml +22 -0
  61. package/templates/monitoring/traefik/traefik.yml +32 -0
  62. package/templates/monitoring/vector/vector.toml/vector.yaml +77 -0
  63. package/templates/monitoring/vector/vector.yaml +106 -0
  64. package/requirements.txt +0 -2
  65. package/server.py +0 -347
  66. package/src/index.ts +0 -114
  67. package/test_mcp.py +0 -111
  68. package/tsconfig.json +0 -14
package/README.md CHANGED
@@ -1,49 +1,5 @@
1
1
  # @cybermem/mcp
2
2
 
3
- Official TypeScript MCP Server for CyberMem.
3
+ Universal Long-Term Memory for AI Agents.
4
4
 
5
- ## Configuration
6
-
7
- ### Option A: Local (Standard)
8
- If you are running CyberMem locally on your machine, use `npx` to spawn the MCP server. It will bridge the connection to your local Docker instance.
9
-
10
- **Claude Desktop Config (`~/Library/Application Support/Claude/claude_desktop_config.json`):**
11
- ```json
12
- {
13
- "mcpServers": {
14
- "cybermem": {
15
- "command": "npx",
16
- "args": [
17
- "-y",
18
- "@cybermem/mcp"
19
- ],
20
- "env": {
21
- "CYBERMEM_URL": "http://localhost:8080/memory",
22
- "CYBERMEM_API_KEY": "your-api-key"
23
- }
24
- }
25
- }
26
- }
27
- ```
28
-
29
- ### Option B: Remote (RPi / Cloud)
30
- If you have deployed CyberMem to a Raspberry Pi or Cloud VPS, **do not use npx**. Instead, connect directly to the remote SSE endpoint.
31
-
32
- **Claude Desktop Config:**
33
- ```json
34
- {
35
- "mcpServers": {
36
- "cybermem-remote": {
37
- "url": "http://<your-rpi-ip>:8080/mcp",
38
- "transport": "sse",
39
- "headers": {
40
- "x-api-key": "your-api-key"
41
- }
42
- }
43
- }
44
- }
45
- ```
46
-
47
- ## Environment Variables
48
- - `CYBERMEM_URL`: URL to the OpenMemory API (default: `http://localhost:8080/memory`)
49
- - `CYBERMEM_API_KEY`: Your API Key (found in `~/.cybermem/.env`)
5
+ 🌐 [cybermem.dev](https://cybermem.dev) Ā· šŸ“– [Docs](https://docs.cybermem.dev) Ā· šŸ“¦ [GitHub](https://github.com/mikhailkogan17/cybermem)
@@ -0,0 +1,230 @@
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.deployCommand = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const commander_1 = require("commander");
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const execa_1 = __importDefault(require("execa"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const inquirer_1 = __importDefault(require("inquirer"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const path_1 = __importDefault(require("path"));
15
+ exports.deployCommand = new commander_1.Command('deploy')
16
+ .description('Deploy CyberMem services')
17
+ .option('-t, --target <target>', 'Deployment target (local, rpi, vps)', 'local')
18
+ .option('-h, --host <host>', 'SSH Host (user@ip) for remote deployment')
19
+ .option('--remote-access', 'Enable remote access via Tailscale Funnel (HTTPS)', false)
20
+ .option('--tailscale', 'Alias for --remote-access (deprecated)', false)
21
+ .option('--caddy', 'Use Caddy for automatic HTTPS (VPS only)')
22
+ .action(async (options) => {
23
+ const target = options.target;
24
+ const useTailscale = options.remoteAccess || options.tailscale;
25
+ const useCaddy = options.caddy;
26
+ console.log(chalk_1.default.blue(`Deploying to ${target}...`));
27
+ try {
28
+ // Resolve Template Directory (Support both Dev and Prod)
29
+ // Resolve Template Directory (Support both Dev and Prod)
30
+ // In Prod: __dirname is dist/commands, so templates is ../templates (dist/templates)
31
+ // In Dev (ts-node): __dirname is src/commands, so templates is ../../templates (root/packages/cli/templates)
32
+ // Try production path first (dist/templates)
33
+ let templateDir = path_1.default.resolve(__dirname, '../templates');
34
+ // If not found, try development path (src/../../templates)
35
+ if (!fs_1.default.existsSync(templateDir)) {
36
+ templateDir = path_1.default.resolve(__dirname, '../../templates');
37
+ }
38
+ // Final sanity check
39
+ if (!fs_1.default.existsSync(templateDir)) {
40
+ // Fallback for when running from root with ts-node directly (unlikely but possible)
41
+ templateDir = path_1.default.resolve(process.cwd(), 'packages/cli/templates');
42
+ }
43
+ if (!fs_1.default.existsSync(templateDir)) {
44
+ throw new Error(`Templates not found at ${templateDir}. Please ensure package is built correctly.`);
45
+ }
46
+ if (target === 'local') {
47
+ const composeFile = path_1.default.join(templateDir, 'docker-compose.yml');
48
+ const internalEnvExample = path_1.default.join(templateDir, 'envs/local.example');
49
+ if (!fs_1.default.existsSync(composeFile)) {
50
+ console.error(chalk_1.default.red(`Internal Error: Template not found at ${composeFile}`));
51
+ process.exit(1);
52
+ }
53
+ // Home Directory Config
54
+ const homeDir = os_1.default.homedir();
55
+ const configDir = path_1.default.join(homeDir, '.cybermem');
56
+ const envFile = path_1.default.join(configDir, '.env');
57
+ const dataDir = path_1.default.join(configDir, 'data');
58
+ // 1. Ensure ~/.cybermem exists
59
+ if (!fs_1.default.existsSync(configDir)) {
60
+ fs_1.default.mkdirSync(configDir, { recursive: true });
61
+ fs_1.default.mkdirSync(dataDir, { recursive: true });
62
+ }
63
+ // 2. Local Mode: Simplified setup without mandatory API key
64
+ if (!fs_1.default.existsSync(envFile)) {
65
+ console.log(chalk_1.default.yellow(`Initializing local configuration in ${configDir}...`));
66
+ const envContent = fs_1.default.readFileSync(internalEnvExample, 'utf-8');
67
+ fs_1.default.writeFileSync(envFile, envContent);
68
+ console.log(chalk_1.default.green(`Created .env at ${envFile}`));
69
+ }
70
+ console.log(chalk_1.default.blue('Starting CyberMem services in Local Mode...'));
71
+ // Execute docker-compose with internal file and USER HOME env
72
+ // Note: We pass CYBERMEM_API_KEY="" explicitly for local mode to trigger keyless bypass
73
+ await (0, execa_1.default)('docker-compose', [
74
+ '-f', composeFile,
75
+ '--env-file', envFile,
76
+ '--project-name', 'cybermem',
77
+ 'up', '-d', '--remove-orphans'
78
+ ], {
79
+ stdio: 'inherit',
80
+ env: {
81
+ ...process.env,
82
+ DATA_DIR: dataDir,
83
+ CYBERMEM_ENV_PATH: envFile,
84
+ CYBERMEM_API_KEY: ''
85
+ }
86
+ });
87
+ console.log(chalk_1.default.green('\nšŸŽ‰ CyberMem Installed!'));
88
+ console.log('');
89
+ console.log(chalk_1.default.bold('Next Steps:'));
90
+ console.log(` 1. Open ${chalk_1.default.underline('http://localhost:3000/client-connect')} to connect your MCP clients`);
91
+ console.log(` 2. Default password: ${chalk_1.default.bold('admin')} (you'll be prompted to change it)`);
92
+ console.log('');
93
+ console.log(chalk_1.default.dim('Local mode is active: No API key required for connections from this laptop.'));
94
+ }
95
+ else if (target === 'rpi') {
96
+ const composeFile = path_1.default.join(templateDir, 'docker-compose.yml');
97
+ const internalEnvExample = path_1.default.join(templateDir, 'envs/rpi.example');
98
+ let sshHost = options.host;
99
+ if (!sshHost) {
100
+ const answers = await inquirer_1.default.prompt([
101
+ {
102
+ type: 'input',
103
+ name: 'host',
104
+ message: 'Enter SSH Host (e.g. pi@raspberrypi.local):',
105
+ validate: (input) => input.includes('@') ? true : 'Format must be user@host'
106
+ }
107
+ ]);
108
+ sshHost = answers.host;
109
+ }
110
+ console.log(chalk_1.default.blue(`Remote deploying to ${sshHost}...`));
111
+ // 1. Create remote directory
112
+ await (0, execa_1.default)('ssh', [sshHost, 'mkdir -p ~/.cybermem/data']);
113
+ // 2. Initial Env Setup (if missing)
114
+ // We read remote file check using ssh
115
+ try {
116
+ await (0, execa_1.default)('ssh', [sshHost, '[ -f ~/.cybermem/.env ]']);
117
+ console.log(chalk_1.default.gray('Remote .env exists, skipping generation.'));
118
+ }
119
+ catch (e) {
120
+ console.log(chalk_1.default.yellow('Generating remote .env...'));
121
+ let envContent = fs_1.default.readFileSync(internalEnvExample, 'utf-8');
122
+ const newKey = `sk-${crypto_1.default.randomBytes(16).toString('hex')}`;
123
+ if (envContent.includes('CYBERMEM_API_KEY=')) {
124
+ envContent = envContent.replace(/CYBERMEM_API_KEY=.*/, `CYBERMEM_API_KEY=${newKey}`);
125
+ }
126
+ // Write to temp file then scp
127
+ const tempEnv = path_1.default.join(os_1.default.tmpdir(), 'cybermem-rpi.env');
128
+ fs_1.default.writeFileSync(tempEnv, envContent);
129
+ await (0, execa_1.default)('scp', [tempEnv, `${sshHost}:~/.cybermem/.env`]);
130
+ fs_1.default.unlinkSync(tempEnv);
131
+ }
132
+ // 3. Copy Docker Compose
133
+ console.log(chalk_1.default.blue('Uploading templates...'));
134
+ await (0, execa_1.default)('scp', [composeFile, `${sshHost}:~/.cybermem/docker-compose.yml`]);
135
+ // 4. Run Docker Compose Remotely
136
+ console.log(chalk_1.default.blue('Starting services on RPi...'));
137
+ // We pass CYBERMEM_ENV_PATH explicitly as ~/.cybermem/.env and DATA_DIR as ~/.cybermem/data
138
+ // The template uses ${CYBERMEM_ENV_PATH} and maps volumes.
139
+ // We need to set these vars in the shell when running docker-compose
140
+ const remoteCmd = `
141
+ export CYBERMEM_ENV_PATH=~/.cybermem/.env
142
+ export DATA_DIR=~/.cybermem/data
143
+ docker-compose -f ~/.cybermem/docker-compose.yml up -d --remove-orphans
144
+ `;
145
+ await (0, execa_1.default)('ssh', [sshHost, remoteCmd], { stdio: 'inherit' });
146
+ console.log(chalk_1.default.green('\nāœ… RPi deployment successful!'));
147
+ // Parse host from ssh string for convenience
148
+ const hostIp = sshHost.split('@')[1];
149
+ console.log(chalk_1.default.bold('Access Points (LAN):'));
150
+ console.log(` - Dashboard: ${chalk_1.default.underline(`http://${hostIp}:3000`)} (admin/admin)`);
151
+ console.log(` - OpenMemory: ${chalk_1.default.underline(`http://${hostIp}:8080`)}`);
152
+ // Tailscale Funnel setup
153
+ if (useTailscale) {
154
+ console.log(chalk_1.default.blue('\nšŸ”— Setting up Remote Access (Tailscale Funnel)...'));
155
+ try {
156
+ // 1. Check/Install Tailscale
157
+ try {
158
+ await (0, execa_1.default)('ssh', [sshHost, 'which tailscale']);
159
+ }
160
+ catch (e) {
161
+ console.log(chalk_1.default.yellow(' Tailscale not found. Installing...'));
162
+ await (0, execa_1.default)('ssh', [sshHost, 'curl -fsSL https://tailscale.com/install.sh | sh'], { stdio: 'inherit' });
163
+ }
164
+ // 2. Auth (interactive if needed)
165
+ console.log(chalk_1.default.blue(' Ensuring Tailscale is up...'));
166
+ try {
167
+ // Check status first to avoid re-auth if already up
168
+ await (0, execa_1.default)('ssh', [sshHost, 'tailscale status']);
169
+ }
170
+ catch (e) {
171
+ // Interactive auth
172
+ console.log(chalk_1.default.yellow(' āš ļø Tailscale authentication required. Please follow the prompts:'));
173
+ await (0, execa_1.default)('ssh', [sshHost, 'sudo tailscale up'], { stdio: 'inherit' });
174
+ }
175
+ // 3. Configure Funnel (Verified commands)
176
+ console.log(chalk_1.default.blue(' Configuring HTTPS Funnel (requires sudo access)...'));
177
+ console.log(chalk_1.default.gray(' You may be prompted for your RPi password.'));
178
+ // Routes:
179
+ // - / -> Dashboard (3000)
180
+ // - /cybermem/mcp -> MCP API (8626/mcp)
181
+ await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale serve reset'], { stdio: 'inherit' }).catch(() => { });
182
+ await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale serve --bg --set-path /cybermem http://127.0.0.1:8626'], { stdio: 'inherit' });
183
+ await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale serve --bg http://127.0.0.1:3000'], { stdio: 'inherit' });
184
+ await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale funnel --bg 443'], { stdio: 'inherit' });
185
+ // Get DNS name
186
+ const { stdout } = await (0, execa_1.default)('ssh', [sshHost, "tailscale status --json | jq -r '.Self.DNSName' | sed 's/\\.$//'"], { shell: true });
187
+ const dnsName = stdout.trim();
188
+ console.log(chalk_1.default.green('\n🌐 Remote Access Active (HTTPS):'));
189
+ console.log(` - Dashboard: ${chalk_1.default.underline(`https://${dnsName}/`)}`);
190
+ console.log(` - MCP API: ${chalk_1.default.underline(`https://${dnsName}/cybermem/mcp`)}`);
191
+ }
192
+ catch (e) {
193
+ console.log(chalk_1.default.red('\nāŒ Remote Access setup failed:'));
194
+ console.error(e);
195
+ console.log(chalk_1.default.gray('Manual setup: curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up'));
196
+ }
197
+ }
198
+ else {
199
+ console.log(chalk_1.default.gray('\nšŸ’” For remote access, re-run with: cybermem deploy --target rpi --remote-access'));
200
+ }
201
+ }
202
+ else if (target === 'vps') {
203
+ console.log(chalk_1.default.yellow('VPS deployment is similar to RPi.'));
204
+ console.log(chalk_1.default.blue('\nšŸ“‹ VPS Deployment Steps:'));
205
+ console.log('1. Run: cybermem deploy --target rpi --host user@your-vps-ip');
206
+ console.log('2. For HTTPS, choose one of:');
207
+ console.log(chalk_1.default.gray(' a) Tailscale Funnel: --tailscale flag'));
208
+ console.log(chalk_1.default.gray(' b) Caddy (recommended for public VPS):'));
209
+ console.log(chalk_1.default.gray(' - Install Caddy: sudo apt install caddy'));
210
+ console.log(chalk_1.default.gray(' - Configure /etc/caddy/Caddyfile:'));
211
+ console.log(chalk_1.default.cyan(`
212
+ cybermem.yourdomain.com {
213
+ reverse_proxy localhost:3000
214
+ }
215
+ api.cybermem.yourdomain.com {
216
+ reverse_proxy localhost:8080
217
+ }
218
+ `));
219
+ console.log(chalk_1.default.gray(' - Restart: sudo systemctl restart caddy'));
220
+ console.log(chalk_1.default.green('\nšŸ“š Full docs: https://cybermem.dev/docs#https'));
221
+ }
222
+ else {
223
+ console.error(chalk_1.default.red(`Unknown target: ${target}. Use: local, rpi, or vps`));
224
+ }
225
+ }
226
+ catch (error) {
227
+ console.error(chalk_1.default.red('Deployment failed:'), error);
228
+ process.exit(1);
229
+ }
230
+ });
@@ -0,0 +1,65 @@
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.initCommand = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const commander_1 = require("commander");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
+ const path_1 = __importDefault(require("path"));
12
+ exports.initCommand = new commander_1.Command('init')
13
+ .description('Initialize CyberMem configuration')
14
+ .action(async () => {
15
+ console.log(chalk_1.default.bold.blue('šŸ¤– CyberMem Setup Wizard'));
16
+ const { target } = await inquirer_1.default.prompt([
17
+ {
18
+ type: 'list',
19
+ name: 'target',
20
+ message: 'Where do you want to deploy?',
21
+ choices: [
22
+ { name: 'Local (Docker Compose)', value: 'local' },
23
+ { name: 'Raspberry Pi (Ansible)', value: 'rpi' },
24
+ { name: 'VPS (Kubernetes/Helm)', value: 'vps' }
25
+ ]
26
+ }
27
+ ]);
28
+ console.log(chalk_1.default.green(`Selected target: ${target}`));
29
+ if (target === 'local') {
30
+ const { confirm } = await inquirer_1.default.prompt([
31
+ {
32
+ type: 'confirm',
33
+ name: 'confirm',
34
+ message: 'This will create docker-compose.yml and .env in current directory. Continue?',
35
+ default: true
36
+ }
37
+ ]);
38
+ if (confirm) {
39
+ const templateDir = path_1.default.resolve(__dirname, '../../templates');
40
+ const envSrc = path_1.default.join(templateDir, 'envs/local.example');
41
+ if (!fs_1.default.existsSync(envSrc)) {
42
+ console.error(chalk_1.default.red(`Template not found at ${envSrc}.`));
43
+ return;
44
+ }
45
+ // Generate .env
46
+ let envContent = fs_1.default.readFileSync(envSrc, 'utf-8');
47
+ const crypto = require('crypto');
48
+ const newKey = `sk-${crypto.randomBytes(16).toString('hex')}`;
49
+ // Replace placeholder or key
50
+ if (envContent.includes('key-change-me') || envContent.includes('OM_API_KEY=')) {
51
+ envContent = envContent.replace(/OM_API_KEY=.*/, `OM_API_KEY=${newKey}`);
52
+ }
53
+ else {
54
+ envContent += `\nOM_API_KEY=${newKey}\n`;
55
+ }
56
+ fs_1.default.writeFileSync('.env', envContent);
57
+ console.log(chalk_1.default.gray('Created .env with generated API Key'));
58
+ console.log(chalk_1.default.gray('(Docker Compose configuration is managed internally by the CLI)'));
59
+ console.log(chalk_1.default.green('\nInitialization complete! Run "cybermem deploy" to start.'));
60
+ }
61
+ }
62
+ else {
63
+ console.log(chalk_1.default.yellow('Target init not fully implemented in CLI yet.'));
64
+ }
65
+ });
package/dist/index.js CHANGED
@@ -1,104 +1,212 @@
1
+ #!/usr/bin/env node
1
2
  "use strict";
2
3
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
5
  };
5
6
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
7
- const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
8
- const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
9
- const axios_1 = __importDefault(require("axios"));
10
- const dotenv_1 = __importDefault(require("dotenv"));
11
- dotenv_1.default.config();
12
- const API_URL = process.env.CYBERMEM_URL || "http://localhost:8080/memory";
13
- const API_KEY = process.env.CYBERMEM_API_KEY || "dev-secret-key";
14
- const server = new index_js_1.Server({
15
- name: "cybermem-mcp",
16
- version: "0.1.0",
17
- }, {
18
- capabilities: {
19
- tools: {},
20
- },
21
- });
22
- const tools = [
23
- {
24
- name: "add_memory",
25
- description: "Store a new memory in CyberMem",
26
- inputSchema: {
27
- type: "object",
28
- properties: {
29
- content: { type: "string" },
30
- user_id: { type: "string" },
31
- tags: { type: "array", items: { type: "string" } },
32
- },
33
- required: ["content"],
34
- },
35
- },
36
- {
37
- name: "query_memory",
38
- description: "Search for relevant memories",
39
- inputSchema: {
40
- type: "object",
41
- properties: {
42
- query: { type: "string" },
43
- k: { type: "number", default: 5 },
44
- },
45
- required: ["query"],
46
- },
47
- },
48
- {
49
- name: "list_memories",
50
- description: "List recent memories",
51
- inputSchema: {
52
- type: "object",
53
- properties: {
54
- limit: { type: "number", default: 10 },
55
- },
56
- },
57
- }
58
- ];
59
- server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
60
- tools,
61
- }));
62
- server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
63
- const { name, arguments: args } = request.params;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const commander_1 = require("commander");
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const execa_1 = __importDefault(require("execa"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const inquirer_1 = __importDefault(require("inquirer"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const program = new commander_1.Command();
16
+ program
17
+ .name('mcp')
18
+ .description('CyberMem - Deploy your AI memory server in one command')
19
+ .version('1.0.0')
20
+ .option('--rpi', 'Deploy to Raspberry Pi (default: local)')
21
+ .option('--vps', 'Deploy to VPS/Cloud server')
22
+ .option('-h, --host <host>', 'SSH Host (user@ip) for remote deployment')
23
+ .option('--remote-access', 'Enable Tailscale Funnel for HTTPS remote access')
24
+ .action(async (options) => {
25
+ // Determine target from flags
26
+ let target = 'local';
27
+ if (options.rpi)
28
+ target = 'rpi';
29
+ if (options.vps)
30
+ target = 'vps';
31
+ const useTailscale = options.remoteAccess;
32
+ console.log(chalk_1.default.blue(`Deploying CyberMem (${target})...`));
64
33
  try {
65
- switch (name) {
66
- case "add_memory": {
67
- const response = await axios_1.default.post(`${API_URL}/add`, args, {
68
- headers: { "Authorization": `Bearer ${API_KEY}` }
69
- });
70
- return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
34
+ // Resolve Template Directory (Support both Dev and Prod)
35
+ let templateDir = path_1.default.resolve(__dirname, '../templates');
36
+ if (!fs_1.default.existsSync(templateDir)) {
37
+ templateDir = path_1.default.resolve(__dirname, '../../templates');
38
+ }
39
+ if (!fs_1.default.existsSync(templateDir)) {
40
+ templateDir = path_1.default.resolve(process.cwd(), 'packages/cli/templates');
41
+ }
42
+ if (!fs_1.default.existsSync(templateDir)) {
43
+ throw new Error(`Templates not found at ${templateDir}. Please ensure package is built correctly.`);
44
+ }
45
+ if (target === 'local') {
46
+ const composeFile = path_1.default.join(templateDir, 'docker-compose.yml');
47
+ const internalEnvExample = path_1.default.join(templateDir, 'envs/local.example');
48
+ if (!fs_1.default.existsSync(composeFile)) {
49
+ console.error(chalk_1.default.red(`Internal Error: Template not found at ${composeFile}`));
50
+ process.exit(1);
71
51
  }
72
- case "query_memory": {
73
- const response = await axios_1.default.post(`${API_URL}/query`, args, {
74
- headers: { "Authorization": `Bearer ${API_KEY}` }
75
- });
76
- return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
52
+ // Home Directory Config
53
+ const homeDir = os_1.default.homedir();
54
+ const configDir = path_1.default.join(homeDir, '.cybermem');
55
+ const envFile = path_1.default.join(configDir, '.env');
56
+ const dataDir = path_1.default.join(configDir, 'data');
57
+ // 1. Ensure ~/.cybermem exists
58
+ if (!fs_1.default.existsSync(configDir)) {
59
+ fs_1.default.mkdirSync(configDir, { recursive: true });
60
+ fs_1.default.mkdirSync(dataDir, { recursive: true });
77
61
  }
78
- case "list_memories": {
79
- const limit = args?.limit || 10;
80
- const response = await axios_1.default.get(`${API_URL}/list?limit=${limit}`, {
81
- headers: { "Authorization": `Bearer ${API_KEY}` }
82
- });
83
- return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
62
+ // 2. Local Mode: Simplified setup without mandatory API key
63
+ if (!fs_1.default.existsSync(envFile)) {
64
+ console.log(chalk_1.default.yellow(`Initializing local configuration in ${configDir}...`));
65
+ const envContent = fs_1.default.readFileSync(internalEnvExample, 'utf-8');
66
+ fs_1.default.writeFileSync(envFile, envContent);
67
+ console.log(chalk_1.default.green(`Created .env at ${envFile}`));
84
68
  }
85
- default:
86
- throw new Error(`Unknown tool: ${name}`);
69
+ console.log(chalk_1.default.blue('Starting CyberMem services in Local Mode...'));
70
+ await (0, execa_1.default)('docker-compose', [
71
+ '-f', composeFile,
72
+ '--env-file', envFile,
73
+ '--project-name', 'cybermem',
74
+ 'up', '-d', '--remove-orphans'
75
+ ], {
76
+ stdio: 'inherit',
77
+ env: {
78
+ ...process.env,
79
+ DATA_DIR: dataDir,
80
+ CYBERMEM_ENV_PATH: envFile,
81
+ CYBERMEM_API_KEY: ''
82
+ }
83
+ });
84
+ console.log(chalk_1.default.green('\nšŸŽ‰ CyberMem Installed!'));
85
+ console.log('');
86
+ console.log(chalk_1.default.bold('Next Steps:'));
87
+ console.log(` 1. Open ${chalk_1.default.underline('http://localhost:3000/client-connect')} to connect your MCP clients`);
88
+ console.log(` 2. Default password: ${chalk_1.default.bold('admin')} (you'll be prompted to change it)`);
89
+ console.log('');
90
+ console.log(chalk_1.default.dim('Local mode is active: No API key required for connections from this laptop.'));
91
+ }
92
+ else if (target === 'rpi') {
93
+ const composeFile = path_1.default.join(templateDir, 'docker-compose.yml');
94
+ const internalEnvExample = path_1.default.join(templateDir, 'envs/rpi.example');
95
+ let sshHost = options.host;
96
+ if (!sshHost) {
97
+ const answers = await inquirer_1.default.prompt([
98
+ {
99
+ type: 'input',
100
+ name: 'host',
101
+ message: 'Enter SSH Host (e.g. pi@raspberrypi.local):',
102
+ validate: (input) => input.includes('@') ? true : 'Format must be user@host'
103
+ }
104
+ ]);
105
+ sshHost = answers.host;
106
+ }
107
+ console.log(chalk_1.default.blue(`Remote deploying to ${sshHost}...`));
108
+ // 1. Create remote directory
109
+ await (0, execa_1.default)('ssh', [sshHost, 'mkdir -p ~/.cybermem/data']);
110
+ // 2. Initial Env Setup (if missing)
111
+ try {
112
+ await (0, execa_1.default)('ssh', [sshHost, '[ -f ~/.cybermem/.env ]']);
113
+ console.log(chalk_1.default.gray('Remote .env exists, skipping generation.'));
114
+ }
115
+ catch (e) {
116
+ console.log(chalk_1.default.yellow('Generating remote .env...'));
117
+ let envContent = fs_1.default.readFileSync(internalEnvExample, 'utf-8');
118
+ const newKey = `sk-${crypto_1.default.randomBytes(16).toString('hex')}`;
119
+ if (envContent.includes('CYBERMEM_API_KEY=')) {
120
+ envContent = envContent.replace(/CYBERMEM_API_KEY=.*/, `CYBERMEM_API_KEY=${newKey}`);
121
+ }
122
+ const tempEnv = path_1.default.join(os_1.default.tmpdir(), 'cybermem-rpi.env');
123
+ fs_1.default.writeFileSync(tempEnv, envContent);
124
+ await (0, execa_1.default)('scp', [tempEnv, `${sshHost}:~/.cybermem/.env`]);
125
+ fs_1.default.unlinkSync(tempEnv);
126
+ }
127
+ // 3. Copy Docker Compose
128
+ console.log(chalk_1.default.blue('Uploading templates...'));
129
+ await (0, execa_1.default)('scp', [composeFile, `${sshHost}:~/.cybermem/docker-compose.yml`]);
130
+ // 4. Run Docker Compose Remotely
131
+ console.log(chalk_1.default.blue('Starting services on RPi...'));
132
+ // DOCKER_DEFAULT_PLATFORM=linux/arm64 forces arm64 images on RPi with 64-bit kernel but 32-bit Docker
133
+ const remoteCmd = `
134
+ export CYBERMEM_ENV_PATH=~/.cybermem/.env
135
+ export DATA_DIR=~/.cybermem/data
136
+ export DOCKER_DEFAULT_PLATFORM=linux/arm64
137
+ docker-compose -f ~/.cybermem/docker-compose.yml up -d --remove-orphans
138
+ `;
139
+ await (0, execa_1.default)('ssh', [sshHost, remoteCmd], { stdio: 'inherit' });
140
+ console.log(chalk_1.default.green('\nāœ… RPi deployment successful!'));
141
+ const hostIp = sshHost.split('@')[1];
142
+ console.log(chalk_1.default.bold('Access Points (LAN):'));
143
+ console.log(` - Dashboard: ${chalk_1.default.underline(`http://${hostIp}:3000`)} (admin/admin)`);
144
+ console.log(` - OpenMemory: ${chalk_1.default.underline(`http://${hostIp}:8080`)}`);
145
+ // Tailscale Funnel setup
146
+ if (useTailscale) {
147
+ console.log(chalk_1.default.blue('\nšŸ”— Setting up Remote Access (Tailscale Funnel)...'));
148
+ try {
149
+ try {
150
+ await (0, execa_1.default)('ssh', [sshHost, 'which tailscale']);
151
+ }
152
+ catch (e) {
153
+ console.log(chalk_1.default.yellow(' Tailscale not found. Installing...'));
154
+ await (0, execa_1.default)('ssh', [sshHost, 'curl -fsSL https://tailscale.com/install.sh | sh'], { stdio: 'inherit' });
155
+ }
156
+ console.log(chalk_1.default.blue(' Ensuring Tailscale is up...'));
157
+ try {
158
+ await (0, execa_1.default)('ssh', [sshHost, 'tailscale status']);
159
+ }
160
+ catch (e) {
161
+ console.log(chalk_1.default.yellow(' āš ļø Tailscale authentication required. Please follow the prompts:'));
162
+ await (0, execa_1.default)('ssh', [sshHost, 'sudo tailscale up'], { stdio: 'inherit' });
163
+ }
164
+ console.log(chalk_1.default.blue(' Configuring HTTPS Funnel (requires sudo access)...'));
165
+ console.log(chalk_1.default.gray(' You may be prompted for your RPi password.'));
166
+ await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale serve reset'], { stdio: 'inherit' }).catch(() => { });
167
+ await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale serve --bg --set-path /cybermem http://127.0.0.1:8626'], { stdio: 'inherit' });
168
+ await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale serve --bg http://127.0.0.1:3000'], { stdio: 'inherit' });
169
+ await (0, execa_1.default)('ssh', ['-t', sshHost, 'sudo tailscale funnel --bg 443'], { stdio: 'inherit' });
170
+ const { stdout } = await (0, execa_1.default)('ssh', [sshHost, "tailscale status --json | jq -r '.Self.DNSName' | sed 's/\\.$//'"]);
171
+ const dnsName = stdout.trim();
172
+ console.log(chalk_1.default.green('\n🌐 Remote Access Active (HTTPS):'));
173
+ console.log(` - Dashboard: ${chalk_1.default.underline(`https://${dnsName}/`)}`);
174
+ console.log(` - MCP API: ${chalk_1.default.underline(`https://${dnsName}/cybermem/mcp`)}`);
175
+ }
176
+ catch (e) {
177
+ console.log(chalk_1.default.red('\nāŒ Remote Access setup failed:'));
178
+ console.error(e);
179
+ console.log(chalk_1.default.gray('Manual setup: curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up'));
180
+ }
181
+ }
182
+ else {
183
+ console.log(chalk_1.default.gray('\nšŸ’” For remote access, re-run with: npx @cybermem/mcp --rpi --remote-access'));
184
+ }
185
+ }
186
+ else if (target === 'vps') {
187
+ console.log(chalk_1.default.yellow('VPS deployment is similar to RPi.'));
188
+ console.log(chalk_1.default.blue('\nšŸ“‹ VPS Deployment Steps:'));
189
+ console.log('1. Run: npx @cybermem/mcp --rpi --host user@your-vps-ip');
190
+ console.log('2. For HTTPS, choose one of:');
191
+ console.log(chalk_1.default.gray(' a) Tailscale Funnel: --remote-access flag'));
192
+ console.log(chalk_1.default.gray(' b) Caddy (recommended for public VPS):'));
193
+ console.log(chalk_1.default.gray(' - Install Caddy: sudo apt install caddy'));
194
+ console.log(chalk_1.default.gray(' - Configure /etc/caddy/Caddyfile:'));
195
+ console.log(chalk_1.default.cyan(`
196
+ cybermem.yourdomain.com {
197
+ reverse_proxy localhost:3000
198
+ }
199
+ api.cybermem.yourdomain.com {
200
+ reverse_proxy localhost:8080
201
+ }
202
+ `));
203
+ console.log(chalk_1.default.gray(' - Restart: sudo systemctl restart caddy'));
204
+ console.log(chalk_1.default.green('\nšŸ“š Full docs: https://cybermem.dev/docs#https'));
87
205
  }
88
206
  }
89
207
  catch (error) {
90
- return {
91
- content: [{ type: "text", text: `Error: ${error.message}` }],
92
- isError: true,
93
- };
208
+ console.error(chalk_1.default.red('Deployment failed:'), error);
209
+ process.exit(1);
94
210
  }
95
211
  });
96
- async function run() {
97
- const transport = new stdio_js_1.StdioServerTransport();
98
- await server.connect(transport);
99
- console.error("CyberMem MCP Server running on stdio");
100
- }
101
- run().catch((error) => {
102
- console.error("Fatal error running server:", error);
103
- process.exit(1);
104
- });
212
+ program.parse(process.argv);