@praveencs/agent 0.9.8 → 0.9.10
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/README.md +52 -2
- package/dist/src/cli/index.js +1 -1
- package/dist/src/cli/repl.js +2 -2
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +535 -31
- package/dist/src/server/app.js.map +1 -1
- package/docs/API.md +304 -0
- package/docs/CONFIGURATION.md +189 -0
- package/docs/articles/09-agent-studio.md +154 -0
- package/docs/articles/10-llm-providers.md +118 -0
- package/docs/articles/11-policy-approvals.md +118 -0
- package/docs/articles/12-daemon-automation.md +123 -0
- package/docs/studio-screenshot-1.png +0 -0
- package/docs/studio-screenshot-agent-console-2.png +0 -0
- package/package.json +1 -1
package/dist/src/server/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import cors from 'cors';
|
|
|
3
3
|
import { createServer } from 'node:http';
|
|
4
4
|
import { Server } from 'socket.io';
|
|
5
5
|
import { InstanceRegistry } from '../instance/registry.js';
|
|
6
|
-
import { resolve } from 'node:path';
|
|
6
|
+
import { resolve, join } from 'node:path';
|
|
7
7
|
import { MemoryStore } from '../memory/store.js';
|
|
8
8
|
import { GoalStore } from '../goals/store.js';
|
|
9
9
|
import { ConfigLoader } from '../config/loader.js';
|
|
@@ -12,6 +12,8 @@ import { SkillLoader } from '../skills/loader.js';
|
|
|
12
12
|
import { CommandLoader } from '../commands/loader.js';
|
|
13
13
|
import { ScriptLoader } from '../scripts/loader.js';
|
|
14
14
|
import { HookRegistry } from '../hooks/registry.js';
|
|
15
|
+
import { DaemonManager } from '../daemon/manager.js';
|
|
16
|
+
import { writeFile, mkdir, readFile, readdir, rm, access } from 'node:fs/promises';
|
|
15
17
|
export function createStudioServer() {
|
|
16
18
|
const app = express();
|
|
17
19
|
const server = createServer(app);
|
|
@@ -19,39 +21,44 @@ export function createStudioServer() {
|
|
|
19
21
|
const registry = new InstanceRegistry();
|
|
20
22
|
app.use(cors());
|
|
21
23
|
app.use(express.json());
|
|
22
|
-
//
|
|
24
|
+
// ═══════════════════════════════════════════════
|
|
25
|
+
// HELPER: resolve instance by ID
|
|
26
|
+
// ═══════════════════════════════════════════════
|
|
27
|
+
async function resolveInstance(id) {
|
|
28
|
+
const instances = await registry.listActive();
|
|
29
|
+
return instances.find(i => i.id === id) ?? null;
|
|
30
|
+
}
|
|
31
|
+
// ═══════════════════════════════════════════════
|
|
32
|
+
// INSTANCES API
|
|
33
|
+
// ═══════════════════════════════════════════════
|
|
23
34
|
app.get('/api/instances', async (_req, res) => {
|
|
24
35
|
try {
|
|
25
36
|
const instances = await registry.listActive();
|
|
26
37
|
res.json(instances);
|
|
27
|
-
return;
|
|
28
38
|
}
|
|
29
39
|
catch (err) {
|
|
30
40
|
res.status(500).json({ error: err.message });
|
|
31
|
-
return;
|
|
32
41
|
}
|
|
33
42
|
});
|
|
34
43
|
app.get('/api/instances/:id', async (req, res) => {
|
|
35
44
|
try {
|
|
36
|
-
const
|
|
37
|
-
const inst = instances.find(i => i.id === req.params.id);
|
|
45
|
+
const inst = await resolveInstance(req.params.id);
|
|
38
46
|
if (!inst) {
|
|
39
47
|
res.status(404).json({ error: 'Instance not found' });
|
|
40
48
|
return;
|
|
41
49
|
}
|
|
42
50
|
res.json(inst);
|
|
43
|
-
return;
|
|
44
51
|
}
|
|
45
52
|
catch (err) {
|
|
46
53
|
res.status(500).json({ error: err.message });
|
|
47
|
-
return;
|
|
48
54
|
}
|
|
49
55
|
});
|
|
50
|
-
//
|
|
56
|
+
// ═══════════════════════════════════════════════
|
|
57
|
+
// MEMORY API
|
|
58
|
+
// ═══════════════════════════════════════════════
|
|
51
59
|
app.get('/api/instances/:id/memory', async (req, res) => {
|
|
52
60
|
try {
|
|
53
|
-
const
|
|
54
|
-
const inst = instances.find(i => i.id === req.params.id);
|
|
61
|
+
const inst = await resolveInstance(req.params.id);
|
|
55
62
|
if (!inst) {
|
|
56
63
|
res.status(404).json({ error: 'Instance not found' });
|
|
57
64
|
return;
|
|
@@ -62,24 +69,78 @@ export function createStudioServer() {
|
|
|
62
69
|
stats: memoryStore.stats(),
|
|
63
70
|
memories
|
|
64
71
|
});
|
|
65
|
-
return;
|
|
66
72
|
}
|
|
67
73
|
catch (err) {
|
|
68
74
|
res.status(500).json({ error: err.message });
|
|
69
|
-
return;
|
|
70
75
|
}
|
|
71
76
|
});
|
|
77
|
+
app.get('/api/instances/:id/memory/search', async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const inst = await resolveInstance(req.params.id);
|
|
80
|
+
if (!inst) {
|
|
81
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const q = req.query.q;
|
|
85
|
+
if (!q) {
|
|
86
|
+
res.status(400).json({ error: 'Query parameter "q" required' });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const memoryStore = MemoryStore.open(inst.cwd);
|
|
90
|
+
const results = memoryStore.search(q, 20);
|
|
91
|
+
res.json({ results });
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
res.status(500).json({ error: err.message });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
app.post('/api/instances/:id/memory', async (req, res) => {
|
|
98
|
+
try {
|
|
99
|
+
const inst = await resolveInstance(req.params.id);
|
|
100
|
+
if (!inst) {
|
|
101
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const { content, category, tags } = req.body;
|
|
105
|
+
if (!content) {
|
|
106
|
+
res.status(400).json({ error: 'Content required' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const memoryStore = MemoryStore.open(inst.cwd);
|
|
110
|
+
const memory = memoryStore.save(content, category || 'general', 'user', tags || []);
|
|
111
|
+
res.json(memory);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
res.status(500).json({ error: err.message });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
app.delete('/api/instances/:id/memory/:memoryId', async (req, res) => {
|
|
118
|
+
try {
|
|
119
|
+
const inst = await resolveInstance(req.params.id);
|
|
120
|
+
if (!inst) {
|
|
121
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const memoryStore = MemoryStore.open(inst.cwd);
|
|
125
|
+
const ok = memoryStore.forget(parseInt(req.params.memoryId));
|
|
126
|
+
res.json({ success: ok });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
res.status(500).json({ error: err.message });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
// ═══════════════════════════════════════════════
|
|
133
|
+
// GOALS & TASKS API
|
|
134
|
+
// ═══════════════════════════════════════════════
|
|
72
135
|
app.get('/api/instances/:id/goals', async (req, res) => {
|
|
73
136
|
try {
|
|
74
|
-
const
|
|
75
|
-
const inst = instances.find(i => i.id === req.params.id);
|
|
137
|
+
const inst = await resolveInstance(req.params.id);
|
|
76
138
|
if (!inst) {
|
|
77
139
|
res.status(404).json({ error: 'Instance not found' });
|
|
78
140
|
return;
|
|
79
141
|
}
|
|
80
142
|
const memoryStore = MemoryStore.open(inst.cwd);
|
|
81
143
|
const goalStore = new GoalStore(memoryStore);
|
|
82
|
-
// Get all tasks and cluster them by goal
|
|
83
144
|
const db = memoryStore.db;
|
|
84
145
|
const goals = db.prepare('SELECT * FROM goals ORDER BY created_at DESC').all();
|
|
85
146
|
const tasks = db.prepare('SELECT * FROM tasks ORDER BY created_at ASC').all();
|
|
@@ -91,17 +152,110 @@ export function createStudioServer() {
|
|
|
91
152
|
stats: goalStore.stats(),
|
|
92
153
|
goals: goalsWithTasks
|
|
93
154
|
});
|
|
94
|
-
return;
|
|
95
155
|
}
|
|
96
156
|
catch (err) {
|
|
97
157
|
res.status(500).json({ error: err.message });
|
|
98
|
-
return;
|
|
99
158
|
}
|
|
100
159
|
});
|
|
160
|
+
app.post('/api/instances/:id/goals', async (req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
const inst = await resolveInstance(req.params.id);
|
|
163
|
+
if (!inst) {
|
|
164
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const { title, description, priority, deadline } = req.body;
|
|
168
|
+
if (!title) {
|
|
169
|
+
res.status(400).json({ error: 'Title required' });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const memoryStore = MemoryStore.open(inst.cwd);
|
|
173
|
+
const goalStore = new GoalStore(memoryStore);
|
|
174
|
+
const goal = goalStore.addGoal(title, { description, priority, deadline });
|
|
175
|
+
res.json(goal);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
res.status(500).json({ error: err.message });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
app.put('/api/instances/:id/goals/:goalId/status', async (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const inst = await resolveInstance(req.params.id);
|
|
184
|
+
if (!inst) {
|
|
185
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const { status } = req.body;
|
|
189
|
+
const memoryStore = MemoryStore.open(inst.cwd);
|
|
190
|
+
const goalStore = new GoalStore(memoryStore);
|
|
191
|
+
goalStore.updateGoalStatus(parseInt(req.params.goalId), status);
|
|
192
|
+
res.json({ success: true });
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
res.status(500).json({ error: err.message });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
app.delete('/api/instances/:id/goals/:goalId', async (req, res) => {
|
|
199
|
+
try {
|
|
200
|
+
const inst = await resolveInstance(req.params.id);
|
|
201
|
+
if (!inst) {
|
|
202
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const memoryStore = MemoryStore.open(inst.cwd);
|
|
206
|
+
const goalStore = new GoalStore(memoryStore);
|
|
207
|
+
const ok = goalStore.removeGoal(parseInt(req.params.goalId));
|
|
208
|
+
res.json({ success: ok });
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
res.status(500).json({ error: err.message });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
app.post('/api/instances/:id/goals/:goalId/tasks', async (req, res) => {
|
|
215
|
+
try {
|
|
216
|
+
const inst = await resolveInstance(req.params.id);
|
|
217
|
+
if (!inst) {
|
|
218
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const { title, description, skill, dependsOn, requiresApproval } = req.body;
|
|
222
|
+
if (!title) {
|
|
223
|
+
res.status(400).json({ error: 'Title required' });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const memoryStore = MemoryStore.open(inst.cwd);
|
|
227
|
+
const goalStore = new GoalStore(memoryStore);
|
|
228
|
+
const task = goalStore.addTask(parseInt(req.params.goalId), title, {
|
|
229
|
+
description, skill, dependsOn, requiresApproval
|
|
230
|
+
});
|
|
231
|
+
res.json(task);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
res.status(500).json({ error: err.message });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
app.post('/api/instances/:id/tasks/:taskId/approve', async (req, res) => {
|
|
238
|
+
try {
|
|
239
|
+
const inst = await resolveInstance(req.params.id);
|
|
240
|
+
if (!inst) {
|
|
241
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const memoryStore = MemoryStore.open(inst.cwd);
|
|
245
|
+
const goalStore = new GoalStore(memoryStore);
|
|
246
|
+
const ok = goalStore.approveTask(parseInt(req.params.taskId));
|
|
247
|
+
res.json({ success: ok });
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
res.status(500).json({ error: err.message });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
// ═══════════════════════════════════════════════
|
|
254
|
+
// CAPABILITIES API (read-only)
|
|
255
|
+
// ═══════════════════════════════════════════════
|
|
101
256
|
app.get('/api/instances/:id/capabilities', async (req, res) => {
|
|
102
257
|
try {
|
|
103
|
-
const
|
|
104
|
-
const inst = instances.find(i => i.id === req.params.id);
|
|
258
|
+
const inst = await resolveInstance(req.params.id);
|
|
105
259
|
if (!inst) {
|
|
106
260
|
res.status(404).json({ error: 'Instance not found' });
|
|
107
261
|
return;
|
|
@@ -113,7 +267,6 @@ export function createStudioServer() {
|
|
|
113
267
|
const pluginLoader = new PluginLoader();
|
|
114
268
|
const scriptLoader = new ScriptLoader();
|
|
115
269
|
const hookRegistry = new HookRegistry();
|
|
116
|
-
// Load all capabilities from the agent's CWD
|
|
117
270
|
await skillLoader.loadAll();
|
|
118
271
|
await cmdLoader.loadProjectCommands(inst.cwd);
|
|
119
272
|
await scriptLoader.loadAll(config.scripts?.installPaths ?? ['.agent/scripts'], inst.cwd);
|
|
@@ -125,35 +278,384 @@ export function createStudioServer() {
|
|
|
125
278
|
scripts: scriptLoader.list(),
|
|
126
279
|
plugins: loadedPlugins
|
|
127
280
|
});
|
|
128
|
-
return;
|
|
129
281
|
}
|
|
130
282
|
catch (err) {
|
|
131
283
|
res.status(500).json({ error: err.message });
|
|
132
|
-
return;
|
|
133
284
|
}
|
|
134
285
|
});
|
|
135
|
-
//
|
|
286
|
+
// ═══════════════════════════════════════════════
|
|
287
|
+
// SKILLS CRUD API
|
|
288
|
+
// ═══════════════════════════════════════════════
|
|
289
|
+
app.get('/api/instances/:id/skills', async (req, res) => {
|
|
290
|
+
try {
|
|
291
|
+
const inst = await resolveInstance(req.params.id);
|
|
292
|
+
if (!inst) {
|
|
293
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const configLoader = new ConfigLoader();
|
|
297
|
+
const config = await configLoader.load();
|
|
298
|
+
const skillLoader = new SkillLoader(config);
|
|
299
|
+
await skillLoader.loadAll();
|
|
300
|
+
const skills = skillLoader.list().map(s => ({
|
|
301
|
+
name: s.manifest.name,
|
|
302
|
+
description: s.manifest.description,
|
|
303
|
+
version: s.manifest.version,
|
|
304
|
+
entrypoint: s.manifest.entrypoint,
|
|
305
|
+
tools: s.manifest.tools,
|
|
306
|
+
path: s.path,
|
|
307
|
+
promptContent: s.promptContent ?? null,
|
|
308
|
+
}));
|
|
309
|
+
res.json({ skills });
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
res.status(500).json({ error: err.message });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
app.post('/api/instances/:id/skills', async (req, res) => {
|
|
316
|
+
try {
|
|
317
|
+
const inst = await resolveInstance(req.params.id);
|
|
318
|
+
if (!inst) {
|
|
319
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const { name, description, prompt, tools } = req.body;
|
|
323
|
+
if (!name) {
|
|
324
|
+
res.status(400).json({ error: 'Name required' });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const skillDir = join(inst.cwd, '.agent', 'skills', name);
|
|
328
|
+
await mkdir(skillDir, { recursive: true });
|
|
329
|
+
const manifest = {
|
|
330
|
+
name,
|
|
331
|
+
description: description || `Skill: ${name}`,
|
|
332
|
+
version: '1.0.0',
|
|
333
|
+
entrypoint: 'prompt.md',
|
|
334
|
+
tools: tools || [],
|
|
335
|
+
};
|
|
336
|
+
await writeFile(join(skillDir, 'skill.json'), JSON.stringify(manifest, null, 2));
|
|
337
|
+
await writeFile(join(skillDir, 'prompt.md'), prompt || `# ${name}\n\nDescribe the skill behavior here.`);
|
|
338
|
+
res.json({ success: true, path: skillDir, manifest });
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
res.status(500).json({ error: err.message });
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
app.put('/api/instances/:id/skills/:name', async (req, res) => {
|
|
345
|
+
try {
|
|
346
|
+
const inst = await resolveInstance(req.params.id);
|
|
347
|
+
if (!inst) {
|
|
348
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const { prompt, description, tools } = req.body;
|
|
352
|
+
const skillDir = join(inst.cwd, '.agent', 'skills', req.params.name);
|
|
353
|
+
try {
|
|
354
|
+
await access(skillDir);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
res.status(404).json({ error: 'Skill not found' });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (prompt !== undefined) {
|
|
361
|
+
await writeFile(join(skillDir, 'prompt.md'), prompt);
|
|
362
|
+
}
|
|
363
|
+
if (description !== undefined || tools !== undefined) {
|
|
364
|
+
const manifestRaw = await readFile(join(skillDir, 'skill.json'), 'utf-8');
|
|
365
|
+
const manifest = JSON.parse(manifestRaw);
|
|
366
|
+
if (description !== undefined)
|
|
367
|
+
manifest.description = description;
|
|
368
|
+
if (tools !== undefined)
|
|
369
|
+
manifest.tools = tools;
|
|
370
|
+
await writeFile(join(skillDir, 'skill.json'), JSON.stringify(manifest, null, 2));
|
|
371
|
+
}
|
|
372
|
+
res.json({ success: true });
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
res.status(500).json({ error: err.message });
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
app.delete('/api/instances/:id/skills/:name', async (req, res) => {
|
|
379
|
+
try {
|
|
380
|
+
const inst = await resolveInstance(req.params.id);
|
|
381
|
+
if (!inst) {
|
|
382
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const skillDir = join(inst.cwd, '.agent', 'skills', req.params.name);
|
|
386
|
+
await rm(skillDir, { recursive: true, force: true });
|
|
387
|
+
res.json({ success: true });
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
res.status(500).json({ error: err.message });
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
// ═══════════════════════════════════════════════
|
|
394
|
+
// COMMANDS CRUD API
|
|
395
|
+
// ═══════════════════════════════════════════════
|
|
396
|
+
app.get('/api/instances/:id/commands', async (req, res) => {
|
|
397
|
+
try {
|
|
398
|
+
const inst = await resolveInstance(req.params.id);
|
|
399
|
+
if (!inst) {
|
|
400
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const cmdLoader = new CommandLoader();
|
|
404
|
+
await cmdLoader.loadProjectCommands(inst.cwd);
|
|
405
|
+
res.json({ commands: cmdLoader.list() });
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
res.status(500).json({ error: err.message });
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
app.post('/api/instances/:id/commands', async (req, res) => {
|
|
412
|
+
try {
|
|
413
|
+
const inst = await resolveInstance(req.params.id);
|
|
414
|
+
if (!inst) {
|
|
415
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const { name, description, tools, body } = req.body;
|
|
419
|
+
if (!name) {
|
|
420
|
+
res.status(400).json({ error: 'Name required' });
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const commandsDir = join(inst.cwd, '.agent', 'commands');
|
|
424
|
+
await mkdir(commandsDir, { recursive: true });
|
|
425
|
+
const frontmatter = [
|
|
426
|
+
'---',
|
|
427
|
+
`name: ${name}`,
|
|
428
|
+
`description: ${description || `Command: ${name}`}`,
|
|
429
|
+
`tools: [${(tools || []).join(', ')}]`,
|
|
430
|
+
'---',
|
|
431
|
+
].join('\n');
|
|
432
|
+
const content = `${frontmatter}\n${body || `# ${name}\n\nDescribe what this command does.`}`;
|
|
433
|
+
const filePath = join(commandsDir, `${name}.md`);
|
|
434
|
+
await writeFile(filePath, content);
|
|
435
|
+
res.json({ success: true, path: filePath });
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
res.status(500).json({ error: err.message });
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
app.delete('/api/instances/:id/commands/:name', async (req, res) => {
|
|
442
|
+
try {
|
|
443
|
+
const inst = await resolveInstance(req.params.id);
|
|
444
|
+
if (!inst) {
|
|
445
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const filePath = join(inst.cwd, '.agent', 'commands', `${req.params.name}.md`);
|
|
449
|
+
await rm(filePath, { force: true });
|
|
450
|
+
res.json({ success: true });
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
res.status(500).json({ error: err.message });
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
// ═══════════════════════════════════════════════
|
|
457
|
+
// SCRIPTS CRUD API
|
|
458
|
+
// ═══════════════════════════════════════════════
|
|
459
|
+
app.get('/api/instances/:id/scripts', async (req, res) => {
|
|
460
|
+
try {
|
|
461
|
+
const inst = await resolveInstance(req.params.id);
|
|
462
|
+
if (!inst) {
|
|
463
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const configLoader = new ConfigLoader();
|
|
467
|
+
const config = await configLoader.load();
|
|
468
|
+
const scriptLoader = new ScriptLoader();
|
|
469
|
+
await scriptLoader.loadAll(config.scripts?.installPaths ?? ['.agent/scripts'], inst.cwd);
|
|
470
|
+
res.json({ scripts: scriptLoader.list() });
|
|
471
|
+
}
|
|
472
|
+
catch (err) {
|
|
473
|
+
res.status(500).json({ error: err.message });
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
app.post('/api/instances/:id/scripts', async (req, res) => {
|
|
477
|
+
try {
|
|
478
|
+
const inst = await resolveInstance(req.params.id);
|
|
479
|
+
if (!inst) {
|
|
480
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const { name, description, entrypoint, content, args } = req.body;
|
|
484
|
+
if (!name) {
|
|
485
|
+
res.status(400).json({ error: 'Name required' });
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const scriptDir = join(inst.cwd, '.agent', 'scripts', name);
|
|
489
|
+
await mkdir(scriptDir, { recursive: true });
|
|
490
|
+
// Write script.yaml
|
|
491
|
+
const yamlLines = [
|
|
492
|
+
`name: ${name}`,
|
|
493
|
+
`description: ${description || `Script: ${name}`}`,
|
|
494
|
+
`entrypoint: ${entrypoint || 'run.sh'}`,
|
|
495
|
+
];
|
|
496
|
+
if (args && Object.keys(args).length > 0) {
|
|
497
|
+
yamlLines.push('args:');
|
|
498
|
+
for (const [key, val] of Object.entries(args)) {
|
|
499
|
+
yamlLines.push(` ${key}:`);
|
|
500
|
+
yamlLines.push(` description: ${val.description || key}`);
|
|
501
|
+
if (val.default)
|
|
502
|
+
yamlLines.push(` default: ${val.default}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
await writeFile(join(scriptDir, 'script.yaml'), yamlLines.join('\n'));
|
|
506
|
+
// Write entrypoint file
|
|
507
|
+
const ep = entrypoint || 'run.sh';
|
|
508
|
+
await writeFile(join(scriptDir, ep), content || `#!/bin/bash\necho "Running ${name}..."\n`);
|
|
509
|
+
res.json({ success: true, path: scriptDir });
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
res.status(500).json({ error: err.message });
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
app.delete('/api/instances/:id/scripts/:name', async (req, res) => {
|
|
516
|
+
try {
|
|
517
|
+
const inst = await resolveInstance(req.params.id);
|
|
518
|
+
if (!inst) {
|
|
519
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const scriptDir = join(inst.cwd, '.agent', 'scripts', req.params.name);
|
|
523
|
+
await rm(scriptDir, { recursive: true, force: true });
|
|
524
|
+
res.json({ success: true });
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
res.status(500).json({ error: err.message });
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
// ═══════════════════════════════════════════════
|
|
531
|
+
// PLUGINS API
|
|
532
|
+
// ═══════════════════════════════════════════════
|
|
533
|
+
app.get('/api/instances/:id/plugins', async (req, res) => {
|
|
534
|
+
try {
|
|
535
|
+
const inst = await resolveInstance(req.params.id);
|
|
536
|
+
if (!inst) {
|
|
537
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const pluginsDir = join(inst.cwd, '.agent', 'plugins');
|
|
541
|
+
const plugins = [];
|
|
542
|
+
try {
|
|
543
|
+
await access(pluginsDir);
|
|
544
|
+
const entries = await readdir(pluginsDir, { withFileTypes: true });
|
|
545
|
+
for (const entry of entries) {
|
|
546
|
+
if (!entry.isDirectory())
|
|
547
|
+
continue;
|
|
548
|
+
try {
|
|
549
|
+
const manifestRaw = await readFile(join(pluginsDir, entry.name, 'plugin.json'), 'utf-8');
|
|
550
|
+
plugins.push(JSON.parse(manifestRaw));
|
|
551
|
+
}
|
|
552
|
+
catch { /* skip invalid */ }
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
catch { /* no plugin dir */ }
|
|
556
|
+
res.json({ plugins });
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
res.status(500).json({ error: err.message });
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
app.delete('/api/instances/:id/plugins/:name', async (req, res) => {
|
|
563
|
+
try {
|
|
564
|
+
const inst = await resolveInstance(req.params.id);
|
|
565
|
+
if (!inst) {
|
|
566
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const pluginDir = join(inst.cwd, '.agent', 'plugins', req.params.name);
|
|
570
|
+
await rm(pluginDir, { recursive: true, force: true });
|
|
571
|
+
res.json({ success: true });
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
res.status(500).json({ error: err.message });
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
// ═══════════════════════════════════════════════
|
|
578
|
+
// DAEMON CONTROL API
|
|
579
|
+
// ═══════════════════════════════════════════════
|
|
580
|
+
app.get('/api/instances/:id/daemon/status', async (req, res) => {
|
|
581
|
+
try {
|
|
582
|
+
const inst = await resolveInstance(req.params.id);
|
|
583
|
+
if (!inst) {
|
|
584
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const daemon = new DaemonManager();
|
|
588
|
+
const status = await daemon.status();
|
|
589
|
+
res.json(status);
|
|
590
|
+
}
|
|
591
|
+
catch (err) {
|
|
592
|
+
res.status(500).json({ error: err.message });
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
app.post('/api/instances/:id/daemon/start', async (req, res) => {
|
|
596
|
+
try {
|
|
597
|
+
const inst = await resolveInstance(req.params.id);
|
|
598
|
+
if (!inst) {
|
|
599
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const daemon = new DaemonManager();
|
|
603
|
+
const result = await daemon.start();
|
|
604
|
+
res.json(result);
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
res.status(500).json({ error: err.message });
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
app.post('/api/instances/:id/daemon/stop', async (req, res) => {
|
|
611
|
+
try {
|
|
612
|
+
const inst = await resolveInstance(req.params.id);
|
|
613
|
+
if (!inst) {
|
|
614
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const daemon = new DaemonManager();
|
|
618
|
+
const result = await daemon.stop();
|
|
619
|
+
res.json(result);
|
|
620
|
+
}
|
|
621
|
+
catch (err) {
|
|
622
|
+
res.status(500).json({ error: err.message });
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
app.get('/api/instances/:id/daemon/logs', async (req, res) => {
|
|
626
|
+
try {
|
|
627
|
+
const inst = await resolveInstance(req.params.id);
|
|
628
|
+
if (!inst) {
|
|
629
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const daemon = new DaemonManager();
|
|
633
|
+
const lines = parseInt(req.query.lines) || 50;
|
|
634
|
+
const logs = await daemon.getLogs(lines);
|
|
635
|
+
res.json({ logs });
|
|
636
|
+
}
|
|
637
|
+
catch (err) {
|
|
638
|
+
res.status(500).json({ error: err.message });
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
// ═══════════════════════════════════════════════
|
|
642
|
+
// SOCKET.IO FOR LIVE LOGS & APPROVAL RELAY
|
|
643
|
+
// ═══════════════════════════════════════════════
|
|
136
644
|
io.on('connection', (socket) => {
|
|
137
645
|
console.log(`[Studio] Client connected: ${socket.id}`);
|
|
138
646
|
socket.on('subscribe', (instanceId) => {
|
|
139
647
|
console.log(`[Studio] Client subscribed to ${instanceId}`);
|
|
140
648
|
socket.join(instanceId);
|
|
141
649
|
});
|
|
142
|
-
// Relays commands from the UI downwards to the actual Agent CLI in the terminal
|
|
143
650
|
socket.on('agent:command', (data) => {
|
|
144
|
-
console.log(`[Studio] Relaying command to instance ${data.instanceId}`);
|
|
145
651
|
socket.to(data.instanceId).emit('agent:command', data);
|
|
146
652
|
});
|
|
147
|
-
// Relays live logs from the Agent CLI upwards to all active UI Dashboards
|
|
148
653
|
socket.on('agent:log', (data) => {
|
|
149
|
-
// we broadcast to the room EXCEPT the sender
|
|
150
654
|
socket.to(data.instanceId).emit('agent:log', data);
|
|
151
655
|
});
|
|
152
|
-
// Relay approval requests from Agent -> UI
|
|
153
656
|
socket.on('agent:approval:request', (data) => {
|
|
154
657
|
socket.to(data.instanceId).emit('agent:approval:request', data);
|
|
155
658
|
});
|
|
156
|
-
// Relay approval responses from UI -> Agent
|
|
157
659
|
socket.on('agent:approval:response', (data) => {
|
|
158
660
|
socket.to(data.instanceId).emit(`agent:approval:response:${data.tool}`, data);
|
|
159
661
|
});
|
|
@@ -161,7 +663,9 @@ export function createStudioServer() {
|
|
|
161
663
|
console.log(`[Studio] Client disconnected: ${socket.id}`);
|
|
162
664
|
});
|
|
163
665
|
});
|
|
164
|
-
//
|
|
666
|
+
// ═══════════════════════════════════════════════
|
|
667
|
+
// SERVE FRONTEND
|
|
668
|
+
// ═══════════════════════════════════════════════
|
|
165
669
|
const reactAppPath = resolve(process.cwd(), 'studio/dist');
|
|
166
670
|
app.use(express.static(reactAppPath));
|
|
167
671
|
app.use((_req, res) => { res.sendFile(resolve(reactAppPath, 'index.html')); });
|