groove-dev 0.27.8 → 0.27.12

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.
Files changed (125) hide show
  1. package/groove-icon.png +0 -0
  2. package/node_modules/@groove-dev/daemon/src/api.js +460 -25
  3. package/node_modules/@groove-dev/daemon/src/index.js +7 -0
  4. package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
  5. package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
  6. package/node_modules/@groove-dev/daemon/src/process.js +67 -7
  7. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  8. package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
  9. package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
  10. package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
  11. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
  12. package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
  13. package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
  14. package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
  15. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
  16. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
  17. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  18. package/node_modules/@groove-dev/gui/src/app.css +14 -0
  19. package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  22. package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  23. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
  24. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  25. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +4 -4
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  27. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  28. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
  29. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
  30. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  31. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
  32. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
  33. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
  34. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
  35. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  36. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
  37. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
  38. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
  39. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
  40. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
  41. package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
  42. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
  43. package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
  44. package/node_modules/@groove-dev/gui/src/lib/electron.js +25 -0
  45. package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
  46. package/node_modules/@groove-dev/gui/src/stores/groove.js +139 -6
  47. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +38 -39
  48. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
  49. package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
  50. package/node_modules/@groove-dev/gui/vite.config.js +3 -0
  51. package/package.json +7 -2
  52. package/packages/daemon/src/api.js +460 -25
  53. package/packages/daemon/src/index.js +7 -0
  54. package/packages/daemon/src/introducer.js +72 -4
  55. package/packages/daemon/src/journalist.js +66 -11
  56. package/packages/daemon/src/process.js +67 -7
  57. package/packages/daemon/src/registry.js +1 -1
  58. package/packages/daemon/src/repo-import.js +541 -0
  59. package/packages/daemon/src/rotator.js +28 -1
  60. package/packages/daemon/src/supervisor.js +2 -1
  61. package/packages/daemon/src/tunnel-manager.js +504 -0
  62. package/packages/daemon/src/validate.js +13 -0
  63. package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
  64. package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
  65. package/packages/gui/dist/index.html +2 -2
  66. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
  67. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
  68. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
  69. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
  70. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
  71. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
  72. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
  73. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
  74. package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
  75. package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
  76. package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
  77. package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
  78. package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
  79. package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
  80. package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
  81. package/packages/gui/src/app.css +14 -0
  82. package/packages/gui/src/app.jsx +13 -0
  83. package/packages/gui/src/components/agents/agent-config.jsx +130 -1
  84. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  85. package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  86. package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
  87. package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  88. package/packages/gui/src/components/dashboard/intel-panel.jsx +4 -4
  89. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  90. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  91. package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
  92. package/packages/gui/src/components/layout/app-shell.jsx +7 -1
  93. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  94. package/packages/gui/src/components/layout/command-palette.jsx +14 -4
  95. package/packages/gui/src/components/layout/status-bar.jsx +46 -11
  96. package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
  97. package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
  98. package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  99. package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
  100. package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
  101. package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
  102. package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
  103. package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
  104. package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
  105. package/packages/gui/src/components/ui/toast.jsx +1 -1
  106. package/packages/gui/src/lib/edition.js +4 -0
  107. package/packages/gui/src/lib/electron.js +25 -0
  108. package/packages/gui/src/lib/status.js +1 -0
  109. package/packages/gui/src/stores/groove.js +139 -6
  110. package/packages/gui/src/views/dashboard.jsx +38 -39
  111. package/packages/gui/src/views/marketplace.jsx +82 -0
  112. package/packages/gui/src/views/settings.jsx +66 -0
  113. package/packages/gui/vite.config.js +3 -0
  114. package/integrations/FEDERATION_PLAN.md +0 -583
  115. package/integrations/VOICE_PLAN.md +0 -232
  116. package/node_modules/@groove-dev/gui/dist/assets/index-CwmR3-HY.css +0 -1
  117. package/node_modules/@groove-dev/gui/dist/assets/index-DiCjVtQL.js +0 -652
  118. package/packages/gui/dist/assets/index-CwmR3-HY.css +0 -1
  119. package/packages/gui/dist/assets/index-DiCjVtQL.js +0 -652
  120. package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
  121. package/test-slack.mjs +0 -28
  122. /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
  123. /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
  124. /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
  125. /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
@@ -4,13 +4,64 @@
4
4
  import express from 'express';
5
5
  import { resolve, dirname } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
- import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream } from 'fs';
7
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream, copyFileSync } from 'fs';
8
8
  import { lookup as mimeLookup } from './mimetypes.js';
9
9
  import { listProviders, getProvider } from './providers/index.js';
10
10
  import { OllamaProvider } from './providers/ollama.js';
11
11
  import { validateAgentConfig } from './validate.js';
12
12
 
