agentgui 1.0.994 → 1.0.996
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/AGENTS.md +3 -10
- package/README.md +7 -9
- 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/database-plugin.js +0 -43
- package/lib/plugins/stream-plugin.js +0 -89
- package/lib/plugins/websocket-plugin.js +0 -58
- package/lib/plugins/workflow-plugin.js +0 -104
package/AGENTS.md
CHANGED
|
@@ -116,8 +116,7 @@ When `PASSWORD` env var is set, every HTTP route is gated by `lib/http-handler.j
|
|
|
116
116
|
- `site/app/js/app.js` — webjsx view + state; renders the `AgentChat` kit (from `anentrypoint-design`) for the chat surface and wires agentgui's WS/ccsniff state as kit callbacks; history/settings remain agentgui-local. Exposes `window.__agentgui`
|
|
117
117
|
|
|
118
118
|
The chat GUI lives in the design kit, not in agentgui. The reusable multi-agent chat surface is the `AgentChat` component in `anentrypoint-design` (`src/components/agent-chat.js`); agentgui keeps only the transport glue (WS `backend.js`, ccsniff history wiring, agent orchestration) and passes state + callbacks into the kit. To change chat UI, edit the kit and push it (CI publishes to npm -> unpkg `@latest`), not agentgui.
|
|
119
|
-
- `server.js` —
|
|
120
|
-
- Plugins kept (lib/plugins/): acp, agents, database, files, stream, websocket, workflow
|
|
119
|
+
- `server.js` — initializes the ACP-SDK manager + agent registry, registers WS handlers (`ws-handlers-util.js`), mounts `createHistoryRouter()` from `ccsniff` at `/`, serves `site/app/` as static root. The `lib/plugins/` system was removed (2026-06-20, 20th run) — it was dead scaffolding (routes 404, wsHandlers never wired, api unconsumed); all server logic lives directly in `server.js` + `lib/ws-handlers-util.js` + `lib/http-handler.js` + `lib/routes-upload.js` + `database.js`.
|
|
121
120
|
|
|
122
121
|
Dependencies:
|
|
123
122
|
- `ccsniff` (>=1.1.0) — exports `createHistoryRouter({projectsDir})` mountable on Express; serves `/v1/history/{sessions,sessions/:sid/events,search,snapshot,reindex,stream}`. Reads `~/.claude/projects` (override via `CLAUDE_PROJECTS_DIR`).
|
|
@@ -157,15 +156,9 @@ Why it works: `process.execPath` becomes bun, so the spawned child server also r
|
|
|
157
156
|
|
|
158
157
|
Rule: any CI step that spawns the agentgui server (directly or via a script that inherits `process.execPath`) must invoke it with `bun`.
|
|
159
158
|
|
|
160
|
-
## Plugin
|
|
159
|
+
## Plugin system removed (2026-06-20 — 20th run)
|
|
161
160
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
The plugin loader runs at startup and logs failures silently if a plugin import fails. If stream plugin fails to load due to missing dependency (e.g., `uuid`), the error cascades: `agents` and `websocket` plugins both declare `stream` as a dependency and fail with "Plugin stream not found in registry", cascading to boot failure.
|
|
165
|
-
|
|
166
|
-
Fix for uuid: replaced `import { v4 as uuidv4 } from 'uuid'` with `import { randomUUID as uuidv4 } from 'crypto'` (Node.js built-in, zero deps).
|
|
167
|
-
|
|
168
|
-
Pattern: all imports in lib/plugins/* must be either built-in (crypto, fs, path, etc.) or already in package.json. No new npm packages should be added to plugins without adding them to dependencies.
|
|
161
|
+
The entire `lib/plugins/` system + `plugin-loader.js` was deleted as dead scaffolding (commits up to e73ed0e): plugin routes were unreachable (http-handler forwarded only a few prefixes -> 404), plugin `wsHandlers` were never wired (the real WS handlers come from `registerWsHandlers` in `ws-handlers-util.js`), plugin `api` objects were never consumed, and the inits were no-ops or managed local Maps nobody read. Removed across 7 commits (acp, files, agents, then database/stream/workflow/websocket + loader). The general lesson survives: any import under `lib/` must be a built-in (crypto, fs, path) or already in package.json — a missing dep crashes boot.
|
|
169
162
|
|
|
170
163
|
## Plugin Tool Provisioning on Windows
|
|
171
164
|
|
package/README.md
CHANGED
|
@@ -133,14 +133,12 @@ lib/ws-optimizer.js Per-client priority queue for WS event batching
|
|
|
133
133
|
lib/ws-handlers-*.js WebSocket RPC handlers (conv, msg, queue, session, run, util, oauth, scripts)
|
|
134
134
|
lib/speech.js Speech-to-text and text-to-speech via @huggingface/transformers
|
|
135
135
|
lib/tool-manager.js Tool detection, install, update facade
|
|
136
|
-
lib/
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
static/js/websocket-manager.js WebSocket connection handling
|
|
143
|
-
static/js/*.machine.js XState v5 state machines (ws, conv, tool-install, voice, conv-list, prompt)
|
|
136
|
+
lib/ws-handlers-util.js WS RPC handler registration (agents.list, chat.sendMessage, ...)
|
|
137
|
+
lib/http-handler.js Static serving + /api/* routes + auth gate + path confinement
|
|
138
|
+
lib/routes-upload.js Express app for multipart upload + fsbrowse file serving
|
|
139
|
+
site/app/index.html SPA shell; imports the anentrypoint-design kit
|
|
140
|
+
site/app/js/app.js webjsx view + state; renders the AgentChat kit, wires WS callbacks
|
|
141
|
+
site/app/js/backend.js Same-origin WS/HTTP client (transport glue)
|
|
144
142
|
```
|
|
145
143
|
|
|
146
144
|
### Key Details
|
|
@@ -148,7 +146,7 @@ static/js/*.machine.js XState v5 state machines (ws, conv, tool-instal
|
|
|
148
146
|
- Agent discovery scans PATH for known CLI binaries at startup
|
|
149
147
|
- Database lives at `~/.gmgui/data.db` (WAL mode for concurrent access)
|
|
150
148
|
- WebSocket endpoint at `/gm/sync` for real-time updates
|
|
151
|
-
- ACP tools (OpenCode, Kilo)
|
|
149
|
+
- ACP tools (OpenCode, Kilo, Codex) start on demand as HTTP servers with health checks + restart backoff
|
|
152
150
|
|
|
153
151
|
## Use Cases
|
|
154
152
|
|
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,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,58 +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'],
|
|
9
|
-
|
|
10
|
-
async init(config, plugins) {
|
|
11
|
-
const subscribers = new Map(); // sessionId/conversationId => Set<client>
|
|
12
|
-
const routingTable = new Map(); // eventType => handler
|
|
13
|
-
|
|
14
|
-
const broadcast = (eventType, data) => {
|
|
15
|
-
// Broadcast to all subscribed clients
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const subscribe = (client, id, type) => {
|
|
19
|
-
if (!subscribers.has(id)) {
|
|
20
|
-
subscribers.set(id, new Set());
|
|
21
|
-
}
|
|
22
|
-
subscribers.get(id).add(client);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const unsubscribe = (client, id) => {
|
|
26
|
-
const set = subscribers.get(id);
|
|
27
|
-
if (set) set.delete(client);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
routes: [],
|
|
32
|
-
wsHandlers: {
|
|
33
|
-
// Routing handlers for all conversation/session events
|
|
34
|
-
subscribe: (data, clients) => {},
|
|
35
|
-
unsubscribe: (data, clients) => {},
|
|
36
|
-
ping: (data, clients) => {},
|
|
37
|
-
},
|
|
38
|
-
api: {
|
|
39
|
-
broadcast,
|
|
40
|
-
subscribe,
|
|
41
|
-
unsubscribe,
|
|
42
|
-
addHandler: (eventType, handler) => {
|
|
43
|
-
routingTable.set(eventType, handler);
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
stop: async () => {
|
|
47
|
-
subscribers.clear();
|
|
48
|
-
routingTable.clear();
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
async reload(state) {
|
|
54
|
-
return state;
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
async stop() {},
|
|
58
|
-
};
|
|
@@ -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
|
-
};
|