groove-dev 0.27.142 → 0.27.144
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/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
- package/node_modules/@groove-dev/daemon/src/index.js +3 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
- package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
- package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +2 -2
- package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
- package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +889 -0
- package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
- package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
- package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
- package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
- package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
- package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
- package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
- package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
- package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
- package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
- package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
- package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
- package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +1012 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/{packages/gui/src/app.jsx → node_modules/@groove-dev/gui/src/App.jsx} +0 -2
- package/node_modules/@groove-dev/gui/src/app.css +35 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -31
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
- package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +195 -14
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +286 -102
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
- package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +24 -24
- package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +34 -3144
- package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +452 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +227 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +17 -6
- package/node_modules/@groove-dev/gui/src/views/models.jsx +410 -509
- package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +1086 -6532
- package/packages/daemon/src/gateways/manager.js +35 -1
- package/packages/daemon/src/index.js +3 -0
- package/packages/daemon/src/journalist.js +23 -13
- package/packages/daemon/src/mlx-server.js +365 -0
- package/packages/daemon/src/model-lab.js +308 -12
- package/packages/daemon/src/pm.js +1 -1
- package/packages/daemon/src/process.js +2 -2
- package/packages/daemon/src/providers/local.js +36 -8
- package/packages/daemon/src/registry.js +21 -5
- package/packages/daemon/src/routes/agents.js +889 -0
- package/packages/daemon/src/routes/coordination.js +318 -0
- package/packages/daemon/src/routes/files.js +751 -0
- package/packages/daemon/src/routes/integrations.js +485 -0
- package/packages/daemon/src/routes/network.js +1784 -0
- package/packages/daemon/src/routes/providers.js +755 -0
- package/packages/daemon/src/routes/schedules.js +110 -0
- package/packages/daemon/src/routes/teams.js +650 -0
- package/packages/daemon/src/scheduler.js +456 -24
- package/packages/daemon/src/teams.js +1 -1
- package/packages/daemon/src/validate.js +38 -1
- package/packages/daemon/templates/mlx-setup.json +12 -0
- package/packages/daemon/templates/tgi-setup.json +1 -1
- package/packages/daemon/templates/vllm-setup.json +1 -1
- package/packages/gui/dist/assets/index-BcoF6_eF.js +1012 -0
- package/packages/gui/dist/assets/index-Dd7qhiEd.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/{node_modules/@groove-dev/gui/src/app.jsx → packages/gui/src/App.jsx} +0 -2
- package/packages/gui/src/app.css +35 -0
- package/packages/gui/src/components/agents/agent-config.jsx +1 -128
- package/packages/gui/src/components/agents/agent-feed.jsx +144 -31
- package/packages/gui/src/components/agents/agent-node.jsx +8 -13
- package/packages/gui/src/components/agents/code-review.jsx +159 -122
- package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/packages/gui/src/components/automations/automation-card.jsx +274 -0
- package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
- package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
- package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
- package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/packages/gui/src/components/lab/parameter-panel.jsx +195 -14
- package/packages/gui/src/components/lab/runtime-config.jsx +286 -102
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
- package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/packages/gui/src/components/network/network-health.jsx +2 -2
- package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/packages/gui/src/components/ui/sheet.jsx +5 -2
- package/packages/gui/src/lib/cron.js +64 -0
- package/packages/gui/src/lib/status.js +24 -24
- package/packages/gui/src/lib/theme-hex.js +1 -0
- package/packages/gui/src/stores/groove.js +34 -3144
- package/packages/gui/src/stores/helpers.js +10 -0
- package/packages/gui/src/stores/slices/agents-slice.js +452 -0
- package/packages/gui/src/stores/slices/automations-slice.js +96 -0
- package/packages/gui/src/stores/slices/chat-slice.js +227 -0
- package/packages/gui/src/stores/slices/editor-slice.js +285 -0
- package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/packages/gui/src/stores/slices/network-slice.js +361 -0
- package/packages/gui/src/stores/slices/preview-slice.js +109 -0
- package/packages/gui/src/stores/slices/providers-slice.js +897 -0
- package/packages/gui/src/stores/slices/teams-slice.js +413 -0
- package/packages/gui/src/stores/slices/ui-slice.js +98 -0
- package/packages/gui/src/views/agents.jsx +5 -5
- package/packages/gui/src/views/dashboard.jsx +12 -13
- package/packages/gui/src/views/marketplace.jsx +191 -3
- package/packages/gui/src/views/model-lab.jsx +17 -6
- package/packages/gui/src/views/models.jsx +410 -509
- package/packages/gui/src/views/network.jsx +3 -3
- package/packages/gui/src/views/settings.jsx +81 -94
- package/packages/gui/src/views/teams.jsx +40 -483
- package/SECURITY_SWEEP.md +0 -228
- package/TRAINING_DATA_v4.md +0 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +0 -984
- package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +0 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -322
- package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
- package/packages/gui/dist/assets/index-Bjd91ufV.js +0 -984
- package/packages/gui/dist/assets/index-BqdwIFn4.css +0 -1
- package/packages/gui/src/components/agents/agent-chat.jsx +0 -322
- package/packages/gui/src/views/preview.jsx +0 -6
- package/packages/gui/src/views/subscription-panel.jsx +0 -327
- package/test.py +0 -571
|
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlink
|
|
|
5
5
|
import { resolve } from 'path';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import { validateAgentConfig } from '../validate.js';
|
|
8
|
-
import { eventToSummary, agentListText, statusText, approvalsText, teamsText, schedulesText, briefText, tokensText, logText, planText, truncate, formatTokens } from './formatter.js';
|
|
8
|
+
import { eventToSummary, agentListText, statusText, approvalsText, teamsText, schedulesText, briefText, tokensText, logText, planText, truncate, formatTokens, formatDuration, formatCost } from './formatter.js';
|
|
9
9
|
|
|
10
10
|
const GATEWAY_TYPES = ['telegram', 'discord', 'slack'];
|
|
11
11
|
|
|
@@ -304,6 +304,40 @@ export class GatewayManager {
|
|
|
304
304
|
this.daemon.credentials.deleteKey(`gateway:${id}:${key}`);
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
// -------------------------------------------------------------------
|
|
308
|
+
// Schedule Notifications — direct, targeted notification for automations
|
|
309
|
+
// -------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Send a completion notification for a scheduled automation run.
|
|
313
|
+
* Called directly by the scheduler — bypasses global event routing.
|
|
314
|
+
*/
|
|
315
|
+
sendScheduleNotification(gatewayIds, summary) {
|
|
316
|
+
if (!gatewayIds || gatewayIds.length === 0) return;
|
|
317
|
+
|
|
318
|
+
const statusIcon = summary.status === 'success' ? '✅' : '❌';
|
|
319
|
+
const lines = [
|
|
320
|
+
`${statusIcon} Automation: ${summary.name} — ${summary.status}`,
|
|
321
|
+
];
|
|
322
|
+
if (summary.description) {
|
|
323
|
+
lines.push(summary.description);
|
|
324
|
+
}
|
|
325
|
+
lines.push(`Duration: ${formatDuration(summary.duration)} | Cost: ${formatCost(summary.cost)} | Agents: ${summary.agentCount}`);
|
|
326
|
+
if (summary.errors) {
|
|
327
|
+
lines.push(`Error: ${truncate(summary.errors, 500)}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const message = lines.join('\n');
|
|
331
|
+
|
|
332
|
+
for (const gid of gatewayIds) {
|
|
333
|
+
const gw = this.gateways.get(gid);
|
|
334
|
+
if (!gw || !gw.connected) continue;
|
|
335
|
+
gw.send(message).catch((err) => {
|
|
336
|
+
console.log(`[Groove:Gateway] Schedule notification failed (${gid}): ${err.message}`);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
307
341
|
// -------------------------------------------------------------------
|
|
308
342
|
// Command Routing — chat command → daemon internals
|
|
309
343
|
// -------------------------------------------------------------------
|
|
@@ -42,6 +42,7 @@ import { TunnelManager } from './tunnel-manager.js';
|
|
|
42
42
|
import { ModelManager } from './model-manager.js';
|
|
43
43
|
import { ModelLab } from './model-lab.js';
|
|
44
44
|
import { LlamaServerManager } from './llama-server.js';
|
|
45
|
+
import { MLXServerManager } from './mlx-server.js';
|
|
45
46
|
import { RepoImporter } from './repo-import.js';
|
|
46
47
|
import { ConversationManager } from './conversations.js';
|
|
47
48
|
import { Toys } from './toys.js';
|
|
@@ -151,6 +152,7 @@ export class Daemon {
|
|
|
151
152
|
this.preview = new PreviewService(this);
|
|
152
153
|
this.modelManager = new ModelManager(this);
|
|
153
154
|
this.llamaServer = new LlamaServerManager(this);
|
|
155
|
+
this.mlxServer = new MLXServerManager(this);
|
|
154
156
|
this.mcpManager = new McpManager(this);
|
|
155
157
|
this.tunnelManager = new TunnelManager(this);
|
|
156
158
|
this.repoImporter = new RepoImporter(this);
|
|
@@ -848,6 +850,7 @@ export class Daemon {
|
|
|
848
850
|
if (this.preview) await this.preview.killAll();
|
|
849
851
|
this.mcpManager.stopAll();
|
|
850
852
|
await this.llamaServer.stopAll();
|
|
853
|
+
await this.mlxServer.stopAll();
|
|
851
854
|
|
|
852
855
|
// Clean up PID and host files
|
|
853
856
|
if (existsSync(this.pidFile)) {
|
|
@@ -168,7 +168,7 @@ export class Journalist {
|
|
|
168
168
|
|
|
169
169
|
hasNewActivity(agents) {
|
|
170
170
|
for (const agent of agents) {
|
|
171
|
-
const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.
|
|
171
|
+
const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.id}.log`);
|
|
172
172
|
if (!existsSync(logPath)) continue;
|
|
173
173
|
try {
|
|
174
174
|
const size = statSync(logPath).size;
|
|
@@ -182,7 +182,7 @@ export class Journalist {
|
|
|
182
182
|
const result = {};
|
|
183
183
|
|
|
184
184
|
for (const agent of agents) {
|
|
185
|
-
const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.
|
|
185
|
+
const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.id}.log`);
|
|
186
186
|
if (!existsSync(logPath)) {
|
|
187
187
|
result[agent.id] = { agent, entries: [], explorationEntries: [] };
|
|
188
188
|
continue;
|
|
@@ -846,7 +846,7 @@ export class Journalist {
|
|
|
846
846
|
for (const [agentId, { agent, entries }] of Object.entries(filteredLogs)) {
|
|
847
847
|
if (entries.length === 0) continue;
|
|
848
848
|
|
|
849
|
-
const agentDir = resolve(logsDir, agent.
|
|
849
|
+
const agentDir = resolve(logsDir, agent.id);
|
|
850
850
|
mkdirSync(agentDir, { recursive: true });
|
|
851
851
|
|
|
852
852
|
const logPath = resolve(agentDir, `${dateStr}-session.md`);
|
|
@@ -904,13 +904,24 @@ export class Journalist {
|
|
|
904
904
|
.map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || e.text || '').slice(0, 200)}`)
|
|
905
905
|
.join('\n');
|
|
906
906
|
|
|
907
|
-
// Try AI-synthesized session summary
|
|
907
|
+
// Try AI-synthesized session summary — but only if the session log has
|
|
908
|
+
// meaningful entries. With an empty/minimal log the headless Claude sees
|
|
909
|
+
// repo-wide git status (from its own project context) and fills the brief
|
|
910
|
+
// with unrelated changes from other teams, leaking cross-team state.
|
|
911
|
+
const meaningfulEntries = entries.filter((e) => e.type === 'tool' || e.type === 'error' || e.type === 'user' || e.type === 'result');
|
|
908
912
|
let sessionSummary = '';
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
913
|
+
let needsFallback = meaningfulEntries.length < 3;
|
|
914
|
+
|
|
915
|
+
if (!needsFallback) {
|
|
916
|
+
try {
|
|
917
|
+
const prompt = this.buildRotationSynthesisPrompt(agent, entries, options);
|
|
918
|
+
sessionSummary = await this.callHeadless(prompt, { trackAs: '__rotation__' });
|
|
919
|
+
} catch {
|
|
920
|
+
needsFallback = true;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (needsFallback) {
|
|
914
925
|
const errorSummary = entries
|
|
915
926
|
.filter((e) => e.type === 'error')
|
|
916
927
|
.map((e) => `- ${e.text}`)
|
|
@@ -941,7 +952,6 @@ export class Journalist {
|
|
|
941
952
|
.map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || '').slice(0, 200)}`)
|
|
942
953
|
.join('\n');
|
|
943
954
|
|
|
944
|
-
// Build investigation timeline from thinking entries — these capture reasoning and decisions
|
|
945
955
|
const thinkingEntries = entries
|
|
946
956
|
.filter((e) => e.type === 'thinking' && e.text && e.text.length > 80)
|
|
947
957
|
.slice(-10)
|
|
@@ -1005,7 +1015,7 @@ export class Journalist {
|
|
|
1005
1015
|
* Budget: keeps recent turns verbatim, summarizes oldest if over maxChars.
|
|
1006
1016
|
*/
|
|
1007
1017
|
extractConversationThread(agent, { maxChars = 60000 } = {}) {
|
|
1008
|
-
const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.
|
|
1018
|
+
const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.id}.log`);
|
|
1009
1019
|
if (!existsSync(logPath)) return null;
|
|
1010
1020
|
|
|
1011
1021
|
let content;
|
|
@@ -1211,7 +1221,7 @@ export class Journalist {
|
|
|
1211
1221
|
* Used by the Introducer to tell new agents what their teammates built.
|
|
1212
1222
|
*/
|
|
1213
1223
|
getAgentFiles(agent) {
|
|
1214
|
-
const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.
|
|
1224
|
+
const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.id}.log`);
|
|
1215
1225
|
if (!existsSync(logPath)) return [];
|
|
1216
1226
|
|
|
1217
1227
|
try {
|
|
@@ -1249,7 +1259,7 @@ export class Journalist {
|
|
|
1249
1259
|
* Used to capture planner conclusions, build summaries, etc.
|
|
1250
1260
|
*/
|
|
1251
1261
|
getAgentResult(agent) {
|
|
1252
|
-
const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.
|
|
1262
|
+
const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.id}.log`);
|
|
1253
1263
|
if (!existsSync(logPath)) return '';
|
|
1254
1264
|
|
|
1255
1265
|
try {
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
// GROOVE — MLX Server Manager
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
//
|
|
4
|
+
// Manages mlx_lm.server inference server instances on Apple Silicon.
|
|
5
|
+
// Scans ~/.cache/huggingface/hub/ for cached MLX models.
|
|
6
|
+
// Mirrors LlamaServerManager API: ensureServer, stopServer, getStatus.
|
|
7
|
+
|
|
8
|
+
import { spawn, execSync } from 'child_process';
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
10
|
+
import { resolve } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
|
|
13
|
+
const BASE_PORT = 8080;
|
|
14
|
+
const MAX_SERVERS = 3;
|
|
15
|
+
const HEALTH_TIMEOUT = 60000; // 60s — MLX may need to load model into memory
|
|
16
|
+
const HEALTH_POLL_INTERVAL = 1000;
|
|
17
|
+
const IDLE_TIMEOUT = 300000; // 5 minutes
|
|
18
|
+
|
|
19
|
+
const HF_CACHE_DIR = resolve(homedir(), '.cache', 'huggingface', 'hub');
|
|
20
|
+
const HF_MODEL_DIR_PREFIX = 'models--';
|
|
21
|
+
|
|
22
|
+
export class MLXServerManager {
|
|
23
|
+
constructor(daemon) {
|
|
24
|
+
this.daemon = daemon;
|
|
25
|
+
this.servers = new Map(); // modelId -> { proc, port, users, startedAt, lastUsed, ready }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static isInstalled() {
|
|
29
|
+
try {
|
|
30
|
+
execSync('python3 -c "import mlx_lm; print(mlx_lm.__version__)"', {
|
|
31
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
32
|
+
timeout: 10000,
|
|
33
|
+
});
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static getVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const out = execSync('python3 -c "import mlx_lm; print(mlx_lm.__version__)"', {
|
|
43
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
44
|
+
timeout: 10000,
|
|
45
|
+
});
|
|
46
|
+
return out.toString().trim();
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static getPythonPath() {
|
|
53
|
+
// Check venv first, then system python
|
|
54
|
+
const venvPython = resolve(homedir(), '.mlx-env', 'bin', 'python3');
|
|
55
|
+
if (existsSync(venvPython)) {
|
|
56
|
+
try {
|
|
57
|
+
execSync(`${venvPython} -c "import mlx_lm"`, { stdio: 'ignore', timeout: 10000 });
|
|
58
|
+
return venvPython;
|
|
59
|
+
} catch { /* fall through */ }
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
execSync('python3 -c "import mlx_lm"', { stdio: 'ignore', timeout: 10000 });
|
|
63
|
+
return 'python3';
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Model Scanning ---
|
|
70
|
+
|
|
71
|
+
static scanModels() {
|
|
72
|
+
const models = [];
|
|
73
|
+
if (!existsSync(HF_CACHE_DIR)) return models;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const entries = readdirSync(HF_CACHE_DIR);
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (!entry.startsWith(HF_MODEL_DIR_PREFIX)) continue;
|
|
79
|
+
|
|
80
|
+
const modelName = entry.slice(HF_MODEL_DIR_PREFIX.length).replace(/--/g, '/');
|
|
81
|
+
const snapshotsDir = resolve(HF_CACHE_DIR, entry, 'snapshots');
|
|
82
|
+
if (!existsSync(snapshotsDir)) continue;
|
|
83
|
+
|
|
84
|
+
let snapshotDir = null;
|
|
85
|
+
try {
|
|
86
|
+
const snapshots = readdirSync(snapshotsDir);
|
|
87
|
+
if (snapshots.length === 0) continue;
|
|
88
|
+
snapshotDir = resolve(snapshotsDir, snapshots[snapshots.length - 1]);
|
|
89
|
+
} catch { continue; }
|
|
90
|
+
|
|
91
|
+
let hasWeights = false;
|
|
92
|
+
let hasNpz = false;
|
|
93
|
+
let configData = null;
|
|
94
|
+
try {
|
|
95
|
+
const files = readdirSync(snapshotDir);
|
|
96
|
+
hasWeights = files.some((f) =>
|
|
97
|
+
f.endsWith('.safetensors') || f.endsWith('.npz') || f === 'weights.npz'
|
|
98
|
+
);
|
|
99
|
+
if (!hasWeights) continue;
|
|
100
|
+
hasNpz = files.some((f) => f.endsWith('.npz'));
|
|
101
|
+
|
|
102
|
+
const configPath = resolve(snapshotDir, 'config.json');
|
|
103
|
+
if (existsSync(configPath)) {
|
|
104
|
+
configData = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
105
|
+
}
|
|
106
|
+
} catch { continue; }
|
|
107
|
+
|
|
108
|
+
const isMLX = isMLXModel(modelName, hasNpz, configData);
|
|
109
|
+
const type = isMLX ? 'mlx' : 'hf';
|
|
110
|
+
const prefix = isMLX ? 'mlx:' : 'hf:';
|
|
111
|
+
const shortName = modelName.split('/').pop() || modelName;
|
|
112
|
+
const params = parseMLXParams(shortName, configData);
|
|
113
|
+
const quant = parseMLXQuantization(shortName);
|
|
114
|
+
|
|
115
|
+
models.push({
|
|
116
|
+
id: `${prefix}${modelName}`,
|
|
117
|
+
modelId: modelName,
|
|
118
|
+
filename: shortName,
|
|
119
|
+
type,
|
|
120
|
+
compatibleBackends: isMLX ? ['mlx'] : ['vllm', 'tgi'],
|
|
121
|
+
parameters: params,
|
|
122
|
+
quantization: quant,
|
|
123
|
+
snapshotPath: snapshotDir,
|
|
124
|
+
cachedAt: entry,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
} catch { /* best effort */ }
|
|
128
|
+
|
|
129
|
+
return models;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- Server Lifecycle ---
|
|
133
|
+
|
|
134
|
+
async ensureServer(modelId, options = {}) {
|
|
135
|
+
if (this.servers.has(modelId)) {
|
|
136
|
+
const server = this.servers.get(modelId);
|
|
137
|
+
server.users++;
|
|
138
|
+
server.lastUsed = Date.now();
|
|
139
|
+
return `http://127.0.0.1:${server.port}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (this.servers.size >= MAX_SERVERS) {
|
|
143
|
+
await this._evictLRU();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const pythonPath = MLXServerManager.getPythonPath();
|
|
147
|
+
if (!pythonPath) {
|
|
148
|
+
throw new Error('mlx_lm not installed — run: pip3 install "mlx-lm[server]"');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const port = this._allocatePort();
|
|
152
|
+
|
|
153
|
+
const args = [
|
|
154
|
+
'-m', 'mlx_lm.server',
|
|
155
|
+
'--model', modelId,
|
|
156
|
+
'--port', String(port),
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const proc = spawn(pythonPath, args, {
|
|
160
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
161
|
+
detached: false,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!proc.pid) {
|
|
165
|
+
throw new Error('Failed to start mlx_lm.server — check installation');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const server = {
|
|
169
|
+
proc,
|
|
170
|
+
port,
|
|
171
|
+
modelId,
|
|
172
|
+
users: 1,
|
|
173
|
+
startedAt: Date.now(),
|
|
174
|
+
lastUsed: Date.now(),
|
|
175
|
+
ready: false,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
this.servers.set(modelId, server);
|
|
179
|
+
|
|
180
|
+
const stderrBuf = [];
|
|
181
|
+
proc.stderr.on('data', (chunk) => {
|
|
182
|
+
stderrBuf.push(chunk.toString());
|
|
183
|
+
if (stderrBuf.join('').length > 4096) stderrBuf.shift();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
proc.on('exit', (code, signal) => {
|
|
187
|
+
this.servers.delete(modelId);
|
|
188
|
+
this.daemon?.broadcast({
|
|
189
|
+
type: 'mlx:server:stopped',
|
|
190
|
+
data: { modelId, port, code, signal },
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
await this._waitForHealth(port);
|
|
196
|
+
server.ready = true;
|
|
197
|
+
|
|
198
|
+
this.daemon?.broadcast({
|
|
199
|
+
type: 'mlx:server:ready',
|
|
200
|
+
data: { modelId, port },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return `http://127.0.0.1:${port}`;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
await this.stopServer(modelId);
|
|
206
|
+
const stderr = stderrBuf.join('').slice(-500);
|
|
207
|
+
throw new Error(`mlx_lm.server failed to start: ${stderr || err.message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
releaseServer(modelId) {
|
|
212
|
+
const server = this.servers.get(modelId);
|
|
213
|
+
if (!server) return;
|
|
214
|
+
|
|
215
|
+
server.users = Math.max(0, server.users - 1);
|
|
216
|
+
server.lastUsed = Date.now();
|
|
217
|
+
|
|
218
|
+
if (server.users === 0) {
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
const s = this.servers.get(modelId);
|
|
221
|
+
if (s && s.users === 0 && Date.now() - s.lastUsed >= IDLE_TIMEOUT) {
|
|
222
|
+
this.stopServer(modelId);
|
|
223
|
+
}
|
|
224
|
+
}, IDLE_TIMEOUT + 1000);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async stopServer(modelId) {
|
|
229
|
+
const server = this.servers.get(modelId);
|
|
230
|
+
if (!server) return false;
|
|
231
|
+
|
|
232
|
+
return new Promise((resolve) => {
|
|
233
|
+
const timeout = setTimeout(() => {
|
|
234
|
+
try { server.proc.kill('SIGKILL'); } catch {}
|
|
235
|
+
}, 5000);
|
|
236
|
+
|
|
237
|
+
server.proc.on('exit', () => {
|
|
238
|
+
clearTimeout(timeout);
|
|
239
|
+
this.servers.delete(modelId);
|
|
240
|
+
resolve(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
server.proc.kill('SIGTERM');
|
|
245
|
+
} catch {
|
|
246
|
+
clearTimeout(timeout);
|
|
247
|
+
this.servers.delete(modelId);
|
|
248
|
+
resolve(true);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async stopAll() {
|
|
254
|
+
const ids = Array.from(this.servers.keys());
|
|
255
|
+
await Promise.all(ids.map((id) => this.stopServer(id)));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- Health Check ---
|
|
259
|
+
|
|
260
|
+
async _waitForHealth(port) {
|
|
261
|
+
const start = Date.now();
|
|
262
|
+
while (Date.now() - start < HEALTH_TIMEOUT) {
|
|
263
|
+
try {
|
|
264
|
+
const res = await fetch(`http://127.0.0.1:${port}/v1/models`, {
|
|
265
|
+
signal: AbortSignal.timeout(2000),
|
|
266
|
+
});
|
|
267
|
+
if (res.ok) return true;
|
|
268
|
+
} catch { /* server still loading */ }
|
|
269
|
+
await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL));
|
|
270
|
+
}
|
|
271
|
+
throw new Error(`mlx_lm.server health check timed out after ${HEALTH_TIMEOUT / 1000}s`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --- Port Management ---
|
|
275
|
+
|
|
276
|
+
_allocatePort() {
|
|
277
|
+
const usedPorts = new Set(Array.from(this.servers.values()).map((s) => s.port));
|
|
278
|
+
let port = BASE_PORT;
|
|
279
|
+
while (usedPorts.has(port) && port < BASE_PORT + 100) {
|
|
280
|
+
port++;
|
|
281
|
+
}
|
|
282
|
+
return port;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async _evictLRU() {
|
|
286
|
+
let lru = null;
|
|
287
|
+
for (const [id, server] of this.servers) {
|
|
288
|
+
if (!lru || server.users < lru.users ||
|
|
289
|
+
(server.users === lru.users && server.lastUsed < lru.lastUsed)) {
|
|
290
|
+
lru = { id, ...server };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (lru) {
|
|
294
|
+
await this.stopServer(lru.id);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// --- Status ---
|
|
299
|
+
|
|
300
|
+
getRunningServers() {
|
|
301
|
+
return Array.from(this.servers.entries()).map(([modelId, s]) => ({
|
|
302
|
+
modelId,
|
|
303
|
+
port: s.port,
|
|
304
|
+
users: s.users,
|
|
305
|
+
ready: s.ready,
|
|
306
|
+
uptime: Date.now() - s.startedAt,
|
|
307
|
+
lastUsed: s.lastUsed,
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
getStatus() {
|
|
312
|
+
return {
|
|
313
|
+
installed: MLXServerManager.isInstalled(),
|
|
314
|
+
version: MLXServerManager.getVersion(),
|
|
315
|
+
running: this.servers.size,
|
|
316
|
+
maxServers: MAX_SERVERS,
|
|
317
|
+
servers: this.getRunningServers(),
|
|
318
|
+
cachedModels: MLXServerManager.scanModels().length,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Format Detection ---
|
|
324
|
+
|
|
325
|
+
function isMLXModel(modelName, hasNpz, configData) {
|
|
326
|
+
if (modelName.startsWith('mlx-community/')) return true;
|
|
327
|
+
if (hasNpz) return true;
|
|
328
|
+
if (/[-_]mlx[-_]/i.test(modelName) || modelName.toLowerCase().endsWith('-mlx')) return true;
|
|
329
|
+
if (configData?.quantization_config?.quant_method === 'mlx') return true;
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// --- Parsing Utilities ---
|
|
334
|
+
|
|
335
|
+
function parseMLXParams(name, config) {
|
|
336
|
+
// Try config.json first
|
|
337
|
+
if (config) {
|
|
338
|
+
const hidden = config.hidden_size;
|
|
339
|
+
const layers = config.num_hidden_layers;
|
|
340
|
+
const vocab = config.vocab_size;
|
|
341
|
+
if (hidden && layers) {
|
|
342
|
+
const approx = (hidden * layers * vocab * 4) / 1e9;
|
|
343
|
+
if (approx > 0.1) {
|
|
344
|
+
if (approx < 1.5) return '0.5-1B';
|
|
345
|
+
if (approx < 5) return `${Math.round(approx)}B`;
|
|
346
|
+
if (approx < 10) return `${Math.round(approx)}B`;
|
|
347
|
+
return `${Math.round(approx)}B`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Fallback: parse from name
|
|
353
|
+
const match = name.match(/(\d+\.?\d*)[bB]/);
|
|
354
|
+
if (match) return `${match[1]}B`;
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function parseMLXQuantization(name) {
|
|
359
|
+
const lower = name.toLowerCase();
|
|
360
|
+
if (lower.includes('8bit') || lower.includes('8-bit')) return 'W8';
|
|
361
|
+
if (lower.includes('4bit') || lower.includes('4-bit')) return 'W4';
|
|
362
|
+
if (lower.includes('3bit') || lower.includes('3-bit')) return 'W3';
|
|
363
|
+
if (lower.includes('bf16') || lower.includes('fp16')) return 'FP16';
|
|
364
|
+
return null;
|
|
365
|
+
}
|