13
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const isPro = process.env.GROOVE_EDITION === 'pro';
15
+
16
+ let _subscriptionCache = { active: true, checkedAt: 0 };
17
+
18
+ function proOnly(req, res, next) {
19
+ if (!isPro) {
20
+ return res.status(403).json({
21
+ error: 'Pro feature',
22
+ edition: 'community',
23
+ upgrade: 'https://groovedev.ai/pro',
24
+ });
25
+ }
26
+ if (!_subscriptionCache.active) {
27
+ return res.status(403).json({
28
+ error: 'Pro subscription required',
29
+ edition: 'pro',
30
+ subscriptionActive: false,
31
+ upgrade: 'https://groovedev.ai/pro',
32
+ });
33
+ }
34
+ next();
35
+ }
36
+
37
+ async function _executeApprovalRetry(daemon, approval) {
38
+ const rp = approval.retryPayload;
39
+ if (!rp) return;
40
+ try {
41
+ let resultText;
42
+ if (rp.type === 'integration_exec') {
43
+ const result = await daemon.mcpManager.execTool(rp.integrationId, rp.tool, rp.params);
44
+ resultText = JSON.stringify(result).slice(0, 2000);
45
+ daemon.audit.log('approval.autoRetry', { type: rp.type, integrationId: rp.integrationId, tool: rp.tool, agentId: rp.agentId, approvalId: approval.id });
46
+ } else if (rp.type === 'google_drive_upload') {
47
+ const result = await daemon.integrations.uploadToGoogleDrive(rp.filePath, {
48
+ name: rp.name, folderId: rp.folderId, convert: rp.convert !== false,
49
+ });
50
+ resultText = JSON.stringify(result).slice(0, 2000);
51
+ daemon.audit.log('approval.autoRetry', { type: rp.type, filePath: rp.filePath, agentId: rp.agentId, approvalId: approval.id });
52
+ } else {
53
+ return;
54
+ }
55
+ if (rp.agentId) {
56
+ await daemon.processes.sendMessage(rp.agentId, `Your ${rp.type === 'integration_exec' ? 'integration action' : 'upload'} was approved and executed successfully. Result: ${resultText}`);
57
+ }
58
+ } catch (err) {
59
+ console.log(`[Groove] Auto-retry for approval ${approval.id} failed: ${err.message}`);
60
+ if (rp.agentId) {
61
+ daemon.processes.sendMessage(rp.agentId, `Your ${rp.type === 'integration_exec' ? 'integration action' : 'upload'} was approved but execution failed: ${err.message}`).catch(() => {});
62
+ }
63
+ }
64
+ }
14
65
 
15
66
  export function createApi(app, daemon) {
16
67
  // CORS — restrict to localhost + bound interface origins
@@ -493,6 +544,11 @@ export function createApi(app, daemon) {
493
544
  res.json(suggestion);
494
545
  });
495
546
 
547
+ // Edition
548
+ app.get('/api/edition', (req, res) => {
549
+ res.json({ edition: isPro ? 'pro' : 'community' });
550
+ });
551
+
496
552
  // Daemon status
497
553
  app.get('/api/status', (req, res) => {
498
554
  res.json({
@@ -503,6 +559,7 @@ export function createApi(app, daemon) {
503
559
  host: daemon.host,
504
560
  port: daemon.port,
505
561
  projectDir: daemon.projectDir,
562
+ edition: isPro ? 'pro' : 'community',
506
563
  });
507
564
  });
508
565
 
@@ -557,10 +614,15 @@ export function createApi(app, daemon) {
557
614
  });
558
615
  });
559
616
 
560
- app.post('/api/approvals/:id/approve', (req, res) => {
617
+ app.post('/api/approvals/:id/approve', async (req, res) => {
561
618
  const result = daemon.supervisor.approve(req.params.id);
562
619
  if (!result) return res.status(404).json({ error: 'Approval not found' });
563
620
  daemon.audit.log('approval.approve', { id: req.params.id });
621
+ if (result.retryPayload) {
622
+ _executeApprovalRetry(daemon, result).catch((err) => {
623
+ console.log(`[Groove] Approval auto-retry failed: ${err.message}`);
624
+ });
625
+ }
564
626
  res.json(result);
565
627
  });
566
628
 
@@ -774,7 +836,7 @@ export function createApi(app, daemon) {
774
836
  if (entry.endsWith('.md') && !entry.startsWith('.')) {
775
837
  const fullPath = resolve(dir, entry);
776
838
  if (statSync(fullPath).isFile()) {
777
- files.push({ name: entry, path: entry, size: statSync(fullPath).size });
839
+ files.push({ name: entry, path: entry, size: statSync(fullPath).size, source: 'project' });
778
840
  }
779
841
  }
780
842
  }
@@ -784,13 +846,37 @@ export function createApi(app, daemon) {
784
846
  if (entry.endsWith('.md')) {
785
847
  const fullPath = resolve(grooveDir, entry);
786
848
  if (statSync(fullPath).isFile()) {
787
- files.push({ name: entry, path: `.groove/${entry}`, size: statSync(fullPath).size });
849
+ files.push({ name: entry, path: `.groove/${entry}`, size: statSync(fullPath).size, source: 'project' });
788
850
  }
789
851
  }
790
852
  }
791
853
  }
792
854
  } catch { /* dir might not exist */ }
