groove-dev 0.27.140 → 0.27.142

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 (98) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/integrations-registry.json +12 -44
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +100 -23
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
  7. package/node_modules/@groove-dev/daemon/src/journalist.js +171 -1
  8. package/node_modules/@groove-dev/daemon/src/keeper.js +2 -2
  9. package/node_modules/@groove-dev/daemon/src/memory.js +8 -5
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  11. package/node_modules/@groove-dev/daemon/src/process.js +65 -0
  12. package/node_modules/@groove-dev/daemon/src/rotator.js +25 -8
  13. package/node_modules/@groove-dev/daemon/src/validate.js +8 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  15. package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +984 -0
  16. package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +1 -0
  17. package/node_modules/@groove-dev/gui/dist/index.html +3 -3
  18. package/node_modules/@groove-dev/gui/package.json +1 -1
  19. package/node_modules/@groove-dev/gui/src/app.jsx +0 -2
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +3 -4
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +8 -2
  22. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +12 -8
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +79 -5
  24. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  25. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +109 -12
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +111 -0
  27. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +70 -33
  28. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  29. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +2 -68
  30. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -49
  31. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  32. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +10 -10
  33. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +1 -2
  34. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +151 -3
  35. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  36. package/node_modules/@groove-dev/gui/src/stores/groove.js +107 -29
  37. package/node_modules/@groove-dev/gui/src/views/agents.jsx +114 -56
  38. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +2 -0
  39. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +3 -71
  40. package/node_modules/@groove-dev/gui/src/views/memory.jsx +9 -9
  41. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  42. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  43. package/package.json +1 -1
  44. package/packages/cli/package.json +1 -1
  45. package/packages/daemon/integrations-registry.json +12 -44
  46. package/packages/daemon/package.json +1 -1
  47. package/packages/daemon/src/api.js +100 -23
  48. package/packages/daemon/src/integrations.js +10 -0
  49. package/packages/daemon/src/introducer.js +1 -1
  50. package/packages/daemon/src/journalist.js +171 -1
  51. package/packages/daemon/src/keeper.js +2 -2
  52. package/packages/daemon/src/memory.js +8 -5
  53. package/packages/daemon/src/model-lab.js +11 -0
  54. package/packages/daemon/src/process.js +65 -0
  55. package/packages/daemon/src/rotator.js +25 -8
  56. package/packages/daemon/src/validate.js +8 -0
  57. package/packages/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  58. package/packages/gui/dist/assets/index-Bjd91ufV.js +984 -0
  59. package/packages/gui/dist/assets/index-BqdwIFn4.css +1 -0
  60. package/packages/gui/dist/index.html +3 -3
  61. package/packages/gui/package.json +1 -1
  62. package/packages/gui/src/app.jsx +0 -2
  63. package/packages/gui/src/components/agents/agent-chat.jsx +3 -4
  64. package/packages/gui/src/components/agents/agent-feed.jsx +8 -2
  65. package/packages/gui/src/components/agents/agent-file-tree.jsx +12 -8
  66. package/packages/gui/src/components/agents/agent-panel.jsx +79 -5
  67. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  68. package/packages/gui/src/components/agents/workspace-mode.jsx +109 -12
  69. package/packages/gui/src/components/dashboard/context-gauges.jsx +111 -0
  70. package/packages/gui/src/components/dashboard/routing-chart.jsx +70 -33
  71. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  72. package/packages/gui/src/components/editor/code-editor.jsx +2 -68
  73. package/packages/gui/src/components/editor/file-tree.jsx +2 -49
  74. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  75. package/packages/gui/src/components/keeper/global-modals.jsx +10 -10
  76. package/packages/gui/src/components/layout/activity-bar.jsx +1 -2
  77. package/packages/gui/src/components/layout/terminal-panel.jsx +151 -3
  78. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  79. package/packages/gui/src/stores/groove.js +107 -29
  80. package/packages/gui/src/views/agents.jsx +114 -56
  81. package/packages/gui/src/views/dashboard.jsx +2 -0
  82. package/packages/gui/src/views/marketplace.jsx +3 -71
  83. package/packages/gui/src/views/memory.jsx +9 -9
  84. package/packages/gui/src/views/model-lab.jsx +1 -6
  85. package/packages/gui/src/views/models.jsx +658 -565
  86. package/plan_files/keeper-manual.md +53 -42
  87. package/node_modules/@groove-dev/gui/dist/assets/index-BV9CAiw1.css +0 -1
  88. package/node_modules/@groove-dev/gui/dist/assets/index-DK6UIz0n.js +0 -8698
  89. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +0 -78
  90. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +0 -144
  91. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +0 -187
  92. package/node_modules/@groove-dev/gui/src/views/toys.jsx +0 -162
  93. package/packages/gui/dist/assets/index-BV9CAiw1.css +0 -1
  94. package/packages/gui/dist/assets/index-DK6UIz0n.js +0 -8698
  95. package/packages/gui/src/components/toys/toy-card.jsx +0 -78
  96. package/packages/gui/src/components/toys/toy-creator.jsx +0 -144
  97. package/packages/gui/src/components/toys/toy-launcher.jsx +0 -187
  98. package/packages/gui/src/views/toys.jsx +0 -162
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.140",
3
+ "version": "0.27.142",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -432,10 +432,10 @@
432
432
  "icon": "elevenlabs",
