agentgui 1.0.993 → 1.0.995
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/lib/server-startup2.js +0 -26
- package/package.json +1 -1
- package/server.js +1 -3
- package/test.js +1 -3
- package/lib/plugin-loader.js +0 -162
- package/lib/plugins/agents-plugin.js +0 -76
- package/lib/plugins/database-plugin.js +0 -43
- package/lib/plugins/stream-plugin.js +0 -89
- package/lib/plugins/websocket-plugin.js +0 -62
- package/lib/plugins/workflow-plugin.js +0 -104
package/lib/server-startup2.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import PluginLoader from './plugin-loader.js';
|
|
5
4
|
|
|
6
5
|
export function createAutoImport({ queries, broadcastSync }) {
|
|
7
6
|
const importMtimeCache = new Map();
|
|
@@ -57,28 +56,3 @@ export function createDbRecovery({ queries, debugLog }) {
|
|
|
57
56
|
|
|
58
57
|
return { performDbRecovery };
|
|
59
58
|
}
|
|
60
|
-
|
|
61
|
-
export function createPluginLoader({ pluginsDir, expressApp, BASE_URL }) {
|
|
62
|
-
const pluginLoader = new PluginLoader(pluginsDir);
|
|
63
|
-
|
|
64
|
-
async function loadPluginExtensions() {
|
|
65
|
-
try {
|
|
66
|
-
await pluginLoader.loadAllPlugins({ router: expressApp, baseUrl: BASE_URL, logger: console, env: process.env });
|
|
67
|
-
const names = Array.from(pluginLoader.registry.keys());
|
|
68
|
-
if (names.length > 0) {
|
|
69
|
-
for (const name of names) {
|
|
70
|
-
const state = pluginLoader.get(name);
|
|
71
|
-
if (!state || !state.routes) continue;
|
|
72
|
-
for (const route of state.routes) {
|
|
73
|
-
const fullPath = BASE_URL + route.path;
|
|
74
|
-
const method = (route.method || 'GET').toLowerCase();
|
|
75
|
-
if (expressApp[method]) expressApp[method](fullPath, route.handler);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
console.log(`[PLUGINS] Loaded extensions: ${names.join(', ')}`);
|
|
79
|
-
}
|
|
80
|
-
} catch (err) { console.error('[PLUGINS] Extension loading failed (non-fatal):', err.message); }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return { loadPluginExtensions };
|
|
84
|
-
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -19,7 +19,7 @@ import { parseBody, acceptsEncoding, compressAndSend, sendJSON } from './lib/htt
|
|
|
19
19
|
import { createWsSetup } from './lib/ws-setup.js';
|
|
20
20
|
import { createHttpHandler } from './lib/http-handler.js';
|
|
21
21
|
import { createOnServerReady } from './lib/server-startup.js';
|
|
22
|
-
import { createAutoImport, createDbRecovery
|
|
22
|
+
import { createAutoImport, createDbRecovery } from './lib/server-startup2.js';
|
|
23
23
|
const sendWs = (ws, obj) => { if (ws.readyState === 1) ws.send(wsEncode(obj)); };
|
|
24
24
|
import { startAll as startACPTools, stopAll as stopACPTools, getStatus as getACPStatus, getPort as getACPPort, ensureRunning, queryModels as queryACPModels, touch as touchACP } from './lib/acp-sdk-manager.js';
|
|
25
25
|
import * as execMachine from './lib/execution-machine.js';
|
|
@@ -195,7 +195,6 @@ const onServerListenStart = () => {
|
|
|
195
195
|
if (_serverReadyFired) return;
|
|
196
196
|
_serverReadyFired = true;
|
|
197
197
|
onServerReady();
|
|
198
|
-
loadPluginExtensions();
|
|
199
198
|
};
|
|
200
199
|
|
|
201
200
|
let _portRetries = 0;
|
|
@@ -218,7 +217,6 @@ server.on('error', (err) => {
|
|
|
218
217
|
|
|
219
218
|
const { performAutoImport } = createAutoImport({ queries, broadcastSync });
|
|
220
219
|
const { performDbRecovery } = createDbRecovery({ queries, debugLog });
|
|
221
|
-
const { loadPluginExtensions } = createPluginLoader({ pluginsDir: path.join(__dirname, 'lib', 'plugins'), expressApp, BASE_URL });
|
|
222
220
|
|
|
223
221
|
setInterval(performDbRecovery, 300000);
|
|
224
222
|
|
package/test.js
CHANGED
|
@@ -76,9 +76,7 @@ await ok('machines: execution + acp-server lifecycle', () => {
|
|
|
76
76
|
assert.equal(asm.isHealthy('test-tool'), true);
|
|
77
77
|
asm.stopAll();
|
|
78
78
|
});
|
|
79
|
-
await ok('
|
|
80
|
-
const wp = await import('./lib/plugins/workflow-plugin.js');
|
|
81
|
-
assert.deepEqual(wp.default.dependencies, ['database']);
|
|
79
|
+
await ok('agent-registry: hermes ACP descriptor', async () => {
|
|
82
80
|
const { registry } = await import('./lib/claude-runner-agents.js');
|
|
83
81
|
const h = registry.get('hermes');
|
|
84
82
|
assert.equal(h.protocol, 'acp'); assert.deepEqual(h.buildArgs(), ['acp']);
|
package/lib/plugin-loader.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { EventEmitter } from 'events';
|
|
4
|
-
|
|
5
|
-
class PluginLoader extends EventEmitter {
|
|
6
|
-
constructor(pluginDir) {
|
|
7
|
-
super();
|
|
8
|
-
this.pluginDir = pluginDir;
|
|
9
|
-
this.registry = new Map();
|
|
10
|
-
this.instances = new Map();
|
|
11
|
-
this.states = new Map();
|
|
12
|
-
this.watchers = new Map();
|
|
13
|
-
this.errorCounts = new Map();
|
|
14
|
-
this.fileMap = new Map();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async loadPlugin(fileName) {
|
|
18
|
-
const filePath = path.join(this.pluginDir, `${fileName}.js`);
|
|
19
|
-
if (!fs.existsSync(filePath)) {
|
|
20
|
-
throw new Error(`Plugin file not found: ${filePath}`);
|
|
21
|
-
}
|
|
22
|
-
const fileUrl = `file://${filePath}?v=${Date.now()}`;
|
|
23
|
-
try {
|
|
24
|
-
const plugin = await import(fileUrl);
|
|
25
|
-
const mod = plugin.default || plugin;
|
|
26
|
-
const regName = mod.name || fileName;
|
|
27
|
-
this.registry.set(regName, mod);
|
|
28
|
-
this.fileMap.set(regName, fileName);
|
|
29
|
-
return mod;
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.error(`Failed to load plugin ${fileName}:`, error.message);
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async initializePlugin(name, config) {
|
|
37
|
-
const plugin = this.registry.get(name);
|
|
38
|
-
if (!plugin) {
|
|
39
|
-
throw new Error(`Plugin ${name} not found in registry`);
|
|
40
|
-
}
|
|
41
|
-
if (this.instances.has(name)) {
|
|
42
|
-
return this.instances.get(name);
|
|
43
|
-
}
|
|
44
|
-
for (const depName of (plugin.dependencies || [])) {
|
|
45
|
-
if (!this.instances.has(depName)) {
|
|
46
|
-
await this.initializePlugin(depName, config);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
const result = await plugin.init(config, this.instances);
|
|
51
|
-
this.instances.set(name, result);
|
|
52
|
-
return result;
|
|
53
|
-
} catch (error) {
|
|
54
|
-
console.error(`[PluginLoader] Error initializing ${name}:`, error.message);
|
|
55
|
-
throw error;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
get(name) {
|
|
60
|
-
return this.instances.get(name);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async reloadPlugin(name) {
|
|
64
|
-
const plugin = this.registry.get(name);
|
|
65
|
-
if (!plugin) {
|
|
66
|
-
console.warn(`[PluginLoader] Cannot reload ${name}: not found`);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const state = this.instances.get(name);
|
|
70
|
-
if (!state) {
|
|
71
|
-
console.warn(`[PluginLoader] Cannot reload ${name}: not initialized`);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
if (state.stop) await state.stop();
|
|
76
|
-
const fileName = this.fileMap.get(name) || name;
|
|
77
|
-
await this.loadPlugin(fileName);
|
|
78
|
-
const reloadedPlugin = this.registry.get(name);
|
|
79
|
-
const newState = await reloadedPlugin.reload(state);
|
|
80
|
-
this.instances.set(name, newState);
|
|
81
|
-
this.emit('reload', { name, success: true });
|
|
82
|
-
console.log(`[PluginLoader] Reloaded plugin: ${name}`);
|
|
83
|
-
} catch (error) {
|
|
84
|
-
console.error(`[PluginLoader] Error reloading ${name}:`, error.message);
|
|
85
|
-
this.emit('reload', { name, success: false, error: error.message });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
watchPlugin(name, callback) {
|
|
90
|
-
const fileName = this.fileMap.get(name) || name;
|
|
91
|
-
const filePath = path.join(this.pluginDir, `${fileName}.js`);
|
|
92
|
-
if (this.watchers.has(name)) return;
|
|
93
|
-
const watcher = fs.watch(filePath, async (eventType) => {
|
|
94
|
-
if (eventType === 'change') {
|
|
95
|
-
setTimeout(() => callback(name), 100);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
this.watchers.set(name, watcher);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
unwatchPlugin(name) {
|
|
102
|
-
const watcher = this.watchers.get(name);
|
|
103
|
-
if (watcher) {
|
|
104
|
-
watcher.close();
|
|
105
|
-
this.watchers.delete(name);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async loadAllPlugins(config) {
|
|
110
|
-
if (!fs.existsSync(this.pluginDir)) {
|
|
111
|
-
fs.mkdirSync(this.pluginDir, { recursive: true });
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
const files = fs.readdirSync(this.pluginDir).filter(f => f.endsWith('.js'));
|
|
115
|
-
for (const file of files) {
|
|
116
|
-
const fileName = file.replace('.js', '');
|
|
117
|
-
try {
|
|
118
|
-
await this.loadPlugin(fileName);
|
|
119
|
-
} catch (error) {
|
|
120
|
-
console.error(`[PluginLoader] Failed to load ${fileName}:`, error.message);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
const sorted = this.topologicalSort();
|
|
124
|
-
for (const name of sorted) {
|
|
125
|
-
try {
|
|
126
|
-
await this.initializePlugin(name, config);
|
|
127
|
-
} catch (error) {
|
|
128
|
-
console.error(`[PluginLoader] Failed to initialize ${name}:`, error.message);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
topologicalSort() {
|
|
134
|
-
const visited = new Set(), result = [];
|
|
135
|
-
const visit = (name) => {
|
|
136
|
-
if (visited.has(name)) return;
|
|
137
|
-
visited.add(name);
|
|
138
|
-
for (const dep of (this.registry.get(name)?.dependencies || [])) { if (this.registry.has(dep)) visit(dep); }
|
|
139
|
-
result.push(name);
|
|
140
|
-
};
|
|
141
|
-
for (const name of this.registry.keys()) visit(name);
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async shutdown() {
|
|
146
|
-
const sorted = this.topologicalSort().reverse();
|
|
147
|
-
for (const name of sorted) {
|
|
148
|
-
const state = this.instances.get(name);
|
|
149
|
-
if (state && state.stop) {
|
|
150
|
-
try {
|
|
151
|
-
await state.stop();
|
|
152
|
-
} catch (error) {
|
|
153
|
-
console.error(`[PluginLoader] Error stopping ${name}:`, error.message);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
this.unwatchPlugin(name);
|
|
157
|
-
}
|
|
158
|
-
this.instances.clear();
|
|
159
|
-
this.registry.clear();
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
export default PluginLoader;
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
name: 'agents',
|
|
5
|
-
version: '1.0.0',
|
|
6
|
-
dependencies: ['database', 'stream'],
|
|
7
|
-
|
|
8
|
-
async init(config, plugins) {
|
|
9
|
-
const db = plugins.get('database');
|
|
10
|
-
const stream = plugins.get('stream');
|
|
11
|
-
const discoveredAgents = new Map();
|
|
12
|
-
const activeExecutions = new Map();
|
|
13
|
-
|
|
14
|
-
// Discover agents on startup
|
|
15
|
-
const discoverAgents = async () => {
|
|
16
|
-
const agents = [
|
|
17
|
-
{ id: 'gm-cc', name: 'Claude Code', bin: 'claude', installed: true },
|
|
18
|
-
{ id: 'gm-oc', name: 'OpenCode', bin: 'opencode', installed: false },
|
|
19
|
-
{ id: 'gm-gc', name: 'Gemini CLI', bin: 'gemini', installed: false },
|
|
20
|
-
{ id: 'gm-kilo', name: 'Kilo', bin: 'kilo', installed: false },
|
|
21
|
-
];
|
|
22
|
-
agents.forEach(a => discoveredAgents.set(a.id, a));
|
|
23
|
-
return agents;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
await discoverAgents();
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
routes: [
|
|
30
|
-
{
|
|
31
|
-
method: 'GET',
|
|
32
|
-
path: '/api/agents',
|
|
33
|
-
handler: (req, res) => {
|
|
34
|
-
res.json({ agents: Array.from(discoveredAgents.values()) });
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
method: 'POST',
|
|
39
|
-
path: '/api/conversations/:id/stream',
|
|
40
|
-
handler: async (req, res) => {
|
|
41
|
-
const { id } = req.params;
|
|
42
|
-
const { agentId, message } = req.body;
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const agent = discoveredAgents.get(agentId);
|
|
46
|
-
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
47
|
-
|
|
48
|
-
const session = stream.api.createSession(id);
|
|
49
|
-
activeExecutions.set(id, { sessionId: session.id });
|
|
50
|
-
|
|
51
|
-
res.json({ sessionId: session.id });
|
|
52
|
-
} catch (e) {
|
|
53
|
-
res.status(500).json({ error: e.message });
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
wsHandlers: {},
|
|
59
|
-
api: {
|
|
60
|
-
getAgents: () => Array.from(discoveredAgents.values()),
|
|
61
|
-
discoverAgents,
|
|
62
|
-
},
|
|
63
|
-
stop: async () => {
|
|
64
|
-
for (const proc of activeExecutions.values()) {
|
|
65
|
-
if (proc && !proc.killed) proc.kill();
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
async reload(state) {
|
|
72
|
-
return state;
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
async stop() {},
|
|
76
|
-
};
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// Database plugin - SQLite init, schema
|
|
2
|
-
|
|
3
|
-
import * as dbModule from '../../database.js';
|
|
4
|
-
|
|
5
|
-
export default {
|
|
6
|
-
name: 'database',
|
|
7
|
-
version: '1.0.0',
|
|
8
|
-
dependencies: [],
|
|
9
|
-
|
|
10
|
-
async init(config, plugins) {
|
|
11
|
-
// Initialize database schema
|
|
12
|
-
if (dbModule.initializeSchema) dbModule.initializeSchema();
|
|
13
|
-
|
|
14
|
-
// Return API for other plugins
|
|
15
|
-
return {
|
|
16
|
-
routes: [],
|
|
17
|
-
wsHandlers: {},
|
|
18
|
-
api: {
|
|
19
|
-
// Query functions from database.js
|
|
20
|
-
queries: dbModule.queries || {},
|
|
21
|
-
|
|
22
|
-
// Checkpoint/recovery
|
|
23
|
-
checkpoint: (label) => {
|
|
24
|
-
console.log(`[Database] Checkpoint: ${label}`);
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
recover: async (label) => {
|
|
28
|
-
console.log(`[Database] Recover from: ${label}`);
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
// Direct DB access
|
|
32
|
-
db: dbModule.dataDir,
|
|
33
|
-
},
|
|
34
|
-
stop: async () => {},
|
|
35
|
-
};
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
async reload(state) {
|
|
39
|
-
return state;
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
async stop() {},
|
|
43
|
-
};
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// Stream plugin - session management, streaming execution, rate limiting
|
|
2
|
-
|
|
3
|
-
import { randomUUID as uuidv4 } from 'crypto';
|
|
4
|
-
|
|
5
|
-
export default {
|
|
6
|
-
name: 'stream',
|
|
7
|
-
version: '1.0.0',
|
|
8
|
-
dependencies: ['database'],
|
|
9
|
-
|
|
10
|
-
async init(config, plugins) {
|
|
11
|
-
const dbPlugin = plugins.get('database');
|
|
12
|
-
const db = dbPlugin?.api || {};
|
|
13
|
-
const activeSessions = new Map();
|
|
14
|
-
const pendingMessages = new Map();
|
|
15
|
-
const rateLimitState = new Map();
|
|
16
|
-
const recoveryCheckpoints = new Map();
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
routes: [
|
|
20
|
-
{
|
|
21
|
-
method: 'GET',
|
|
22
|
-
path: '/api/sessions/:id',
|
|
23
|
-
handler: async (req, res) => {
|
|
24
|
-
const { id } = req.params;
|
|
25
|
-
const session = activeSessions.get(id);
|
|
26
|
-
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
27
|
-
res.json(session);
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
method: 'GET',
|
|
32
|
-
path: '/api/sessions/:id/chunks',
|
|
33
|
-
handler: async (req, res) => {
|
|
34
|
-
const { id } = req.params;
|
|
35
|
-
const { since } = req.query;
|
|
36
|
-
const chunks = db.queries.getStreamChunks(id, since ? parseInt(since) : 0);
|
|
37
|
-
res.json({ chunks });
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
method: 'GET',
|
|
42
|
-
path: '/api/sessions/:id/execution',
|
|
43
|
-
handler: async (req, res) => {
|
|
44
|
-
const { id } = req.params;
|
|
45
|
-
const { limit, offset, filterType } = req.query;
|
|
46
|
-
const events = db.queries.getExecutionEvents(id, parseInt(limit) || 100, parseInt(offset) || 0);
|
|
47
|
-
res.json({ events });
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
method: 'GET',
|
|
52
|
-
path: '/api/conversations/:id/sessions/latest',
|
|
53
|
-
handler: async (req, res) => {
|
|
54
|
-
const { id } = req.params;
|
|
55
|
-
const sessions = Array.from(activeSessions.values()).filter(s => s.conversationId === id);
|
|
56
|
-
const latest = sessions[sessions.length - 1];
|
|
57
|
-
res.json(latest || { error: 'No sessions' });
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
wsHandlers: {
|
|
62
|
-
streaming_start: (data, clients) => {},
|
|
63
|
-
streaming_progress: (data, clients) => {},
|
|
64
|
-
streaming_complete: (data, clients) => {},
|
|
65
|
-
streaming_error: (data, clients) => {},
|
|
66
|
-
rate_limit_hit: (data, clients) => {},
|
|
67
|
-
},
|
|
68
|
-
api: {
|
|
69
|
-
createSession: (conversationId) => {
|
|
70
|
-
const session = { id: uuidv4(), conversationId, createdAt: Date.now() };
|
|
71
|
-
activeSessions.set(session.id, session);
|
|
72
|
-
return session;
|
|
73
|
-
},
|
|
74
|
-
getSession: (id) => activeSessions.get(id),
|
|
75
|
-
closeSession: (id) => activeSessions.delete(id),
|
|
76
|
-
},
|
|
77
|
-
stop: async () => {
|
|
78
|
-
activeSessions.clear();
|
|
79
|
-
pendingMessages.clear();
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
async reload(state) {
|
|
85
|
-
return state;
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
async stop() {},
|
|
89
|
-
};
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// WebSocket plugin - message routing, optimization, real-time sync
|
|
2
|
-
|
|
3
|
-
import { WebSocketServer } from 'ws';
|
|
4
|
-
|
|
5
|
-
export default {
|
|
6
|
-
name: 'websocket',
|
|
7
|
-
version: '1.0.0',
|
|
8
|
-
dependencies: ['database', 'stream', 'agents'],
|
|
9
|
-
|
|
10
|
-
async init(config, plugins) {
|
|
11
|
-
const db = plugins.get('database');
|
|
12
|
-
const stream = plugins.get('stream');
|
|
13
|
-
const agents = plugins.get('agents');
|
|
14
|
-
|
|
15
|
-
const subscribers = new Map(); // sessionId/conversationId => Set<client>
|
|
16
|
-
const routingTable = new Map(); // eventType => handler
|
|
17
|
-
|
|
18
|
-
const broadcast = (eventType, data) => {
|
|
19
|
-
// Broadcast to all subscribed clients
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const subscribe = (client, id, type) => {
|
|
23
|
-
if (!subscribers.has(id)) {
|
|
24
|
-
subscribers.set(id, new Set());
|
|
25
|
-
}
|
|
26
|
-
subscribers.get(id).add(client);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const unsubscribe = (client, id) => {
|
|
30
|
-
const set = subscribers.get(id);
|
|
31
|
-
if (set) set.delete(client);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
routes: [],
|
|
36
|
-
wsHandlers: {
|
|
37
|
-
// Routing handlers for all conversation/session events
|
|
38
|
-
subscribe: (data, clients) => {},
|
|
39
|
-
unsubscribe: (data, clients) => {},
|
|
40
|
-
ping: (data, clients) => {},
|
|
41
|
-
},
|
|
42
|
-
api: {
|
|
43
|
-
broadcast,
|
|
44
|
-
subscribe,
|
|
45
|
-
unsubscribe,
|
|
46
|
-
addHandler: (eventType, handler) => {
|
|
47
|
-
routingTable.set(eventType, handler);
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
stop: async () => {
|
|
51
|
-
subscribers.clear();
|
|
52
|
-
routingTable.clear();
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
async reload(state) {
|
|
58
|
-
return state;
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
async stop() {},
|
|
62
|
-
};
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
export default {
|
|
5
|
-
name: 'workflow',
|
|
6
|
-
version: '1.0.0',
|
|
7
|
-
dependencies: ['database'],
|
|
8
|
-
|
|
9
|
-
async init(config, plugins) {
|
|
10
|
-
plugins.get('database');
|
|
11
|
-
const workflowPolls = new Map();
|
|
12
|
-
|
|
13
|
-
const getWorkflows = () => {
|
|
14
|
-
const repoRoot = process.cwd();
|
|
15
|
-
const workflowDir = path.join(repoRoot, '.github', 'workflows');
|
|
16
|
-
if (!fs.existsSync(workflowDir)) return [];
|
|
17
|
-
return fs.readdirSync(workflowDir)
|
|
18
|
-
.filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
|
|
19
|
-
.map(name => ({ name, path: path.join(workflowDir, name) }));
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const resolvedWorkflowDir = path.resolve(process.cwd(), '.github', 'workflows');
|
|
23
|
-
|
|
24
|
-
const isNameSafe = (name) => !/[/\\]/.test(name);
|
|
25
|
-
|
|
26
|
-
const parseWorkflow = (filePath) => {
|
|
27
|
-
try {
|
|
28
|
-
// Confine to workflowDir: resolve and verify the path stays within the allowed directory
|
|
29
|
-
const resolved = path.resolve(filePath);
|
|
30
|
-
if (!resolved.startsWith(resolvedWorkflowDir + path.sep) && resolved !== resolvedWorkflowDir) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
const content = fs.readFileSync(resolved, 'utf8');
|
|
34
|
-
// Parse YAML manually or return raw content
|
|
35
|
-
return { name: path.basename(resolved), content };
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
routes: [
|
|
43
|
-
{
|
|
44
|
-
method: 'GET',
|
|
45
|
-
path: '/api/workflows',
|
|
46
|
-
handler: (req, res) => {
|
|
47
|
-
const workflows = getWorkflows();
|
|
48
|
-
res.json({ workflows });
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
method: 'GET',
|
|
53
|
-
path: '/api/workflows/:name/history',
|
|
54
|
-
handler: (req, res) => {
|
|
55
|
-
if (!isNameSafe(req.params.name)) {
|
|
56
|
-
return res.status(400).json({ error: 'Invalid workflow name' });
|
|
57
|
-
}
|
|
58
|
-
res.json({ history: [] });
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
method: 'POST',
|
|
63
|
-
path: '/api/workflows/:name/trigger',
|
|
64
|
-
handler: async (req, res) => {
|
|
65
|
-
if (!isNameSafe(req.params.name)) {
|
|
66
|
-
return res.status(400).json({ error: 'Invalid workflow name' });
|
|
67
|
-
}
|
|
68
|
-
res.json({ success: true, message: 'Workflow trigger requires GitHub API' });
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
method: 'GET',
|
|
73
|
-
path: '/api/workflows/:name/status',
|
|
74
|
-
handler: (req, res) => {
|
|
75
|
-
if (!isNameSafe(req.params.name)) {
|
|
76
|
-
return res.status(400).json({ error: 'Invalid workflow name' });
|
|
77
|
-
}
|
|
78
|
-
res.json({ status: 'unknown' });
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
wsHandlers: {
|
|
83
|
-
workflow_triggered: (data, clients) => {},
|
|
84
|
-
workflow_progress: (data, clients) => {},
|
|
85
|
-
workflow_complete: (data, clients) => {},
|
|
86
|
-
},
|
|
87
|
-
api: {
|
|
88
|
-
getWorkflows,
|
|
89
|
-
parseWorkflow,
|
|
90
|
-
},
|
|
91
|
-
stop: async () => {
|
|
92
|
-
for (const interval of workflowPolls.values()) {
|
|
93
|
-
clearInterval(interval);
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
async reload(state) {
|
|
100
|
-
return state;
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
async stop() {},
|
|
104
|
-
};
|