793
855
 
856
+ // Include personality file from .groove/personalities/
857
+ try {
858
+ const personalityFile = resolve(daemon.grooveDir, 'personalities', `${agent.name}.md`);
859
+ if (existsSync(personalityFile)) {
860
+ const size = statSync(personalityFile).size;
861
+ files.unshift({ name: 'personality.md', path: '__personality__', size, source: 'personality' });
862
+ }
863
+ } catch { /* ignore */ }
864
+
865
+ // Include user-created agent files from .groove/agent-files/<name>/
866
+ try {
867
+ const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
868
+ if (existsSync(agentFilesDir)) {
869
+ for (const entry of readdirSync(agentFilesDir)) {
870
+ if (entry.endsWith('.md')) {
871
+ const fullPath = resolve(agentFilesDir, entry);
872
+ if (statSync(fullPath).isFile()) {
873
+ files.push({ name: entry, path: `__user__/${entry}`, size: statSync(fullPath).size, source: 'user' });
874
+ }
875
+ }
876
+ }
877
+ }
878
+ } catch { /* ignore */ }
879
+
794
880
  res.json({ files, workingDir: dir });
795
881
  });
796
882
 
@@ -803,6 +889,22 @@ export function createApi(app, daemon) {
803
889
  const relPath = req.query.path;
804
890
  if (!relPath || relPath.includes('..')) return res.status(400).json({ error: 'Invalid path' });
805
891
 
892
+ if (relPath === '__personality__') {
893
+ const personalityFile = resolve(daemon.grooveDir, 'personalities', `${agent.name}.md`);
894
+ if (existsSync(personalityFile)) {
895
+ return res.json({ content: readFileSync(personalityFile, 'utf8') });
896
+ }
897
+ return res.json({ content: '' });
898
+ }
899
+
900
+ if (relPath.startsWith('__user__/')) {
901
+ const fileName = relPath.slice('__user__/'.length);
902
+ if (!fileName || fileName.includes('/') || fileName.includes('..')) return res.status(400).json({ error: 'Invalid path' });
903
+ const filePath = resolve(daemon.grooveDir, 'agent-files', agent.name, fileName);
904
+ if (existsSync(filePath)) return res.json({ content: readFileSync(filePath, 'utf8') });
905
+ return res.json({ content: '' });
906
+ }
907
+
806
908
  const fullPath = resolve(dir, relPath);
807
909
  if (!fullPath.startsWith(dir)) return res.status(400).json({ error: 'Path traversal' });
808
910
 
@@ -824,6 +926,24 @@ export function createApi(app, daemon) {
824
926
  if (!relPath || relPath.includes('..')) return res.status(400).json({ error: 'Invalid path' });
825
927
  if (typeof content !== 'string') return res.status(400).json({ error: 'Content required' });
826
928
 
929
+ if (relPath === '__personality__') {
930
+ const personalityDir = resolve(daemon.grooveDir, 'personalities');
931
+ mkdirSync(personalityDir, { recursive: true });
932
+ writeFileSync(resolve(personalityDir, `${agent.name}.md`), content || '', { mode: 0o600 });
933
+ daemon.audit.log('personality.update', { name: agent.name, agentId: agent.id });
934
+ return res.json({ saved: true });
935
+ }
936
+
937
+ if (relPath.startsWith('__user__/')) {
938
+ const fileName = relPath.slice('__user__/'.length);
939
+ if (!fileName || fileName.includes('/') || fileName.includes('..')) return res.status(400).json({ error: 'Invalid path' });
940
+ const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
941
+ mkdirSync(agentFilesDir, { recursive: true });
942
+ writeFileSync(resolve(agentFilesDir, fileName), content || '', { mode: 0o600 });
943
+ daemon.audit.log('mdfile.write.user', { agentId: agent.id, name: fileName });
944
+ return res.json({ saved: true });
945
+ }
946
+
827
947
  const fullPath = resolve(dir, relPath);
828
948
  if (!fullPath.startsWith(dir)) return res.status(400).json({ error: 'Path traversal' });
829
949
 
@@ -836,6 +956,24 @@ export function createApi(app, daemon) {
836
956
  }
837
957
  });
838
958
 
