@cybermem/cli 0.6.11 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/backup.js +3 -3
- package/dist/commands/dashboard.js +90 -0
- package/dist/commands/init.js +231 -47
- package/dist/commands/login.js +165 -0
- package/dist/commands/reset.js +1 -1
- package/dist/commands/restore.js +4 -4
- package/dist/commands/upgrade.js +141 -0
- package/dist/index.js +39 -18
- package/dist/templates/auth-sidecar/server.js +54 -2
- package/dist/templates/docker-compose.yml +57 -65
- package/dist/templates/envs/local.env +17 -0
- package/dist/templates/envs/rpi-tailscale.env +23 -0
- package/dist/templates/envs/rpi.env +22 -0
- package/dist/templates/envs/vps.env +29 -0
- package/dist/templates/monitoring/db_exporter/exporter.py +9 -4
- package/package.json +7 -1
- package/templates/auth-sidecar/server.js +54 -2
- package/templates/docker-compose.yml +57 -65
- package/templates/envs/local.env +17 -0
- package/templates/envs/rpi-tailscale.env +23 -0
- package/templates/envs/rpi.env +22 -0
- package/templates/envs/vps.env +29 -0
- package/templates/monitoring/db_exporter/exporter.py +9 -4
- package/templates/envs/local.example +0 -27
- package/templates/envs/rpi.example +0 -27
- package/templates/envs/vps.example +0 -25
package/dist/commands/backup.js
CHANGED
|
@@ -16,10 +16,10 @@ async function backup(options) {
|
|
|
16
16
|
try {
|
|
17
17
|
// Check if container exists
|
|
18
18
|
try {
|
|
19
|
-
await (0, execa_1.default)('docker', ['inspect', 'cybermem-
|
|
19
|
+
await (0, execa_1.default)('docker', ['inspect', 'cybermem-mcp']);
|
|
20
20
|
}
|
|
21
21
|
catch (e) {
|
|
22
|
-
console.error(chalk_1.default.red('Error: cybermem-
|
|
22
|
+
console.error(chalk_1.default.red('Error: cybermem-mcp container not found. Is CyberMem installed?'));
|
|
23
23
|
process.exit(1);
|
|
24
24
|
}
|
|
25
25
|
// Use a transient alpine container to tar the /data volume
|
|
@@ -27,7 +27,7 @@ async function backup(options) {
|
|
|
27
27
|
// And we use --volumes-from to access the data volume of the running service
|
|
28
28
|
const cmd = [
|
|
29
29
|
'run', '--rm',
|
|
30
|
-
'--volumes-from', 'cybermem-
|
|
30
|
+
'--volumes-from', 'cybermem-mcp',
|
|
31
31
|
'-v', `${process.cwd()}:/backup`,
|
|
32
32
|
'alpine',
|
|
33
33
|
'tar', 'czf', `/backup/${filename}`, '-C', '/', 'data'
|
|
@@ -0,0 +1,90 @@
|
|
|
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.dashboard = dashboard;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const net_1 = __importDefault(require("net"));
|
|
10
|
+
const open_1 = __importDefault(require("open"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const TOKEN_FILE = path_1.default.join(os_1.default.homedir(), ".cybermem", "token.json");
|
|
14
|
+
const checkPort = (port) => {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const socket = new net_1.default.Socket();
|
|
17
|
+
const onError = () => {
|
|
18
|
+
socket.destroy();
|
|
19
|
+
resolve(false);
|
|
20
|
+
};
|
|
21
|
+
socket.setTimeout(500);
|
|
22
|
+
socket.once("error", onError);
|
|
23
|
+
socket.once("timeout", onError);
|
|
24
|
+
socket.connect(port, "localhost", () => {
|
|
25
|
+
socket.end();
|
|
26
|
+
resolve(true);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Get stored token from ~/.cybermem/token.json
|
|
32
|
+
*/
|
|
33
|
+
function getStoredToken() {
|
|
34
|
+
try {
|
|
35
|
+
if (!fs_1.default.existsSync(TOKEN_FILE))
|
|
36
|
+
return null;
|
|
37
|
+
const data = JSON.parse(fs_1.default.readFileSync(TOKEN_FILE, "utf-8"));
|
|
38
|
+
if (new Date(data.expires_at) < new Date()) {
|
|
39
|
+
console.warn(chalk_1.default.yellow("Token expired. Run: cybermem-cli login"));
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return data.access_token;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function dashboard(options) {
|
|
49
|
+
console.log(chalk_1.default.blue("Checking CyberMem stack status..."));
|
|
50
|
+
const [dashboardUp, prometheusUp] = await Promise.all([
|
|
51
|
+
checkPort(3000),
|
|
52
|
+
checkPort(9092),
|
|
53
|
+
]);
|
|
54
|
+
if (!dashboardUp) {
|
|
55
|
+
console.error(chalk_1.default.red("❌ Dashboard is NOT running on port 3000."));
|
|
56
|
+
console.log(chalk_1.default.yellow("Run 'cybermem up' or 'cd packages/dashboard && npm run dev' to start it."));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(chalk_1.default.green("✅ Dashboard is running on port 3000."));
|
|
60
|
+
}
|
|
61
|
+
if (!prometheusUp) {
|
|
62
|
+
console.warn(chalk_1.default.yellow("⚠️ Prometheus is NOT running on port 9092."));
|
|
63
|
+
console.warn(chalk_1.default.gray(" Charts will be empty. Run 'cybermem up' or 'docker-compose up' to enable metrics."));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.log(chalk_1.default.green("✅ Prometheus is running on port 9092."));
|
|
67
|
+
}
|
|
68
|
+
if (dashboardUp) {
|
|
69
|
+
console.log(chalk_1.default.blue("\nOpening dashboard..."));
|
|
70
|
+
await (0, open_1.default)("http://localhost:3000");
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Try remote dashboard if local isn't up
|
|
74
|
+
const token = getStoredToken();
|
|
75
|
+
if (token) {
|
|
76
|
+
// Check for remote URL from environment or config
|
|
77
|
+
const remoteUrl = process.env.CYBERMEM_DASHBOARD_URL;
|
|
78
|
+
if (remoteUrl) {
|
|
79
|
+
console.log(chalk_1.default.blue("\nOpening remote dashboard..."));
|
|
80
|
+
await (0, open_1.default)(`${remoteUrl}/api/auth/token?token=${token}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(chalk_1.default.gray("\nTip: Set CYBERMEM_DASHBOARD_URL to open remote dashboard."));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.log(chalk_1.default.gray("\nTip: Run 'cybermem-cli login' to enable remote access."));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -3,63 +3,247 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.init = init;
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
-
const
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const execa_1 = __importDefault(require("execa"));
|
|
9
10
|
const fs_1 = __importDefault(require("fs"));
|
|
10
11
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
12
|
+
const os_1 = __importDefault(require("os"));
|
|
11
13
|
const path_1 = __importDefault(require("path"));
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
async function init(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(`Initializing 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");
|
|
26
28
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
if (!fs_1.default.existsSync(composeFile)) {
|
|
42
|
+
console.error(chalk_1.default.red(`Internal Error: Template not found at ${composeFile}`));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
// Home Directory Config
|
|
46
|
+
const homeDir = os_1.default.homedir();
|
|
47
|
+
const configDir = path_1.default.join(homeDir, ".cybermem");
|
|
48
|
+
const envFile = path_1.default.join(configDir, ".env");
|
|
49
|
+
const dataDir = path_1.default.join(configDir, "data");
|
|
50
|
+
// 1. Ensure ~/.cybermem exists
|
|
51
|
+
if (!fs_1.default.existsSync(configDir)) {
|
|
52
|
+
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
53
|
+
fs_1.default.mkdirSync(dataDir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
// 2. Local Mode
|
|
56
|
+
if (!fs_1.default.existsSync(envFile)) {
|
|
57
|
+
console.log(chalk_1.default.yellow(`Initializing local configuration in ${configDir}...`));
|
|
58
|
+
const templateEnv = path_1.default.join(templateDir, "envs/local.env");
|
|
59
|
+
const envContent = fs_1.default.readFileSync(templateEnv, "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",
|
|
66
|
+
composeFile,
|
|
67
|
+
"--env-file",
|
|
68
|
+
envFile,
|
|
69
|
+
"--project-name",
|
|
70
|
+
"cybermem",
|
|
71
|
+
"up",
|
|
72
|
+
"-d",
|
|
73
|
+
"--remove-orphans",
|
|
74
|
+
], {
|
|
75
|
+
stdio: "inherit",
|
|
76
|
+
env: {
|
|
77
|
+
...process.env,
|
|
78
|
+
DATA_DIR: dataDir,
|
|
79
|
+
CYBERMEM_ENV_PATH: envFile,
|
|
80
|
+
OM_API_KEY: "",
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
console.log(chalk_1.default.green("\n🎉 CyberMem Installed!"));
|
|
84
|
+
console.log("");
|
|
85
|
+
console.log(chalk_1.default.bold("Next Steps:"));
|
|
86
|
+
console.log(` 1. Open ${chalk_1.default.underline("http://localhost:3000/client-connect")} to connect your MCP clients`);
|
|
87
|
+
console.log(` 2. Default password: ${chalk_1.default.bold("admin")} (you'll be prompted to change it)`);
|
|
88
|
+
console.log("");
|
|
89
|
+
console.log(chalk_1.default.dim("Local mode is active: No API key required for connections from this laptop."));
|
|
90
|
+
}
|
|
91
|
+
else if (target === "rpi" || target === "vps") {
|
|
92
|
+
const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
|
|
93
|
+
const answers = await inquirer_1.default.prompt([
|
|
94
|
+
{
|
|
95
|
+
type: "input",
|
|
96
|
+
name: "host",
|
|
97
|
+
message: "Enter SSH Host (e.g. pi@raspberrypi.local):",
|
|
98
|
+
validate: (input) => input.includes("@") ? true : "Format must be user@host",
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
const sshHost = answers.host;
|
|
102
|
+
console.log(chalk_1.default.blue(`Remote deploying to ${sshHost}...`));
|
|
103
|
+
// 1. Create remote directory
|
|
104
|
+
await (0, execa_1.default)("ssh", [sshHost, "mkdir -p ~/.cybermem/data"]);
|
|
105
|
+
// 1.5 Check and fix Docker architecture (64-bit kernel with 32-bit Docker)
|
|
106
|
+
console.log(chalk_1.default.blue("Checking Docker architecture..."));
|
|
107
|
+
try {
|
|
108
|
+
const { stdout: kernelArch } = await (0, execa_1.default)("ssh", [
|
|
109
|
+
sshHost,
|
|
110
|
+
"uname -m",
|
|
111
|
+
]);
|
|
112
|
+
const { stdout: dockerArch } = await (0, execa_1.default)("ssh", [
|
|
113
|
+
sshHost,
|
|
114
|
+
'docker version --format "{{.Server.Arch}}" 2>/dev/null || echo "unknown"',
|
|
115
|
+
]);
|
|
116
|
+
if (kernelArch.trim() === "aarch64" && dockerArch.trim() !== "arm64") {
|
|
117
|
+
console.log(chalk_1.default.yellow(`⚠️ Docker is ${dockerArch.trim()}, kernel is aarch64. Installing arm64 Docker...`));
|
|
118
|
+
const installCmd = `
|
|
119
|
+
sudo systemctl stop docker docker.socket 2>/dev/null || true
|
|
120
|
+
curl -fsSL https://download.docker.com/linux/static/stable/aarch64/docker-27.5.1.tgz -o /tmp/docker.tgz
|
|
121
|
+
sudo tar -xzf /tmp/docker.tgz -C /usr/local/bin --strip-components=1
|
|
122
|
+
sudo /usr/local/bin/dockerd &
|
|
123
|
+
sleep 5
|
|
124
|
+
docker version --format "{{.Server.Arch}}"
|
|
125
|
+
`;
|
|
126
|
+
const { stdout } = await (0, execa_1.default)("ssh", [sshHost, installCmd], {
|
|
127
|
+
shell: true,
|
|
128
|
+
});
|
|
129
|
+
if (stdout.includes("arm64")) {
|
|
130
|
+
console.log(chalk_1.default.green("✅ Docker arm64 installed successfully"));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
console.log(chalk_1.default.yellow("⚠️ Docker arm64 install may need manual verification"));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (dockerArch.trim() === "arm64") {
|
|
137
|
+
console.log(chalk_1.default.green(`✅ Docker is already arm64`));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.log(chalk_1.default.gray(`Docker arch: ${dockerArch.trim()}, kernel: ${kernelArch.trim()}`));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
console.log(chalk_1.default.yellow(`⚠️ Docker arch check skipped: ${e.message}`));
|
|
145
|
+
}
|
|
146
|
+
// 2. Initial Env Setup (if missing)
|
|
147
|
+
try {
|
|
148
|
+
await (0, execa_1.default)("ssh", [sshHost, "[ -f ~/.cybermem/.env ]"]);
|
|
149
|
+
console.log(chalk_1.default.gray("Remote .env exists, skipping generation."));
|
|
36
150
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
151
|
+
catch (e) {
|
|
152
|
+
console.log(chalk_1.default.yellow("Generating remote configuration..."));
|
|
153
|
+
let templateName = "rpi.env";
|
|
154
|
+
if (target === "vps")
|
|
155
|
+
templateName = "vps.env";
|
|
156
|
+
else if (useTailscale)
|
|
157
|
+
templateName = "rpi-tailscale.env";
|
|
158
|
+
const templateEnv = path_1.default.join(templateDir, "envs", templateName);
|
|
159
|
+
let envContent = fs_1.default.readFileSync(templateEnv, "utf-8");
|
|
160
|
+
const newKey = `cm-${crypto_1.default.randomBytes(16).toString("hex")}`;
|
|
161
|
+
// Replace OM_API_KEY with generated key
|
|
162
|
+
if (envContent.includes("OM_API_KEY=")) {
|
|
163
|
+
envContent = envContent.replace(/OM_API_KEY=.*/, `OM_API_KEY=${newKey}`);
|
|
164
|
+
}
|
|
165
|
+
const tempEnv = path_1.default.join(os_1.default.tmpdir(), "cybermem-remote.env");
|
|
166
|
+
fs_1.default.writeFileSync(tempEnv, envContent);
|
|
167
|
+
await (0, execa_1.default)("scp", [tempEnv, `${sshHost}:~/.cybermem/.env`]);
|
|
168
|
+
fs_1.default.unlinkSync(tempEnv);
|
|
169
|
+
console.log(chalk_1.default.green(`✅ Security configuration generated (${templateName}).`));
|
|
44
170
|
}
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
171
|
+
// 3. Copy Docker Compose
|
|
172
|
+
console.log(chalk_1.default.blue("Uploading templates..."));
|
|
173
|
+
await (0, execa_1.default)("scp", [
|
|
174
|
+
composeFile,
|
|
175
|
+
`${sshHost}:~/.cybermem/docker-compose.yml`,
|
|
176
|
+
]);
|
|
177
|
+
// 4. Run Docker Compose Remotely
|
|
178
|
+
console.log(chalk_1.default.blue("Starting services on RPi..."));
|
|
179
|
+
// DOCKER_DEFAULT_PLATFORM=linux/arm64 forces arm64 images on RPi with 64-bit kernel but 32-bit Docker
|
|
180
|
+
const remoteCmd = `
|
|
181
|
+
export CYBERMEM_ENV_PATH=~/.cybermem/.env
|
|
182
|
+
export DATA_DIR=~/.cybermem/data
|
|
183
|
+
export DOCKER_DEFAULT_PLATFORM=linux/arm64
|
|
184
|
+
docker-compose -f ~/.cybermem/docker-compose.yml up -d --remove-orphans
|
|
185
|
+
`;
|
|
186
|
+
await (0, execa_1.default)("ssh", [sshHost, remoteCmd], { stdio: "inherit" });
|
|
187
|
+
console.log(chalk_1.default.green("\n✅ RPi deployment successful!"));
|
|
188
|
+
const hostIp = sshHost.split("@")[1];
|
|
189
|
+
console.log(chalk_1.default.bold("Access Points (LAN):"));
|
|
190
|
+
console.log(` - Dashboard: ${chalk_1.default.underline(`http://${hostIp}:3000`)} (admin/admin)`);
|
|
191
|
+
console.log(` - OpenMemory: ${chalk_1.default.underline(`http://${hostIp}:8080`)}`);
|
|
192
|
+
// Tailscale Funnel setup
|
|
193
|
+
if (useTailscale) {
|
|
194
|
+
console.log(chalk_1.default.blue("\n🔗 Setting up Remote Access (Tailscale Funnel)..."));
|
|
195
|
+
try {
|
|
196
|
+
try {
|
|
197
|
+
await (0, execa_1.default)("ssh", [sshHost, "which tailscale"]);
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
console.log(chalk_1.default.yellow(" Tailscale not found. Installing..."));
|
|
201
|
+
await (0, execa_1.default)("ssh", [sshHost, "curl -fsSL https://tailscale.com/install.sh | sh"], { stdio: "inherit" });
|
|
202
|
+
}
|
|
203
|
+
console.log(chalk_1.default.blue(" Ensuring Tailscale is up..."));
|
|
204
|
+
try {
|
|
205
|
+
await (0, execa_1.default)("ssh", [sshHost, "tailscale status"]);
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
console.log(chalk_1.default.yellow(" ⚠️ Tailscale authentication required. Please follow the prompts:"));
|
|
209
|
+
await (0, execa_1.default)("ssh", [sshHost, "sudo tailscale up"], {
|
|
210
|
+
stdio: "inherit",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
console.log(chalk_1.default.blue(" Configuring HTTPS Funnel (requires sudo access)..."));
|
|
214
|
+
console.log(chalk_1.default.gray(" You may be prompted for your RPi password."));
|
|
215
|
+
await (0, execa_1.default)("ssh", ["-t", sshHost, "sudo tailscale serve reset"], {
|
|
216
|
+
stdio: "inherit",
|
|
217
|
+
}).catch(() => { });
|
|
218
|
+
await (0, execa_1.default)("ssh", [
|
|
219
|
+
"-t",
|
|
220
|
+
sshHost,
|
|
221
|
+
"sudo tailscale serve --bg --set-path /cybermem http://127.0.0.1:8626",
|
|
222
|
+
], { stdio: "inherit" });
|
|
223
|
+
await (0, execa_1.default)("ssh", ["-t", sshHost, "sudo tailscale serve --bg http://127.0.0.1:3000"], { stdio: "inherit" });
|
|
224
|
+
await (0, execa_1.default)("ssh", ["-t", sshHost, "sudo tailscale funnel --bg 443"], { stdio: "inherit" });
|
|
225
|
+
const { stdout } = await (0, execa_1.default)("ssh", [
|
|
226
|
+
sshHost,
|
|
227
|
+
"tailscale status --json | jq -r '.Self.DNSName' | sed 's/\\.$//'",
|
|
228
|
+
]);
|
|
229
|
+
const dnsName = stdout.trim();
|
|
230
|
+
console.log(chalk_1.default.green("\n🌐 Remote Access Active (HTTPS):"));
|
|
231
|
+
console.log(` - Dashboard: ${chalk_1.default.underline(`https://${dnsName}/`)}`);
|
|
232
|
+
console.log(` - MCP API: ${chalk_1.default.underline(`https://${dnsName}/cybermem/mcp`)}`);
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
console.log(chalk_1.default.red("\n❌ Remote Access setup failed:"));
|
|
236
|
+
console.error(e);
|
|
237
|
+
console.log(chalk_1.default.gray("Manual setup: curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up"));
|
|
238
|
+
}
|
|
52
239
|
}
|
|
53
240
|
else {
|
|
54
|
-
|
|
241
|
+
console.log(chalk_1.default.gray("\n💡 For remote access, re-run with: npx @cybermem/cli --rpi --remote-access"));
|
|
55
242
|
}
|
|
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
243
|
}
|
|
61
244
|
}
|
|
62
|
-
|
|
63
|
-
console.
|
|
245
|
+
catch (error) {
|
|
246
|
+
console.error(chalk_1.default.red("Deployment failed:"), error);
|
|
247
|
+
process.exit(1);
|
|
64
248
|
}
|
|
65
|
-
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CyberMem CLI Auth Module
|
|
4
|
+
*
|
|
5
|
+
* Token storage and browser-based OAuth login flow.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
41
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
42
|
+
};
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.login = login;
|
|
45
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const http = __importStar(require("http"));
|
|
48
|
+
const os = __importStar(require("os"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const AUTH_DIR = path.join(os.homedir(), ".cybermem");
|
|
51
|
+
const TOKEN_FILE = path.join(AUTH_DIR, "token.json");
|
|
52
|
+
const AUTH_URL = process.env.CYBERMEM_AUTH_URL || "https://cybermem.dev";
|
|
53
|
+
/**
|
|
54
|
+
* Ensure the .cybermem directory exists
|
|
55
|
+
*/
|
|
56
|
+
function ensureAuthDir() {
|
|
57
|
+
if (!fs.existsSync(AUTH_DIR)) {
|
|
58
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Save token to disk
|
|
63
|
+
*/
|
|
64
|
+
function saveToken(token, expiresIn, email, name) {
|
|
65
|
+
ensureAuthDir();
|
|
66
|
+
const expiresAt = new Date(Date.now() + expiresIn * 1000);
|
|
67
|
+
const data = {
|
|
68
|
+
access_token: token,
|
|
69
|
+
expires_at: expiresAt.toISOString(),
|
|
70
|
+
email,
|
|
71
|
+
name,
|
|
72
|
+
};
|
|
73
|
+
fs.writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Start OAuth login flow
|
|
77
|
+
* Opens browser and waits for callback with token
|
|
78
|
+
*/
|
|
79
|
+
async function login() {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
// Find available port
|
|
82
|
+
const server = http.createServer();
|
|
83
|
+
server.listen(0, "127.0.0.1", () => {
|
|
84
|
+
const address = server.address();
|
|
85
|
+
if (!address || typeof address === "string") {
|
|
86
|
+
reject(new Error("Failed to start callback server"));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const port = address.port;
|
|
90
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
91
|
+
// Redirect to landing auth endpoint which starts GitHub flow
|
|
92
|
+
// We pass our local callbackUrl as 'redirect' param to the intermediate CLI callback handler
|
|
93
|
+
const authUrl = `${AUTH_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(`${AUTH_URL}/api/auth/cli/callback?redirect=${encodeURIComponent(callbackUrl)}`)}`;
|
|
94
|
+
console.log(chalk_1.default.blue("🔐 Opening browser for GitHub login..."));
|
|
95
|
+
console.log(chalk_1.default.gray(` If browser doesn't open, visit: ${authUrl}`));
|
|
96
|
+
// Open browser
|
|
97
|
+
const open = async (url) => {
|
|
98
|
+
const { default: openBrowser } = await Promise.resolve().then(() => __importStar(require("open")));
|
|
99
|
+
await openBrowser(url);
|
|
100
|
+
};
|
|
101
|
+
open(authUrl);
|
|
102
|
+
// Handle callback
|
|
103
|
+
server.on("request", async (req, res) => {
|
|
104
|
+
if (!req.url?.startsWith("/callback")) {
|
|
105
|
+
res.writeHead(404);
|
|
106
|
+
res.end("Not found");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
110
|
+
const token = url.searchParams.get("token");
|
|
111
|
+
if (!token) {
|
|
112
|
+
res.writeHead(400);
|
|
113
|
+
res.end("Missing token");
|
|
114
|
+
server.close();
|
|
115
|
+
reject(new Error("No token received"));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Decode token to get user info (JWT payload)
|
|
119
|
+
let email;
|
|
120
|
+
let name;
|
|
121
|
+
try {
|
|
122
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
|
123
|
+
email = payload.email;
|
|
124
|
+
name = payload.name;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Ignore decode errors
|
|
128
|
+
}
|
|
129
|
+
// Save token (30 days expiry)
|
|
130
|
+
saveToken(token, 30 * 24 * 60 * 60, email, name);
|
|
131
|
+
// Send success page
|
|
132
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
133
|
+
res.end(`
|
|
134
|
+
<!DOCTYPE html>
|
|
135
|
+
<html>
|
|
136
|
+
<head>
|
|
137
|
+
<title>CyberMem - Logged In</title>
|
|
138
|
+
<style>
|
|
139
|
+
body { font-family: system-ui; text-align: center; padding: 50px; background: #0a0a0a; color: #fff; }
|
|
140
|
+
h1 { color: #22c55e; }
|
|
141
|
+
.logo { font-size: 48px; margin-bottom: 20px; }
|
|
142
|
+
</style>
|
|
143
|
+
</head>
|
|
144
|
+
<body>
|
|
145
|
+
<div class="logo">🧠</div>
|
|
146
|
+
<h1>Successfully Logged In!</h1>
|
|
147
|
+
<p>You can close this window and return to your terminal.</p>
|
|
148
|
+
<p style="color: #888;">Logged in as: ${email || name || "Unknown"}</p>
|
|
149
|
+
</body>
|
|
150
|
+
</html>
|
|
151
|
+
`);
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(chalk_1.default.green("✅ Successfully logged in as:"), chalk_1.default.bold(email || name || "Unknown"));
|
|
154
|
+
console.log(chalk_1.default.gray(` Token saved to: ${TOKEN_FILE}`));
|
|
155
|
+
server.close();
|
|
156
|
+
resolve();
|
|
157
|
+
});
|
|
158
|
+
// Timeout after 5 minutes
|
|
159
|
+
setTimeout(() => {
|
|
160
|
+
server.close();
|
|
161
|
+
reject(new Error("Login timeout - no callback received"));
|
|
162
|
+
}, 5 * 60 * 1000);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
package/dist/commands/reset.js
CHANGED
|
@@ -10,7 +10,7 @@ const ora_1 = __importDefault(require("ora"));
|
|
|
10
10
|
async function reset() {
|
|
11
11
|
const spinner = (0, ora_1.default)('Resetting CyberMem database...').start();
|
|
12
12
|
try {
|
|
13
|
-
const containerName = 'cybermem-
|
|
13
|
+
const containerName = 'cybermem-mcp';
|
|
14
14
|
// Check if container exists
|
|
15
15
|
try {
|
|
16
16
|
(0, child_process_1.execSync)(`docker inspect ${containerName}`, { stdio: 'pipe' });
|
package/dist/commands/restore.js
CHANGED
|
@@ -25,7 +25,7 @@ async function restore(file, options) {
|
|
|
25
25
|
// 1. Stop the OpenMemory service to safely write to DB
|
|
26
26
|
console.log(chalk_1.default.blue('Stopping OpenMemory service...'));
|
|
27
27
|
try {
|
|
28
|
-
await (0, execa_1.default)('docker', ['stop', 'cybermem-
|
|
28
|
+
await (0, execa_1.default)('docker', ['stop', 'cybermem-mcp']);
|
|
29
29
|
}
|
|
30
30
|
catch (e) {
|
|
31
31
|
console.log(chalk_1.default.gray('Container not running (or not found), proceeding...'));
|
|
@@ -37,7 +37,7 @@ async function restore(file, options) {
|
|
|
37
37
|
const filename = path_1.default.basename(backupPath);
|
|
38
38
|
const cmd = [
|
|
39
39
|
'run', '--rm',
|
|
40
|
-
'--volumes-from', 'cybermem-
|
|
40
|
+
'--volumes-from', 'cybermem-mcp', // Access the volume even if container is stopped
|
|
41
41
|
'-v', `${dir}:/backup`,
|
|
42
42
|
'alpine',
|
|
43
43
|
'sh', '-c',
|
|
@@ -50,13 +50,13 @@ async function restore(file, options) {
|
|
|
50
50
|
await (0, execa_1.default)('docker', cmd, { stdio: 'inherit' });
|
|
51
51
|
// 3. Restart the service
|
|
52
52
|
console.log(chalk_1.default.blue('Restarting OpenMemory service...'));
|
|
53
|
-
await (0, execa_1.default)('docker', ['start', 'cybermem-
|
|
53
|
+
await (0, execa_1.default)('docker', ['start', 'cybermem-mcp']);
|
|
54
54
|
console.log(chalk_1.default.green(`\n✅ Restore completed successfully!`));
|
|
55
55
|
console.log('Your memory has been recovered.');
|
|
56
56
|
}
|
|
57
57
|
catch (error) {
|
|
58
58
|
console.error(chalk_1.default.red('Restore failed:'), error);
|
|
59
|
-
console.log(chalk_1.default.yellow('Suggestion: Check if Docker is running and "cybermem-
|
|
59
|
+
console.log(chalk_1.default.yellow('Suggestion: Check if Docker is running and "cybermem-mcp" container exists.'));
|
|
60
60
|
process.exit(1);
|
|
61
61
|
}
|
|
62
62
|
}
|