groove-dev 0.27.139 → 0.27.141

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 (75) 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 +225 -16
  5. package/node_modules/@groove-dev/daemon/src/index.js +2 -0
  6. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  7. package/node_modules/@groove-dev/daemon/src/introducer.js +17 -1
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
  9. package/node_modules/@groove-dev/daemon/src/keeper.js +277 -0
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  11. package/node_modules/@groove-dev/daemon/src/process.js +76 -0
  12. package/node_modules/@groove-dev/daemon/src/validate.js +9 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/package.json +1 -1
  17. package/node_modules/@groove-dev/gui/src/app.jsx +4 -0
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +41 -17
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +10 -3
  20. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  21. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
  22. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  23. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -86
  24. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  25. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +177 -0
  26. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +11 -6
  27. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
  28. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  29. package/node_modules/@groove-dev/gui/src/stores/groove.js +302 -20
  30. package/node_modules/@groove-dev/gui/src/views/agents.jsx +118 -60
  31. package/node_modules/@groove-dev/gui/src/views/editor.jsx +67 -219
  32. package/node_modules/@groove-dev/gui/src/views/memory.jsx +460 -0
  33. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  34. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  35. package/package.json +1 -1
  36. package/packages/cli/package.json +1 -1
  37. package/packages/daemon/integrations-registry.json +12 -44
  38. package/packages/daemon/package.json +1 -1
  39. package/packages/daemon/src/api.js +225 -16
  40. package/packages/daemon/src/index.js +2 -0
  41. package/packages/daemon/src/integrations.js +10 -0
  42. package/packages/daemon/src/introducer.js +17 -1
  43. package/packages/daemon/src/journalist.js +169 -0
  44. package/packages/daemon/src/keeper.js +277 -0
  45. package/packages/daemon/src/model-lab.js +11 -0
  46. package/packages/daemon/src/process.js +76 -0
  47. package/packages/daemon/src/validate.js +9 -0
  48. package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
  49. package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
  50. package/packages/gui/dist/index.html +2 -2
  51. package/packages/gui/package.json +1 -1
  52. package/packages/gui/src/app.jsx +4 -0
  53. package/packages/gui/src/components/agents/agent-chat.jsx +41 -17
  54. package/packages/gui/src/components/agents/agent-file-tree.jsx +10 -3
  55. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  56. package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
  57. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  58. package/packages/gui/src/components/editor/file-tree.jsx +2 -86
  59. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  60. package/packages/gui/src/components/keeper/global-modals.jsx +177 -0
  61. package/packages/gui/src/components/layout/activity-bar.jsx +11 -6
  62. package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
  63. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  64. package/packages/gui/src/stores/groove.js +302 -20
  65. package/packages/gui/src/views/agents.jsx +118 -60
  66. package/packages/gui/src/views/editor.jsx +67 -219
  67. package/packages/gui/src/views/memory.jsx +460 -0
  68. package/packages/gui/src/views/model-lab.jsx +1 -6
  69. package/packages/gui/src/views/models.jsx +658 -565
  70. package/plan_files/keeper-manual.md +295 -0
  71. package/plan_files/keeper-memory-system.md +223 -0
  72. package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +0 -1
  73. package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +0 -8694
  74. package/packages/gui/dist/assets/index-AkOtskHS.css +0 -1
  75. package/packages/gui/dist/assets/index-B4uYLR57.js +0 -8694
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.139",
3
+ "version": "0.27.141",
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.139",
3
+ "version": "0.27.141",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -18,6 +18,7 @@ import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groo
18
18
  import { ConsentManager } from '../../../moe-training/client/index.js';
19
19
  import { validateAgentConfig, validateReasoningEffort, validateVerbosity, validateTeamMode, validateLabRuntimeConfig, validateLabInferenceParams, validateLabPresetConfig } from './validate.js';
20
20
  import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from './process.js';
21
+ import { Keeper, KEEPER_COMMANDS } from './keeper.js';
21
22
 
22
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
24
  const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
