repoburg 1.3.162 → 1.3.165
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/backend/.env +1 -27
- package/backend/dist/src/app.module.js +2 -0
- package/backend/dist/src/app.module.js.map +1 -1
- package/backend/dist/src/application-state/application-state.service.d.ts +4 -0
- package/backend/dist/src/application-state/application-state.service.js +24 -0
- package/backend/dist/src/application-state/application-state.service.js.map +1 -1
- package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.d.ts +2 -1
- package/backend/dist/src/llm-orchestration/llm-orchestration.interfaces.js.map +1 -1
- package/backend/dist/src/llm-orchestration/tool-schema.service.js +2 -24
- package/backend/dist/src/llm-orchestration/tool-schema.service.js.map +1 -1
- package/backend/dist/src/mcp-server/mcp-internal.controller.d.ts +12 -0
- package/backend/dist/src/mcp-server/mcp-internal.controller.js +50 -0
- package/backend/dist/src/mcp-server/mcp-internal.controller.js.map +1 -0
- package/backend/dist/src/mcp-server/mcp-server.bootstrap.d.ts +2 -0
- package/backend/dist/src/mcp-server/mcp-server.bootstrap.js +162 -0
- package/backend/dist/src/mcp-server/mcp-server.bootstrap.js.map +1 -0
- package/backend/dist/src/mcp-server/mcp-server.constants.d.ts +8 -0
- package/backend/dist/src/mcp-server/mcp-server.constants.js +19 -0
- package/backend/dist/src/mcp-server/mcp-server.constants.js.map +1 -0
- package/backend/dist/src/mcp-server/mcp-server.controller.d.ts +19 -0
- package/backend/dist/src/mcp-server/mcp-server.controller.js +72 -0
- package/backend/dist/src/mcp-server/mcp-server.controller.js.map +1 -0
- package/backend/dist/src/mcp-server/mcp-server.module.d.ts +2 -0
- package/backend/dist/src/mcp-server/mcp-server.module.js +29 -0
- package/backend/dist/src/mcp-server/mcp-server.module.js.map +1 -0
- package/backend/dist/src/mcp-server/mcp-server.process.service.d.ts +23 -0
- package/backend/dist/src/mcp-server/mcp-server.process.service.js +130 -0
- package/backend/dist/src/mcp-server/mcp-server.process.service.js.map +1 -0
- package/backend/dist/src/mcp-server/mcp-server.service.d.ts +34 -0
- package/backend/dist/src/mcp-server/mcp-server.service.js +260 -0
- package/backend/dist/src/mcp-server/mcp-server.service.js.map +1 -0
- package/backend/dist/src/tool-hooks/hook-context.interface.d.ts +2 -1
- package/backend/dist/src/utils/index.d.ts +1 -0
- package/backend/dist/src/utils/index.js +1 -0
- package/backend/dist/src/utils/index.js.map +1 -1
- package/backend/dist/src/utils/tool-schema-converter.d.ts +13 -0
- package/backend/dist/src/utils/tool-schema-converter.js +33 -0
- package/backend/dist/src/utils/tool-schema-converter.js.map +1 -0
- package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
- package/backend/package.json +2 -0
- package/daemon/dist/index.js +25 -0
- package/daemon/dist/mcp-gateway.js +401 -0
- package/daemon/dist/mcp-gateway.spec.js +295 -0
- package/daemon/package.json +15 -2
- package/package.json +1 -1
- package/platform-cli.js +264 -1
- package/backend/database.sqlite +0 -0
- package/backend/section-modified.txt +0 -1
- package/daemon/dist/api/system.js +0 -25
- package/daemon/dist/daemon.blob +0 -0
- package/visual-editor-proxy/.repoburg/data.sqlite +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the MCP gateway — the daemon-hosted single MCP entry point that
|
|
4
|
+
* manages all Repoburg instances.
|
|
5
|
+
*
|
|
6
|
+
* Tests focus on:
|
|
7
|
+
* - Tool cache refresh (fetch from running instance, augment with instance_name)
|
|
8
|
+
* - Cache invalidation
|
|
9
|
+
* - getGatewayInfo() shape
|
|
10
|
+
* - Daemon-level tool routing (list_instances, start_instance, stop_instance)
|
|
11
|
+
* - manage_* proxy routing (missing instance_name, unknown instance, not running,
|
|
12
|
+
* valid POST, fetch failure, result mapping)
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
// Mock dependencies before importing the gateway.
|
|
16
|
+
jest.mock('./stateManager', () => ({
|
|
17
|
+
stateManager: {
|
|
18
|
+
getServices: jest.fn(),
|
|
19
|
+
getService: jest.fn(),
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
jest.mock('./serviceManager', () => ({
|
|
23
|
+
serviceManager: {
|
|
24
|
+
start: jest.fn(),
|
|
25
|
+
stop: jest.fn(),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
// Mock MCP SDK — we test the gateway's logic, not the SDK.
|
|
29
|
+
jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
30
|
+
Server: jest.fn().mockImplementation(() => ({
|
|
31
|
+
setRequestHandler: jest.fn(),
|
|
32
|
+
connect: jest.fn(),
|
|
33
|
+
close: jest.fn(),
|
|
34
|
+
})),
|
|
35
|
+
}));
|
|
36
|
+
jest.mock('@modelcontextprotocol/sdk/server/streamableHttp.js', () => ({
|
|
37
|
+
StreamableHTTPServerTransport: jest.fn(),
|
|
38
|
+
}));
|
|
39
|
+
jest.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
|
40
|
+
CallToolRequestSchema: 'callTool',
|
|
41
|
+
ListToolsRequestSchema: 'listTools',
|
|
42
|
+
isInitializeRequest: jest.fn(),
|
|
43
|
+
}));
|
|
44
|
+
const mcp_gateway_1 = require("./mcp-gateway");
|
|
45
|
+
const stateManager_1 = require("./stateManager");
|
|
46
|
+
const serviceManager_1 = require("./serviceManager");
|
|
47
|
+
const mockStateManager = stateManager_1.stateManager;
|
|
48
|
+
const mockServiceManager = serviceManager_1.serviceManager;
|
|
49
|
+
// Helper: create a mock Service object.
|
|
50
|
+
function mockService(name, status = 'running') {
|
|
51
|
+
return { name, path: `/projects/${name}`, port: 3400, status, pid: 12345 };
|
|
52
|
+
}
|
|
53
|
+
// Helper: raw tool definition as returned by backend's GET /mcp-internal/tools.
|
|
54
|
+
function rawToolDef(name, description, required = []) {
|
|
55
|
+
return {
|
|
56
|
+
tool_name: name,
|
|
57
|
+
description,
|
|
58
|
+
input_schema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: required.reduce((acc, r) => {
|
|
61
|
+
acc[r] = { type: 'string', description: `${r} param` };
|
|
62
|
+
return acc;
|
|
63
|
+
}, {}),
|
|
64
|
+
required: required.length > 0 ? required : undefined,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
describe('McpGateway', () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
jest.clearAllMocks();
|
|
71
|
+
// Reset gateway state between tests.
|
|
72
|
+
mcp_gateway_1.mcpGateway.invalidateToolCache();
|
|
73
|
+
// Default: no instances.
|
|
74
|
+
mockStateManager.getServices.mockReturnValue([]);
|
|
75
|
+
mockStateManager.getService.mockReturnValue(undefined);
|
|
76
|
+
mockServiceManager.start.mockReset();
|
|
77
|
+
mockServiceManager.stop.mockReset();
|
|
78
|
+
});
|
|
79
|
+
describe('refreshToolCache', () => {
|
|
80
|
+
it('should fetch tools from a running instance and augment with instance_name', async () => {
|
|
81
|
+
mockStateManager.getServices.mockReturnValue([mockService('alpha', 'running')]);
|
|
82
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
83
|
+
ok: true,
|
|
84
|
+
json: async () => [rawToolDef('manage_agents', 'Manage agents', ['action'])],
|
|
85
|
+
});
|
|
86
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
87
|
+
const info = await mcp_gateway_1.mcpGateway.getGatewayInfo();
|
|
88
|
+
const manageTools = info.tools.filter((t) => t.name.startsWith('manage_'));
|
|
89
|
+
expect(manageTools).toHaveLength(1);
|
|
90
|
+
expect(manageTools[0].name).toBe('manage_agents');
|
|
91
|
+
expect(manageTools[0].inputSchema.properties).toHaveProperty('instance_name');
|
|
92
|
+
expect(manageTools[0].inputSchema.required).toContain('instance_name');
|
|
93
|
+
});
|
|
94
|
+
it('should leave cache null when no instance is running', async () => {
|
|
95
|
+
mockStateManager.getServices.mockReturnValue([mockService('alpha', 'stopped')]);
|
|
96
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
97
|
+
const info = await mcp_gateway_1.mcpGateway.getGatewayInfo();
|
|
98
|
+
// Only daemon-level tools, no manage_* tools.
|
|
99
|
+
const manageTools = info.tools.filter((t) => t.name.startsWith('manage_'));
|
|
100
|
+
expect(manageTools).toHaveLength(0);
|
|
101
|
+
});
|
|
102
|
+
it('should skip refresh when cache is already populated', async () => {
|
|
103
|
+
mockStateManager.getServices.mockReturnValue([mockService('alpha', 'running')]);
|
|
104
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: async () => [rawToolDef('manage_agents', 'Manage agents')],
|
|
107
|
+
});
|
|
108
|
+
global.fetch = fetchMock;
|
|
109
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
110
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache(); // second call
|
|
111
|
+
// fetch should only be called once (cache hit on second call).
|
|
112
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
it('should handle fetch failure gracefully (cache stays null)', async () => {
|
|
115
|
+
mockStateManager.getServices.mockReturnValue([mockService('alpha', 'running')]);
|
|
116
|
+
global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 500 });
|
|
117
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
118
|
+
const info = await mcp_gateway_1.mcpGateway.getGatewayInfo();
|
|
119
|
+
const manageTools = info.tools.filter((t) => t.name.startsWith('manage_'));
|
|
120
|
+
expect(manageTools).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('invalidateToolCache', () => {
|
|
124
|
+
it('should clear cache so next refresh re-fetches', async () => {
|
|
125
|
+
mockStateManager.getServices.mockReturnValue([mockService('alpha', 'running')]);
|
|
126
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
127
|
+
ok: true,
|
|
128
|
+
json: async () => [rawToolDef('manage_agents', 'Manage agents')],
|
|
129
|
+
});
|
|
130
|
+
global.fetch = fetchMock;
|
|
131
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
132
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
133
|
+
mcp_gateway_1.mcpGateway.invalidateToolCache();
|
|
134
|
+
await mcp_gateway_1.mcpGateway.refreshToolCache();
|
|
135
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('getGatewayInfo', () => {
|
|
139
|
+
it('should return descriptor with server_name, url, instances, tools, client_config', async () => {
|
|
140
|
+
mockStateManager.getServices.mockReturnValue([
|
|
141
|
+
mockService('alpha', 'running'),
|
|
142
|
+
mockService('beta', 'stopped'),
|
|
143
|
+
]);
|
|
144
|
+
const info = await mcp_gateway_1.mcpGateway.getGatewayInfo();
|
|
145
|
+
expect(info.server_name).toBe('repoburg-gateway');
|
|
146
|
+
expect(info.url).toBe('http://localhost:9998/mcp');
|
|
147
|
+
expect(info.instances).toHaveLength(2);
|
|
148
|
+
expect(info.instances[0].name).toBe('alpha');
|
|
149
|
+
expect(info.instances[0].status).toBe('running');
|
|
150
|
+
expect(info.client_config).toContain('http://localhost:9998/mcp');
|
|
151
|
+
// Should always include daemon-level tools.
|
|
152
|
+
const toolNames = info.tools.map((t) => t.name);
|
|
153
|
+
expect(toolNames).toContain('list_instances');
|
|
154
|
+
expect(toolNames).toContain('start_instance');
|
|
155
|
+
expect(toolNames).toContain('stop_instance');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('daemon-level tool routing', () => {
|
|
159
|
+
it('list_instances should return text with instance info', () => {
|
|
160
|
+
mockStateManager.getServices.mockReturnValue([
|
|
161
|
+
mockService('alpha', 'running'),
|
|
162
|
+
mockService('beta', 'stopped'),
|
|
163
|
+
]);
|
|
164
|
+
const result = mcp_gateway_1.mcpGateway.handleListInstances();
|
|
165
|
+
const text = result.content[0].text;
|
|
166
|
+
expect(text).toContain('alpha');
|
|
167
|
+
expect(text).toContain('running');
|
|
168
|
+
expect(text).toContain('beta');
|
|
169
|
+
expect(text).toContain('stopped');
|
|
170
|
+
expect(result.isError).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
it('list_instances should show message when no instances', () => {
|
|
173
|
+
mockStateManager.getServices.mockReturnValue([]);
|
|
174
|
+
const result = mcp_gateway_1.mcpGateway.handleListInstances();
|
|
175
|
+
expect(result.content[0].text).toContain('No Repoburg instances');
|
|
176
|
+
});
|
|
177
|
+
it('start_instance should call serviceManager.start and invalidate cache', async () => {
|
|
178
|
+
mockServiceManager.start.mockResolvedValue(mockService('gamma', 'running'));
|
|
179
|
+
const result = await mcp_gateway_1.mcpGateway.handleStartInstance({
|
|
180
|
+
path: '/projects/gamma',
|
|
181
|
+
name: 'gamma',
|
|
182
|
+
});
|
|
183
|
+
expect(mockServiceManager.start).toHaveBeenCalledWith('/projects/gamma', 'gamma');
|
|
184
|
+
expect(result.content[0].text).toContain('gamma');
|
|
185
|
+
expect(result.isError).toBeUndefined();
|
|
186
|
+
});
|
|
187
|
+
it('start_instance should error when path is missing', async () => {
|
|
188
|
+
const result = await mcp_gateway_1.mcpGateway.handleStartInstance({});
|
|
189
|
+
expect(result.isError).toBe(true);
|
|
190
|
+
expect(result.content[0].text).toContain('Missing required parameter: path');
|
|
191
|
+
});
|
|
192
|
+
it('stop_instance should call serviceManager.stop', async () => {
|
|
193
|
+
const result = await mcp_gateway_1.mcpGateway.handleStopInstance({ name: 'alpha' });
|
|
194
|
+
expect(mockServiceManager.stop).toHaveBeenCalledWith('alpha');
|
|
195
|
+
expect(result.content[0].text).toContain('alpha');
|
|
196
|
+
expect(result.isError).toBeUndefined();
|
|
197
|
+
});
|
|
198
|
+
it('stop_instance should error when name is missing', async () => {
|
|
199
|
+
const result = await mcp_gateway_1.mcpGateway.handleStopInstance({});
|
|
200
|
+
expect(result.isError).toBe(true);
|
|
201
|
+
expect(result.content[0].text).toContain('Missing required parameter: name');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe('manage_* proxy routing', () => {
|
|
205
|
+
it('should error when instance_name is missing', async () => {
|
|
206
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
207
|
+
action: 'create',
|
|
208
|
+
});
|
|
209
|
+
expect(result.isError).toBe(true);
|
|
210
|
+
expect(result.content[0].text).toContain('instance_name');
|
|
211
|
+
});
|
|
212
|
+
it('should error when instance is not found', async () => {
|
|
213
|
+
mockStateManager.getService.mockReturnValue(undefined);
|
|
214
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
215
|
+
instance_name: 'unknown',
|
|
216
|
+
action: 'create',
|
|
217
|
+
});
|
|
218
|
+
expect(result.isError).toBe(true);
|
|
219
|
+
expect(result.content[0].text).toContain("not found");
|
|
220
|
+
});
|
|
221
|
+
it('should error when instance is not running', async () => {
|
|
222
|
+
mockStateManager.getService.mockReturnValue(mockService('alpha', 'stopped'));
|
|
223
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
224
|
+
instance_name: 'alpha',
|
|
225
|
+
action: 'create',
|
|
226
|
+
});
|
|
227
|
+
expect(result.isError).toBe(true);
|
|
228
|
+
expect(result.content[0].text).toContain('not running');
|
|
229
|
+
});
|
|
230
|
+
it('should strip instance_name and POST to the instance backend', async () => {
|
|
231
|
+
mockStateManager.getService.mockReturnValue(mockService('alpha', 'running'));
|
|
232
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
233
|
+
ok: true,
|
|
234
|
+
json: async () => ({
|
|
235
|
+
status: 'SUCCESS',
|
|
236
|
+
summary: 'Agent created',
|
|
237
|
+
execution_log: { output: 'Created agent "test"' },
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
global.fetch = fetchMock;
|
|
241
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
242
|
+
instance_name: 'alpha',
|
|
243
|
+
action: 'create',
|
|
244
|
+
name: 'test',
|
|
245
|
+
});
|
|
246
|
+
expect(fetchMock).toHaveBeenCalledWith('http://localhost:3400/mcp-internal/execute-tool', expect.objectContaining({
|
|
247
|
+
method: 'POST',
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
tool_name: 'manage_agents',
|
|
250
|
+
arguments: { action: 'create', name: 'test' }, // instance_name stripped
|
|
251
|
+
}),
|
|
252
|
+
}));
|
|
253
|
+
expect(result.isError).toBeUndefined();
|
|
254
|
+
expect(result.content[0].text).toContain('Created agent "test"');
|
|
255
|
+
});
|
|
256
|
+
it('should map FAILURE result to isError: true', async () => {
|
|
257
|
+
mockStateManager.getService.mockReturnValue(mockService('alpha', 'running'));
|
|
258
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
259
|
+
ok: true,
|
|
260
|
+
json: async () => ({
|
|
261
|
+
status: 'FAILURE',
|
|
262
|
+
summary: 'Validation error',
|
|
263
|
+
error_message: 'Name is required',
|
|
264
|
+
execution_log: { output: '', error_message: 'Name is required' },
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
268
|
+
instance_name: 'alpha',
|
|
269
|
+
action: 'create',
|
|
270
|
+
});
|
|
271
|
+
expect(result.isError).toBe(true);
|
|
272
|
+
expect(result.content[0].text).toContain('Name is required');
|
|
273
|
+
});
|
|
274
|
+
it('should error when fetch to backend fails', async () => {
|
|
275
|
+
mockStateManager.getService.mockReturnValue(mockService('alpha', 'running'));
|
|
276
|
+
global.fetch = jest.fn().mockRejectedValue(new Error('ECONNREFUSED'));
|
|
277
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
278
|
+
instance_name: 'alpha',
|
|
279
|
+
action: 'create',
|
|
280
|
+
});
|
|
281
|
+
expect(result.isError).toBe(true);
|
|
282
|
+
expect(result.content[0].text).toContain('ECONNREFUSED');
|
|
283
|
+
});
|
|
284
|
+
it('should error when backend returns non-ok HTTP status', async () => {
|
|
285
|
+
mockStateManager.getService.mockReturnValue(mockService('alpha', 'running'));
|
|
286
|
+
global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 500 });
|
|
287
|
+
const result = await mcp_gateway_1.mcpGateway.handleManageTool('manage_agents', {
|
|
288
|
+
instance_name: 'alpha',
|
|
289
|
+
action: 'create',
|
|
290
|
+
});
|
|
291
|
+
expect(result.isError).toBe(true);
|
|
292
|
+
expect(result.content[0].text).toContain('500');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
package/daemon/package.json
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"start": "cross-env NODE_ENV=production node dist/index.js",
|
|
10
|
-
"dev": "NODE_ENV=development ts-node-dev --respawn --transpile-only src/index.ts"
|
|
10
|
+
"dev": "NODE_ENV=development ts-node-dev --respawn --transpile-only src/index.ts",
|
|
11
|
+
"test": "jest"
|
|
11
12
|
},
|
|
12
13
|
"keywords": [],
|
|
13
14
|
"author": "Celal Ertug",
|
|
@@ -16,9 +17,21 @@
|
|
|
16
17
|
"@types/cors": "^2.8.17",
|
|
17
18
|
"@types/express": "^4.17.21",
|
|
18
19
|
"@types/fs-extra": "^11.0.4",
|
|
20
|
+
"@types/jest": "^29.5.14",
|
|
19
21
|
"@types/node": "^20.14.9",
|
|
20
22
|
"@types/ws": "^8.5.10",
|
|
23
|
+
"jest": "^29.7.0",
|
|
24
|
+
"ts-jest": "^29.4.11",
|
|
21
25
|
"ts-node-dev": "^2.0.0",
|
|
22
26
|
"typescript": "^5.5.2"
|
|
27
|
+
},
|
|
28
|
+
"jest": {
|
|
29
|
+
"moduleFileExtensions": ["js", "json", "ts"],
|
|
30
|
+
"rootDir": "src",
|
|
31
|
+
"testRegex": ".*\\.spec\\.ts$",
|
|
32
|
+
"transform": {
|
|
33
|
+
"^.+\\.(t|j)s$": ["ts-jest", { "isolatedModules": true }]
|
|
34
|
+
},
|
|
35
|
+
"testEnvironment": "node"
|
|
23
36
|
}
|
|
24
|
-
}
|
|
37
|
+
}
|
package/package.json
CHANGED
package/platform-cli.js
CHANGED
|
@@ -5,7 +5,8 @@ const axios = require('axios');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const fs = require('fs/promises');
|
|
7
7
|
const packageJson = require('./package.json');
|
|
8
|
-
const { spawn } = require('child_process');
|
|
8
|
+
const { spawn, spawnSync } = require('child_process');
|
|
9
|
+
const os = require('os');
|
|
9
10
|
|
|
10
11
|
const DAEMON_BASE_URL = 'http://localhost:9998';
|
|
11
12
|
|
|
@@ -517,4 +518,266 @@ program
|
|
|
517
518
|
}
|
|
518
519
|
});
|
|
519
520
|
|
|
521
|
+
// ===== Tunnel Commands =====
|
|
522
|
+
|
|
523
|
+
const TUNNEL_CONFIG_DIR = path.join(os.homedir(), '.config', 'tunnel-client');
|
|
524
|
+
const DEFAULT_MCP_URL = 'http://localhost:9998/mcp';
|
|
525
|
+
const TUNNEL_CLIENT_INSTALL_URL = 'https://github.com/openai/tunnel-client/releases/latest';
|
|
526
|
+
|
|
527
|
+
const getTunnelConfigPath = (profile) => path.join(TUNNEL_CONFIG_DIR, `${profile}.yaml`);
|
|
528
|
+
|
|
529
|
+
const generateTunnelYaml = (tunnelId, apiKeyRef, mcpUrl) => {
|
|
530
|
+
return `config_version: 1
|
|
531
|
+
control_plane:
|
|
532
|
+
base_url: "https://api.openai.com"
|
|
533
|
+
|
|
534
|
+
tunnel_id: "${tunnelId}"
|
|
535
|
+
api_key: "${apiKeyRef}"
|
|
536
|
+
health:
|
|
537
|
+
listen_addr: "127.0.0.1:8080"
|
|
538
|
+
admin_ui:
|
|
539
|
+
open_browser: false
|
|
540
|
+
log:
|
|
541
|
+
level: info
|
|
542
|
+
format: json
|
|
543
|
+
mcp:
|
|
544
|
+
server_urls:
|
|
545
|
+
- channel: main
|
|
546
|
+
url: "${mcpUrl}"
|
|
547
|
+
`;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
/** Checks if the tunnel-client binary is on PATH. */
|
|
551
|
+
const validateTunnelBinary = () => {
|
|
552
|
+
const result = spawnSync('tunnel-client', ['--version'], { stdio: 'pipe' });
|
|
553
|
+
return !result.error;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const tunnelCmd = program.command('tunnel').description('Manage MCP tunnel profiles for ChatGPT/OpenAI connectivity.');
|
|
557
|
+
|
|
558
|
+
tunnelCmd
|
|
559
|
+
.command('create')
|
|
560
|
+
.description('Create a new tunnel profile.')
|
|
561
|
+
.requiredOption('-p, --profile <name>', 'Profile name')
|
|
562
|
+
.option('-t, --tunnel-id <id>', 'Tunnel ID from OpenAI Platform')
|
|
563
|
+
.option('-k, --api-key <key>', 'API key (defaults to CONTROL_PLANE_API_KEY env var)')
|
|
564
|
+
.option('-m, --mcp-url <url>', 'MCP server URL', DEFAULT_MCP_URL)
|
|
565
|
+
.action(async (options) => {
|
|
566
|
+
const { default: chalk } = await import('chalk');
|
|
567
|
+
|
|
568
|
+
if (!options.tunnelId) {
|
|
569
|
+
console.error(chalk.red('Missing required option: --tunnel-id.'));
|
|
570
|
+
console.error(chalk.gray('Get your tunnel ID from https://platform.openai.com/settings/organization/tunnels'));
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const apiKeyRef = options.apiKey
|
|
575
|
+
? options.apiKey
|
|
576
|
+
: process.env.CONTROL_PLANE_API_KEY
|
|
577
|
+
? 'env:CONTROL_PLANE_API_KEY'
|
|
578
|
+
: null;
|
|
579
|
+
|
|
580
|
+
if (!apiKeyRef) {
|
|
581
|
+
console.error(chalk.red('Missing API key.'));
|
|
582
|
+
console.error(chalk.yellow('Set the CONTROL_PLANE_API_KEY env var or use --api-key.'));
|
|
583
|
+
console.error(chalk.gray('Create a key at https://platform.openai.com/settings/organization/api-keys'));
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const configPath = getTunnelConfigPath(options.profile);
|
|
588
|
+
|
|
589
|
+
// Warn if overwriting an existing profile.
|
|
590
|
+
try {
|
|
591
|
+
await fs.access(configPath);
|
|
592
|
+
console.log(chalk.yellow(`Warning: Profile '${options.profile}' already exists. Overwriting.`));
|
|
593
|
+
} catch {
|
|
594
|
+
// File doesn't exist — fine.
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
await fs.mkdir(TUNNEL_CONFIG_DIR, { recursive: true });
|
|
598
|
+
const yaml = generateTunnelYaml(options.tunnelId, apiKeyRef, options.mcpUrl);
|
|
599
|
+
await fs.writeFile(configPath, yaml, 'utf-8');
|
|
600
|
+
|
|
601
|
+
console.log(chalk.green(`\u2713 Tunnel profile '${options.profile}' created.`));
|
|
602
|
+
console.log(chalk.gray(` Config: ${configPath}`));
|
|
603
|
+
console.log(chalk.cyan(` Run with: repoburg tunnel run --profile ${options.profile}`));
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
tunnelCmd
|
|
607
|
+
.command('delete <profile>')
|
|
608
|
+
.description('Delete a tunnel profile.')
|
|
609
|
+
.action(async (profile) => {
|
|
610
|
+
const { default: chalk } = await import('chalk');
|
|
611
|
+
const configPath = getTunnelConfigPath(profile);
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
await fs.access(configPath);
|
|
615
|
+
} catch {
|
|
616
|
+
console.error(chalk.red(`Tunnel profile '${profile}' not found.`));
|
|
617
|
+
console.error(chalk.gray(` Expected at: ${configPath}`));
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
await fs.unlink(configPath);
|
|
622
|
+
console.log(chalk.green(`\u2713 Tunnel profile '${profile}' deleted.`));
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
tunnelCmd
|
|
626
|
+
.command('run <profile>')
|
|
627
|
+
.description('Run a tunnel profile (connects to ChatGPT/OpenAI).')
|
|
628
|
+
.action(async (profile) => {
|
|
629
|
+
const { default: chalk } = await import('chalk');
|
|
630
|
+
|
|
631
|
+
if (!validateTunnelBinary()) {
|
|
632
|
+
console.error(chalk.red('tunnel-client not found on PATH.'));
|
|
633
|
+
console.error(chalk.yellow(`Install it from ${TUNNEL_CLIENT_INSTALL_URL}`));
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const configPath = getTunnelConfigPath(profile);
|
|
638
|
+
|
|
639
|
+
let configContent;
|
|
640
|
+
try {
|
|
641
|
+
configContent = await fs.readFile(configPath, 'utf-8');
|
|
642
|
+
} catch {
|
|
643
|
+
console.error(chalk.red(`Tunnel profile '${profile}' not found.`));
|
|
644
|
+
console.error(chalk.yellow(`Create one with: repoburg tunnel create --profile ${profile} --tunnel-id <id>`));
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Validate tunnel_id is present and non-empty.
|
|
649
|
+
const tunnelIdMatch = configContent.match(/tunnel_id:\s*"?([^\s"]+)"?/);
|
|
650
|
+
if (!tunnelIdMatch || !tunnelIdMatch[1]) {
|
|
651
|
+
console.error(chalk.red(`Config error: tunnel_id is missing or empty in profile '${profile}'.`));
|
|
652
|
+
console.error(chalk.yellow(`Recreate with: repoburg tunnel create --profile ${profile} --tunnel-id <id>`));
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Validate api_key is present and non-empty.
|
|
657
|
+
const apiKeyMatch = configContent.match(/api_key:\s*"?([^\s"]+)"?/);
|
|
658
|
+
if (!apiKeyMatch || !apiKeyMatch[1]) {
|
|
659
|
+
console.error(chalk.red(`Config error: api_key is missing or empty in profile '${profile}'.`));
|
|
660
|
+
console.error(chalk.yellow(`Recreate with: repoburg tunnel create --profile ${profile} --tunnel-id <id>`));
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// If api_key references an env var, verify it's set.
|
|
665
|
+
if (apiKeyMatch[1].startsWith('env:')) {
|
|
666
|
+
const envVarName = apiKeyMatch[1].slice(4);
|
|
667
|
+
if (!process.env[envVarName]) {
|
|
668
|
+
console.error(chalk.red(`${envVarName} environment variable is not set.`));
|
|
669
|
+
console.error(chalk.yellow(`Export it first: export ${envVarName}=sk-...`));
|
|
670
|
+
process.exit(1);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
console.log(chalk.cyan(`Starting tunnel-client with profile '${profile}'...`));
|
|
675
|
+
console.log(chalk.gray(` Config: ${configPath}`));
|
|
676
|
+
|
|
677
|
+
const child = spawn('tunnel-client', ['run', '--profile', profile], {
|
|
678
|
+
stdio: 'inherit',
|
|
679
|
+
shell: true,
|
|
680
|
+
env: { ...process.env },
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
child.on('error', (error) => {
|
|
684
|
+
console.error(chalk.red('Failed to start tunnel-client:'), error.message);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
child.on('exit', (code) => {
|
|
689
|
+
if (code !== 0) {
|
|
690
|
+
console.log(chalk.yellow(`tunnel-client exited with code ${code}`));
|
|
691
|
+
}
|
|
692
|
+
process.exit(code || 0);
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
tunnelCmd
|
|
697
|
+
.command('list')
|
|
698
|
+
.description('List all tunnel profiles.')
|
|
699
|
+
.action(async () => {
|
|
700
|
+
const { default: chalk } = await import('chalk');
|
|
701
|
+
const Table = require('cli-table3');
|
|
702
|
+
|
|
703
|
+
let files;
|
|
704
|
+
try {
|
|
705
|
+
files = await fs.readdir(TUNNEL_CONFIG_DIR);
|
|
706
|
+
} catch {
|
|
707
|
+
console.log(chalk.yellow('No tunnel profiles found.'));
|
|
708
|
+
console.log(chalk.gray('Create one with: repoburg tunnel create --profile <name> --tunnel-id <id>'));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const yamlFiles = files.filter(f => f.endsWith('.yaml'));
|
|
713
|
+
if (yamlFiles.length === 0) {
|
|
714
|
+
console.log(chalk.yellow('No tunnel profiles found.'));
|
|
715
|
+
console.log(chalk.gray('Create one with: repoburg tunnel create --profile <name> --tunnel-id <id>'));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const table = new Table({
|
|
720
|
+
head: ['PROFILE', 'TUNNEL_ID', 'MCP_URL'].map(h => chalk.cyan(h)),
|
|
721
|
+
colWidths: [20, 35, 40]
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
for (const file of yamlFiles) {
|
|
725
|
+
const profileName = file.replace(/\.yaml$/, '');
|
|
726
|
+
const configPath = path.join(TUNNEL_CONFIG_DIR, file);
|
|
727
|
+
try {
|
|
728
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
729
|
+
const tunnelIdMatch = content.match(/(?:^|\s)tunnel_id:\s*"?([^\s"]+)"?/);
|
|
730
|
+
const urlMatch = content.match(/(?:^|\s)url:\s*"?([^\s"]+)"?/);
|
|
731
|
+
table.push([
|
|
732
|
+
profileName,
|
|
733
|
+
tunnelIdMatch ? tunnelIdMatch[1] : chalk.gray('(missing)'),
|
|
734
|
+
urlMatch ? urlMatch[1] : chalk.gray('(missing)'),
|
|
735
|
+
]);
|
|
736
|
+
} catch {
|
|
737
|
+
table.push([profileName, chalk.red('(read error)'), '-']);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
console.log(table.toString());
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
tunnelCmd
|
|
745
|
+
.command('doctor <profile>')
|
|
746
|
+
.description('Run tunnel-client doctor on a profile.')
|
|
747
|
+
.action(async (profile) => {
|
|
748
|
+
const { default: chalk } = await import('chalk');
|
|
749
|
+
|
|
750
|
+
if (!validateTunnelBinary()) {
|
|
751
|
+
console.error(chalk.red('tunnel-client not found on PATH.'));
|
|
752
|
+
console.error(chalk.yellow(`Install it from ${TUNNEL_CLIENT_INSTALL_URL}`));
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const configPath = getTunnelConfigPath(profile);
|
|
757
|
+
try {
|
|
758
|
+
await fs.access(configPath);
|
|
759
|
+
} catch {
|
|
760
|
+
console.error(chalk.red(`Tunnel profile '${profile}' not found.`));
|
|
761
|
+
console.error(chalk.yellow(`Create one with: repoburg tunnel create --profile ${profile} --tunnel-id <id>`));
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
console.log(chalk.cyan(`Running tunnel-client doctor for profile '${profile}'...`));
|
|
766
|
+
|
|
767
|
+
const child = spawn('tunnel-client', ['doctor', '--profile', profile, '--explain'], {
|
|
768
|
+
stdio: 'inherit',
|
|
769
|
+
shell: true,
|
|
770
|
+
env: { ...process.env },
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
child.on('error', (error) => {
|
|
774
|
+
console.error(chalk.red('Failed to run tunnel-client doctor:'), error.message);
|
|
775
|
+
process.exit(1);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
child.on('exit', (code) => {
|
|
779
|
+
process.exit(code || 0);
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
520
783
|
program.parse(process.argv);
|
package/backend/database.sqlite
DELETED
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
current
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const express_1 = require("express");
|
|
4
|
-
const child_process_1 = require("child_process");
|
|
5
|
-
const checkAuth_1 = require("../middleware/checkAuth");
|
|
6
|
-
const router = (0, express_1.Router)();
|
|
7
|
-
router.post('/update', checkAuth_1.checkAuth, (req, res) => {
|
|
8
|
-
try {
|
|
9
|
-
console.log('[DAEMON] Received request to start update process.');
|
|
10
|
-
// Using `spawn` with `detached: true` and `stdio: 'ignore'` allows the parent (daemon)
|
|
11
|
-
// to exit while the child (update script) continues running. `unref()` is crucial.
|
|
12
|
-
const child = (0, child_process_1.spawn)('repoburg', ['update'], {
|
|
13
|
-
detached: true,
|
|
14
|
-
stdio: 'ignore',
|
|
15
|
-
shell: true,
|
|
16
|
-
});
|
|
17
|
-
child.unref();
|
|
18
|
-
res.status(202).json({ message: 'Update process initiated. The daemon and services will restart shortly.' });
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
console.error('Failed to spawn update process:', error);
|
|
22
|
-
res.status(500).json({ error: 'Failed to start update process.' });
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
exports.default = router;
|
package/daemon/dist/daemon.blob
DELETED
|
Binary file
|
|
Binary file
|