@praveencs/agent 0.9.8 → 0.9.9

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.
@@ -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
- // --- INSTANCES API ---
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 instances = await registry.listActive();
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
- // --- OBSERVABILITY API ---
56
+ // ═══════════════════════════════════════════════
57
+ // MEMORY API
58
+ // ═══════════════════════════════════════════════
51
59
  app.get('/api/instances/:id/memory', async (req, res) => {
52
60
  try {
53
- const instances = await registry.listActive();
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 instances = await registry.listActive();
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 instances = await registry.listActive();
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
- // --- SOCKET.IO FOR LIVE LOGS ---
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
- // --- SERVE FRONTEND ---
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')); });