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.
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/integrations-registry.json +12 -44
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +225 -16
- package/node_modules/@groove-dev/daemon/src/index.js +2 -0
- package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +17 -1
- package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
- package/node_modules/@groove-dev/daemon/src/keeper.js +277 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
- package/node_modules/@groove-dev/daemon/src/process.js +76 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +9 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/app.jsx +4 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +41 -17
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +10 -3
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
- package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -86
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
- package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +177 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +11 -6
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/node_modules/@groove-dev/gui/src/stores/groove.js +302 -20
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +118 -60
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +67 -219
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +460 -0
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
- package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/integrations-registry.json +12 -44
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +225 -16
- package/packages/daemon/src/index.js +2 -0
- package/packages/daemon/src/integrations.js +10 -0
- package/packages/daemon/src/introducer.js +17 -1
- package/packages/daemon/src/journalist.js +169 -0
- package/packages/daemon/src/keeper.js +277 -0
- package/packages/daemon/src/model-lab.js +11 -0
- package/packages/daemon/src/process.js +76 -0
- package/packages/daemon/src/validate.js +9 -0
- package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
- package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +4 -0
- package/packages/gui/src/components/agents/agent-chat.jsx +41 -17
- package/packages/gui/src/components/agents/agent-file-tree.jsx +10 -3
- package/packages/gui/src/components/agents/code-review.jsx +5 -4
- package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
- package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
- package/packages/gui/src/components/editor/file-tree.jsx +2 -86
- package/packages/gui/src/components/editor/terminal.jsx +15 -4
- package/packages/gui/src/components/keeper/global-modals.jsx +177 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +11 -6
- package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/packages/gui/src/stores/groove.js +302 -20
- package/packages/gui/src/views/agents.jsx +118 -60
- package/packages/gui/src/views/editor.jsx +67 -219
- package/packages/gui/src/views/memory.jsx +460 -0
- package/packages/gui/src/views/model-lab.jsx +1 -6
- package/packages/gui/src/views/models.jsx +658 -565
- package/plan_files/keeper-manual.md +295 -0
- package/plan_files/keeper-memory-system.md +223 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +0 -8694
- package/packages/gui/dist/assets/index-AkOtskHS.css +0 -1
- package/packages/gui/dist/assets/index-B4uYLR57.js +0 -8694
|
@@ -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-
|
|
494
|
+
"npmPackage": "@hubspot/mcp-server",
|
|
495
495
|
"transport": "stdio",
|
|
496
496
|
"command": "npx",
|
|
497
|
-
"args": ["-y", "hubspot-
|
|
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
|
|
704
|
+
"npmPackage": "@mercuryml/mcp-mixpanel",
|
|
705
705
|
"transport": "stdio",
|
|
706
706
|
"command": "npx",
|
|
707
|
-
"args": ["-y", "mixpanel
|
|
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
|
|
826
|
+
"npmPackage": "@pipeworx/mcp-intercom",
|
|
827
827
|
"transport": "stdio",
|
|
828
828
|
"command": "npx",
|
|
829
|
-
"args": ["-y", "intercom
|
|
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
|
]
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
/**
|