@@ -487,6 +488,148 @@ export function createApi(app, daemon) {
487
488
  res.json(spec);
488
489
  });
489
490
 
491
+ // ── Keeper (tagged memory) ──────────────────────────────────
492
+
493
+ app.get('/api/keeper', (req, res) => {
494
+ res.json({ items: daemon.keeper.list() });
495
+ });
496
+
497
+ app.get('/api/keeper/tree', (req, res) => {
498
+ res.json({ tree: daemon.keeper.tree() });
499
+ });
500
+
501
+ app.get('/api/keeper/search', (req, res) => {
502
+ const q = req.query.q || '';
503
+ res.json({ results: daemon.keeper.search(q) });
504
+ });
505
+
506
+ app.get('/api/keeper/commands', (_req, res) => {
507
+ res.json({ commands: KEEPER_COMMANDS });
508
+ });
509
+
510
+ app.get('/api/keeper/:tag(*)', (req, res) => {
511
+ const item = daemon.keeper.get(req.params.tag);
512
+ if (!item) return res.status(404).json({ error: `Memory #${req.params.tag} not found` });
513
+ res.json(item);
514
+ });
515
+
516
+ app.post('/api/keeper', (req, res) => {
517
+ try {
518
+ const { tag, content } = req.body || {};
519
+ const item = daemon.keeper.save(tag, content);
520
+ daemon.audit.log('keeper.save', { tag: item.tag });
521
+ daemon.broadcast({ type: 'keeper:saved', item });
522
+ res.status(201).json(item);
523
+ } catch (err) {
524
+ res.status(400).json({ error: err.message });
525
+ }
526
+ });
527
+
528
+ app.post('/api/keeper/append', (req, res) => {
529
+ try {
530
+ const { tag, content } = req.body || {};
531
+ const item = daemon.keeper.append(tag, content);
532
+ daemon.audit.log('keeper.append', { tag: item.tag });
533
+ daemon.broadcast({ type: 'keeper:updated', item });
534
+ res.json(item);
535
+ } catch (err) {
536
+ res.status(400).json({ error: err.message });
537
+ }
538
+ });
539
+
540
+ app.post('/api/keeper/pull', (req, res) => {
541
+ try {
542
+ const { tags } = req.body || {};
543
+ if (!Array.isArray(tags) || tags.length === 0) {
544
+ return res.status(400).json({ error: 'Tags array is required' });
545
+ }
546
+ const brief = daemon.keeper.pull(tags);
547
+ if (!brief) return res.status(404).json({ error: 'No memories found for the given tags' });
548
+ res.json({ brief, tags });
549
+ } catch (err) {
550
+ res.status(400).json({ error: err.message });
551
+ }
552
+ });
553
+
554
+ app.patch('/api/keeper/:tag(*)', (req, res) => {
555
+ try {
556
+ const { content } = req.body || {};
557
+ const item = daemon.keeper.update(req.params.tag, content);
558
+ daemon.audit.log('keeper.update', { tag: item.tag });
559
+ daemon.broadcast({ type: 'keeper:updated', item });
560
+ res.json(item);
561
+ } catch (err) {
562
+ res.status(err.message.includes('does not exist') ? 404 : 400).json({ error: err.message });
563
+ }
564
+ });
565
+
566
+ app.delete('/api/keeper/link/:tag(*)', (req, res) => {
567
+ try {
568
+ const { docPath } = req.body || {};
569
+ daemon.keeper.unlink(req.params.tag, docPath);
570
+ daemon.audit.log('keeper.unlink', { tag: req.params.tag, docPath });
571
+ res.json({ ok: true });
572
+ } catch (err) {
573
+ res.status(400).json({ error: err.message });
574
+ }
575
+ });
576
+
577
+ app.delete('/api/keeper/:tag(*)', (req, res) => {
578
+ try {
579
+ const removed = daemon.keeper.delete(req.params.tag);
580
+ if (!removed) return res.status(404).json({ error: `Memory #${req.params.tag} not found` });
581
+ daemon.audit.log('keeper.delete', { tag: req.params.tag });
582
+ daemon.broadcast({ type: 'keeper:deleted', tag: req.params.tag });
583
+ res.json({ ok: true });
584
+ } catch (err) {
585
+ res.status(400).json({ error: err.message });
586
+ }
587
+ });
588
+
589
+ app.post('/api/keeper/doc', async (req, res) => {
590
+ try {
591
+ const { tag, chatHistory, agentId } = req.body || {};
592
+ if (!tag) return res.status(400).json({ error: 'Tag is required' });
593
+ if (!chatHistory || !Array.isArray(chatHistory) || chatHistory.length === 0) {
594
+ return res.status(400).json({ error: 'Chat history is required' });
595
+ }
596
+ const transcript = chatHistory
597
+ .map(m => `**${m.from === 'user' ? 'User' : 'Agent'}:** ${m.text}`)
598
+ .join('\n\n');
599
+ const prompt = `You are a technical writer. Below is a conversation exploring an idea or feature. Write a comprehensive document that captures:\n\n1. The core idea and motivation\n2. Key decisions made during the discussion\n3. Architecture / design choices\n4. Implementation plan (if discussed)\n5. Open questions or next steps\n\nWrite in clear, structured markdown with headers. Be thorough — this document will be the reference for future work on this topic. Do not include a meta-summary about the conversation itself.\n\n---\n\nConversation:\n\n${transcript.slice(0, 40000)}`;
600
+ let doc;
601
+ if (daemon.journalist && typeof daemon.journalist.callHeadless === 'function') {
602
+ doc = await daemon.journalist.callHeadless(prompt, { trackAs: '__keeper_doc__' });
603
+ } else {
604
+ doc = `# ${tag}\n\n*Auto-generated document from conversation*\n\n${transcript.slice(0, 5000)}`;
605
+ }
606
+ const item = daemon.keeper.saveDoc(tag, doc);
607
+ daemon.audit.log('keeper.doc', { tag: item.tag, agentId });
608
+ daemon.broadcast({ type: 'keeper:saved', item });
609
+ res.status(201).json({ ...item, content: doc });
610
+ } catch (err) {
611
+ res.status(500).json({ error: err.message });
612
+ }
613
+ });
614
+
615
+ app.post('/api/keeper/link', (req, res) => {
616
+ try {
617
+ const { tag, docPath } = req.body || {};
618
+ const item = daemon.keeper.link(tag, docPath);
619
+ daemon.audit.log('keeper.link', { tag: item.tag, docPath });
620
+ daemon.broadcast({ type: 'keeper:updated', item });
621
+ res.json(item);
622
+ } catch (err) {
623
+ res.status(400).json({ error: err.message });
624
+ }
625
+ });
626
+
627
+ app.post('/api/keeper/parse', (req, res) => {
628
+ const { text } = req.body || {};
629
+ const parsed = Keeper.parseCommand(text || '');
630
+ res.json({ parsed });
631
+ });
632
+
490
633
  // Token usage
