@cybermem/cli 0.6.13 → 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 +41 -56
- 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 +41 -56
- 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
|
@@ -0,0 +1,141 @@
|
|
|
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.upgrade = upgrade;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const execa_1 = __importDefault(require("execa"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
async function upgrade(options) {
|
|
14
|
+
let target = "local";
|
|
15
|
+
if (options.rpi)
|
|
16
|
+
target = "rpi";
|
|
17
|
+
if (options.vps)
|
|
18
|
+
target = "vps";
|
|
19
|
+
console.log(chalk_1.default.blue(`Upgrading CyberMem (${target})...`));
|
|
20
|
+
try {
|
|
21
|
+
if (target === "local") {
|
|
22
|
+
console.log(chalk_1.default.blue("Pulling latest Docker images..."));
|
|
23
|
+
// Re-use logic: find compose file from template if needed, OR use installed ~/.cybermem one?
|
|
24
|
+
// "upgrade" implies updating running instance.
|
|
25
|
+
// running instance uses ~/.cybermem/docker-compose.yml (if init copied it?)
|
|
26
|
+
// Wait, init uses template directly?
|
|
27
|
+
// "deploy/init" logic for local:
|
|
28
|
+
// "docker-compose -f composeFile ...". composeFile was from templateDir.
|
|
29
|
+
// It did NOT copy compose file to ~/.cybermem for local.
|
|
30
|
+
// It uses the installed package's template.
|
|
31
|
+
// So for upgrade (which might be run from newer CLI version), we use the NEW CLI's template.
|
|
32
|
+
// Resolve Template Directory (Same logic as init)
|
|
33
|
+
let templateDir = path_1.default.resolve(__dirname, "../../templates");
|
|
34
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
35
|
+
templateDir = path_1.default.resolve(__dirname, "../../../templates");
|
|
36
|
+
}
|
|
37
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
38
|
+
templateDir = path_1.default.resolve(process.cwd(), "packages/cli/templates");
|
|
39
|
+
}
|
|
40
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
41
|
+
templateDir = path_1.default.resolve(__dirname, "../templates");
|
|
42
|
+
}
|
|
43
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
44
|
+
throw new Error(`Templates not found at ${templateDir}.`);
|
|
45
|
+
}
|
|
46
|
+
const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
|
|
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
|
+
// Pull images
|
|
52
|
+
await (0, execa_1.default)("docker-compose", [
|
|
53
|
+
"-f",
|
|
54
|
+
composeFile,
|
|
55
|
+
"--env-file",
|
|
56
|
+
envFile,
|
|
57
|
+
"--project-name",
|
|
58
|
+
"cybermem",
|
|
59
|
+
"pull",
|
|
60
|
+
], {
|
|
61
|
+
stdio: "inherit",
|
|
62
|
+
env: {
|
|
63
|
+
...process.env,
|
|
64
|
+
DATA_DIR: dataDir,
|
|
65
|
+
CYBERMEM_ENV_PATH: envFile,
|
|
66
|
+
OM_API_KEY: "", // Local bypass
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
// Up (recreate)
|
|
70
|
+
console.log(chalk_1.default.blue("Restarting services..."));
|
|
71
|
+
await (0, execa_1.default)("docker-compose", [
|
|
72
|
+
"-f",
|
|
73
|
+
composeFile,
|
|
74
|
+
"--env-file",
|
|
75
|
+
envFile,
|
|
76
|
+
"--project-name",
|
|
77
|
+
"cybermem",
|
|
78
|
+
"up",
|
|
79
|
+
"-d",
|
|
80
|
+
"--remove-orphans",
|
|
81
|
+
], {
|
|
82
|
+
stdio: "inherit",
|
|
83
|
+
env: {
|
|
84
|
+
...process.env,
|
|
85
|
+
DATA_DIR: dataDir,
|
|
86
|
+
CYBERMEM_ENV_PATH: envFile,
|
|
87
|
+
OM_API_KEY: "", // Local bypass
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
console.log(chalk_1.default.green("✅ Upgrade complete!"));
|
|
91
|
+
}
|
|
92
|
+
else if (target === "rpi" || target === "vps") {
|
|
93
|
+
// Remote upgrade
|
|
94
|
+
let sshHost = options.host;
|
|
95
|
+
if (!sshHost) {
|
|
96
|
+
const answers = await inquirer_1.default.prompt([
|
|
97
|
+
{
|
|
98
|
+
type: "input",
|
|
99
|
+
name: "host",
|
|
100
|
+
message: "Enter SSH Host (e.g. pi@raspberrypi.local):",
|
|
101
|
+
validate: (input) => input.includes("@") ? true : "Format must be user@host",
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
sshHost = answers.host;
|
|
105
|
+
}
|
|
106
|
+
console.log(chalk_1.default.blue(`Upgrading remote host ${sshHost}...`));
|
|
107
|
+
// 1. Upload NEW docker-compose.yml (from this CLI version)
|
|
108
|
+
// Resolve Template Directory
|
|
109
|
+
let templateDir = path_1.default.resolve(__dirname, "../../templates");
|
|
110
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
111
|
+
templateDir = path_1.default.resolve(__dirname, "../../../templates");
|
|
112
|
+
}
|
|
113
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
114
|
+
templateDir = path_1.default.resolve(process.cwd(), "packages/cli/templates");
|
|
115
|
+
}
|
|
116
|
+
if (!fs_1.default.existsSync(templateDir)) {
|
|
117
|
+
templateDir = path_1.default.resolve(__dirname, "../templates");
|
|
118
|
+
}
|
|
119
|
+
const composeFile = path_1.default.join(templateDir, "docker-compose.yml");
|
|
120
|
+
console.log(chalk_1.default.blue("Uploading newest definitions..."));
|
|
121
|
+
await (0, execa_1.default)("scp", [
|
|
122
|
+
composeFile,
|
|
123
|
+
`${sshHost}:~/.cybermem/docker-compose.yml`,
|
|
124
|
+
]);
|
|
125
|
+
// 2. Pull and Up on Remote
|
|
126
|
+
const remoteCmd = `
|
|
127
|
+
export CYBERMEM_ENV_PATH=~/.cybermem/.env
|
|
128
|
+
export DATA_DIR=~/.cybermem/data
|
|
129
|
+
export DOCKER_DEFAULT_PLATFORM=linux/arm64
|
|
130
|
+
docker-compose -f ~/.cybermem/docker-compose.yml pull
|
|
131
|
+
docker-compose -f ~/.cybermem/docker-compose.yml up -d --remove-orphans
|
|
132
|
+
`;
|
|
133
|
+
await (0, execa_1.default)("ssh", [sshHost, remoteCmd], { stdio: "inherit" });
|
|
134
|
+
console.log(chalk_1.default.green("✅ Remote upgrade complete!"));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
console.error(chalk_1.default.red("Upgrade failed:"), error);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -3,33 +3,54 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
5
|
const backup_1 = require("./commands/backup");
|
|
6
|
-
const
|
|
6
|
+
const dashboard_1 = require("./commands/dashboard");
|
|
7
|
+
const init_1 = require("./commands/init");
|
|
8
|
+
const login_1 = require("./commands/login");
|
|
7
9
|
const reset_1 = require("./commands/reset");
|
|
8
10
|
const restore_1 = require("./commands/restore");
|
|
11
|
+
const upgrade_1 = require("./commands/upgrade");
|
|
9
12
|
const program = new commander_1.Command();
|
|
10
13
|
program
|
|
11
|
-
.name(
|
|
12
|
-
.description(
|
|
13
|
-
.version(
|
|
14
|
-
//
|
|
14
|
+
.name("mcp")
|
|
15
|
+
.description("CyberMem - Deploy your AI memory server in one command")
|
|
16
|
+
.version("1.0.0");
|
|
17
|
+
// Command: Login
|
|
15
18
|
program
|
|
16
|
-
.command(
|
|
17
|
-
.description(
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
.option('--remote-access', 'Enable Tailscale Funnel for HTTPS remote access')
|
|
21
|
-
.action(deploy_1.deploy);
|
|
19
|
+
.command("login")
|
|
20
|
+
.description("Login to CyberMem via GitHub (OAuth)")
|
|
21
|
+
.action(login_1.login);
|
|
22
|
+
// Command: Init (formerly deploy)
|
|
22
23
|
program
|
|
23
|
-
.command(
|
|
24
|
-
.description(
|
|
24
|
+
.command("init")
|
|
25
|
+
.description("Initialize CyberMem (Scaffold & Start)")
|
|
26
|
+
.option("--rpi", "Deploy to Raspberry Pi")
|
|
27
|
+
.option("--vps", "Deploy to VPS/Cloud server")
|
|
28
|
+
.option("--remote-access", "Enable Tailscale Funnel for HTTPS remote access")
|
|
29
|
+
.action(init_1.init);
|
|
30
|
+
// Command: Upgrade
|
|
31
|
+
program
|
|
32
|
+
.command("upgrade")
|
|
33
|
+
.description("Upgrade CyberMem instance (pull latest images)")
|
|
34
|
+
.option("--local", "Upgrade local instance (default)")
|
|
35
|
+
.option("--rpi", "Upgrade remote RPi")
|
|
36
|
+
.option("--vps", "Upgrade remote VPS")
|
|
37
|
+
.option("--host <host>", "SSH host for remote upgrade (e.g. pi@raspberrypi.local)")
|
|
38
|
+
.action(upgrade_1.upgrade);
|
|
39
|
+
program
|
|
40
|
+
.command("backup")
|
|
41
|
+
.description("Backup CyberMem data to a tarball")
|
|
25
42
|
.action(backup_1.backup);
|
|
26
43
|
program
|
|
27
|
-
.command(
|
|
28
|
-
.description(
|
|
29
|
-
.argument(
|
|
44
|
+
.command("restore")
|
|
45
|
+
.description("Restore CyberMem data from a backup file")
|
|
46
|
+
.argument("<file>", "Backup file to restore")
|
|
30
47
|
.action(restore_1.restore);
|
|
31
48
|
program
|
|
32
|
-
.command(
|
|
33
|
-
.description(
|
|
49
|
+
.command("reset")
|
|
50
|
+
.description("Reset (wipe) the CyberMem database - DESTRUCTIVE!")
|
|
34
51
|
.action(reset_1.reset);
|
|
52
|
+
program
|
|
53
|
+
.command("dashboard")
|
|
54
|
+
.description("Open the CyberMem dashboard and check stack status")
|
|
55
|
+
.action(dashboard_1.dashboard);
|
|
35
56
|
program.parse(process.argv);
|
|
@@ -97,7 +97,7 @@ function isLocalRequest(req) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// ForwardAuth handler
|
|
100
|
-
const server = http.createServer((req, res) => {
|
|
100
|
+
const server = http.createServer(async (req, res) => {
|
|
101
101
|
// Health check
|
|
102
102
|
if (req.url === "/health") {
|
|
103
103
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -108,9 +108,22 @@ const server = http.createServer((req, res) => {
|
|
|
108
108
|
const authHeader = req.headers["authorization"];
|
|
109
109
|
const apiKeyHeader = req.headers["x-api-key"];
|
|
110
110
|
|
|
111
|
-
// 1. Check
|
|
111
|
+
// 1. Check Bearer token (JWT or API Key)
|
|
112
112
|
if (authHeader?.startsWith("Bearer ")) {
|
|
113
113
|
const token = authHeader.substring(7);
|
|
114
|
+
|
|
115
|
+
// 1a. Check if Bearer token is actually an API key (MCP clients like Claude Desktop)
|
|
116
|
+
const expectedKey = loadApiKey();
|
|
117
|
+
if (expectedKey && token === expectedKey) {
|
|
118
|
+
console.log("Auth OK: Bearer API Key");
|
|
119
|
+
res.writeHead(200, {
|
|
120
|
+
"X-Auth-Method": "bearer-api-key",
|
|
121
|
+
});
|
|
122
|
+
res.end();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 1b. Try JWT RS256 validation
|
|
114
127
|
const payload = validateJwt(token);
|
|
115
128
|
|
|
116
129
|
if (payload) {
|
|
@@ -124,6 +137,45 @@ const server = http.createServer((req, res) => {
|
|
|
124
137
|
res.end();
|
|
125
138
|
return;
|
|
126
139
|
}
|
|
140
|
+
|
|
141
|
+
// 1c. Try GitHub OAuth token verification
|
|
142
|
+
try {
|
|
143
|
+
const https = require("https");
|
|
144
|
+
const ghRes = await new Promise((resolve, reject) => {
|
|
145
|
+
const req = https.get(
|
|
146
|
+
"https://api.github.com/user",
|
|
147
|
+
{
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Bearer ${token}`,
|
|
150
|
+
"User-Agent": "CyberMem-Auth-Sidecar/1.0",
|
|
151
|
+
Accept: "application/vnd.github+json",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
(res) => {
|
|
155
|
+
let data = "";
|
|
156
|
+
res.on("data", (chunk) => (data += chunk));
|
|
157
|
+
res.on("end", () => resolve({ status: res.statusCode, data }));
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
req.on("error", reject);
|
|
161
|
+
req.end();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (ghRes.status === 200) {
|
|
165
|
+
const user = JSON.parse(ghRes.data);
|
|
166
|
+
console.log(`Auth OK: GitHub OAuth (${user.login})`);
|
|
167
|
+
res.writeHead(200, {
|
|
168
|
+
"X-User-Id": String(user.id),
|
|
169
|
+
"X-User-Email": user.email || "",
|
|
170
|
+
"X-User-Name": user.login,
|
|
171
|
+
"X-Auth-Method": "github-oauth",
|
|
172
|
+
});
|
|
173
|
+
res.end();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
// GitHub verification failed, continue to other methods
|
|
178
|
+
}
|
|
127
179
|
}
|
|
128
180
|
|
|
129
181
|
// 2. Check API Key (deprecated fallback)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json
|
|
1
2
|
# CyberMem - OpenMemory + DevOps monitoring stack
|
|
2
3
|
# For local development only
|
|
3
4
|
# Production deployment: use Helm chart (charts/cybermem/)
|
|
@@ -24,28 +25,53 @@ services:
|
|
|
24
25
|
- traefik-logs:/var/log/traefik
|
|
25
26
|
labels:
|
|
26
27
|
- traefik.enable=true
|
|
28
|
+
- traefik.http.middlewares.auth-check.forwardauth.address=http://auth-sidecar:3001/auth
|
|
29
|
+
- traefik.http.middlewares.auth-check.forwardauth.authResponseHeaders=X-User-Id,X-User-Email,X-User-Name,X-Auth-Method
|
|
27
30
|
restart: unless-stopped
|
|
28
31
|
|
|
29
32
|
# MCP Server (Node.js/SDK)
|
|
30
33
|
mcp-server:
|
|
34
|
+
build:
|
|
35
|
+
context: ../../mcp
|
|
36
|
+
dockerfile: Dockerfile
|
|
31
37
|
image: ghcr.io/mikhailkogan17/cybermem-mcp:latest
|
|
38
|
+
# Note: platform omitted - docker auto-detects (amd64 for CI, arm64 for RPi)
|
|
32
39
|
restart: unless-stopped
|
|
33
40
|
container_name: cybermem-mcp
|
|
34
41
|
environment:
|
|
42
|
+
OM_TIER: "hybrid"
|
|
35
43
|
PORT: "8080"
|
|
44
|
+
OM_PORT: "0"
|
|
36
45
|
OM_DB_PATH: /data/openmemory.sqlite
|
|
37
46
|
volumes:
|
|
38
47
|
- ${CYBERMEM_ENV_PATH:-${HOME}/.cybermem/.env}:/.env
|
|
39
48
|
- ${HOME}/.cybermem/data:/data
|
|
49
|
+
depends_on:
|
|
50
|
+
- traefik
|
|
51
|
+
- auth-sidecar
|
|
40
52
|
labels:
|
|
41
53
|
- traefik.enable=true
|
|
42
|
-
|
|
54
|
+
# Middleware definitions (must be on a service that starts BEFORE routes reference them)
|
|
55
|
+
- traefik.http.middlewares.strip-cybermem.stripprefix.prefixes=/cybermem
|
|
56
|
+
# Authenticated routes
|
|
57
|
+
- traefik.http.routers.mcp.rule=PathPrefix(`/cybermem/mcp`) || PathPrefix(`/cybermem/sse`) || PathPrefix(`/mcp`) || PathPrefix(`/sse`)
|
|
43
58
|
- traefik.http.routers.mcp.entrypoints=web
|
|
44
|
-
- traefik.http.
|
|
59
|
+
- traefik.http.routers.mcp.priority=100
|
|
60
|
+
- traefik.http.routers.mcp.service=mcp-service
|
|
61
|
+
- traefik.http.routers.mcp.middlewares=auth-check,strip-cybermem
|
|
62
|
+
# Public routes (Health/Metrics)
|
|
63
|
+
- traefik.http.routers.mcp-public.rule=PathPrefix(`/cybermem/health`) || PathPrefix(`/cybermem/metrics`) || PathPrefix(`/health`) || PathPrefix(`/metrics`)
|
|
64
|
+
- traefik.http.routers.mcp-public.entrypoints=web
|
|
65
|
+
- traefik.http.routers.mcp-public.priority=100
|
|
66
|
+
- traefik.http.routers.mcp-public.service=mcp-service
|
|
67
|
+
- traefik.http.routers.mcp-public.middlewares=strip-cybermem
|
|
68
|
+
- traefik.http.services.mcp-service.loadbalancer.server.port=8080
|
|
45
69
|
# Legacy API support (for simple REST clients)
|
|
46
|
-
- traefik.http.routers.legacy-api.rule=
|
|
70
|
+
- traefik.http.routers.legacy-api.rule=PathPrefix(`/cybermem/add`) || PathPrefix(`/cybermem/query`) || PathPrefix(`/cybermem/all`) || PathPrefix(`/add`) || PathPrefix(`/query`) || PathPrefix(`/all`)
|
|
47
71
|
- traefik.http.routers.legacy-api.entrypoints=web
|
|
48
|
-
- traefik.http.
|
|
72
|
+
- traefik.http.routers.legacy-api.priority=100
|
|
73
|
+
- traefik.http.routers.legacy-api.service=mcp-service
|
|
74
|
+
- traefik.http.routers.legacy-api.middlewares=auth-check,strip-cybermem
|
|
49
75
|
|
|
50
76
|
# Auth sidecar for JWT/API key validation (ForwardAuth)
|
|
51
77
|
auth-sidecar:
|
|
@@ -58,9 +84,6 @@ services:
|
|
|
58
84
|
- ${CYBERMEM_ENV_PATH:-${HOME}/.cybermem/.env}:/.env:ro
|
|
59
85
|
labels:
|
|
60
86
|
- traefik.enable=true
|
|
61
|
-
# ForwardAuth middleware
|
|
62
|
-
- traefik.http.middlewares.auth-check.forwardauth.address=http://auth-sidecar:3001/auth
|
|
63
|
-
- traefik.http.middlewares.auth-check.forwardauth.authResponseHeaders=X-User-Id,X-User-Email,X-User-Name,X-Auth-Method
|
|
64
87
|
healthcheck:
|
|
65
88
|
test:
|
|
66
89
|
[
|
|
@@ -76,9 +99,8 @@ services:
|
|
|
76
99
|
retries: 3
|
|
77
100
|
restart: unless-stopped
|
|
78
101
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
# SQLite stored at ~/.cybermem/data/openmemory.sqlite
|
|
102
|
+
# Memory engine (MCP + Persistence)
|
|
103
|
+
# Uses embedded SQLite stored at ~/.cybermem/data/openmemory.sqlite
|
|
82
104
|
|
|
83
105
|
db-exporter:
|
|
84
106
|
image: ghcr.io/mikhailkogan17/cybermem-db_exporter:latest
|
|
@@ -130,22 +152,6 @@ services:
|
|
|
130
152
|
profiles:
|
|
131
153
|
- postgres
|
|
132
154
|
|
|
133
|
-
postgres-exporter:
|
|
134
|
-
image: prometheuscommunity/postgres-exporter:v0.15.0
|
|
135
|
-
container_name: cybermem-postgres-exporter
|
|
136
|
-
environment:
|
|
137
|
-
DATA_SOURCE_NAME: postgresql://${PG_USER:-openmemory}:${PG_PASSWORD:-postgres}@postgres:5432/${PG_DB:-openmemory}?sslmode=disable
|
|
138
|
-
PG_EXPORTER_EXTEND_QUERY_PATH: /queries.yml
|
|
139
|
-
ports:
|
|
140
|
-
- "9187:9187"
|
|
141
|
-
volumes:
|
|
142
|
-
- ./monitoring/postgres_exporter/queries.yml:/queries.yml:ro
|
|
143
|
-
restart: unless-stopped
|
|
144
|
-
depends_on:
|
|
145
|
-
- postgres
|
|
146
|
-
profiles:
|
|
147
|
-
- postgres
|
|
148
|
-
|
|
149
155
|
ollama:
|
|
150
156
|
image: ollama/ollama:latest
|
|
151
157
|
container_name: cybermem-ollama
|
|
@@ -157,55 +163,37 @@ services:
|
|
|
157
163
|
profiles:
|
|
158
164
|
- ollama
|
|
159
165
|
|
|
160
|
-
prometheus:
|
|
161
|
-
image: prom/prometheus:v2.48.0
|
|
162
|
-
container_name: cybermem-prometheus
|
|
163
|
-
command:
|
|
164
|
-
- --config.file=/etc/prometheus/prometheus.yml
|
|
165
|
-
- --storage.tsdb.path=/prometheus
|
|
166
|
-
- --storage.tsdb.retention.time=${PROM_RETENTION:-7d}
|
|
167
|
-
- --web.console.libraries=/usr/share/prometheus/console_libraries
|
|
168
|
-
- --web.console.templates=/usr/share/prometheus/consoles
|
|
169
|
-
ports:
|
|
170
|
-
- "9092:9090"
|
|
171
|
-
volumes:
|
|
172
|
-
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
173
|
-
- prometheus-data:/prometheus
|
|
174
|
-
restart: unless-stopped
|
|
175
|
-
depends_on:
|
|
176
|
-
- db-exporter
|
|
177
|
-
|
|
178
166
|
dashboard:
|
|
179
167
|
image: ghcr.io/mikhailkogan17/cybermem-dashboard:latest
|
|
168
|
+
# Note: platform omitted - docker auto-detects (amd64 for CI, arm64 for RPi)
|
|
180
169
|
container_name: cybermem-dashboard
|
|
181
170
|
environment:
|
|
182
171
|
DB_EXPORTER_URL: http://db-exporter:8000
|
|
183
|
-
|
|
184
|
-
|
|
172
|
+
# Required for Health Check to find the MCP API internally
|
|
173
|
+
CYBERMEM_URL: http://mcp-server:8080
|
|
174
|
+
OM_DB_PATH: /data/openmemory.sqlite
|
|
185
175
|
OM_API_KEY: ${OM_API_KEY:-dev-secret-key}
|
|
186
|
-
# WATCHPACK_POLLING: "true" # Enable if hot reload blocks (high CPU usage)
|
|
187
176
|
ports:
|
|
188
177
|
- "3000:3000"
|
|
189
178
|
volumes:
|
|
190
|
-
-
|
|
179
|
+
- ${HOME}/.cybermem/data:/data
|
|
191
180
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
192
181
|
- ${CYBERMEM_ENV_PATH:-${HOME}/.cybermem/.env}:/app/shared.env
|
|
193
182
|
labels:
|
|
194
183
|
- traefik.enable=true
|
|
195
184
|
# Dashboard route: /cybermem -> dashboard on port 3000
|
|
196
185
|
- traefik.http.routers.dashboard.entrypoints=web
|
|
197
|
-
- traefik.http.routers.dashboard.rule=PathPrefix(`/cybermem`)
|
|
198
|
-
- traefik.http.routers.dashboard.
|
|
199
|
-
- traefik.http.middlewares
|
|
186
|
+
- traefik.http.routers.dashboard.rule=PathPrefix(`/cybermem`) && !PathPrefix(`/cybermem/mcp`) && !PathPrefix(`/cybermem/health`)
|
|
187
|
+
- traefik.http.routers.dashboard.priority=10
|
|
188
|
+
- traefik.http.routers.dashboard.middlewares=auth-check,strip-cybermem
|
|
200
189
|
- traefik.http.services.dashboard.loadbalancer.server.port=3000
|
|
201
190
|
restart: unless-stopped
|
|
202
191
|
depends_on:
|
|
203
|
-
- prometheus
|
|
204
192
|
- db-exporter
|
|
205
193
|
|
|
206
194
|
volumes:
|
|
207
195
|
openmemory-data:
|
|
208
|
-
name: cybermem-
|
|
196
|
+
name: cybermem-data
|
|
209
197
|
driver: local
|
|
210
198
|
postgres-data:
|
|
211
199
|
name: cybermem-postgres-data
|
|
@@ -213,9 +201,6 @@ volumes:
|
|
|
213
201
|
ollama-models:
|
|
214
202
|
name: cybermem-ollama-models
|
|
215
203
|
driver: local
|
|
216
|
-
prometheus-data:
|
|
217
|
-
name: cybermem-prometheus-data
|
|
218
|
-
driver: local
|
|
219
204
|
traefik-logs:
|
|
220
205
|
name: cybermem-traefik-logs
|
|
221
206
|
driver: local
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Environment: LOCAL (Dev)
|
|
2
|
+
# No API key required for localhost access.
|
|
3
|
+
# Used by: npx @cybermem/cli init --local
|
|
4
|
+
|
|
5
|
+
# Application Settings
|
|
6
|
+
LOG_LEVEL=info
|
|
7
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
8
|
+
|
|
9
|
+
# Database (SQLite)
|
|
10
|
+
# OM_DB_PATH is mounted to /data/openmemory.sqlite in Docker
|
|
11
|
+
|
|
12
|
+
# Security
|
|
13
|
+
# OM_API_KEY is intentionally empty for local development to bypass auth.
|
|
14
|
+
OM_API_KEY=
|
|
15
|
+
|
|
16
|
+
# Optional: Add your OpenAI Key if using cloud embeddings
|
|
17
|
+
# OPENAI_API_KEY=
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Environment: RPi + Tailscale (Remote Access)
|
|
2
|
+
# Secured via Tailscale Funnel.
|
|
3
|
+
# Used by: npx @cybermem/cli init --rpi --remote-access
|
|
4
|
+
|
|
5
|
+
# Application Settings
|
|
6
|
+
LOG_LEVEL=info
|
|
7
|
+
|
|
8
|
+
# Database (SQLite)
|
|
9
|
+
# OM_DB_PATH is mounted to /data/openmemory.sqlite in Docker
|
|
10
|
+
|
|
11
|
+
# Security
|
|
12
|
+
# GENERATED AUTOMATICALLY BY CLI
|
|
13
|
+
OM_API_KEY=
|
|
14
|
+
|
|
15
|
+
# Tailscale Configuration
|
|
16
|
+
# Ensure "tailscale serve" is running (handled by CLI)
|
|
17
|
+
TS_HOSTNAME=raspberrypi
|
|
18
|
+
|
|
19
|
+
# Dashboard
|
|
20
|
+
NEXT_PUBLIC_APP_URL=https://raspberrypi.tailnet-name.ts.net
|
|
21
|
+
|
|
22
|
+
# Optional: Local LLM
|
|
23
|
+
OLLAMA_URL=http://ollama:11434
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Environment: RPi (Local Network)
|
|
2
|
+
# Internal API key required.
|
|
3
|
+
# Used by: npx @cybermem/cli init --rpi
|
|
4
|
+
|
|
5
|
+
# Application Settings
|
|
6
|
+
LOG_LEVEL=info
|
|
7
|
+
|
|
8
|
+
# Database (SQLite)
|
|
9
|
+
# OM_DB_PATH is mounted to /data/openmemory.sqlite in Docker
|
|
10
|
+
|
|
11
|
+
# Security
|
|
12
|
+
# GENERATED AUTOMATICALLY BY CLI
|
|
13
|
+
# DO NOT SHARE THIS KEY. It is for internal service communication.
|
|
14
|
+
OM_API_KEY=
|
|
15
|
+
|
|
16
|
+
# Dashboard
|
|
17
|
+
# Clients connecting via MCP must use OAuth or this key (if configured for direct access).
|
|
18
|
+
# OAUTH_CLIENT_ID=...
|
|
19
|
+
# OAUTH_CLIENT_SECRET=...
|
|
20
|
+
|
|
21
|
+
# Optional: Local LLM
|
|
22
|
+
OLLAMA_URL=http://ollama:11434
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Environment: VPS (Public Cloud)
|
|
2
|
+
# High security setup.
|
|
3
|
+
# Used by: npx @cybermem/cli init --vps
|
|
4
|
+
|
|
5
|
+
# Application Settings
|
|
6
|
+
LOG_LEVEL=warn
|
|
7
|
+
# Set your domain here
|
|
8
|
+
NEXT_PUBLIC_APP_URL=https://cybermem.yourdomain.com
|
|
9
|
+
|
|
10
|
+
# Database (PostgreSQL recommended for VPS, but SQLite default)
|
|
11
|
+
# To use Postgres:
|
|
12
|
+
# DB_TYPE=postgres
|
|
13
|
+
# POSTGRES_URL=postgresql://user:pass@db:5432/cybermem
|
|
14
|
+
|
|
15
|
+
# Security
|
|
16
|
+
# GENERATED AUTOMATICALLY BY CLI
|
|
17
|
+
OM_API_KEY=
|
|
18
|
+
|
|
19
|
+
# Auth Sidecar (JWT)
|
|
20
|
+
# Generate a random secret: openssl rand -hex 32
|
|
21
|
+
JWT_SECRET=
|
|
22
|
+
JWT_PRIVATE_KEY=
|
|
23
|
+
|
|
24
|
+
# GitHub OAuth (Recommended for dashboard access)
|
|
25
|
+
OAUTH_CLIENT_ID=
|
|
26
|
+
OAUTH_CLIENT_SECRET=
|
|
27
|
+
|
|
28
|
+
# OpenAI (Recommended for VPS performance)
|
|
29
|
+
OPENAI_API_KEY=
|
|
@@ -123,11 +123,16 @@ def collect_metrics():
|
|
|
123
123
|
FROM memories
|
|
124
124
|
GROUP BY user_id
|
|
125
125
|
""")
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
rows = cursor.fetchall()
|
|
127
|
+
if not rows:
|
|
128
|
+
# Emit a default anonymous:0 if no data
|
|
129
|
+
memories_total.labels(client="anonymous").set(0)
|
|
130
|
+
else:
|
|
131
|
+
for row in rows:
|
|
132
|
+
client = row["client"] or "anonymous"
|
|
133
|
+
memories_total.labels(client=client).set(row["count"])
|
|
129
134
|
|
|
130
|
-
logger.debug(f"Collected total memories for {
|
|
135
|
+
logger.debug(f"Collected total memories for {len(rows)} clients")
|
|
131
136
|
|
|
132
137
|
# Metric 2: Recent memories (24h)
|
|
133
138
|
# Note: created_at is stored as milliseconds since epoch
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cybermem/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "CyberMem — Universal Long-Term Memory for AI Agents",
|
|
5
5
|
"homepage": "https://cybermem.dev",
|
|
6
6
|
"repository": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc && cp -r templates dist/",
|
|
20
|
+
"lint": "eslint src/ --ext .ts",
|
|
20
21
|
"start": "ts-node src/index.ts",
|
|
21
22
|
"test:e2e": "ts-node e2e/test-mcp.ts",
|
|
22
23
|
"prepublishOnly": "npm run build"
|
|
@@ -34,16 +35,21 @@
|
|
|
34
35
|
},
|
|
35
36
|
"type": "commonjs",
|
|
36
37
|
"dependencies": {
|
|
38
|
+
"@types/open": "^6.1.0",
|
|
37
39
|
"chalk": "^4.1.2",
|
|
38
40
|
"commander": "^9.0.0",
|
|
39
41
|
"dotenv": "^16.0.0",
|
|
40
42
|
"execa": "^5.1.1",
|
|
41
43
|
"inquirer": "^8.2.0",
|
|
44
|
+
"open": "^11.0.0",
|
|
42
45
|
"ora": "^5.4.1"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@types/inquirer": "^9.0.3",
|
|
46
49
|
"@types/node": "^18.0.0",
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
|
51
|
+
"@typescript-eslint/parser": "^8.52.0",
|
|
52
|
+
"eslint": "^9.39.2",
|
|
47
53
|
"ts-node": "^10.9.1",
|
|
48
54
|
"typescript": "^5.0.0"
|
|
49
55
|
}
|