@cybermem/mcp 0.5.3 → 0.6.0

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 (80) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +203 -28
  3. package/package.json +29 -28
  4. package/requirements.txt +2 -0
  5. package/server.py +347 -0
  6. package/src/index.ts +227 -0
  7. package/test_mcp.py +111 -0
  8. package/tsconfig.json +14 -0
  9. package/dist/commands/__tests__/backup.test.js +0 -75
  10. package/dist/commands/__tests__/restore.test.js +0 -70
  11. package/dist/commands/backup.js +0 -52
  12. package/dist/commands/deploy.js +0 -242
  13. package/dist/commands/init.js +0 -65
  14. package/dist/commands/restore.js +0 -62
  15. package/dist/templates/ansible/inventory/hosts.ini +0 -3
  16. package/dist/templates/ansible/playbooks/deploy-cybermem.yml +0 -71
  17. package/dist/templates/ansible/playbooks/stop-cybermem.yml +0 -17
  18. package/dist/templates/charts/cybermem/Chart.yaml +0 -6
  19. package/dist/templates/charts/cybermem/templates/dashboard-deployment.yaml +0 -29
  20. package/dist/templates/charts/cybermem/templates/dashboard-service.yaml +0 -20
  21. package/dist/templates/charts/cybermem/templates/openmemory-deployment.yaml +0 -40
  22. package/dist/templates/charts/cybermem/templates/openmemory-pvc.yaml +0 -10
  23. package/dist/templates/charts/cybermem/templates/openmemory-service.yaml +0 -13
  24. package/dist/templates/charts/cybermem/values-vps.yaml +0 -18
  25. package/dist/templates/charts/cybermem/values.yaml +0 -42
  26. package/dist/templates/docker-compose.yml +0 -236
  27. package/dist/templates/envs/local.example +0 -27
  28. package/dist/templates/envs/rpi.example +0 -27
  29. package/dist/templates/envs/vps.example +0 -25
  30. package/dist/templates/mcp-responder/Dockerfile +0 -6
  31. package/dist/templates/mcp-responder/server.js +0 -22
  32. package/dist/templates/monitoring/db_exporter/Dockerfile +0 -19
  33. package/dist/templates/monitoring/db_exporter/exporter.py +0 -313
  34. package/dist/templates/monitoring/db_exporter/requirements.txt +0 -2
  35. package/dist/templates/monitoring/grafana/dashboards/cybermem.json +0 -1088
  36. package/dist/templates/monitoring/grafana/provisioning/dashboards/default.yml +0 -12
  37. package/dist/templates/monitoring/grafana/provisioning/datasources/prometheus.yml +0 -9
  38. package/dist/templates/monitoring/log_exporter/Dockerfile +0 -13
  39. package/dist/templates/monitoring/log_exporter/exporter.py +0 -274
  40. package/dist/templates/monitoring/log_exporter/requirements.txt +0 -1
  41. package/dist/templates/monitoring/postgres_exporter/queries.yml +0 -22
  42. package/dist/templates/monitoring/prometheus/prometheus.yml +0 -22
  43. package/dist/templates/monitoring/traefik/dynamic/.gitkeep +0 -0
  44. package/dist/templates/monitoring/traefik/traefik.yml +0 -32
  45. package/dist/templates/monitoring/vector/vector.toml/vector.yaml +0 -77
  46. package/dist/templates/monitoring/vector/vector.yaml +0 -106
  47. package/dist/templates/openmemory/Dockerfile +0 -19
  48. package/templates/ansible/inventory/hosts.ini +0 -3
  49. package/templates/ansible/playbooks/deploy-cybermem.yml +0 -71
  50. package/templates/ansible/playbooks/stop-cybermem.yml +0 -17
  51. package/templates/charts/cybermem/Chart.yaml +0 -6
  52. package/templates/charts/cybermem/templates/dashboard-deployment.yaml +0 -29
  53. package/templates/charts/cybermem/templates/dashboard-service.yaml +0 -20
  54. package/templates/charts/cybermem/templates/openmemory-deployment.yaml +0 -40
  55. package/templates/charts/cybermem/templates/openmemory-pvc.yaml +0 -10
  56. package/templates/charts/cybermem/templates/openmemory-service.yaml +0 -13
  57. package/templates/charts/cybermem/values-vps.yaml +0 -18
  58. package/templates/charts/cybermem/values.yaml +0 -42
  59. package/templates/docker-compose.yml +0 -236
  60. package/templates/envs/local.example +0 -27
  61. package/templates/envs/rpi.example +0 -27
  62. package/templates/envs/vps.example +0 -25
  63. package/templates/mcp-responder/Dockerfile +0 -6
  64. package/templates/mcp-responder/server.js +0 -22
  65. package/templates/monitoring/db_exporter/Dockerfile +0 -19
  66. package/templates/monitoring/db_exporter/exporter.py +0 -313
  67. package/templates/monitoring/db_exporter/requirements.txt +0 -2
  68. package/templates/monitoring/grafana/dashboards/cybermem.json +0 -1088
  69. package/templates/monitoring/grafana/provisioning/dashboards/default.yml +0 -12
  70. package/templates/monitoring/grafana/provisioning/datasources/prometheus.yml +0 -9
  71. package/templates/monitoring/log_exporter/Dockerfile +0 -13
  72. package/templates/monitoring/log_exporter/exporter.py +0 -274
  73. package/templates/monitoring/log_exporter/requirements.txt +0 -1
  74. package/templates/monitoring/postgres_exporter/queries.yml +0 -22
  75. package/templates/monitoring/prometheus/prometheus.yml +0 -22
  76. package/templates/monitoring/traefik/dynamic/.gitkeep +0 -0
  77. package/templates/monitoring/traefik/traefik.yml +0 -32
  78. package/templates/monitoring/vector/vector.toml/vector.yaml +0 -77
  79. package/templates/monitoring/vector/vector.yaml +0 -106
  80. package/templates/openmemory/Dockerfile +0 -19