491
634
  app.get('/api/tokens', (req, res) => {
492
635
  res.json(daemon.tokens.getAll());
@@ -3088,8 +3231,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
3088
3231
  const raw = readdirSync(fullPath, { withFileTypes: true });
3089
3232
  const entries = [];
3090
3233
 
3234
+ const HIDDEN_DIRS = new Set(['.git', 'node_modules', '.groove', '.next', '.nuxt', '__pycache__', '.venv', 'dist', '.cache']);
3235
+ const HIDDEN_FILES = new Set(['.DS_Store']);
3236
+
3091
3237
  const dirs = raw.filter((e) => {
3092
- if (e.name === '.DS_Store') return false;
3238
+ if (HIDDEN_FILES.has(e.name) || HIDDEN_DIRS.has(e.name)) return false;
3093
3239
  if (e.isDirectory()) return true;
3094
3240
  if (e.isSymbolicLink()) {
3095
3241
  try { return statSync(resolve(fullPath, e.name)).isDirectory(); }
@@ -3098,7 +3244,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3098
3244
  return false;
3099
3245
  }).sort((a, b) => a.name.localeCompare(b.name));
3100
3246
  const files = raw.filter((e) => {
3101
- if (e.name === '.DS_Store') return false;
3247
+ if (HIDDEN_FILES.has(e.name)) return false;
3102
3248
  if (e.isFile()) return true;
3103
3249
  if (e.isSymbolicLink()) {
3104
3250
  try { return statSync(resolve(fullPath, e.name)).isFile(); }
@@ -6843,30 +6989,93 @@ Keep responses concise. Help them think, don't lecture them about the system the
6843
6989
  app.post('/api/lab/inference', async (req, res) => {
6844
6990
  try {
6845
6991
  const params = validateLabInferenceParams(req.body);
6992
+ const rt = daemon.modelLab.getRuntime(params.runtimeId);
6993
+ if (!rt) throw new Error('Runtime not found');
6846
6994
 
6995
+ const url = new URL(`${rt.endpoint}/v1/chat/completions`);
6996
+ const reqHeaders = { 'Content-Type': 'application/json' };
6997
+ if (rt.apiKey) reqHeaders['Authorization'] = `Bearer ${rt.apiKey}`;
6998
+
6999
+ const body = {
7000
+ model: params.model,
7001
+ messages: params.messages,
7002
+ stream: true,
7003
+ };
7004
+ const pb = params.parameters || {};
7005
+ if (pb.temperature !== undefined) body.temperature = pb.temperature;
7006
+ if (pb.top_p !== undefined) body.top_p = pb.top_p;
7007
+ if (pb.top_k !== undefined) body.top_k = pb.top_k;
7008
+ if (pb.repeat_penalty !== undefined) body.repeat_penalty = pb.repeat_penalty;
7009
+ if (pb.max_tokens !== undefined) body.max_tokens = pb.max_tokens;
7010
+ if (pb.stop !== undefined) body.stop = pb.stop;
7011
+ if (pb.frequency_penalty !== undefined) body.frequency_penalty = pb.frequency_penalty;
7012
+ if (pb.presence_penalty !== undefined) body.presence_penalty = pb.presence_penalty;
7013
+
7014
+ const payload = JSON.stringify(body);
7015
+
7016
+ // Use Node http module directly — Electron's fetch has stream issues
7017
+ const { request: httpRequest } = await import('http');
7018
+ const upstream = await new Promise((resolve, reject) => {
7019
+ const r = httpRequest({
7020
+ hostname: url.hostname,
7021
+ port: url.port,
7022
+ path: url.pathname,
7023
+ method: 'POST',
7024
+ headers: { ...reqHeaders, 'Content-Length': Buffer.byteLength(payload) },
7025
+ timeout: 300000,
7026
+ }, resolve);
7027
+ r.on('error', reject);
7028
+ r.on('timeout', () => { r.destroy(); reject(new Error('Upstream timeout')); });
7029
+ r.write(payload);
7030
+ r.end();
7031
+ });
7032
+
7033
+ if (upstream.statusCode !== 200) {
7034
+ let errMsg = `HTTP ${upstream.statusCode}`;
7035
+ try {
7036
+ const chunks = [];
7037
+ for await (const c of upstream) chunks.push(c);
7038
+ const data = JSON.parse(Buffer.concat(chunks).toString());
7039
+ errMsg = data.error?.message || errMsg;
7040
+ } catch { /* ignore */ }
7041
+ throw new Error(errMsg);
7042
+ }
7043
+
7044
+ // Pipe raw OpenAI-compatible SSE straight to client
6847
7045
  res.setHeader('Content-Type', 'text/event-stream');
6848
7046
  res.setHeader('Cache-Control', 'no-cache');
6849
7047
  res.setHeader('Connection', 'keep-alive');
6850
7048
  res.setHeader('X-Accel-Buffering', 'no');
6851
- res.flushHeaders();
6852
-
6853
- let closed = false;
6854
- req.on('close', () => { closed = true; });
6855
-
6856
- await daemon.modelLab.streamInference(params, (event) => {
6857
- if (!closed) res.write(`data: ${JSON.stringify(event)}\n\n`);
6858
- });
6859
-
6860
- if (!closed) {
6861
- res.write('data: [DONE]\n\n');
6862
- res.end();
7049
+ upstream.pipe(res);
7050
+
7051
+ // Collect content for session persistence
7052
+ if (params.sessionId) {
7053
+ let full = '';
7054
+ upstream.on('data', (chunk) => {
7055
+ const text = chunk.toString('utf8');
7056
+ for (const line of text.split('\n')) {
7057
+ const trimmed = line.trim();
7058
+ if (!trimmed.startsWith('data: ')) continue;
7059
+ const d = trimmed.slice(6);
7060
+ if (d === '[DONE]') continue;
7061
+ try {
7062
+ const p = JSON.parse(d);
7063
+ const c = p.choices?.[0]?.delta?.content;
7064
+ if (c) full += c;
7065
+ } catch { /* skip */ }
7066
+ }
7067
+ });
7068
+ upstream.on('end', () => {
7069
+ if (full) daemon.modelLab._appendToSession(params.sessionId, params.messages, { role: 'assistant', content: full });
7070
+ });
6863
7071
  }
7072
+
7073
+ req.on('close', () => { upstream.destroy(); });
6864
7074
  } catch (err) {
6865
7075
  if (!res.headersSent) {
6866
7076
  res.status(400).json({ error: err.message });
6867
7077
  } else {
6868
- res.write(`data: ${JSON.stringify({ type: 'error', error: err.message })}\n\n`);
6869
- res.end();
7078
+ try { res.end(); } catch { /* ignore */ }
6870
7079
  }
6871
7080
  }
6872
7081
  });
@@ -34,6 +34,7 @@ import { Scheduler } from './scheduler.js';
34
34
  import { FileWatcher } from './filewatcher.js';
35
35
  import { TimelineTracker } from './timeline.js';
36
36
  import { MemoryStore } from './memory.js';
37
+ import { Keeper } from './keeper.js';
37
38
  import { TerminalManager } from './terminal-pty.js';
38
39
  import { GatewayManager } from './gateways/manager.js';
39
40
  import { McpManager } from './mcp-manager.js';
@@ -124,6 +125,7 @@ export class Daemon {
124
125
  this.locks = new LockManager(this.grooveDir);
125
126
  this.tokens = new TokenTracker(this.grooveDir);
126
127
  this.memory = new MemoryStore(this.grooveDir);
128
+ this.keeper = new Keeper(this.grooveDir);
127
129
  this.timeline = new TimelineTracker(this);
128
130
  this.processes = new ProcessManager(this);
129
131
  this.introducer = new Introducer(this);
@@ -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,
@@ -462,7 +462,23 @@ export class Introducer {
462
462
  memorySection = '';
463
463
  }
464
464
 
465
- return lines.join('\n') + memorySection;
465
+ // --- Keeper: tagged memory injection via [pull] ---
466
+ let keeperSection = '';
467
+ try {
468
+ if (this.daemon.keeper && newAgent.keeperTags && Array.isArray(newAgent.keeperTags) && newAgent.keeperTags.length > 0) {
469
+ const brief = this.daemon.keeper.pull(newAgent.keeperTags);
470
+ if (brief) {
471
+ keeperSection = `\n## Keeper Context (user-tagged memories)\n\n${brief}\n`;
472
+ if (keeperSection.length > 5000) {
473
+ keeperSection = keeperSection.slice(0, 4997) + '...';
474
+ }
475
+ }
476
+ }
477
+ } catch {
478
+ keeperSection = '';
479
+ }
480
+
481
+ return lines.join('\n') + memorySection + keeperSection;
466
482
  }
467
483
 
468
484
  _loadClaudeMd(workingDir) {
@@ -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) || '';
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
  /**