959
+ // Create a new MD file for an agent
960
+ app.post('/api/agents/:id/mdfiles/create', (req, res) => {
961
+ const agent = daemon.registry.get(req.params.id);
962
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
963
+ let name = req.body?.name;
964
+ if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name required' });
965
+ name = name.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
966
+ if (!name) return res.status(400).json({ error: 'Invalid name' });
967
+ if (!name.endsWith('.md')) name += '.md';
968
+ const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
969
+ mkdirSync(agentFilesDir, { recursive: true });
970
+ const filePath = resolve(agentFilesDir, name);
971
+ if (existsSync(filePath)) return res.status(409).json({ error: 'File already exists' });
972
+ writeFileSync(filePath, '', { mode: 0o600 });
973
+ daemon.audit.log('mdfile.create', { agentId: agent.id, name });
974
+ res.json({ name, path: `__user__/${name}` });
975
+ });
976
+
839
977
  // Rotation stats
840
978
  app.get('/api/rotation', (req, res) => {
841
979
  res.json({
@@ -1145,6 +1283,34 @@ Keep responses concise. Help them think, don't lecture them about the system the
1145
1283
  res.json({ id: agent.id, skills });
1146
1284
  });
1147
1285
 
1286
+ // --- Agent Repos (attach/detach) ---
1287
+
1288
+ app.post('/api/agents/:agentId/repos/:importId', (req, res) => {
1289
+ const agent = daemon.registry.get(req.params.agentId);
1290
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
1291
+ const importId = req.params.importId;
1292
+ const manifest = daemon.repoImporter.getImport(importId);
1293
+ if (!manifest || manifest.status !== 'active') {
1294
+ return res.status(400).json({ error: 'Repo not found or not active' });
1295
+ }
1296
+ const repos = agent.repos || [];
1297
+ if (repos.includes(importId)) {
1298
+ return res.json({ id: agent.id, repos });
1299
+ }
1300
+ daemon.registry.update(agent.id, { repos: [...repos, importId] });
1301
+ daemon.audit.log('repo.attach', { agentId: agent.id, importId });
1302
+ res.json({ id: agent.id, repos: [...repos, importId] });
1303
+ });
1304
+
1305
+ app.delete('/api/agents/:agentId/repos/:importId', (req, res) => {
1306
+ const agent = daemon.registry.get(req.params.agentId);
1307
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
1308
+ const repos = (agent.repos || []).filter((r) => r !== req.params.importId);
1309
+ daemon.registry.update(agent.id, { repos });
1310
+ daemon.audit.log('repo.detach', { agentId: agent.id, importId: req.params.importId });
1311
+ res.json({ id: agent.id, repos });
1312
+ });
1313
+
1148
1314
  // --- Integrations ---
1149
1315
 
1150
1316
  // Google OAuth routes MUST come before parameterized :id routes
@@ -1336,12 +1502,18 @@ Keep responses concise. Help them think, don't lecture them about the system the
1336
1502
  tool,
1337
1503
  params: paramsSummary,
1338
1504
  description: `${entry.name}: ${tool}`,
1505
+ }, {
1506
+ type: 'integration_exec',
1507
+ integrationId,
1508
+ tool,
1509
+ params: params || {},
1510
+ agentId: agentId || null,
1339
1511
  });
1340
1512
  daemon.audit.log('integration.exec.blocked', { integrationId, tool, approvalId: approval.id, agentId });
1341
1513
  return res.status(202).json({
1342
1514
  requiresApproval: true,
1343
1515
  approvalId: approval.id,
1344
- message: `Tool "${tool}" requires approval. Retry with this approvalId once approved.`,
1516
+ message: `Tool "${tool}" requires approval. The user will be prompted automatically. You will receive the result once approved — do not retry.`,
1345
1517
  });
1346
1518
  }
1347
1519
  }
@@ -1390,12 +1562,19 @@ Keep responses concise. Help them think, don't lecture them about the system the
1390
1562
  filePath,
1391
1563
  name: name || filePath.split('/').pop(),
1392
1564
  description: `Upload to Google Drive: ${name || filePath.split('/').pop()}`,
1565
+ }, {
1566
+ type: 'google_drive_upload',
1567
+ filePath,
1568
+ name: name || filePath.split('/').pop(),
1569
+ folderId: folderId || null,
1570
+ convert: convert !== false,
1571
+ agentId: agentId || null,
1393
1572
  });
1394
1573
  daemon.audit.log('integration.upload.blocked', { filePath, approvalId: approval.id, agentId });
1395
1574
  return res.status(202).json({
1396
1575
  requiresApproval: true,
1397
1576
  approvalId: approval.id,
1398
- message: `Upload requires approval. Retry with this approvalId once approved.`,
1577
+ message: `Upload requires approval. The user will be prompted automatically. You will receive the result once approved — do not retry.`,
1399
1578
  });
1400
1579
  }
1401
1580
  }
