@cybermem/mcp 0.5.3 ā 0.6.2
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/README.md +1 -1
- package/dist/index.js +204 -28
- package/package.json +29 -28
- package/requirements.txt +2 -0
- package/server.py +347 -0
- package/src/index.ts +228 -0
- package/test_mcp.py +111 -0
- package/tsconfig.json +14 -0
- package/dist/commands/__tests__/backup.test.js +0 -75
- package/dist/commands/__tests__/restore.test.js +0 -70
- package/dist/commands/backup.js +0 -52
- package/dist/commands/deploy.js +0 -242
- package/dist/commands/init.js +0 -65
- package/dist/commands/restore.js +0 -62
- package/dist/templates/ansible/inventory/hosts.ini +0 -3
- package/dist/templates/ansible/playbooks/deploy-cybermem.yml +0 -71
- package/dist/templates/ansible/playbooks/stop-cybermem.yml +0 -17
- package/dist/templates/charts/cybermem/Chart.yaml +0 -6
- package/dist/templates/charts/cybermem/templates/dashboard-deployment.yaml +0 -29
- package/dist/templates/charts/cybermem/templates/dashboard-service.yaml +0 -20
- package/dist/templates/charts/cybermem/templates/openmemory-deployment.yaml +0 -40
- package/dist/templates/charts/cybermem/templates/openmemory-pvc.yaml +0 -10
- package/dist/templates/charts/cybermem/templates/openmemory-service.yaml +0 -13
- package/dist/templates/charts/cybermem/values-vps.yaml +0 -18
- package/dist/templates/charts/cybermem/values.yaml +0 -42
- package/dist/templates/docker-compose.yml +0 -236
- package/dist/templates/envs/local.example +0 -27
- package/dist/templates/envs/rpi.example +0 -27
- package/dist/templates/envs/vps.example +0 -25
- package/dist/templates/mcp-responder/Dockerfile +0 -6
- package/dist/templates/mcp-responder/server.js +0 -22
- package/dist/templates/monitoring/db_exporter/Dockerfile +0 -19
- package/dist/templates/monitoring/db_exporter/exporter.py +0 -313
- package/dist/templates/monitoring/db_exporter/requirements.txt +0 -2
- package/dist/templates/monitoring/grafana/dashboards/cybermem.json +0 -1088
- package/dist/templates/monitoring/grafana/provisioning/dashboards/default.yml +0 -12
- package/dist/templates/monitoring/grafana/provisioning/datasources/prometheus.yml +0 -9
- package/dist/templates/monitoring/log_exporter/Dockerfile +0 -13
- package/dist/templates/monitoring/log_exporter/exporter.py +0 -274
- package/dist/templates/monitoring/log_exporter/requirements.txt +0 -1
- package/dist/templates/monitoring/postgres_exporter/queries.yml +0 -22
- package/dist/templates/monitoring/prometheus/prometheus.yml +0 -22
- package/dist/templates/monitoring/traefik/dynamic/.gitkeep +0 -0
- package/dist/templates/monitoring/traefik/traefik.yml +0 -32
- package/dist/templates/monitoring/vector/vector.toml/vector.yaml +0 -77
- package/dist/templates/monitoring/vector/vector.yaml +0 -106
- package/dist/templates/openmemory/Dockerfile +0 -19
- package/templates/ansible/inventory/hosts.ini +0 -3
- package/templates/ansible/playbooks/deploy-cybermem.yml +0 -71
- package/templates/ansible/playbooks/stop-cybermem.yml +0 -17
- package/templates/charts/cybermem/Chart.yaml +0 -6
- package/templates/charts/cybermem/templates/dashboard-deployment.yaml +0 -29
- package/templates/charts/cybermem/templates/dashboard-service.yaml +0 -20
- package/templates/charts/cybermem/templates/openmemory-deployment.yaml +0 -40
- package/templates/charts/cybermem/templates/openmemory-pvc.yaml +0 -10
- package/templates/charts/cybermem/templates/openmemory-service.yaml +0 -13
- package/templates/charts/cybermem/values-vps.yaml +0 -18
- package/templates/charts/cybermem/values.yaml +0 -42
- package/templates/docker-compose.yml +0 -236
- package/templates/envs/local.example +0 -27
- package/templates/envs/rpi.example +0 -27
- package/templates/envs/vps.example +0 -25
- package/templates/mcp-responder/Dockerfile +0 -6
- package/templates/mcp-responder/server.js +0 -22
- package/templates/monitoring/db_exporter/Dockerfile +0 -19
- package/templates/monitoring/db_exporter/exporter.py +0 -313
- package/templates/monitoring/db_exporter/requirements.txt +0 -2
- package/templates/monitoring/grafana/dashboards/cybermem.json +0 -1088
- package/templates/monitoring/grafana/provisioning/dashboards/default.yml +0 -12
- package/templates/monitoring/grafana/provisioning/datasources/prometheus.yml +0 -9
- package/templates/monitoring/log_exporter/Dockerfile +0 -13
- package/templates/monitoring/log_exporter/exporter.py +0 -274
- package/templates/monitoring/log_exporter/requirements.txt +0 -1
- package/templates/monitoring/postgres_exporter/queries.yml +0 -22
- package/templates/monitoring/prometheus/prometheus.yml +0 -22
- package/templates/monitoring/traefik/dynamic/.gitkeep +0 -0
- package/templates/monitoring/traefik/traefik.yml +0 -32
- package/templates/monitoring/vector/vector.toml/vector.yaml +0 -77
- package/templates/monitoring/vector/vector.yaml +0 -106
- package/templates/openmemory/Dockerfile +0 -19
package/src/index.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
Tool
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import axios from "axios";
|
|
11
|
+
import cors from "cors";
|
|
12
|
+
import dotenv from "dotenv";
|
|
13
|
+
import express from "express";
|
|
14
|
+
|
|
15
|
+
dotenv.config();
|
|
16
|
+
|
|
17
|
+
// Parse CLI args for remote mode
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const getArg = (name: string): string | undefined => {
|
|
20
|
+
const idx = args.indexOf(name);
|
|
21
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const cliUrl = getArg('--url');
|
|
25
|
+
const cliApiKey = getArg('--api-key');
|
|
26
|
+
const cliClientName = getArg('--client-name');
|
|
27
|
+
|
|
28
|
+
// Use CLI args first, then env, then defaults
|
|
29
|
+
// Default to local CyberMem backend (via Traefik on port 8626)
|
|
30
|
+
const API_URL = cliUrl || process.env.CYBERMEM_URL || "http://localhost:8626/memory";
|
|
31
|
+
const API_KEY = cliApiKey || process.env.OM_API_KEY || "";
|
|
32
|
+
|
|
33
|
+
// Track client name per session
|
|
34
|
+
let currentClientName = cliClientName || "cybermem-mcp";
|
|
35
|
+
|
|
36
|
+
const server = new Server(
|
|
37
|
+
{
|
|
38
|
+
name: "cybermem-mcp",
|
|
39
|
+
version: "0.2.0",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
capabilities: {
|
|
43
|
+
tools: {},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const tools: Tool[] = [
|
|
49
|
+
{
|
|
50
|
+
name: "add_memory",
|
|
51
|
+
description: "Store a new memory in CyberMem",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
content: { type: "string" },
|
|
56
|
+
user_id: { type: "string" },
|
|
57
|
+
tags: { type: "array", items: { type: "string" } },
|
|
58
|
+
},
|
|
59
|
+
required: ["content"],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "query_memory",
|
|
64
|
+
description: "Search for relevant memories",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
query: { type: "string" },
|
|
69
|
+
k: { type: "number", default: 5 },
|
|
70
|
+
},
|
|
71
|
+
required: ["query"],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "list_memories",
|
|
76
|
+
description: "List recent memories",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
limit: { type: "number", default: 10 },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "delete_memory",
|
|
86
|
+
description: "Delete a memory by ID",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
id: { type: "string" },
|
|
91
|
+
},
|
|
92
|
+
required: ["id"],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "update_memory",
|
|
97
|
+
description: "Update a memory by ID",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
id: { type: "string" },
|
|
102
|
+
content: { type: "string" },
|
|
103
|
+
tags: { type: "array", items: { type: "string" } },
|
|
104
|
+
metadata: { type: "object" },
|
|
105
|
+
},
|
|
106
|
+
required: ["id"],
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
112
|
+
tools,
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
// Create axios instance
|
|
116
|
+
const apiClient = axios.create({
|
|
117
|
+
baseURL: API_URL,
|
|
118
|
+
headers: {
|
|
119
|
+
"Authorization": `Bearer ${API_KEY}`,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Helper to get client with context
|
|
124
|
+
function getClient(customHeaders: Record<string, string> = {}) {
|
|
125
|
+
// Identity is taken from currentClientName which is updated per-request in SSE mode
|
|
126
|
+
const clientName = customHeaders["X-Client-Name"] || currentClientName;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...apiClient,
|
|
130
|
+
get: (url: string, config?: any) => apiClient.get(url, { ...config, headers: { "X-Client-Name": clientName, ...config?.headers } }),
|
|
131
|
+
post: (url: string, data?: any, config?: any) => apiClient.post(url, data, { ...config, headers: { "X-Client-Name": clientName, ...config?.headers } }),
|
|
132
|
+
put: (url: string, data?: any, config?: any) => apiClient.put(url, data, { ...config, headers: { "X-Client-Name": clientName, ...config?.headers } }),
|
|
133
|
+
patch: (url: string, data?: any, config?: any) => apiClient.patch(url, data, { ...config, headers: { "X-Client-Name": clientName, ...config?.headers } }),
|
|
134
|
+
delete: (url: string, config?: any) => apiClient.delete(url, { ...config, headers: { "X-Client-Name": clientName, ...config?.headers } }),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
139
|
+
const { name, arguments: args } = request.params;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
switch (name) {
|
|
143
|
+
case "add_memory": {
|
|
144
|
+
const response = await getClient().post("/add", args);
|
|
145
|
+
return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
|
|
146
|
+
}
|
|
147
|
+
case "query_memory": {
|
|
148
|
+
const response = await getClient().post("/query", args);
|
|
149
|
+
return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
|
|
150
|
+
}
|
|
151
|
+
case "list_memories": {
|
|
152
|
+
const limit = args?.limit || 10;
|
|
153
|
+
const response = await getClient().get(`/all?l=${limit}`);
|
|
154
|
+
return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
|
|
155
|
+
}
|
|
156
|
+
case "delete_memory": {
|
|
157
|
+
const { id } = args as { id: string };
|
|
158
|
+
await getClient().delete(`/${id}`);
|
|
159
|
+
return { content: [{ type: "text", text: `Memory ${id} deleted` }] };
|
|
160
|
+
}
|
|
161
|
+
case "update_memory": {
|
|
162
|
+
const { id, ...updates } = args as { id: string; [key: string]: any };
|
|
163
|
+
const response = await getClient().patch(`/${id}`, updates);
|
|
164
|
+
return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
168
|
+
}
|
|
169
|
+
} catch (error: any) {
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
172
|
+
isError: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
async function run() {
|
|
178
|
+
const isSse = process.argv.includes("--sse") || !!process.env.PORT;
|
|
179
|
+
|
|
180
|
+
if (isSse) {
|
|
181
|
+
const app = express();
|
|
182
|
+
app.use(cors());
|
|
183
|
+
const port = process.env.PORT || 8627;
|
|
184
|
+
|
|
185
|
+
let transport: SSEServerTransport | null = null;
|
|
186
|
+
|
|
187
|
+
app.get("/sse", async (req, res) => {
|
|
188
|
+
// Extract client name from header
|
|
189
|
+
const clientName = req.headers["x-client-name"] as string;
|
|
190
|
+
if (clientName) {
|
|
191
|
+
currentClientName = clientName;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
transport = new SSEServerTransport("/messages", res);
|
|
195
|
+
await server.connect(transport);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
app.post("/messages", async (req, res) => {
|
|
199
|
+
// Also check headers on messages
|
|
200
|
+
const clientName = req.headers["x-client-name"] as string;
|
|
201
|
+
if (clientName) {
|
|
202
|
+
currentClientName = clientName;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (transport) {
|
|
206
|
+
await transport.handlePostMessage(req, res);
|
|
207
|
+
} else {
|
|
208
|
+
res.status(400).send("Session not established");
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
app.listen(port, () => {
|
|
213
|
+
console.error(`CyberMem MCP Server running on SSE at http://localhost:${port}`);
|
|
214
|
+
console.error(` - SSE endpoint: http://localhost:${port}/sse`);
|
|
215
|
+
console.error(` - Message endpoint: http://localhost:${port}/messages`);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
} else {
|
|
219
|
+
const transport = new StdioServerTransport();
|
|
220
|
+
await server.connect(transport);
|
|
221
|
+
console.error("CyberMem MCP Server running on stdio");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
run().catch((error) => {
|
|
226
|
+
console.error("Fatal error running server:", error);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
});
|
package/test_mcp.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test script for OpenMemory MCP Server
|
|
4
|
+
|
|
5
|
+
Demonstrates MCP server functionality by simulating tool calls.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import httpx
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
OPENMEMORY_URL = "http://localhost/memory"
|
|
14
|
+
OPENMEMORY_API_KEY = "dev-secret-key"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def test_add_memory():
|
|
18
|
+
"""Test adding a memory."""
|
|
19
|
+
print("\nš§Ŗ Test 1: Adding memory...")
|
|
20
|
+
|
|
21
|
+
headers = {
|
|
22
|
+
"Authorization": f"Bearer {OPENMEMORY_API_KEY}",
|
|
23
|
+
"Content-Type": "application/json"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
27
|
+
response = await client.post(
|
|
28
|
+
f"{OPENMEMORY_URL}/add",
|
|
29
|
+
json={
|
|
30
|
+
"content": "MCP server test: Claude Code can now remember conversations and context",
|
|
31
|
+
"metadata": {
|
|
32
|
+
"source": "mcp_test",
|
|
33
|
+
"category": "integration_test",
|
|
34
|
+
"timestamp": "2025-11-30"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
headers=headers
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if response.status_code == 200:
|
|
41
|
+
result = response.json()
|
|
42
|
+
print(f"ā
Memory added successfully!")
|
|
43
|
+
print(f" ID: {result.get('id')}")
|
|
44
|
+
print(f" Chunks: {result.get('chunks')}")
|
|
45
|
+
print(f" Sectors: {', '.join(result.get('sectors', []))}")
|
|
46
|
+
return result.get('id')
|
|
47
|
+
else:
|
|
48
|
+
print(f"ā Failed: {response.status_code} - {response.text}")
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def test_search_memory():
|
|
53
|
+
"""Test searching memories."""
|
|
54
|
+
print("\nš§Ŗ Test 2: Searching memory...")
|
|
55
|
+
|
|
56
|
+
headers = {
|
|
57
|
+
"Authorization": f"Bearer {OPENMEMORY_API_KEY}",
|
|
58
|
+
"Content-Type": "application/json"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
62
|
+
response = await client.post(
|
|
63
|
+
f"{OPENMEMORY_URL}/query",
|
|
64
|
+
json={
|
|
65
|
+
"query": "Claude Code remember conversations",
|
|
66
|
+
"k": 3
|
|
67
|
+
},
|
|
68
|
+
headers=headers
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if response.status_code == 200:
|
|
72
|
+
data = response.json()
|
|
73
|
+
matches = data.get("matches", [])
|
|
74
|
+
print(f"ā
Found {len(matches)} memories:")
|
|
75
|
+
for i, memory in enumerate(matches, 1):
|
|
76
|
+
content = memory.get("content", "")
|
|
77
|
+
score = memory.get("score", 0)
|
|
78
|
+
sector = memory.get("primary_sector", "")
|
|
79
|
+
print(f"\n Result {i} (score: {score:.2f}, sector: {sector}):")
|
|
80
|
+
print(f" {content[:100]}...")
|
|
81
|
+
else:
|
|
82
|
+
print(f"ā Failed: {response.status_code} - {response.text}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def test_mcp_integration():
|
|
86
|
+
"""Run full MCP integration test."""
|
|
87
|
+
print("=" * 60)
|
|
88
|
+
print("OpenMemory MCP Server - Integration Test")
|
|
89
|
+
print("=" * 60)
|
|
90
|
+
|
|
91
|
+
# Test 1: Add memory
|
|
92
|
+
memory_id = await test_add_memory()
|
|
93
|
+
|
|
94
|
+
# Wait a bit for indexing
|
|
95
|
+
await asyncio.sleep(2)
|
|
96
|
+
|
|
97
|
+
# Test 2: Search memory
|
|
98
|
+
await test_search_memory()
|
|
99
|
+
|
|
100
|
+
print("\n" + "=" * 60)
|
|
101
|
+
print("ā
Integration test completed!")
|
|
102
|
+
print("=" * 60)
|
|
103
|
+
print("\nNext steps:")
|
|
104
|
+
print("1. Restart Claude Code to load the MCP server")
|
|
105
|
+
print("2. Try asking Claude Code to remember something")
|
|
106
|
+
print("3. Check Grafana dashboard for audit trail")
|
|
107
|
+
print(" ā http://localhost:3000/d/cybermem-memory")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
asyncio.run(test_mcp_integration())
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const execa_1 = __importDefault(require("execa"));
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const backup_1 = require("../backup");
|
|
9
|
-
// Mock dependencies
|
|
10
|
-
jest.mock('execa');
|
|
11
|
-
jest.mock('fs');
|
|
12
|
-
jest.mock('path', () => ({
|
|
13
|
-
...jest.requireActual('path'),
|
|
14
|
-
resolve: jest.fn((...args) => args.join('/')),
|
|
15
|
-
basename: jest.fn((p) => p.split('/').pop() || ''),
|
|
16
|
-
dirname: jest.fn((p) => p.split('/').slice(0, -1).join('/') || '/'),
|
|
17
|
-
}));
|
|
18
|
-
// Mock console to keep output clean
|
|
19
|
-
const originalConsoleLog = console.log;
|
|
20
|
-
const originalConsoleError = console.error;
|
|
21
|
-
describe('backup command', () => {
|
|
22
|
-
beforeAll(() => {
|
|
23
|
-
console.log = jest.fn();
|
|
24
|
-
console.error = jest.fn();
|
|
25
|
-
});
|
|
26
|
-
afterAll(() => {
|
|
27
|
-
console.log = originalConsoleLog;
|
|
28
|
-
console.error = originalConsoleError;
|
|
29
|
-
});
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
jest.clearAllMocks();
|
|
32
|
-
// Default process.cwd
|
|
33
|
-
jest.spyOn(process, 'cwd').mockReturnValue('/mock/cwd');
|
|
34
|
-
});
|
|
35
|
-
it('should create a backup successfully when openmemory container exists', async () => {
|
|
36
|
-
// Mock docker inspect success
|
|
37
|
-
execa_1.default.mockResolvedValueOnce({ exitCode: 0 });
|
|
38
|
-
// Mock docker run success
|
|
39
|
-
execa_1.default.mockResolvedValueOnce({ exitCode: 0 });
|
|
40
|
-
// Mock fs.existsSync to true (file created)
|
|
41
|
-
fs_1.default.existsSync.mockReturnValue(true);
|
|
42
|
-
// Mock fs.statSync
|
|
43
|
-
fs_1.default.statSync.mockReturnValue({ size: 5 * 1024 * 1024 }); // 5MB
|
|
44
|
-
await (0, backup_1.backup)({});
|
|
45
|
-
// Expect docker inspect check
|
|
46
|
-
expect(execa_1.default).toHaveBeenCalledWith('docker', ['inspect', 'cybermem-openmemory']);
|
|
47
|
-
// Expect docker run tar
|
|
48
|
-
const tarCall = execa_1.default.mock.calls[1];
|
|
49
|
-
expect(tarCall[0]).toBe('docker');
|
|
50
|
-
expect(tarCall[1]).toContain('run');
|
|
51
|
-
expect(tarCall[1]).toContain('tar');
|
|
52
|
-
expect(tarCall[1]).toContain('czf');
|
|
53
|
-
// Check volumes from
|
|
54
|
-
expect(tarCall[1]).toContain('--volumes-from');
|
|
55
|
-
expect(tarCall[1]).toContain('cybermem-openmemory');
|
|
56
|
-
});
|
|
57
|
-
it('should fail if openmemory container does not exist', async () => {
|
|
58
|
-
// Mock inspect failure
|
|
59
|
-
execa_1.default.mockRejectedValueOnce(new Error('No such container'));
|
|
60
|
-
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => { }));
|
|
61
|
-
await (0, backup_1.backup)({});
|
|
62
|
-
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
|
|
63
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
64
|
-
});
|
|
65
|
-
it('should error if backup file is not created', async () => {
|
|
66
|
-
execa_1.default.mockResolvedValue({ exitCode: 0 }); // inspect OK
|
|
67
|
-
execa_1.default.mockResolvedValue({ exitCode: 0 }); // run OK
|
|
68
|
-
// Mock file check false
|
|
69
|
-
fs_1.default.existsSync.mockReturnValue(false);
|
|
70
|
-
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => { }));
|
|
71
|
-
await (0, backup_1.backup)({});
|
|
72
|
-
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Backup failed'));
|
|
73
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const execa_1 = __importDefault(require("execa"));
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const restore_1 = require("../restore");
|
|
9
|
-
jest.mock('execa');
|
|
10
|
-
jest.mock('fs');
|
|
11
|
-
jest.mock('path', () => ({
|
|
12
|
-
...jest.requireActual('path'),
|
|
13
|
-
resolve: jest.fn((...args) => args.join('/')),
|
|
14
|
-
basename: jest.fn((p) => p.split('/').pop() || ''),
|
|
15
|
-
dirname: jest.fn((p) => p.split('/').slice(0, -1).join('/') || '/'),
|
|
16
|
-
}));
|
|
17
|
-
const originalConsoleLog = console.log;
|
|
18
|
-
const originalConsoleError = console.error;
|
|
19
|
-
describe('restore command', () => {
|
|
20
|
-
beforeAll(() => {
|
|
21
|
-
console.log = jest.fn();
|
|
22
|
-
console.error = jest.fn();
|
|
23
|
-
});
|
|
24
|
-
afterAll(() => {
|
|
25
|
-
console.log = originalConsoleLog;
|
|
26
|
-
console.error = originalConsoleError;
|
|
27
|
-
});
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
jest.clearAllMocks();
|
|
30
|
-
jest.spyOn(process, 'cwd').mockReturnValue('/mock/cwd');
|
|
31
|
-
});
|
|
32
|
-
it('should restore successfully', async () => {
|
|
33
|
-
// Mock file exists
|
|
34
|
-
fs_1.default.existsSync.mockReturnValue(true);
|
|
35
|
-
// Mock docker stop (success)
|
|
36
|
-
execa_1.default.mockResolvedValueOnce({ exitCode: 0 });
|
|
37
|
-
// Mock docker run tar (success)
|
|
38
|
-
execa_1.default.mockResolvedValueOnce({ exitCode: 0 });
|
|
39
|
-
// Mock docker start (success)
|
|
40
|
-
execa_1.default.mockResolvedValueOnce({ exitCode: 0 });
|
|
41
|
-
await (0, restore_1.restore)('backup.tar.gz', {});
|
|
42
|
-
// Check docker stop
|
|
43
|
-
expect(execa_1.default).toHaveBeenCalledWith('docker', ['stop', 'cybermem-openmemory']);
|
|
44
|
-
// Check docker run tar
|
|
45
|
-
const tarCall = execa_1.default.mock.calls[1];
|
|
46
|
-
expect(tarCall[0]).toBe('docker');
|
|
47
|
-
expect(tarCall[1]).toContain('run');
|
|
48
|
-
expect(tarCall[1]).toContain('tar xzf /backup/backup.tar.gz -C / && chown -R 1001:1001 /data');
|
|
49
|
-
// Check docker start
|
|
50
|
-
expect(execa_1.default).toHaveBeenCalledWith('docker', ['start', 'cybermem-openmemory']);
|
|
51
|
-
});
|
|
52
|
-
it('should ignore docker stop error (if container not running)', async () => {
|
|
53
|
-
fs_1.default.existsSync.mockReturnValue(true);
|
|
54
|
-
// Mock docker stop FAIL
|
|
55
|
-
execa_1.default.mockRejectedValueOnce(new Error('No such container'));
|
|
56
|
-
// Mock succeeding calls
|
|
57
|
-
execa_1.default.mockResolvedValue({ exitCode: 0 });
|
|
58
|
-
await (0, restore_1.restore)('backup.tar.gz', {});
|
|
59
|
-
// Should still proceed to restore
|
|
60
|
-
expect(execa_1.default).toHaveBeenCalledWith('docker', expect.arrayContaining(['run']));
|
|
61
|
-
expect(execa_1.default).toHaveBeenCalledWith('docker', ['start', 'cybermem-openmemory']);
|
|
62
|
-
});
|
|
63
|
-
it('should fail if backup file missing', async () => {
|
|
64
|
-
fs_1.default.existsSync.mockReturnValue(false);
|
|
65
|
-
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => { }));
|
|
66
|
-
await (0, restore_1.restore)('mia.tar.gz', {});
|
|
67
|
-
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('not found'));
|
|
68
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
69
|
-
});
|
|
70
|
-
});
|
package/dist/commands/backup.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.backup = backup;
|
|
7
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
-
const execa_1 = __importDefault(require("execa"));
|
|
9
|
-
const fs_1 = __importDefault(require("fs"));
|
|
10
|
-
const path_1 = __importDefault(require("path"));
|
|
11
|
-
async function backup(options) {
|
|
12
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
13
|
-
const filename = `cybermem-backup-${timestamp}.tar.gz`;
|
|
14
|
-
const outputPath = path_1.default.resolve(process.cwd(), filename);
|
|
15
|
-
console.log(chalk_1.default.blue(`š¦ Creating backup: ${filename}...`));
|
|
16
|
-
try {
|
|
17
|
-
// Check if container exists
|
|
18
|
-
try {
|
|
19
|
-
await (0, execa_1.default)('docker', ['inspect', 'cybermem-openmemory']);
|
|
20
|
-
}
|
|
21
|
-
catch (e) {
|
|
22
|
-
console.error(chalk_1.default.red('Error: cybermem-openmemory container not found. Is CyberMem installed?'));
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
// Use a transient alpine container to tar the /data volume
|
|
26
|
-
// We mount the current working directory to /backup in the container
|
|
27
|
-
// And we use --volumes-from to access the data volume of the running service
|
|
28
|
-
const cmd = [
|
|
29
|
-
'run', '--rm',
|
|
30
|
-
'--volumes-from', 'cybermem-openmemory',
|
|
31
|
-
'-v', `${process.cwd()}:/backup`,
|
|
32
|
-
'alpine',
|
|
33
|
-
'tar', 'czf', `/backup/${filename}`, '-C', '/', 'data'
|
|
34
|
-
];
|
|
35
|
-
console.log(chalk_1.default.gray(`Running: docker ${cmd.join(' ')}`));
|
|
36
|
-
await (0, execa_1.default)('docker', cmd, { stdio: 'inherit' });
|
|
37
|
-
if (fs_1.default.existsSync(outputPath)) {
|
|
38
|
-
const stats = fs_1.default.statSync(outputPath);
|
|
39
|
-
const sizeMb = (stats.size / 1024 / 1024).toFixed(2);
|
|
40
|
-
console.log(chalk_1.default.green(`\nā
Backup created successfully!`));
|
|
41
|
-
console.log(`File: ${chalk_1.default.bold(outputPath)}`);
|
|
42
|
-
console.log(`Size: ${sizeMb} MB`);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
throw new Error('Backup file not found after generation.');
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
console.error(chalk_1.default.red('Backup failed:'), error);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
}
|