433
433
  "tags": ["voice", "tts", "audio", "speech"],
434
434
  "roles": ["creative", "cmo", "support"],
435
- "npmPackage": "elevenlabs-mcp",
435
+ "npmPackage": "@angelogiacco/elevenlabs-mcp-server",
436
436
  "transport": "stdio",
437
437
  "command": "npx",
438
- "args": ["-y", "elevenlabs-mcp"],
438
+ "args": ["-y", "@angelogiacco/elevenlabs-mcp-server"],
439
439
  "authType": "api-key",
440
440
  "envKeys": [
441
441
  { "key": "ELEVENLABS_API_KEY", "label": "API Key", "required": true }
@@ -491,10 +491,10 @@
491
491
  "icon": "hubspot",
492
492
  "tags": ["crm", "sales", "contacts", "marketing"],
493
493
  "roles": ["cmo", "analyst", "support"],
494
- "npmPackage": "hubspot-mcp",
494
+ "npmPackage": "@hubspot/mcp-server",
495
495
  "transport": "stdio",
496
496
  "command": "npx",
497
- "args": ["-y", "hubspot-mcp"],
497
+ "args": ["-y", "@hubspot/mcp-server"],
498
498
  "authType": "api-key",
499
499
  "envKeys": [
500
500
  { "key": "HUBSPOT_ACCESS_TOKEN", "label": "Private App Token", "required": true }
@@ -701,10 +701,10 @@
701
701
  "icon": "mixpanel",
702
702
  "tags": ["analytics", "events", "funnels", "users"],
703
703
  "roles": ["analyst", "cmo", "cfo"],
704
- "npmPackage": "mixpanel-mcp",
704
+ "npmPackage": "@mercuryml/mcp-mixpanel",
705
705
  "transport": "stdio",
706
706
  "command": "npx",
707
- "args": ["-y", "mixpanel-mcp"],
707
+ "args": ["-y", "@mercuryml/mcp-mixpanel"],
708
708
  "authType": "api-key",
709
709
  "envKeys": [
710
710
  { "key": "MIXPANEL_API_SECRET", "label": "API Secret", "required": true }
@@ -761,10 +761,10 @@
761
761
  "icon": "airtable",
762
762
  "tags": ["database", "spreadsheet", "content", "crm"],
763
763
  "roles": ["ea", "cmo", "analyst"],
764
- "npmPackage": "airtable-mcp",
764
+ "npmPackage": "airtable-mcp-server",
765
765
  "transport": "stdio",
766
766
  "command": "npx",
767
- "args": ["-y", "airtable-mcp"],
767
+ "args": ["-y", "airtable-mcp-server"],
768
768
  "authType": "api-key",
769
769
  "envKeys": [
770
770
  { "key": "AIRTABLE_API_KEY", "label": "Personal Access Token", "required": true }
@@ -823,10 +823,10 @@
823
823
  "icon": "intercom",
824
824
  "tags": ["chat", "support", "messaging", "customers"],
825
825
  "roles": ["support", "cmo"],
826
- "npmPackage": "intercom-mcp",
826
+ "npmPackage": "@pipeworx/mcp-intercom",
827
827
  "transport": "stdio",
828
828
  "command": "npx",
829
- "args": ["-y", "intercom-mcp"],
829
+ "args": ["-y", "@pipeworx/mcp-intercom"],
830
830
  "authType": "api-key",
831
831
  "envKeys": [
832
832
  { "key": "INTERCOM_ACCESS_TOKEN", "label": "Access Token", "required": true }
@@ -853,10 +853,10 @@
853
853
  "icon": "twilio",
854
854
  "tags": ["sms", "voice", "phone", "whatsapp"],
855
855
  "roles": ["ea", "support", "cmo"],
856
- "npmPackage": "twilio-mcp",
856
+ "npmPackage": "@twilio-alpha/mcp",
857
857
  "transport": "stdio",
858
858
  "command": "npx",
859
- "args": ["-y", "twilio-mcp"],
859
+ "args": ["-y", "@twilio-alpha/mcp"],
860
860
  "authType": "api-key",
861
861
  "envKeys": [
862
862
  { "key": "TWILIO_ACCOUNT_SID", "label": "Account SID", "placeholder": "AC...", "required": true },
@@ -939,37 +939,5 @@
939
939
  "rating": 0,
940
940
  "ratingCount": 0,
941
941
  "verified": "community"
942
- },
943
- {
944
- "id": "plaid",
945
- "name": "Plaid",
946
- "description": "Bank connections and financial data",
947
- "category": "finance",
948
- "icon": "plaid",
949
- "tags": ["banking", "transactions", "finance", "accounts"],
950
- "roles": ["cfo", "analyst"],
951
- "npmPackage": "plaid-mcp",
952
- "transport": "stdio",
953
- "command": "npx",
954
- "args": ["-y", "plaid-mcp"],
955
- "authType": "api-key",
956
- "envKeys": [
957
- { "key": "PLAID_CLIENT_ID", "label": "Client ID", "required": true },
958
- { "key": "PLAID_SECRET", "label": "Secret", "required": true },
959
- { "key": "PLAID_ENV", "label": "Environment", "placeholder": "sandbox", "required": false }
960
- ],
961
- "setupUrl": "https://dashboard.plaid.com/team/keys",
962
- "setupSteps": [
963
- "Click the link below to open Plaid team keys",
964
- "Copy your Client ID and Secret",
965
- "Use 'sandbox' environment for testing"
966
- ],
967
- "requiresApproval": [],
968
- "agentInstructions": "## Plaid Integration\n\nYou have Plaid connected via GROOVE.\n\n### API\n`POST http://localhost:31415/api/integrations/plaid/exec`\nBody: `{\"tool\": \"<tool>\", \"params\": {...}}`\n\n### Common Tools\n- `get_accounts` — params: {access_token}\n- `get_transactions` — params: {access_token, start_date, end_date}\n- `get_balance` — params: {access_token}\n\n### Rules\n- Use sandbox environment for testing\n- Never expose access_tokens in output",
969
- "featured": false,
970
- "downloads": 0,
971
- "rating": 0,
972
- "ratingCount": 0,
973
- "verified": "community"
974
942
  }
975
943
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.140",
3
+ "version": "0.27.142",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -460,13 +460,14 @@ export function createApi(app, daemon) {
460
460
  // Discoveries (error → fix pairs)
461
461
  app.get('/api/memory/discoveries', (req, res) => {
462
462
  const role = req.query.role;
463
+ const teamId = req.query.teamId;
463
464
  const limit = Math.min(parseInt(req.query.limit) || 100, 500);
464
- res.json({ discoveries: daemon.memory.listDiscoveries({ role, limit }) });
465
+ res.json({ discoveries: daemon.memory.listDiscoveries({ role, teamId, limit }) });
465
466
  });
466
467
 
467
468
  app.post('/api/memory/discoveries', (req, res) => {
468
- const { agentId, role, trigger, fix, outcome } = req.body || {};
469
- const result = daemon.memory.addDiscovery({ agentId, role, trigger, fix, outcome });
469
+ const { agentId, role, trigger, fix, outcome, teamId } = req.body || {};
470
+ const result = daemon.memory.addDiscovery({ agentId, role, trigger, fix, outcome, teamId });
470
471
  if (!result.added && result.error) {
471
472
  return res.status(400).json(result);
472
473
  }
@@ -3171,11 +3172,21 @@ Keep responses concise. Help them think, don't lecture them about the system the
3171
3172
 
3172
3173
  function validateFilePath(relPath, projectDir) {
3173
3174
  if (!relPath || typeof relPath !== 'string') return { error: 'path is required' };
3174
- if (relPath.startsWith('/') || relPath.includes('..') || relPath.includes('\0')) {
3175
- return { error: 'Invalid path' };
3175
+ if (relPath.includes('\0')) return { error: 'Invalid path' };
3176
+
3177
+ let fullPath;
3178
+ if (relPath.startsWith('/')) {
3179
+ if (relPath.includes('..')) return { error: 'Invalid path' };
3180
+ if (!relPath.startsWith(projectDir + '/') && relPath !== projectDir) {
3181
+ return { error: 'Path outside project' };
3182
+ }
3183
+ fullPath = relPath;
3184
+ } else {
3185
+ if (relPath.includes('..')) return { error: 'Invalid path' };
3186
+ fullPath = resolve(projectDir, relPath);
3187
+ if (!fullPath.startsWith(projectDir)) return { error: 'Path outside project' };
3176
3188
  }
3177
- const fullPath = resolve(projectDir, relPath);
3178
- if (!fullPath.startsWith(projectDir)) return { error: 'Path outside project' };
3189
+
3179
3190
  // Symlink resolution — ensure real path is also within project
3180
3191
  try {
3181
3192
  const realPath = realpathSync(fullPath);
@@ -3231,8 +3242,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
3231
3242
  const raw = readdirSync(fullPath, { withFileTypes: true });
3232
3243
  const entries = [];
3233
3244
 
3245
+ const HIDDEN_DIRS = new Set(['.git', 'node_modules', '.groove', '.next', '.nuxt', '__pycache__', '.venv', 'dist', '.cache']);
3246
+ const HIDDEN_FILES = new Set(['.DS_Store']);
3247
+
3234
3248
  const dirs = raw.filter((e) => {
3235
- if (e.name === '.DS_Store') return false;
3249
+ if (HIDDEN_FILES.has(e.name) || HIDDEN_DIRS.has(e.name)) return false;
3236
3250
  if (e.isDirectory()) return true;
3237
3251
  if (e.isSymbolicLink()) {
3238
3252
  try { return statSync(resolve(fullPath, e.name)).isDirectory(); }
@@ -3241,7 +3255,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3241
3255
  return false;
3242
3256
  }).sort((a, b) => a.name.localeCompare(b.name));
3243
3257
  const files = raw.filter((e) => {
3244
- if (e.name === '.DS_Store') return false;
3258
+ if (HIDDEN_FILES.has(e.name)) return false;
3245
3259
  if (e.isFile()) return true;
3246
3260
  if (e.isSymbolicLink()) {
3247
3261
  try { return statSync(resolve(fullPath, e.name)).isFile(); }
@@ -6986,30 +7000,93 @@ Keep responses concise. Help them think, don't lecture them about the system the
6986
7000
  app.post('/api/lab/inference', async (req, res) => {
6987
7001
  try {
6988
7002
  const params = validateLabInferenceParams(req.body);
7003
+ const rt = daemon.modelLab.getRuntime(params.runtimeId);
7004
+ if (!rt) throw new Error('Runtime not found');
7005
+
7006
+ const url = new URL(`${rt.endpoint}/v1/chat/completions`);
7007
+ const reqHeaders = { 'Content-Type': 'application/json' };
7008
+ if (rt.apiKey) reqHeaders['Authorization'] = `Bearer ${rt.apiKey}`;
7009
+
7010
+ const body = {
7011
+ model: params.model,
7012
+ messages: params.messages,
7013
+ stream: true,
7014
+ };
7015
+ const pb = params.parameters || {};
7016
+ if (pb.temperature !== undefined) body.temperature = pb.temperature;
7017
+ if (pb.top_p !== undefined) body.top_p = pb.top_p;
7018
+ if (pb.top_k !== undefined) body.top_k = pb.top_k;
7019
+ if (pb.repeat_penalty !== undefined) body.repeat_penalty = pb.repeat_penalty;
7020
+ if (pb.max_tokens !== undefined) body.max_tokens = pb.max_tokens;
7021
+ if (pb.stop !== undefined) body.stop = pb.stop;
7022
+ if (pb.frequency_penalty !== undefined) body.frequency_penalty = pb.frequency_penalty;
7023
+ if (pb.presence_penalty !== undefined) body.presence_penalty = pb.presence_penalty;
7024
+
7025
+ const payload = JSON.stringify(body);
7026
+
7027
+ // Use Node http module directly — Electron's fetch has stream issues
7028
+ const { request: httpRequest } = await import('http');
7029
+ const upstream = await new Promise((resolve, reject) => {
7030
+ const r = httpRequest({
7031
+ hostname: url.hostname,
7032
+ port: url.port,
7033
+ path: url.pathname,
7034
+ method: 'POST',
7035
+ headers: { ...reqHeaders, 'Content-Length': Buffer.byteLength(payload) },
7036
+ timeout: 300000,
7037
+ }, resolve);
7038
+ r.on('error', reject);
7039
+ r.on('timeout', () => { r.destroy(); reject(new Error('Upstream timeout')); });
7040
+ r.write(payload);
7041
+ r.end();
7042
+ });
7043
+
7044
+ if (upstream.statusCode !== 200) {
7045
+ let errMsg = `HTTP ${upstream.statusCode}`;
7046
+ try {
7047
+ const chunks = [];
7048
+ for await (const c of upstream) chunks.push(c);
7049
+ const data = JSON.parse(Buffer.concat(chunks).toString());
7050
+ errMsg = data.error?.message || errMsg;
7051
+ } catch { /* ignore */ }
7052
+ throw new Error(errMsg);
7053
+ }
6989
7054
 
7055
+ // Pipe raw OpenAI-compatible SSE straight to client
6990
7056
  res.setHeader('Content-Type', 'text/event-stream');
6991
7057
  res.setHeader('Cache-Control', 'no-cache');
6992
7058
  res.setHeader('Connection', 'keep-alive');
6993
7059
  res.setHeader('X-Accel-Buffering', 'no');
6994
- res.flushHeaders();
6995
-
6996
- let closed = false;
6997
- req.on('close', () => { closed = true; });
6998
-
6999
- await daemon.modelLab.streamInference(params, (event) => {
7000
- if (!closed) res.write(`data: ${JSON.stringify(event)}\n\n`);
7001
- });
7002
-
7003
- if (!closed) {
7004
- res.write('data: [DONE]\n\n');
7005
- res.end();
7060
+ upstream.pipe(res);
7061
+
7062
+ // Collect content for session persistence
7063
+ if (params.sessionId) {
7064
+ let full = '';
7065
+ upstream.on('data', (chunk) => {
7066
+ const text = chunk.toString('utf8');
7067
+ for (const line of text.split('\n')) {
7068
+ const trimmed = line.trim();
7069
+ if (!trimmed.startsWith('data: ')) continue;
7070
+ const d = trimmed.slice(6);
7071
+ if (d === '[DONE]') continue;
7072
+ try {
7073
+ const p = JSON.parse(d);
7074
+ const c = p.choices?.[0]?.delta?.content;
7075
+ if (c) full += c;
7076
+ } catch { /* skip */ }
7077
+ }
7078
+ });
7079
+ upstream.on('end', () => {
7080
+ if (full) daemon.modelLab._appendToSession(params.sessionId, params.messages, { role: 'assistant', content: full });
7081
+ });
7006
7082
  }
7083
+
7084
+ req.on('close', () => { upstream.destroy(); });
7007
7085
  } catch (err) {
7008
7086
  if (!res.headersSent) {
7009
7087
  res.status(400).json({ error: err.message });
7010
7088
  } else {
7011
- res.write(`data: ${JSON.stringify({ type: 'error', error: err.message })}\n\n`);
7012
- res.end();
7089
+ try { res.end(); } catch { /* ignore */ }
7013
7090
  }
7014
7091
  }
7015
7092
  });
@@ -127,6 +127,16 @@ export class IntegrationStore {
127
127
  if (this._isInstalled(integrationId)) throw new Error(`Integration already installed: ${integrationId}`);
128
128
 
129
129
  if (entry.npmPackage) {
130
+ // Pre-validate: check the package exists on npm before attempting install
131
+ try {
132
+ execFileSync('npm', ['view', entry.npmPackage, 'version'], {
133
+ stdio: 'pipe',
134
+ timeout: 10_000,
135
+ });
136
+ } catch {
137
+ throw new Error(`Package ${entry.npmPackage} is not available on npm. Use the agent-assisted install flow instead.`);
138
+ }
139
+
130
140
  try {
131
141
  execFileSync('npm', ['install', '--legacy-peer-deps', entry.npmPackage], {
132
142
  cwd: this.integrationsDir,
@@ -437,7 +437,7 @@ export class Introducer {
437
437
  }
438
438
 
439
439
  if (!isLightPlanner && (hasTask || isRotation)) {
440
- const discoveries = this.daemon.memory.getDiscoveriesMarkdown(newAgent.role, 8, 600, newAgent.scope);
440
+ const discoveries = this.daemon.memory.getDiscoveriesMarkdown(newAgent.role, 8, 600, newAgent.scope, newAgent.teamId);
441
441
  if (discoveries) {
442
442
  parts.push(`### Known Fixes for ${newAgent.role} Role\n${discoveries}`);
443
443
  }
@@ -877,7 +877,7 @@ export class Journalist {
877
877
  const entries = agentLog?.entries || [];
878
878
 
879
879
  // Layer 7 memory: discoveries (inline, not pointer — agents lose context with pointers), constraints, specializations
880
- const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 10, 1500) || '';
880
+ const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 10, 1500, agent.scope, agent.teamId) || '';
881
881
  const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
882
882
  const specialization = this.daemon.memory?.getSpecialization(agent.id);
883
883
  const specLine = specialization?.avgQualityScore != null
@@ -994,6 +994,175 @@ export class Journalist {
994
994
  return brief;
995
995
  }
996
996
 
997
+ // --- Conversation Thread Extraction (for idle resume) ---
998
+
999
+ /**
1000
+ * Extract the actual user↔assistant conversation from stream-json logs.
1001
+ * Returns the dialogue in chronological order — user messages interleaved
1002
+ * with Claude's text responses. This preserves the "why" context that
1003
+ * handoff briefs lose through summarization.
1004
+ *
1005
+ * Budget: keeps recent turns verbatim, summarizes oldest if over maxChars.
1006
+ */
1007
+ extractConversationThread(agent, { maxChars = 60000 } = {}) {
1008
+ const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.name}.log`);
1009
+ if (!existsSync(logPath)) return null;
1010
+
1011
+ let content;
1012
+ try {
1013
+ content = readFileSync(logPath, 'utf8');
1014
+ } catch { return null; }
1015
+
1016
+ const lines = content.split('\n');
1017
+ const turns = []; // { role: 'user'|'assistant', text, timestamp }
1018
+
1019
+ for (const line of lines) {
1020
+ if (!line.trim() || line.startsWith('[')) continue;
1021
+ try {
1022
+ const data = JSON.parse(line);
1023
+
1024
+ // User messages
1025
+ if (data.type === 'user' && data.message?.content) {
1026
+ const msgContent = data.message.content;
1027
+ let text = '';
1028
+ if (typeof msgContent === 'string') {
1029
+ text = msgContent;
1030
+ } else if (Array.isArray(msgContent)) {
1031
+ // Extract text blocks, skip tool_result blocks (noise)
1032
+ text = msgContent
1033
+ .filter((b) => b.type === 'text' && b.text)
1034
+ .map((b) => b.text)
1035
+ .join('\n');
1036
+ }
1037
+ if (text.trim().length > 5) {
1038
+ turns.push({ role: 'user', text: text.trim(), timestamp: data.timestamp });
1039
+ }
1040
+ }
1041
+
1042
+ // Assistant text responses (what Claude said — the reasoning/explanations)
1043
+ if (data.type === 'assistant' && data.message?.content) {
1044
+ const blocks = Array.isArray(data.message.content) ? data.message.content : [];
1045
+ const textParts = blocks
1046
+ .filter((b) => b.type === 'text' && b.text && b.text.trim().length > 20)
1047
+ .map((b) => b.text.trim());
1048
+ if (textParts.length > 0) {
1049
+ turns.push({ role: 'assistant', text: textParts.join('\n'), timestamp: data.timestamp });
1050
+ }
1051
+ }
1052
+ } catch { /* skip non-JSON */ }
1053
+ }
1054
+
1055
+ if (turns.length === 0) return null;
1056
+
1057
+ // Deduplicate consecutive same-role turns (merge them)
1058
+ const merged = [];
1059
+ for (const turn of turns) {
1060
+ const last = merged[merged.length - 1];
1061
+ if (last && last.role === turn.role) {
1062
+ last.text += '\n' + turn.text;
1063
+ } else {
1064
+ merged.push({ ...turn });
1065
+ }
1066
+ }
1067
+
1068
+ // Build the thread — keep recent turns verbatim, truncate old ones if over budget
1069
+ let thread = '';
1070
+ const formatted = merged.map((t) => {
1071
+ const label = t.role === 'user' ? 'USER' : 'CLAUDE';
1072
+ return `[${label}]:\n${t.text}`;
1073
+ });
1074
+
1075
+ // Start from the end (most recent) and work backwards to fill budget
1076
+ const recentFirst = [...formatted].reverse();
1077
+ const kept = [];
1078
+ let totalLen = 0;
1079
+
1080
+ for (const entry of recentFirst) {
1081
+ if (totalLen + entry.length > maxChars) {
1082
+ // Truncate this entry to fit remaining budget
1083
+ const remaining = maxChars - totalLen;
1084
+ if (remaining > 200) {
1085
+ kept.push(entry.slice(0, remaining) + '\n[...truncated]');
1086
+ }
1087
+ break;
1088
+ }
1089
+ kept.push(entry);
1090
+ totalLen += entry.length;
1091
+ }
1092
+
1093
+ // Reverse back to chronological order
1094
+ kept.reverse();
1095
+ thread = kept.join('\n\n---\n\n');
1096
+
1097
+ return thread;
1098
+ }
1099
+
1100
+ /**
1101
+ * Build a full context-resume prompt that preserves the conversation
1102
+ * thread so a fresh agent picks up where the previous session left off.
1103
+ */
1104
+ buildConversationResumePrompt(agent, userMessage) {
1105
+ const thread = this.extractConversationThread(agent);
1106
+ if (!thread) return null;
1107
+
1108
+ const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
1109
+ const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 5, 1000, agent.scope, agent.teamId) || '';
1110
+
1111
+ let prompt = [
1112
+ `# Session Context Resume`,
1113
+ ``,
1114
+ `You are continuing a session that went idle. Below is the full conversation`,
1115
+ `from your previous session — your actual exchanges with the user. Pick up`,
1116
+ `exactly where you left off. The user's new message follows at the end.`,
1117
+ ``,
1118
+ `Role: ${agent.role} | Provider: ${agent.provider} | Scope: ${agent.scope?.join(', ') || 'unrestricted'}`,
1119
+ agent.workingDir ? `Working directory: ${agent.workingDir}` : '',
1120
+ ``,
1121
+ constraints ? `## Project Constraints\n\n${constraints}\n` : '',
1122
+ discoveries ? `## Known Issues & Fixes\n\n${discoveries}\n` : '',
1123
+ `## Previous Conversation\n\n${thread}`,
1124
+ ``,
1125
+ `---`,
1126
+ ``,
1127
+ `## New Message From User`,
1128
+ ``,
1129
+ userMessage,
1130
+ ``,
1131
+ `Continue seamlessly from the conversation above. You have the full context of what was discussed, what was tried, what worked and what didn't. Do not ask the user to repeat anything.`,
1132
+ ].filter(Boolean).join('\n');
1133
+
1134
+ // Hard cap at 80K chars (~20K tokens) to leave plenty of room in context window
1135
+ if (prompt.length > 80000) {
1136
+ // Re-extract with smaller budget and rebuild
1137
+ const smallerThread = this.extractConversationThread(agent, { maxChars: 40000 });
1138
+ if (smallerThread) {
1139
+ prompt = [
1140
+ `# Session Context Resume`,
1141
+ ``,
1142
+ `You are continuing a session that went idle. Below is the conversation`,
1143
+ `from your previous session (older turns summarized to fit). Pick up`,
1144
+ `exactly where you left off.`,
1145
+ ``,
1146
+ `Role: ${agent.role} | Scope: ${agent.scope?.join(', ') || 'unrestricted'}`,
1147
+ agent.workingDir ? `Working directory: ${agent.workingDir}` : '',
1148
+ ``,
1149
+ constraints ? `## Project Constraints\n\n${constraints}\n` : '',
1150
+ `## Previous Conversation\n\n${smallerThread}`,
1151
+ ``,
1152
+ `---`,
1153
+ ``,
1154
+ `## New Message From User`,
1155
+ ``,
1156
+ userMessage,
1157
+ ``,
1158
+ `Continue seamlessly. Do not ask the user to repeat anything.`,
1159
+ ].filter(Boolean).join('\n');
1160
+ }
1161
+ }
1162
+
1163
+ return prompt;
1164
+ }
1165
+
997
1166
  // --- Workspace Grouping ---
998
1167
 
999
1168
  /**
@@ -1183,6 +1352,7 @@ export class Journalist {
1183
1352
  this.daemon.memory.addDiscovery({
1184
1353
  agentId: agent.id,
1185
1354
  role: agent.role,
1355
+ teamId: agent.teamId || null,
1186
1356
  trigger: trigger.slice(0, 300),
1187
1357
  fix: fix.slice(0, 500),
1188
1358
  outcome: 'success',
@@ -254,10 +254,10 @@ export class Keeper {
254
254
  // ── Command parser ────────────────────────────────────────
255
255
 
256
256
  static parseCommand(text) {
257
- const cmdMatch = text.match(/^\s*\[(save|append|update|delete|view|doc|link|read|instruct)\]\s*/i);
257
+ const cmdMatch = text.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]/i);
258
258
  if (!cmdMatch) return null;
259
259
  const command = cmdMatch[1].toLowerCase();
260
- const rest = text.slice(cmdMatch[0].length).trim();
260
+ const rest = text.slice(cmdMatch.index + cmdMatch[0].length).trim();
261
261
 
262
262
  if (command === 'instruct') {
263
263
  return { command, tags: [], extra: null };
@@ -261,7 +261,7 @@ export class MemoryStore {
261
261
 
262
262
  // --- Discoveries (error → fix pairs) ---
263
263
 
264
- addDiscovery({ agentId, role, trigger, fix, outcome = 'success' }) {
264
+ addDiscovery({ agentId, role, trigger, fix, outcome = 'success', teamId }) {
265
265
  if (!trigger || !fix) return { added: false, error: 'trigger and fix required' };
266
266
  if (outcome !== 'success') return { added: false, reason: 'only successes stored' };
267
267
 
@@ -277,6 +277,7 @@ export class MemoryStore {
277
277
  ts: new Date().toISOString(),
278
278
  agentId: agentId || null,
279
279
  role: role || 'unknown',
280
+ teamId: teamId || null,
280
281
  trigger: truncate(String(trigger).trim(), 300),
281
282
  fix: truncate(String(fix).trim(), 500),
282
283
  outcome,
@@ -298,7 +299,7 @@ export class MemoryStore {
298
299
  }
299
300
  }
300
301
 
301
- listDiscoveries({ role, limit = 100 } = {}) {
302
+ listDiscoveries({ role, teamId, limit = 100 } = {}) {
302
303
  if (!existsSync(this.discoveriesPath)) return [];
303
304
  try {
304
305
  const lines = readFileSync(this.discoveriesPath, 'utf8').split('\n').filter(Boolean);
@@ -306,7 +307,9 @@ export class MemoryStore {
306
307
  for (const line of lines) {
307
308
  try {
308
309
  const e = JSON.parse(line);
309
- if (!role || e.role === role) entries.push(e);
310
+ if (role && e.role !== role) continue;
311
+ if (teamId && e.teamId && e.teamId !== teamId) continue;
312
+ entries.push(e);
310
313
  } catch { /* skip malformed */ }
311
314
  }
312
315
  return entries.slice(-limit).reverse(); // newest first
@@ -328,8 +331,8 @@ export class MemoryStore {
328
331
  } catch { /* best-effort */ }
329
332
  }
330
333
 
331
- getDiscoveriesMarkdown(role, limit = 20, maxChars = 4000, scope) {
332
- let entries = this.listDiscoveries({ role, limit: limit * 3 });
334
+ getDiscoveriesMarkdown(role, limit = 20, maxChars = 4000, scope, teamId) {
335
+ let entries = this.listDiscoveries({ role, teamId, limit: limit * 3 });
333
336
  if (entries.length === 0) return '';
334
337
 
335
338
  if (scope && Array.isArray(scope) && scope.length > 0) {
@@ -95,6 +95,17 @@ export class ModelLab {
95
95
  removeRuntime(id) {
96
96
  const rt = this.runtimes.get(id);
97
97
  if (!rt) return null;
98
+
99
+ // Stop the llama-server process if this is a local model runtime
100
+ if (rt._localModelId) {
101
+ const mm = this.daemon.modelManager;
102
+ const ls = this.daemon.llamaServer;
103
+ if (mm && ls) {
104
+ const modelPath = mm.getModelPath(rt._localModelId);
105
+ if (modelPath) ls.stopServer(modelPath).catch(() => {});
106
+ }
107
+ }
108
+
98
109
  this.runtimes.delete(id);
99
110
  this._saveRuntimes();
100
111
  this.daemon.broadcast({ type: 'lab:runtime:removed', data: { id } });