@@ -2036,27 +2215,27 @@ Keep responses concise. Help them think, don't lecture them about the system the
2036
2215
  for (const agent of agents) {
2037
2216
  if (agent.workingDir) {
2038
2217
  const p = resolve(agent.workingDir, '.groove', 'recommended-team.json');
2039
- if (existsSync(p)) return p;
2218
+ if (existsSync(p)) return { path: p, teamId: agent.teamId || null, agentId: agent.id || null };
2040
2219
  }
2041
2220
  }
2042
2221
  // Fallback to daemon's .groove dir
2043
2222
  const p = resolve(daemon.grooveDir, 'recommended-team.json');
2044
- if (existsSync(p)) return p;
2223
+ if (existsSync(p)) return { path: p, teamId: null, agentId: null };
2045
2224
  return null;
2046
2225
  }
2047
2226
 
2048
2227
  app.get('/api/recommended-team', (req, res) => {
2049
- const teamPath = findRecommendedTeam();
2050
- if (!teamPath) {
2228
+ const found = findRecommendedTeam();
2229
+ if (!found) {
2051
2230
  return res.json({ exists: false, agents: [] });
2052
2231
  }
2053
2232
  try {
2054
- const raw = JSON.parse(readFileSync(teamPath, 'utf8'));
2233
+ const raw = JSON.parse(readFileSync(found.path, 'utf8'));
2055
2234
  // Support both old format (bare array) and new format ({ projectDir, agents })
2056
2235
  if (Array.isArray(raw)) {
2057
- res.json({ exists: true, agents: raw });
2236
+ res.json({ exists: true, agents: raw, teamId: found.teamId });
2058
2237
  } else if (raw && Array.isArray(raw.agents)) {
2059
- res.json({ exists: true, agents: raw.agents, projectDir: raw.projectDir || null });
2238
+ res.json({ exists: true, agents: raw.agents, projectDir: raw.projectDir || null, teamId: found.teamId });
2060
2239
  } else {
2061
2240
  res.json({ exists: false, agents: [] });
2062
2241
  }
@@ -2066,12 +2245,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
2066
2245
  });
2067
2246
 
2068
2247
  app.post('/api/recommended-team/launch', async (req, res) => {
2069
- const teamPath = findRecommendedTeam();
2070
- if (!teamPath) {
2248
+ const found = findRecommendedTeam();
2249
+ if (!found) {
2071
2250
  return res.status(404).json({ error: 'No recommended team found. Run a planner first.' });
2072
2251
  }
2073
2252
  try {
2074
- const raw = JSON.parse(readFileSync(teamPath, 'utf8'));
2253
+ const raw = JSON.parse(readFileSync(found.path, 'utf8'));
2075
2254
 
2076
2255
  // Support both old format (bare array) and new format ({ projectDir, agents })
2077
2256
  let agentConfigs;
@@ -2092,8 +2271,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
2092
2271
  const baseDir = daemon.config?.defaultWorkingDir || daemon.projectDir;
2093
2272
 
2094
2273
  // Use the planner's teamId so launched agents join the correct team.
2095
- // Accept explicit teamId from request body, or find the most recent planner agent.
2096
- let launchTeamId = req.body?.teamId || null;
2274
+ // Priority: explicit from frontend > agent that wrote the file > most recent planner > default
2275
+ let launchTeamId = req.body?.teamId || found.teamId || null;
2097
2276
  if (!launchTeamId) {
2098
2277
  const planners = daemon.registry.getAll()
2099
2278
  .filter((a) => a.role === 'planner')
@@ -2171,6 +2350,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
2171
2350
  permission: config.permission || existing.permission || 'auto',
2172
2351
  workingDir: existing.workingDir || projectWorkingDir,
2173
2352
  name: existing.name,
2353
+ integrationApproval: config.integrationApproval || existing.integrationApproval || undefined,
2174
2354
  });
2175
2355
  validated.teamId = defaultTeamId;
2176
2356
  const newAgent = await daemon.processes.spawn(validated);
@@ -2192,6 +2372,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
2192
2372
  permission: config.permission || 'auto',
2193
2373
  workingDir: config.workingDir || projectWorkingDir,
2194
2374
  name: config.name || undefined,
2375
+ integrationApproval: config.integrationApproval || undefined,
2195
2376
  });
2196
2377
  validated.teamId = defaultTeamId;
2197
2378
  const agent = await daemon.processes.spawn(validated);
@@ -2412,17 +2593,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
2412
2593
  // --- Federation ---
2413
2594
 
2414
2595
  // Federation status
2415
- app.get('/api/federation', (req, res) => {
2596
+ app.get('/api/federation', proOnly, (req, res) => {
2416
2597
  res.json(daemon.federation.getStatus());
2417
2598
  });
2418
2599
 
2419
2600
  // List peers
2420
- app.get('/api/federation/peers', (req, res) => {
2601
+ app.get('/api/federation/peers', proOnly, (req, res) => {
2421
2602
  res.json(daemon.federation.getPeers());
2422
2603
  });
2423
2604
 
2424
2605
  // Initiate pairing (local CLI calls this with the remote URL)
2425
- app.post('/api/federation/initiate', async (req, res) => {
2606
+ app.post('/api/federation/initiate', proOnly, async (req, res) => {
2426
2607
  try {
2427
2608
  const { remoteUrl } = req.body;
2428
2609
  if (!remoteUrl || typeof remoteUrl !== 'string') {
@@ -2436,7 +2617,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
2436
2617
  });
2437
2618
 
2438
2619
  // Accept pairing (remote daemon calls this during key exchange)
2439
- app.post('/api/federation/pair', (req, res) => {
2620
+ app.post('/api/federation/pair', proOnly, (req, res) => {
2440
2621
  try {
2441
2622
  const result = daemon.federation.acceptPairing(req.body);
2442
2623
  res.json(result);
@@ -2446,7 +2627,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
2446
2627
  });
2447
2628
 
2448
2629
  // Unpair a peer
2449
- app.delete('/api/federation/peers/:id', (req, res) => {
2630
+ app.delete('/api/federation/peers/:id', proOnly, (req, res) => {
2450
2631
  try {
2451
2632
  daemon.federation.unpair(req.params.id);
2452
2633
  res.json({ ok: true });
@@ -2456,7 +2637,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
2456
2637
  });
2457
2638
 
2458
2639
  // Receive a signed contract from a peer
2459
- app.post('/api/federation/contract', (req, res) => {
2640
+ app.post('/api/federation/contract', proOnly, (req, res) => {
2460
2641
  try {
2461
2642
  const { senderId, payload, signature } = req.body;
2462
2643
  if (!senderId || !payload || !signature) {
@@ -2470,7 +2651,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
2470
2651
  });
2471
2652
 
2472
2653
  // Send a contract to a peer (local agents call this)
2473
- app.post('/api/federation/contract/send', async (req, res) => {
2654
+ app.post('/api/federation/contract/send', proOnly, async (req, res) => {
2474
2655
  try {
2475
2656
  const { peerId, contract } = req.body;
2476
2657
  if (!peerId || !contract) {
@@ -2490,6 +2671,260 @@ Keep responses concise. Help them think, don't lecture them about the system the
2490
2671
  res.json(daemon.audit.recent(limit));
2491
2672
  });
2492
2673
 
2674
+ // --- Repo Import ---
2675
+
2676
+ app.post('/api/repos/preview', async (req, res) => {
2677
+ try {
2678
+ const { repoUrl } = req.body;
2679
+ if (!repoUrl || typeof repoUrl !== 'string') {
2680
+ return res.status(400).json({ error: 'repoUrl is required (string)' });
2681
+ }
2682
+ const result = await daemon.repoImporter.preview(repoUrl);
2683
+ res.json(result);
2684
+ } catch (err) {
2685
+ res.status(400).json({ error: err.message });
2686
+ }
2687
+ });
2688
+
2689
+ app.post('/api/repos/import', async (req, res) => {
2690
+ try {
2691
+ const { repoUrl, targetPath, createTeam, teamName } = req.body;
2692
+ if (!repoUrl || typeof repoUrl !== 'string') {
2693
+ return res.status(400).json({ error: 'repoUrl is required (string)' });
2694
+ }
2695
+ if (!targetPath || typeof targetPath !== 'string') {
2696
+ return res.status(400).json({ error: 'targetPath is required (string)' });
2697
+ }
2698
+
2699
+ // Resolve shell shortcuts — GUI sends ~/... and ./...
2700
+ let resolvedPath = targetPath;
2701
+ if (resolvedPath.startsWith('~/') || resolvedPath === '~') {
2702
+ resolvedPath = resolve(process.env.HOME || '/tmp', resolvedPath.slice(2));
2703
+ } else if (!resolvedPath.startsWith('/')) {
2704
+ resolvedPath = resolve(daemon.projectDir, resolvedPath);
2705
+ }
2706
+
2707
+ const result = await daemon.repoImporter.import(repoUrl, resolvedPath, {});
2708
+
2709
+ let teamId = null;
2710
+ if (createTeam) {
2711
+ try {
2712
+ const team = daemon.teams.create(teamName || result.stackInfo?.name || 'imported-repo');
2713
+ teamId = team.id;
2714
+ const manifest = daemon.repoImporter.getImport(result.importId);
2715
+ if (manifest) {
2716
+ manifest.teamId = teamId;
2717
+ daemon.repoImporter._saveManifest(manifest);
2718
+ }
2719
+ } catch { /* team creation is optional */ }
2720
+ }
2721
+
2722
+ // Spawn setup agent
2723
+ let agentId = null;
2724
+ try {
2725
+ const setupPrompt = daemon.repoImporter.generateSetupPrompt(resolvedPath, result.stackInfo, '');
2726
+ const agent = await daemon.processes.spawn({
2727
+ role: 'fullstack',
2728
+ name: `setup-${result.importId.slice(0, 4)}`,
2729
+ workingDir: resolvedPath,
2730
+ prompt: setupPrompt,
2731
+ provider: daemon.config?.defaultProvider || 'claude-code',
2732
+ });
2733
+ agentId = agent.id;
2734
+ const manifest = daemon.repoImporter.getImport(result.importId);
2735
+ if (manifest) {
2736
+ manifest.agents.push(agentId);
2737
+ daemon.repoImporter._saveManifest(manifest);
2738
+ }
2739
+ } catch { /* agent spawn is best-effort */ }
2740
+
2741
+ res.json({ importId: result.importId, path: result.path, agentId, teamId });
2742
+ } catch (err) {
2743
+ res.status(400).json({ error: err.message });
2744
+ }
2745
+ });
2746
+
2747
+ app.get('/api/repos/imported', (req, res) => {
2748
+ res.json(daemon.repoImporter.getImported());
2749
+ });
2750
+
2751
+ app.get('/api/repos/:id', (req, res) => {
2752
+ const manifest = daemon.repoImporter.getImport(req.params.id);
2753
+ if (!manifest) return res.status(404).json({ error: 'Import not found' });
2754
+ res.json(manifest);
2755
+ });
2756
+
2757
+ app.get('/api/repos/:id/sandbox', (req, res) => {
2758
+ const manifest = daemon.repoImporter.getImport(req.params.id);
2759
+ if (!manifest) return res.status(404).json({ error: 'Import not found' });
2760
+ res.json(manifest);
2761
+ });
2762
+
2763
+ app.post('/api/repos/:id/process', (req, res) => {
2764
+ try {
2765
+ const { pid, command } = req.body;
2766
+ daemon.repoImporter.recordProcess(req.params.id, pid, command);
2767
+ res.json({ ok: true });
2768
+ } catch (err) {
2769
+ res.status(400).json({ error: err.message });
2770
+ }
2771
+ });
2772
+
2773
+ app.delete('/api/repos/:id/remove', async (req, res) => {
2774
+ try {
2775
+ await daemon.repoImporter.softRemove(req.params.id);
2776
+ res.json({ ok: true });
2777
+ } catch (err) {
2778
+ res.status(400).json({ error: err.message });
2779
+ }
2780
+ });
2781
+
2782
+ app.delete('/api/repos/:id/nuke', async (req, res) => {
2783
+ try {
2784
+ const deleteFiles = req.query.deleteFiles !== 'false';
2785
+ await daemon.repoImporter.hardNuke(req.params.id, { deleteFiles });
2786
+ res.json({ ok: true });
2787
+ } catch (err) {
2788
+ res.status(400).json({ error: err.message });
2789
+ }
2790
+ });
2791
+
2792
+ // --- Personalities ---
2793
+
2794
+ app.get('/api/personalities', (req, res) => {
2795
+ const dir = resolve(daemon.grooveDir, 'personalities');
2796
+ mkdirSync(dir, { recursive: true });
2797
+ try {
2798
+ const files = readdirSync(dir).filter(f => f.endsWith('.md'));
2799
+ const personalities = files.map(f => ({
2800
+ name: f.replace(/\.md$/, ''),
2801
+ }));
2802
+ res.json({ personalities });
2803
+ } catch {
2804
+ res.json({ personalities: [] });
2805
+ }
2806
+ });
2807
+
2808
+ app.get('/api/personalities/:name', (req, res) => {
2809
+ const name = req.params.name.replace(/[^a-zA-Z0-9_-]/g, '');
2810
+ if (!name) return res.status(400).json({ error: 'Invalid name' });
2811
+ const file = resolve(daemon.grooveDir, 'personalities', `${name}.md`);
2812
+ if (!existsSync(file)) return res.status(404).json({ error: 'Personality not found' });
2813
+ res.json({ name, content: readFileSync(file, 'utf8') });
2814
+ });
2815
+
2816
+ app.put('/api/personalities/:name', (req, res) => {
2817
+ const name = req.params.name.replace(/[^a-zA-Z0-9_-]/g, '');
2818
+ if (!name) return res.status(400).json({ error: 'Invalid name' });
2819
+ const content = typeof req.body?.content === 'string' ? req.body.content.slice(0, 10000) : '';
2820
+ const dir = resolve(daemon.grooveDir, 'personalities');
2821
+ mkdirSync(dir, { recursive: true });
2822
+ writeFileSync(resolve(dir, `${name}.md`), content, { mode: 0o600 });
2823
+ daemon.audit.log('personality.update', { name });
2824
+ res.json({ name, content });
2825
+ });
2826
+
2827
+ app.post('/api/personalities/:name/clone', (req, res) => {
2828
+ const source = req.params.name.replace(/[^a-zA-Z0-9_-]/g, '');
2829
+ const target = (req.body?.name || '').replace(/[^a-zA-Z0-9_-]/g, '');
2830
+ if (!source || !target) return res.status(400).json({ error: 'Source and target name required' });
2831
+ const dir = resolve(daemon.grooveDir, 'personalities');
2832
+ const sourceFile = resolve(dir, `${source}.md`);
2833
+ if (!existsSync(sourceFile)) return res.status(404).json({ error: 'Source personality not found' });
2834
+ copyFileSync(sourceFile, resolve(dir, `${target}.md`));
2835
+ daemon.audit.log('personality.clone', { source, target });
2836
+ res.json({ name: target, clonedFrom: source });
2837
+ });
2838
+
2839
+ // --- Tunnels (Remote Access) ---
2840
+
2841
+ app.get('/api/tunnels', proOnly, (req, res) => {
2842
+ res.json(daemon.tunnelManager.getSaved());
2843
+ });
2844
+
2845
+ app.post('/api/tunnels', proOnly, (req, res) => {
2846
+ try {
2847
+ const { name, host, user, port, sshKeyPath, autoStart, autoConnect } = req.body;
2848
+ if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name is required (string)' });
2849
+ if (!host || typeof host !== 'string') return res.status(400).json({ error: 'host is required (string)' });
2850
+ const result = daemon.tunnelManager.save({ name, host, user, port, sshKeyPath, autoStart, autoConnect });
2851
+ res.json(result);
2852
+ } catch (err) {
2853
+ res.status(400).json({ error: err.message });
2854
+ }
2855
+ });
2856
+
2857
+ app.patch('/api/tunnels/:id', proOnly, (req, res) => {
2858
+ try {
2859
+ const result = daemon.tunnelManager.update(req.params.id, req.body);
2860
+ res.json(result);
2861
+ } catch (err) {
2862
+ res.status(400).json({ error: err.message });
2863
+ }
2864
+ });
2865
+
2866
+ app.delete('/api/tunnels/:id', proOnly, (req, res) => {
2867
+ try {
2868
+ daemon.tunnelManager.delete(req.params.id);
2869
+ res.json({ ok: true });
2870
+ } catch (err) {
2871
+ res.status(400).json({ error: err.message });
2872
+ }
2873
+ });
2874
+
2875
+ app.post('/api/tunnels/:id/test', proOnly, async (req, res) => {
2876
+ try {
2877
+ const result = await daemon.tunnelManager.test(req.params.id);
2878
+ res.json(result);
2879
+ } catch (err) {
2880
+ res.status(400).json({ error: err.message });
2881
+ }
2882
+ });
2883
+
2884
+ app.post('/api/tunnels/:id/connect', proOnly, async (req, res) => {
2885
+ try {
2886
+ const result = await daemon.tunnelManager.connect(req.params.id);
2887
+ res.json(result);
2888
+ } catch (err) {
2889
+ const body = { error: err.message };
2890
+ if (err.testResult) body.testResult = err.testResult;
2891
+ res.status(400).json(body);
2892
+ }
2893
+ });
2894
+
2895
+ app.post('/api/tunnels/:id/disconnect', proOnly, async (req, res) => {
2896
+ try {
2897
+ await daemon.tunnelManager.disconnect(req.params.id);
2898
+ res.json({ ok: true });
2899
+ } catch (err) {
2900
+ res.status(400).json({ error: err.message });
2901
+ }
2902
+ });
2903
+
2904
+ app.post('/api/tunnels/:id/install', proOnly, async (req, res) => {
2905
+ try {
2906
+ const result = await daemon.tunnelManager.remoteInstall(req.params.id);
2907
+ res.json(result);
2908
+ } catch (err) {
2909
+ res.status(400).json({ error: err.message });
2910
+ }
2911
+ });
2912
+
2913
+ app.post('/api/tunnels/:id/start', proOnly, async (req, res) => {
2914
+ try {
2915
+ await daemon.tunnelManager.autoStart(req.params.id);
2916
+ res.json({ ok: true });
2917
+ } catch (err) {
2918
+ res.status(400).json({ error: err.message });
2919
+ }
2920
+ });
2921
+
2922
+ app.get('/api/tunnels/:id/status', proOnly, (req, res) => {
2923
+ const s = daemon.tunnelManager.getStatus(req.params.id);
2924
+ if (!s) return res.status(404).json({ error: 'Remote not found' });
2925
+ res.json(s);
2926
+ });
2927
+
2493
2928
  // --- Config ---
2494
2929
 
2495
2930
  app.get('/api/config', (req, res) => {