agentgui 1.0.589 → 1.0.590
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/database.js +31 -0
- package/lib/plugin-interface.js +36 -0
- package/lib/plugin-loader.js +201 -0
- package/lib/plugins/acp-plugin.js +90 -0
- package/lib/plugins/agents-plugin.js +80 -0
- package/lib/plugins/auth-plugin.js +132 -0
- package/lib/plugins/database-plugin.js +43 -0
- package/lib/plugins/files-plugin.js +83 -0
- package/lib/plugins/git-plugin.js +117 -0
- package/lib/plugins/speech-plugin.js +72 -0
- package/lib/plugins/stream-plugin.js +88 -0
- package/lib/plugins/tools-plugin.js +114 -0
- package/lib/plugins/websocket-plugin.js +62 -0
- package/lib/plugins/workflow-plugin.js +90 -0
- package/package.json +1 -1
- package/server.js +140 -4846
package/database.js
CHANGED
|
@@ -199,6 +199,37 @@ function initSchema() {
|
|
|
199
199
|
CREATE INDEX IF NOT EXISTS idx_tool_install_history_tool ON tool_install_history(tool_id);
|
|
200
200
|
CREATE INDEX IF NOT EXISTS idx_tool_install_history_completed ON tool_install_history(completed_at);
|
|
201
201
|
|
|
202
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
203
|
+
id TEXT PRIMARY KEY,
|
|
204
|
+
workflowName TEXT NOT NULL,
|
|
205
|
+
workflowId TEXT,
|
|
206
|
+
runId TEXT,
|
|
207
|
+
sha TEXT,
|
|
208
|
+
branch TEXT,
|
|
209
|
+
status TEXT,
|
|
210
|
+
conclusion TEXT,
|
|
211
|
+
htmlUrl TEXT,
|
|
212
|
+
triggeredAt INTEGER NOT NULL,
|
|
213
|
+
completedAt INTEGER,
|
|
214
|
+
created_at INTEGER NOT NULL
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_name ON workflow_runs(workflowName);
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_sha ON workflow_runs(sha);
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_completed ON workflow_runs(completedAt);
|
|
220
|
+
|
|
221
|
+
CREATE TABLE IF NOT EXISTS oauth_tokens (
|
|
222
|
+
id TEXT PRIMARY KEY,
|
|
223
|
+
provider TEXT NOT NULL,
|
|
224
|
+
token TEXT NOT NULL,
|
|
225
|
+
email TEXT,
|
|
226
|
+
expires_at INTEGER,
|
|
227
|
+
created_at INTEGER NOT NULL,
|
|
228
|
+
updated_at INTEGER NOT NULL
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
CREATE INDEX IF NOT EXISTS idx_oauth_tokens_provider ON oauth_tokens(provider);
|
|
232
|
+
|
|
202
233
|
`);
|
|
203
234
|
}
|
|
204
235
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Plugin interface contract - every plugin must implement this
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
// Plugin metadata
|
|
5
|
+
name: 'plugin-name', // unique identifier
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
dependencies: [], // list of other plugin names this depends on
|
|
8
|
+
|
|
9
|
+
// Lifecycle methods (all required)
|
|
10
|
+
async init(config, plugins) {
|
|
11
|
+
// config = { router, wsManager, db, logger, env }
|
|
12
|
+
// plugins = Map<name, plugin> of all loaded plugins
|
|
13
|
+
// MUST return: { routes[], wsHandlers{}, api{}, stop() }
|
|
14
|
+
return {
|
|
15
|
+
routes: [], // [ { method, path, handler } ]
|
|
16
|
+
wsHandlers: {}, // { eventType: handler(data, clients) }
|
|
17
|
+
api: {}, // exported functions for other plugins
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
async reload(state) {
|
|
22
|
+
// Called on hot reload. Preserve state from previous instance.
|
|
23
|
+
// Return new state (or updated state from previous)
|
|
24
|
+
return state;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async stop() {
|
|
28
|
+
// Graceful shutdown. Clean up resources.
|
|
29
|
+
// No need to return anything.
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Optional: Called when another plugin throws error
|
|
33
|
+
async handleError(error, context) {
|
|
34
|
+
// context = { pluginName, phase, ... }
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
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();
|
|
160
|
+
const result = [];
|
|
161
|
+
|
|
162
|
+
const visit = (name) => {
|
|
163
|
+
if (visited.has(name)) return;
|
|
164
|
+
visited.add(name);
|
|
165
|
+
|
|
166
|
+
const plugin = this.registry.get(name);
|
|
167
|
+
for (const dep of (plugin?.dependencies || [])) {
|
|
168
|
+
if (this.registry.has(dep)) {
|
|
169
|
+
visit(dep);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
result.push(name);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
for (const name of this.registry.keys()) {
|
|
176
|
+
visit(name);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Graceful shutdown
|
|
183
|
+
async shutdown() {
|
|
184
|
+
const sorted = this.topologicalSort().reverse();
|
|
185
|
+
for (const name of sorted) {
|
|
186
|
+
const state = this.instances.get(name);
|
|
187
|
+
if (state && state.stop) {
|
|
188
|
+
try {
|
|
189
|
+
await state.stop();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error(`[PluginLoader] Error stopping ${name}:`, error.message);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this.unwatchPlugin(name);
|
|
195
|
+
}
|
|
196
|
+
this.instances.clear();
|
|
197
|
+
this.registry.clear();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default PluginLoader;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// ACP plugin - OpenCode, Gemini, Kilo, Codex startup and health checks
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
name: 'acp',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
dependencies: ['tools'],
|
|
11
|
+
|
|
12
|
+
async init(config, plugins) {
|
|
13
|
+
const tools = plugins.get('tools');
|
|
14
|
+
const toolProcesses = new Map();
|
|
15
|
+
const healthCheckIntervals = new Map();
|
|
16
|
+
const restartCounts = new Map();
|
|
17
|
+
const acpPorts = new Map();
|
|
18
|
+
|
|
19
|
+
const toolSpecs = [
|
|
20
|
+
{ name: 'opencode', port: 18100, cmd: 'opencode acp --port 18100' },
|
|
21
|
+
{ name: 'gemini', port: 18101, cmd: 'gemini acp --port 18101' },
|
|
22
|
+
{ name: 'kilo', port: 18102, cmd: 'kilo acp --port 18102' },
|
|
23
|
+
{ name: 'codex', port: 18103, cmd: 'codex acp --port 18103' },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const startTool = async (spec) => {
|
|
27
|
+
try {
|
|
28
|
+
const proc = spawn('bash', ['-c', spec.cmd]);
|
|
29
|
+
toolProcesses.set(spec.name, proc);
|
|
30
|
+
acpPorts.set(spec.name, spec.port);
|
|
31
|
+
restartCounts.set(spec.name, 0);
|
|
32
|
+
|
|
33
|
+
// Health check every 30s
|
|
34
|
+
const interval = setInterval(() => {
|
|
35
|
+
if (proc.killed) {
|
|
36
|
+
clearInterval(interval);
|
|
37
|
+
healthCheckIntervals.delete(spec.name);
|
|
38
|
+
}
|
|
39
|
+
}, 30000);
|
|
40
|
+
healthCheckIntervals.set(spec.name, interval);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error(`[ACP] Failed to start ${spec.name}:`, e.message);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Start all ACP tools
|
|
47
|
+
for (const spec of toolSpecs) {
|
|
48
|
+
await startTool(spec);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
routes: [
|
|
53
|
+
{
|
|
54
|
+
method: 'GET',
|
|
55
|
+
path: '/api/acp/status',
|
|
56
|
+
handler: (req, res) => {
|
|
57
|
+
const status = {};
|
|
58
|
+
for (const [name, proc] of toolProcesses) {
|
|
59
|
+
status[name] = {
|
|
60
|
+
running: !proc.killed,
|
|
61
|
+
port: acpPorts.get(name),
|
|
62
|
+
pid: proc.pid,
|
|
63
|
+
restarts: restartCounts.get(name) || 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
res.json({ tools: status });
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
wsHandlers: {},
|
|
71
|
+
api: {
|
|
72
|
+
getStatus: () => Object.fromEntries(acpPorts),
|
|
73
|
+
},
|
|
74
|
+
stop: async () => {
|
|
75
|
+
for (const [name, interval] of healthCheckIntervals) {
|
|
76
|
+
clearInterval(interval);
|
|
77
|
+
}
|
|
78
|
+
for (const [name, proc] of toolProcesses) {
|
|
79
|
+
if (proc && !proc.killed) proc.kill();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
async reload(state) {
|
|
86
|
+
return state;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async stop() {},
|
|
90
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Agents plugin - agent discovery, runner spawning, process management
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { runClaudeWithStreaming } from '../claude-runner.js';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
name: 'agents',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
dependencies: ['database', 'stream'],
|
|
10
|
+
|
|
11
|
+
async init(config, plugins) {
|
|
12
|
+
const db = plugins.get('database');
|
|
13
|
+
const stream = plugins.get('stream');
|
|
14
|
+
const discoveredAgents = new Map();
|
|
15
|
+
const activeExecutions = new Map();
|
|
16
|
+
|
|
17
|
+
// Discover agents on startup
|
|
18
|
+
const discoverAgents = async () => {
|
|
19
|
+
const agents = [
|
|
20
|
+
{ id: 'gm-cc', name: 'Claude Code', bin: 'claude', installed: true },
|
|
21
|
+
{ id: 'gm-oc', name: 'OpenCode', bin: 'opencode', installed: false },
|
|
22
|
+
{ id: 'gm-gc', name: 'Gemini CLI', bin: 'gemini', installed: false },
|
|
23
|
+
{ id: 'gm-kilo', name: 'Kilo', bin: 'kilo', installed: false },
|
|
24
|
+
];
|
|
25
|
+
agents.forEach(a => discoveredAgents.set(a.id, a));
|
|
26
|
+
return agents;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
await discoverAgents();
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
routes: [
|
|
33
|
+
{
|
|
34
|
+
method: 'GET',
|
|
35
|
+
path: '/api/agents',
|
|
36
|
+
handler: (req, res) => {
|
|
37
|
+
res.json({ agents: Array.from(discoveredAgents.values()) });
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
method: 'POST',
|
|
42
|
+
path: '/api/conversations/:id/stream',
|
|
43
|
+
handler: async (req, res) => {
|
|
44
|
+
const { id } = req.params;
|
|
45
|
+
const { agentId, message } = req.body;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const agent = discoveredAgents.get(agentId);
|
|
49
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
50
|
+
|
|
51
|
+
const session = stream.api.createSession(id);
|
|
52
|
+
// Use runClaudeWithStreaming instead
|
|
53
|
+
activeExecutions.set(id, { sessionId: session.id });
|
|
54
|
+
|
|
55
|
+
res.json({ sessionId: session.id, pid: proc.pid });
|
|
56
|
+
} catch (e) {
|
|
57
|
+
res.status(500).json({ error: e.message });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
wsHandlers: {},
|
|
63
|
+
api: {
|
|
64
|
+
getAgents: () => Array.from(discoveredAgents.values()),
|
|
65
|
+
discoverAgents,
|
|
66
|
+
},
|
|
67
|
+
stop: async () => {
|
|
68
|
+
for (const proc of activeExecutions.values()) {
|
|
69
|
+
if (proc && !proc.killed) proc.kill();
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async reload(state) {
|
|
76
|
+
return state;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async stop() {},
|
|
80
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Auth plugin - OAuth2, provider config, agentauth integration
|
|
2
|
+
|
|
3
|
+
import http from 'http';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: 'auth',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
dependencies: ['database'],
|
|
9
|
+
|
|
10
|
+
async init(config, plugins) {
|
|
11
|
+
const db = plugins.get('database');
|
|
12
|
+
const providerConfigs = new Map();
|
|
13
|
+
const oauthClients = new Map();
|
|
14
|
+
const sessions = new Map();
|
|
15
|
+
|
|
16
|
+
// Detect provider configs on startup
|
|
17
|
+
const detectProviders = () => {
|
|
18
|
+
const providers = [
|
|
19
|
+
{ id: 'anthropic', name: 'Anthropic', configured: false },
|
|
20
|
+
{ id: 'google', name: 'Google', configured: false },
|
|
21
|
+
{ id: 'github', name: 'GitHub', configured: false },
|
|
22
|
+
];
|
|
23
|
+
providers.forEach(p => providerConfigs.set(p.id, p));
|
|
24
|
+
return providers;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
detectProviders();
|
|
28
|
+
|
|
29
|
+
// Agentauth integration
|
|
30
|
+
const agentauthStart = async (provider, scopes) => {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const options = {
|
|
33
|
+
hostname: 'localhost',
|
|
34
|
+
port: 8765,
|
|
35
|
+
path: '/auth/start',
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const req = http.request(options, (res) => {
|
|
41
|
+
let data = '';
|
|
42
|
+
res.on('data', chunk => data += chunk);
|
|
43
|
+
res.on('end', () => {
|
|
44
|
+
try {
|
|
45
|
+
resolve(JSON.parse(data));
|
|
46
|
+
} catch (e) {
|
|
47
|
+
reject(new Error('Agentauth unavailable'));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
req.on('error', () => reject(new Error('Agentauth service not running')));
|
|
53
|
+
req.write(JSON.stringify({ provider, scopes }));
|
|
54
|
+
req.end();
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
routes: [
|
|
60
|
+
{
|
|
61
|
+
method: 'GET',
|
|
62
|
+
path: '/api/auth/status',
|
|
63
|
+
handler: (req, res) => {
|
|
64
|
+
res.json({ authenticated: sessions.size > 0, sessions: Array.from(sessions.keys()) });
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
method: 'GET',
|
|
69
|
+
path: '/api/auth/configs',
|
|
70
|
+
handler: (req, res) => {
|
|
71
|
+
const masked = Array.from(providerConfigs.values()).map(p => ({
|
|
72
|
+
id: p.id,
|
|
73
|
+
name: p.name,
|
|
74
|
+
configured: p.configured,
|
|
75
|
+
}));
|
|
76
|
+
res.json({ providers: masked });
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
method: 'POST',
|
|
81
|
+
path: '/api/auth/callback',
|
|
82
|
+
handler: (req, res) => {
|
|
83
|
+
const { code } = req.body;
|
|
84
|
+
res.json({ success: true, code });
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
method: 'POST',
|
|
89
|
+
path: '/api/auth/logout',
|
|
90
|
+
handler: (req, res) => {
|
|
91
|
+
sessions.clear();
|
|
92
|
+
res.json({ success: true });
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
method: 'POST',
|
|
97
|
+
path: '/api/auth/agentauth-start',
|
|
98
|
+
handler: async (req, res) => {
|
|
99
|
+
const { provider, scopes } = req.body;
|
|
100
|
+
try {
|
|
101
|
+
const result = await agentauthStart(provider, scopes);
|
|
102
|
+
res.json(result);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
res.status(503).json({ error: e.message });
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
method: 'GET',
|
|
110
|
+
path: '/api/auth/agentauth-status',
|
|
111
|
+
handler: async (req, res) => {
|
|
112
|
+
const { code } = req.query;
|
|
113
|
+
res.json({ status: 'polling-not-implemented' });
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
wsHandlers: {},
|
|
118
|
+
api: {
|
|
119
|
+
getProviders: () => Array.from(providerConfigs.values()),
|
|
120
|
+
},
|
|
121
|
+
stop: async () => {
|
|
122
|
+
sessions.clear();
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
async reload(state) {
|
|
128
|
+
return state;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
async stop() {},
|
|
132
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Database plugin - SQLite init, schema, checkpoint recovery
|
|
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
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Files plugin - file browser, upload handler, drag-drop support
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
name: 'files',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
dependencies: ['database'],
|
|
10
|
+
|
|
11
|
+
async init(config, plugins) {
|
|
12
|
+
const db = plugins.get('database');
|
|
13
|
+
const uploadedFiles = new Map();
|
|
14
|
+
|
|
15
|
+
const browseDirectory = (dir) => {
|
|
16
|
+
try {
|
|
17
|
+
const entries = fs.readdirSync(dir);
|
|
18
|
+
return entries.map(entry => {
|
|
19
|
+
const fullPath = path.join(dir, entry);
|
|
20
|
+
const stat = fs.statSync(fullPath);
|
|
21
|
+
return {
|
|
22
|
+
name: entry,
|
|
23
|
+
path: fullPath,
|
|
24
|
+
isDirectory: stat.isDirectory(),
|
|
25
|
+
size: stat.size,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
routes: [
|
|
35
|
+
{
|
|
36
|
+
method: 'GET',
|
|
37
|
+
path: '/files/:conversationId',
|
|
38
|
+
handler: (req, res) => {
|
|
39
|
+
const { conversationId } = req.params;
|
|
40
|
+
const { dir } = req.query;
|
|
41
|
+
const entries = browseDirectory(dir || process.cwd());
|
|
42
|
+
res.json({ entries, currentDir: dir || process.cwd() });
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
method: 'POST',
|
|
47
|
+
path: '/api/upload/:conversationId',
|
|
48
|
+
handler: async (req, res) => {
|
|
49
|
+
const { conversationId } = req.params;
|
|
50
|
+
uploadedFiles.set(conversationId, Date.now());
|
|
51
|
+
res.json({ success: true, conversationId });
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
method: 'POST',
|
|
56
|
+
path: '/api/folders',
|
|
57
|
+
handler: async (req, res) => {
|
|
58
|
+
const { path: folderPath } = req.body;
|
|
59
|
+
try {
|
|
60
|
+
fs.mkdirSync(folderPath, { recursive: true });
|
|
61
|
+
res.json({ success: true, path: folderPath });
|
|
62
|
+
} catch (e) {
|
|
63
|
+
res.status(400).json({ error: e.message });
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
wsHandlers: {},
|
|
69
|
+
api: {
|
|
70
|
+
browseDirectory,
|
|
71
|
+
},
|
|
72
|
+
stop: async () => {
|
|
73
|
+
uploadedFiles.clear();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async reload(state) {
|
|
79
|
+
return state;
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async stop() {},
|
|
83
|
+
};
|