agentgui 1.0.722 → 1.0.723
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/jsonl-watcher.js +34 -5
- package/lib/plugin-loader.js +188 -162
- package/lib/plugins/agents-plugin.js +2 -1
- package/lib/plugins/database-plugin.js +1 -1
- package/lib/plugins/stream-plugin.js +1 -2
- package/lib/tool-provisioner.js +1 -1
- package/package.json +1 -1
- package/server.js +3 -3
package/lib/jsonl-watcher.js
CHANGED
|
@@ -16,6 +16,7 @@ export class JsonlWatcher {
|
|
|
16
16
|
this._timers = new Map();
|
|
17
17
|
this._seqs = new Map();
|
|
18
18
|
this._streaming = new Set();
|
|
19
|
+
this._sessions = new Map();
|
|
19
20
|
this._watcher = null;
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -42,6 +43,7 @@ export class JsonlWatcher {
|
|
|
42
43
|
this._convMap.delete(sid);
|
|
43
44
|
this._seqs.delete(sid);
|
|
44
45
|
this._streaming.delete(sid);
|
|
46
|
+
this._sessions.delete(sid);
|
|
45
47
|
for (const key of [...this._emitted.keys()]) if (key.startsWith(`${sid}:`)) this._emitted.delete(key);
|
|
46
48
|
for (const [fp, s] of this._tails.entries()) {
|
|
47
49
|
if (!fp.includes(sid)) continue;
|
|
@@ -58,6 +60,7 @@ export class JsonlWatcher {
|
|
|
58
60
|
for (const t of this._timers.values()) clearTimeout(t);
|
|
59
61
|
this._tails.clear(); this._convMap.clear(); this._emitted.clear();
|
|
60
62
|
this._timers.clear(); this._seqs.clear(); this._streaming.clear();
|
|
63
|
+
this._sessions.clear();
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
_scanDir(dir, depth) {
|
|
@@ -129,8 +132,19 @@ export class JsonlWatcher {
|
|
|
129
132
|
|
|
130
133
|
_seq(sid) { const n = (this._seqs.get(sid) || 0) + 1; this._seqs.set(sid, n); return n; }
|
|
131
134
|
|
|
135
|
+
_dbSession(cid, sid) {
|
|
136
|
+
if (this._sessions.has(sid)) return this._sessions.get(sid);
|
|
137
|
+
const sess = this._q.createSession(cid);
|
|
138
|
+
this._q.updateSession(sess.id, { status: 'active' });
|
|
139
|
+
this._sessions.set(sid, sess.id);
|
|
140
|
+
return sess.id;
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
_emit(cid, sid, block, role, extra) {
|
|
133
|
-
|
|
144
|
+
const dbSid = this._dbSession(cid, sid);
|
|
145
|
+
const seq = this._seq(sid);
|
|
146
|
+
try { this._q.createChunk(dbSid, cid, seq, block.type || 'unknown', block); } catch (_) {}
|
|
147
|
+
this._bc({ type: 'streaming_progress', sessionId: dbSid, conversationId: cid, block, blockRole: role, seq, timestamp: Date.now(), ...extra });
|
|
134
148
|
}
|
|
135
149
|
|
|
136
150
|
_emitBlocks(cid, sid, blocks, role) {
|
|
@@ -143,20 +157,28 @@ export class JsonlWatcher {
|
|
|
143
157
|
_startStreaming(cid, sid) {
|
|
144
158
|
if (this._streaming.has(sid)) return;
|
|
145
159
|
this._streaming.add(sid);
|
|
146
|
-
this.
|
|
160
|
+
const dbSid = this._dbSession(cid, sid);
|
|
161
|
+
this._q.setIsStreaming(cid, true);
|
|
162
|
+
this._bc({ type: 'streaming_start', sessionId: dbSid, conversationId: cid, agentId: 'cli-claude', timestamp: Date.now() });
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
_endStreaming(cid, sid) {
|
|
150
166
|
if (!this._streaming.has(sid)) return;
|
|
151
167
|
this._streaming.delete(sid);
|
|
152
|
-
|
|
168
|
+
const dbSid = this._sessions.get(sid);
|
|
169
|
+
if (dbSid) {
|
|
170
|
+
try { this._q.updateSession(dbSid, { status: 'completed', completed_at: Date.now() }); } catch (_) {}
|
|
171
|
+
}
|
|
172
|
+
this._q.setIsStreaming(cid, false);
|
|
173
|
+
this._bc({ type: 'streaming_complete', sessionId: dbSid || sid, conversationId: cid, agentId: 'cli-claude', eventCount: 0, seq: this._seq(sid), timestamp: Date.now() });
|
|
174
|
+
this._sessions.delete(sid);
|
|
153
175
|
}
|
|
154
176
|
|
|
155
177
|
_route(cid, sid, e) {
|
|
156
178
|
if (e.type === 'queue-operation' || e.type === 'last-prompt' || (e.type === 'user' && e.isMeta)) return;
|
|
157
179
|
|
|
158
180
|
if (e.isApiErrorMessage && e.error === 'rate_limit') {
|
|
159
|
-
this._bc({ type: 'streaming_error', sessionId: sid, conversationId: cid, error: 'Rate limit hit', recoverable: true, timestamp: Date.now() });
|
|
181
|
+
this._bc({ type: 'streaming_error', sessionId: this._sessions.get(sid) || sid, conversationId: cid, error: 'Rate limit hit', recoverable: true, timestamp: Date.now() });
|
|
160
182
|
return;
|
|
161
183
|
}
|
|
162
184
|
|
|
@@ -185,7 +207,14 @@ export class JsonlWatcher {
|
|
|
185
207
|
if (e.type === 'user' && e.message?.content) {
|
|
186
208
|
if (e.isCompactSummary) { this._emit(cid, sid, { type: 'compact_summary', content: e.message.content }, 'system'); return; }
|
|
187
209
|
this._startStreaming(cid, sid);
|
|
188
|
-
const
|
|
210
|
+
const content = e.message.content;
|
|
211
|
+
const textParts = Array.isArray(content)
|
|
212
|
+
? content.filter(b => b.type === 'text' && b.text && !b.text.startsWith('<task-notification>')).map(b => b.text).join('\n')
|
|
213
|
+
: (typeof content === 'string' && !content.startsWith('<task-notification>') ? content : '');
|
|
214
|
+
if (textParts) {
|
|
215
|
+
try { this._q.createMessage(cid, 'user', textParts); } catch (_) {}
|
|
216
|
+
}
|
|
217
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
189
218
|
for (const b of blocks) if (b.type === 'tool_result') this._emit(cid, sid, b, 'tool_result');
|
|
190
219
|
return;
|
|
191
220
|
}
|
package/lib/plugin-loader.js
CHANGED
|
@@ -1,162 +1,188 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
this.
|
|
11
|
-
this.
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
this.
|
|
29
|
-
return
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.error(`Failed to load plugin ${
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
1
|
+
// Plugin loader - manages registry, dependencies, hot reload, error isolation
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
|
|
7
|
+
class PluginLoader extends EventEmitter {
|
|
8
|
+
constructor(pluginDir) {
|
|
9
|
+
super();
|
|
10
|
+
this.pluginDir = pluginDir;
|
|
11
|
+
this.registry = new Map(); // name => plugin module
|
|
12
|
+
this.instances = new Map(); // name => initialized plugin state
|
|
13
|
+
this.states = new Map(); // name => { routes, wsHandlers, api, ... }
|
|
14
|
+
this.watchers = new Map(); // name => file watcher
|
|
15
|
+
this.errorCounts = new Map(); // name => { count, firstTime }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Load plugin module from disk
|
|
19
|
+
async loadPlugin(name) {
|
|
20
|
+
const filePath = path.join(this.pluginDir, `${name}.js`);
|
|
21
|
+
if (!fs.existsSync(filePath)) {
|
|
22
|
+
throw new Error(`Plugin file not found: ${filePath}`);
|
|
23
|
+
}
|
|
24
|
+
// Clear module cache for hot reload (ES modules use import cache differently)
|
|
25
|
+
const fileUrl = `file://${filePath}?v=${Date.now()}`;
|
|
26
|
+
try {
|
|
27
|
+
const plugin = await import(fileUrl);
|
|
28
|
+
this.registry.set(name, plugin.default || plugin);
|
|
29
|
+
return plugin.default || plugin;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(`Failed to load plugin ${name}:`, error.message);
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Initialize plugin and all dependencies
|
|
37
|
+
async initializePlugin(name, config) {
|
|
38
|
+
const plugin = this.registry.get(name);
|
|
39
|
+
if (!plugin) {
|
|
40
|
+
throw new Error(`Plugin ${name} not found in registry`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if already initialized
|
|
44
|
+
if (this.instances.has(name)) {
|
|
45
|
+
return this.instances.get(name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Initialize dependencies first
|
|
49
|
+
for (const depName of (plugin.dependencies || [])) {
|
|
50
|
+
if (!this.instances.has(depName)) {
|
|
51
|
+
await this.initializePlugin(depName, config);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Initialize this plugin
|
|
56
|
+
try {
|
|
57
|
+
const result = await plugin.init(config, this.instances);
|
|
58
|
+
this.instances.set(name, result);
|
|
59
|
+
return result;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(`[PluginLoader] Error initializing ${name}:`, error.message);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Get initialized plugin result
|
|
67
|
+
get(name) {
|
|
68
|
+
return this.instances.get(name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Hot reload a plugin
|
|
72
|
+
async reloadPlugin(name) {
|
|
73
|
+
const plugin = this.registry.get(name);
|
|
74
|
+
if (!plugin) {
|
|
75
|
+
console.warn(`[PluginLoader] Cannot reload ${name}: not found`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const state = this.instances.get(name);
|
|
80
|
+
if (!state) {
|
|
81
|
+
console.warn(`[PluginLoader] Cannot reload ${name}: not initialized`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// Stop old instance
|
|
87
|
+
if (state.stop) await state.stop();
|
|
88
|
+
|
|
89
|
+
// Reload plugin module
|
|
90
|
+
this.loadPlugin(name);
|
|
91
|
+
const reloadedPlugin = this.registry.get(name);
|
|
92
|
+
|
|
93
|
+
// Reinitialize with preserved state
|
|
94
|
+
const newState = await reloadedPlugin.reload(state);
|
|
95
|
+
this.instances.set(name, newState);
|
|
96
|
+
this.emit('reload', { name, success: true });
|
|
97
|
+
console.log(`[PluginLoader] Reloaded plugin: ${name}`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(`[PluginLoader] Error reloading ${name}:`, error.message);
|
|
100
|
+
this.emit('reload', { name, success: false, error: error.message });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Watch a plugin file for changes
|
|
105
|
+
watchPlugin(name, callback) {
|
|
106
|
+
const filePath = path.join(this.pluginDir, `${name}.js`);
|
|
107
|
+
if (this.watchers.has(name)) {
|
|
108
|
+
return; // Already watching
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const watcher = fs.watch(filePath, async (eventType) => {
|
|
112
|
+
if (eventType === 'change') {
|
|
113
|
+
setTimeout(() => callback(name), 100); // Debounce
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.watchers.set(name, watcher);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Stop watching a plugin
|
|
121
|
+
unwatchPlugin(name) {
|
|
122
|
+
const watcher = this.watchers.get(name);
|
|
123
|
+
if (watcher) {
|
|
124
|
+
watcher.close();
|
|
125
|
+
this.watchers.delete(name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Load all plugins from directory
|
|
130
|
+
async loadAllPlugins(config) {
|
|
131
|
+
if (!fs.existsSync(this.pluginDir)) {
|
|
132
|
+
fs.mkdirSync(this.pluginDir, { recursive: true });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const files = fs.readdirSync(this.pluginDir).filter(f => f.endsWith('.js'));
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
const name = file.replace('.js', '');
|
|
139
|
+
try {
|
|
140
|
+
await this.loadPlugin(name);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`[PluginLoader] Failed to load ${name}:`, error.message);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Initialize in dependency order
|
|
147
|
+
const sorted = this.topologicalSort();
|
|
148
|
+
for (const name of sorted) {
|
|
149
|
+
try {
|
|
150
|
+
await this.initializePlugin(name, config);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(`[PluginLoader] Failed to initialize ${name}:`, error.message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Topological sort by dependencies
|
|
158
|
+
topologicalSort() {
|
|
159
|
+
const visited = new Set(), result = [];
|
|
160
|
+
const visit = (name) => {
|
|
161
|
+
if (visited.has(name)) return;
|
|
162
|
+
visited.add(name);
|
|
163
|
+
for (const dep of (this.registry.get(name)?.dependencies || [])) { if (this.registry.has(dep)) visit(dep); }
|
|
164
|
+
result.push(name);
|
|
165
|
+
};
|
|
166
|
+
for (const name of this.registry.keys()) visit(name);
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Graceful shutdown
|
|
171
|
+
async shutdown() {
|
|
172
|
+
const sorted = this.topologicalSort().reverse();
|
|
173
|
+
for (const name of sorted) {
|
|
174
|
+
const state = this.instances.get(name);
|
|
175
|
+
if (state && state.stop) {
|
|
176
|
+
try {
|
|
177
|
+
await state.stop();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`[PluginLoader] Error stopping ${name}:`, error.message);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
this.unwatchPlugin(name);
|
|
183
|
+
}
|
|
184
|
+
this.instances.clear();
|
|
185
|
+
this.registry.clear();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export default PluginLoader;
|
|
@@ -49,9 +49,10 @@ export default {
|
|
|
49
49
|
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
50
50
|
|
|
51
51
|
const session = stream.api.createSession(id);
|
|
52
|
+
// Use runClaudeWithStreaming instead
|
|
52
53
|
activeExecutions.set(id, { sessionId: session.id });
|
|
53
54
|
|
|
54
|
-
res.json({ sessionId: session.id });
|
|
55
|
+
res.json({ sessionId: session.id, pid: proc.pid });
|
|
55
56
|
} catch (e) {
|
|
56
57
|
res.status(500).json({ error: e.message });
|
|
57
58
|
}
|
|
@@ -8,8 +8,7 @@ export default {
|
|
|
8
8
|
dependencies: ['database'],
|
|
9
9
|
|
|
10
10
|
async init(config, plugins) {
|
|
11
|
-
const
|
|
12
|
-
const db = dbPlugin?.api || {};
|
|
11
|
+
const db = plugins.get('database');
|
|
13
12
|
const activeSessions = new Map();
|
|
14
13
|
const pendingMessages = new Map();
|
|
15
14
|
const rateLimitState = new Map();
|
package/lib/tool-provisioner.js
CHANGED
|
@@ -38,7 +38,7 @@ export async function autoProvision(TOOLS, statusCache, install, broadcast) {
|
|
|
38
38
|
broadcast({ type: 'tool_update_failed', toolId: tool.id, data: result });
|
|
39
39
|
}
|
|
40
40
|
} else {
|
|
41
|
-
log(`${tool.id} v${status.installedVersion
|
|
41
|
+
log(`${tool.id} v${status.installedVersion} up-to-date`);
|
|
42
42
|
broadcast({ type: 'tool_status_update', toolId: tool.id, data: { installed: true, isUpToDate: true, installedVersion: status.installedVersion, status: 'installed' } });
|
|
43
43
|
}
|
|
44
44
|
} catch (err) {
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -4479,7 +4479,7 @@ const BROADCAST_TYPES = new Set([
|
|
|
4479
4479
|
'streaming_start', 'streaming_progress', 'streaming_complete', 'streaming_error',
|
|
4480
4480
|
'tool_install_started', 'tool_install_progress', 'tool_install_complete', 'tool_install_failed',
|
|
4481
4481
|
'tool_update_progress', 'tool_update_complete', 'tool_update_failed',
|
|
4482
|
-
'tool_status_update',
|
|
4482
|
+
'tool_status_update',
|
|
4483
4483
|
'tools_update_started', 'tools_update_complete', 'tools_refresh_complete',
|
|
4484
4484
|
'pm2_monit_update', 'pm2_monitoring_started', 'pm2_monitoring_stopped',
|
|
4485
4485
|
'pm2_list_response', 'pm2_start_response', 'pm2_stop_response',
|
|
@@ -5257,7 +5257,7 @@ import PluginLoader from './lib/plugin-loader.js';
|
|
|
5257
5257
|
const pluginLoader = new PluginLoader(path.join(__dirname, 'lib', 'plugins'));
|
|
5258
5258
|
async function loadPluginExtensions() {
|
|
5259
5259
|
try {
|
|
5260
|
-
await pluginLoader.loadAllPlugins({ router:
|
|
5260
|
+
await pluginLoader.loadAllPlugins({ router: app, baseUrl: BASE_URL, logger: console, env: process.env });
|
|
5261
5261
|
const names = Array.from(pluginLoader.registry.keys());
|
|
5262
5262
|
if (names.length > 0) {
|
|
5263
5263
|
for (const name of names) {
|
|
@@ -5266,7 +5266,7 @@ async function loadPluginExtensions() {
|
|
|
5266
5266
|
for (const route of state.routes) {
|
|
5267
5267
|
const fullPath = BASE_URL + route.path;
|
|
5268
5268
|
const method = (route.method || 'GET').toLowerCase();
|
|
5269
|
-
if (
|
|
5269
|
+
if (app[method]) app[method](fullPath, route.handler);
|
|
5270
5270
|
}
|
|
5271
5271
|
}
|
|
5272
5272
|
console.log(`[PLUGINS] Loaded extensions: ${names.join(', ')}`);
|