package/src/index.ts ADDED
@@ -0,0 +1,227 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ Tool
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+ import axios from "axios";
10
+ import cors from "cors";
11
+ import dotenv from "dotenv";
12
+ import express from "express";
13
+
14
+ dotenv.config();
15
+
16
+ // Parse CLI args for remote mode
17
+ const args = process.argv.slice(2);
18
+ const getArg = (name: string): string | undefined => {
19
+ const idx = args.indexOf(name);
20
+ return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
21
+ };
22
+
23
+ const cliUrl = getArg('--url');
24
+ const cliApiKey = getArg('--api-key');
25
+ const cliClientName = getArg('--client-name');
26
+
27
+ // Use CLI args first, then env, then defaults
28
+ // Default to local CyberMem backend (via Traefik on port 8626)
29
+ const API_URL = cliUrl || process.env.CYBERMEM_URL || "http://localhost:8626/memory";
30
+ const API_KEY = cliApiKey || process.env.OM_API_KEY || "";
31
+
32
+ // Track client name per session
33
+ let currentClientName = cliClientName || "cybermem-mcp";
34
+
35
+ const server = new Server(
36
+ {
37
+ name: "cybermem-mcp",
38
+ version: "0.2.0",
39
+ },
40
+ {
41
+ capabilities: {
42
+ tools: {},
43
+ },
44
+ }
45
+ );
46
+
47
+ const tools: Tool[] = [
48
+ {
49
+ name: "add_memory",
50
+ description: "Store a new memory in CyberMem",
51
+ inputSchema: {
52
+ type: "object",
53
+ properties: {
54
+ content: { type: "string" },
55
+ user_id: { type: "string" },
56
+ tags: { type: "array", items: { type: "string" } },
57
+ },
58
+ required: ["content"],
59
+ },
60
+ },
61
+ {
62
+ name: "query_memory",
63
+ description: "Search for relevant memories",
64
+ inputSchema: {
65
+ type: "object",
66
+ properties: {
67
+ query: { type: "string" },
68
+ k: { type: "number", default: 5 },
69
+ },
70
+ required: ["query"],
71
+ },
72
+ },
73
+ {
74
+ name: "list_memories",
75
+ description: "List recent memories",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: {
79
+ limit: { type: "number", default: 10 },
80
+ },
81
+ },
82
+ },
83
+ {
84
+ name: "delete_memory",
85
+ description: "Delete a memory by ID",
86
+ inputSchema: {
87
+ type: "object",
88
+ properties: {
89
+ id: { type: "string" },
90
+ },
91
+ required: ["id"],
92
+ },
93
+ },
94
+ {
95
+ name: "update_memory",
96
+ description: "Update a memory by ID",
97
+ inputSchema: {
98
+ type: "object",
99
+ properties: {
100
+ id: { type: "string" },
101
+ content: { type: "string" },
102
+ tags: { type: "array", items: { type: "string" } },
103
+ metadata: { type: "object" },
104
+ },
105
+ required: ["id"],
106
+ },
107
+ }
108
+ ];
109
+
110
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
111
+ tools,
112
+ }));
113
+
114
+ // Create axios instance
115
+ const apiClient = axios.create({
116
+ baseURL: API_URL,
117
+ headers: {
118
+ "Authorization": `Bearer ${API_KEY}`,
119
+ },
120
+ });
121
+
122
+ // Helper to get client with context
123
+ function getClient(customHeaders: Record<string, string> = {}) {
124
+ // Identity is taken from currentClientName which is updated per-request in SSE mode
125
+ const clientName = customHeaders["X-Client-Name"] || currentClientName;
126
+
127
+ return {
128
+ ...apiClient,
129
+ get: (url: string, config?: any) => apiClient.get(url, { ...config, headers: { "X-Client-Name": clientName, ...config?.headers } }),
130
+ post: (url: string, data?: any, config?: any) => apiClient.post(url, data, { ...config, headers: { "X-Client-Name": clientName, ...config?.headers } }),
131
+ put: (url: string, data?: any, config?: any) => apiClient.put(url, data, { ...config, headers: { "X-Client-Name": clientName, ...config?.headers } }),
132
+ patch: (url: string, data?: any, config?: any) => apiClient.patch(url, data, { ...config, headers: { "X-Client-Name": clientName, ...config?.headers } }),
133
+ delete: (url: string, config?: any) => apiClient.delete(url, { ...config, headers: { "X-Client-Name": clientName, ...config?.headers } }),
134
+ }
135
+ }
136
+
137
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
138
+ const { name, arguments: args } = request.params;
139
+
140
+ try {
141
+ switch (name) {
142
+ case "add_memory": {
143
+ const response = await getClient().post("/add", args);
144
+ return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
145
+ }
146
+ case "query_memory": {
147
+ const response = await getClient().post("/query", args);
148
+ return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
149
+ }
150
+ case "list_memories": {
151
+ const limit = args?.limit || 10;
152
+ const response = await getClient().get(`/all?l=${limit}`);
153
+ return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
154
+ }
155
+ case "delete_memory": {
156
+ const { id } = args as { id: string };
157
+ await getClient().delete(`/${id}`);
158
+ return { content: [{ type: "text", text: `Memory ${id} deleted` }] };
159
+ }
160
+ case "update_memory": {
161
+ const { id, ...updates } = args as { id: string; [key: string]: any };
162
+ const response = await getClient().patch(`/${id}`, updates);
163
+ return { content: [{ type: "text", text: JSON.stringify(response.data) }] };
164
+ }
165
+ default:
166
+ throw new Error(`Unknown tool: ${name}`);
167
+ }
168
+ } catch (error: any) {
169
+ return {
170
+ content: [{ type: "text", text: `Error: ${error.message}` }],
171
+ isError: true,
172
+ };
173
+ }
174
+ });
175
+
176
+ async function run() {
177
+ const isSse = process.argv.includes("--sse") || !!process.env.PORT;
178
+
179
+ if (isSse) {
180
+ const app = express();
181
+ app.use(cors());
182
+ const port = process.env.PORT || 8627;
183
+
184
+ let transport: SSEServerTransport | null = null;
185
+
186
+ app.get("/sse", async (req, res) => {
187
+ // Extract client name from header
188
+ const clientName = req.headers["x-client-name"] as string;
189
+ if (clientName) {
190
+ currentClientName = clientName;
191
+ }
192
+
193
+ transport = new SSEServerTransport("/messages", res);
194
+ await server.connect(transport);
195
+ });
196
+
197
+ app.post("/messages", async (req, res) => {
198
+ // Also check headers on messages
199
+ const clientName = req.headers["x-client-name"] as string;
200
+ if (clientName) {
201
+ currentClientName = clientName;
202
+ }
203
+
204
+ if (transport) {
205
+ await transport.handlePostMessage(req, res);
206
+ } else {
207
+ res.status(400).send("Session not established");
208
+ }
209
+ });
210
+
211
+ app.listen(port, () => {
212
+ console.error(`CyberMem MCP Server running on SSE at http://localhost:${port}`);
213
+ console.error(` - SSE endpoint: http://localhost:${port}/sse`);
214
+ console.error(` - Message endpoint: http://localhost:${port}/messages`);
215
+ });
216
+
217
+ } else {
218
+ const transport = new StdioServerTransport();
219
+ await server.connect(transport);
220
+ console.error("CyberMem MCP Server running on stdio");
221
+ }
222
+ }
223
+
224
+ run().catch((error) => {
225
+ console.error("Fatal error running server:", error);
226
+ process.exit(1);
227
+ });
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
- });
@@ -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
- }