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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.993",
3
+ "version": "1.0.995",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
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, createPluginLoader } from './lib/server-startup2.js';
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('workflow-plugin + agent-registry hermes', async () => {
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']);
@@ -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
- };