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
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { existsSync, readFileSync, statSync, writeFileSync, unlinkSync } from 'fs';
|
|
4
|
+
import { spawn, execFileSync } from 'child_process';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { isAbsolute } from 'path';
|
|
7
|
+
import { listProviders, getProvider, clearInstallCache, getProviderMetadata, getProviderPath, setProviderPaths } from '../providers/index.js';
|
|
8
|
+
import { OllamaProvider } from '../providers/ollama.js';
|
|
9
|
+
import { ClaudeCodeProvider } from '../providers/claude-code.js';
|
|
10
|
+
|
|
11
|
+
export function registerProviderRoutes(app, daemon) {
|
|
12
|
+
|
|
13
|
+
// List available providers
|
|
14
|
+
app.get('/api/providers', (req, res) => {
|
|
15
|
+
const providers = listProviders();
|
|
16
|
+
for (const p of providers) {
|
|
17
|
+
p.hasKey = daemon.credentials.hasKey(p.id);
|
|
18
|
+
if (p.id === 'claude-code') {
|
|
19
|
+
p.authStatus = ClaudeCodeProvider.getAuthStatus();
|
|
20
|
+
}
|
|
21
|
+
const meta = getProviderMetadata(p.id);
|
|
22
|
+
if (meta) {
|
|
23
|
+
p.setupGuide = meta.setupGuide;
|
|
24
|
+
p.authMethods = meta.authMethods;
|
|
25
|
+
}
|
|
26
|
+
const customPath = getProviderPath(p.id);
|
|
27
|
+
if (customPath) p.providerPath = customPath;
|
|
28
|
+
|
|
29
|
+
// Enrich local provider with GGUF models + lab runtime status
|
|
30
|
+
if (p.id === 'local' && daemon.modelManager && daemon.modelLab) {
|
|
31
|
+
const ollamaModels = p.models || [];
|
|
32
|
+
const ollamaIds = new Set(ollamaModels.map(m => m.id));
|
|
33
|
+
const runtimes = daemon.modelLab.listRuntimes();
|
|
34
|
+
const ggufModels = daemon.modelManager.getInstalled()
|
|
35
|
+
.filter(m => m.exists)
|
|
36
|
+
.map(m => {
|
|
37
|
+
const rt = runtimes.find(r =>
|
|
38
|
+
r._localModelId === m.id ||
|
|
39
|
+
r.models?.some(rm => rm.id === m.filename || rm.name === m.filename)
|
|
40
|
+
);
|
|
41
|
+
return {
|
|
42
|
+
id: `gguf:${m.id}`,
|
|
43
|
+
name: m.filename.replace(/\.gguf$/i, ''),
|
|
44
|
+
tier: m.tier || 'medium',
|
|
45
|
+
category: m.category || 'general',
|
|
46
|
+
source: 'gguf',
|
|
47
|
+
sizeBytes: m.sizeBytes || null,
|
|
48
|
+
quantization: m.quantization || null,
|
|
49
|
+
parameters: m.parameters || null,
|
|
50
|
+
runtimeId: rt?.id || null,
|
|
51
|
+
runtimeEndpoint: rt?.endpoint || null,
|
|
52
|
+
runtimeType: rt?.type || null,
|
|
53
|
+
hasRuntime: !!rt,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
// Also surface models from lab runtimes not backed by a local GGUF
|
|
57
|
+
const runtimeModels = [];
|
|
58
|
+
for (const rt of runtimes) {
|
|
59
|
+
if (rt.type === 'ollama') continue;
|
|
60
|
+
for (const rm of (rt.models || [])) {
|
|
61
|
+
const alreadyGguf = ggufModels.some(g => g.runtimeId === rt.id);
|
|
62
|
+
const alreadyOllama = ollamaIds.has(rm.id) || ollamaIds.has(rm.name);
|
|
63
|
+
if (!alreadyGguf && !alreadyOllama) {
|
|
64
|
+
runtimeModels.push({
|
|
65
|
+
id: `runtime:${rt.id}:${rm.id}`,
|
|
66
|
+
name: rm.name || rm.id,
|
|
67
|
+
tier: 'medium',
|
|
68
|
+
category: 'general',
|
|
69
|
+
source: 'runtime',
|
|
70
|
+
runtimeId: rt.id,
|
|
71
|
+
runtimeEndpoint: rt.endpoint,
|
|
72
|
+
runtimeType: rt.type,
|
|
73
|
+
hasRuntime: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
p.models = [...ollamaModels.map(m => ({ ...m, source: 'ollama', hasRuntime: true })), ...ggufModels, ...runtimeModels];
|
|
79
|
+
p.installed = p.installed || ggufModels.length > 0 || runtimeModels.length > 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
res.json(providers);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// --- Claude Code Auth ---
|
|
86
|
+
|
|
87
|
+
app.get('/api/providers/claude-code/auth', (req, res) => {
|
|
88
|
+
res.json(ClaudeCodeProvider.getAuthStatus());
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
app.post('/api/providers/claude-code/login', (req, res) => {
|
|
92
|
+
ClaudeCodeProvider.triggerLogin();
|
|
93
|
+
daemon.audit.log('claude-code.login.started', {});
|
|
94
|
+
res.json({ ok: true });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// --- Ollama ---
|
|
98
|
+
|
|
99
|
+
const isValidModelId = (id) => typeof id === 'string' && id.length > 0 && id.length < 200 && /^[a-zA-Z0-9._:/-]+$/.test(id);
|
|
100
|
+
|
|
101
|
+
app.get('/api/providers/ollama/hardware', (req, res) => {
|
|
102
|
+
res.json(OllamaProvider.getSystemHardware());
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
app.get('/api/providers/ollama/models', (req, res) => {
|
|
106
|
+
const installed = OllamaProvider.isInstalled() ? OllamaProvider.getInstalledModels() : [];
|
|
107
|
+
const catalog = OllamaProvider.catalog;
|
|
108
|
+
const hardware = OllamaProvider.getSystemHardware();
|
|
109
|
+
res.json({ installed, catalog, hardware });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
app.post('/api/providers/ollama/pull', async (req, res) => {
|
|
113
|
+
const { model } = req.body;
|
|
114
|
+
if (!model) return res.status(400).json({ error: 'model is required' });
|
|
115
|
+
if (!isValidModelId(model)) return res.status(400).json({ error: 'Invalid model ID' });
|
|
116
|
+
if (!OllamaProvider.isInstalled()) {
|
|
117
|
+
const install = OllamaProvider.installCommand();
|
|
118
|
+
return res.status(400).json({ error: `Ollama is not installed. Install with: ${install.command}` });
|
|
119
|
+
}
|
|
120
|
+
const broadcast = daemon.broadcast.bind(daemon);
|
|
121
|
+
try {
|
|
122
|
+
// Auto-start Ollama server if not running
|
|
123
|
+
const running = await OllamaProvider.isServerRunning();
|
|
124
|
+
if (!running) {
|
|
125
|
+
broadcast({ type: 'ollama:serve:starting' });
|
|
126
|
+
OllamaProvider.startServer();
|
|
127
|
+
// Wait for server to be ready (up to 10s)
|
|
128
|
+
for (let i = 0; i < 20; i++) {
|
|
129
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
130
|
+
if (await OllamaProvider.isServerRunning()) break;
|
|
131
|
+
}
|
|
132
|
+
if (!(await OllamaProvider.isServerRunning())) {
|
|
133
|
+
return res.status(500).json({ error: 'Could not start Ollama server. Run `ollama serve` manually.' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
broadcast({ type: 'ollama:pull:start', model });
|
|
137
|
+
await OllamaProvider.pullModel(model, (progress) => {
|
|
138
|
+
broadcast({ type: 'ollama:pull:progress', model, progress: progress.trim() });
|
|
139
|
+
});
|
|
140
|
+
broadcast({ type: 'ollama:pull:complete', model });
|
|
141
|
+
daemon.audit.log('ollama.pull', { model });
|
|
142
|
+
res.json({ ok: true, model });
|
|
143
|
+
} catch (err) {
|
|
144
|
+
broadcast({ type: 'ollama:pull:error', model, error: err.message });
|
|
145
|
+
res.status(500).json({ error: `Pull failed: ${err.message}` });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.delete('/api/providers/ollama/models/:model', (req, res) => {
|
|
150
|
+
if (!isValidModelId(req.params.model)) return res.status(400).json({ error: 'Invalid model ID' });
|
|
151
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
152
|
+
const success = OllamaProvider.deleteModel(req.params.model);
|
|
153
|
+
if (success) {
|
|
154
|
+
daemon.audit.log('ollama.delete', { model: req.params.model });
|
|
155
|
+
res.json({ ok: true });
|
|
156
|
+
} else {
|
|
157
|
+
res.status(500).json({ error: 'Failed to delete model' });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
app.post('/api/providers/ollama/check', async (req, res) => {
|
|
162
|
+
const installed = OllamaProvider.isInstalled();
|
|
163
|
+
const serverRunning = installed ? await OllamaProvider.isServerRunning() : false;
|
|
164
|
+
const install = OllamaProvider.installCommand();
|
|
165
|
+
const hardware = OllamaProvider.getSystemHardware();
|
|
166
|
+
const requirements = OllamaProvider.hardwareRequirements();
|
|
167
|
+
res.json({ installed, serverRunning, install, hardware, requirements });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
app.post('/api/providers/ollama/serve', async (req, res) => {
|
|
171
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
172
|
+
const already = await OllamaProvider.isServerRunning();
|
|
173
|
+
if (already) return res.json({ ok: true, alreadyRunning: true });
|
|
174
|
+
const result = OllamaProvider.startServer();
|
|
175
|
+
if (result.started) {
|
|
176
|
+
// Wait a moment for server to come up
|
|
177
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
178
|
+
const running = await OllamaProvider.isServerRunning();
|
|
179
|
+
res.json({ ok: running, method: result.method });
|
|
180
|
+
} else {
|
|
181
|
+
res.status(500).json({ error: 'Could not start server', command: result.command });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
app.post('/api/providers/ollama/stop', async (req, res) => {
|
|
186
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
187
|
+
const running = await OllamaProvider.isServerRunning();
|
|
188
|
+
if (!running) return res.json({ ok: true, alreadyStopped: true });
|
|
189
|
+
const result = OllamaProvider.stopServer();
|
|
190
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
191
|
+
const stillRunning = await OllamaProvider.isServerRunning();
|
|
192
|
+
res.json({ ok: !stillRunning, method: result.method });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
app.post('/api/providers/ollama/restart', async (req, res) => {
|
|
196
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
197
|
+
// Stop
|
|
198
|
+
const running = await OllamaProvider.isServerRunning();
|
|
199
|
+
if (running) {
|
|
200
|
+
OllamaProvider.stopServer();
|
|
201
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
202
|
+
}
|
|
203
|
+
// Start
|
|
204
|
+
const result = OllamaProvider.startServer();
|
|
205
|
+
if (result.started) {
|
|
206
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
207
|
+
const nowRunning = await OllamaProvider.isServerRunning();
|
|
208
|
+
res.json({ ok: nowRunning, method: result.method });
|
|
209
|
+
} else {
|
|
210
|
+
res.status(500).json({ error: 'Could not restart server' });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
app.get('/api/providers/ollama/running', async (req, res) => {
|
|
215
|
+
if (!OllamaProvider.isInstalled()) return res.json({ models: [] });
|
|
216
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
217
|
+
if (!serverRunning) return res.json({ models: [] });
|
|
218
|
+
try {
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
221
|
+
const apiRes = await fetch('http://localhost:11434/api/ps', { signal: controller.signal });
|
|
222
|
+
clearTimeout(timeout);
|
|
223
|
+
if (!apiRes.ok) return res.json({ models: [] });
|
|
224
|
+
const data = await apiRes.json();
|
|
225
|
+
const models = (data.models || []).map((m) => ({
|
|
226
|
+
name: m.name || m.model || '',
|
|
227
|
+
size: m.size || 0,
|
|
228
|
+
vram: m.size_vram ?? m.size ?? 0,
|
|
229
|
+
expires: m.expires_at || null,
|
|
230
|
+
}));
|
|
231
|
+
res.json({ models });
|
|
232
|
+
} catch {
|
|
233
|
+
res.json({ models: OllamaProvider.getRunningModels() });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
app.post('/api/providers/ollama/load', async (req, res) => {
|
|
238
|
+
const { model } = req.body;
|
|
239
|
+
if (!model) return res.status(400).json({ error: 'model is required' });
|
|
240
|
+
if (!isValidModelId(model)) return res.status(400).json({ error: 'Invalid model ID' });
|
|
241
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
242
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
243
|
+
if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
|
|
244
|
+
try {
|
|
245
|
+
await OllamaProvider.loadModel(model);
|
|
246
|
+
daemon.broadcast({ type: 'ollama:model:loaded', model });
|
|
247
|
+
daemon.audit.log('ollama.model.load', { model });
|
|
248
|
+
res.json({ ok: true, model });
|
|
249
|
+
} catch (err) {
|
|
250
|
+
daemon.broadcast({ type: 'model:error', model, error: err.message });
|
|
251
|
+
res.status(500).json({ error: `Failed to load model: ${err.message}` });
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
app.post('/api/providers/ollama/unload', async (req, res) => {
|
|
256
|
+
const { model } = req.body;
|
|
257
|
+
if (!model) return res.status(400).json({ error: 'model is required' });
|
|
258
|
+
if (!isValidModelId(model)) return res.status(400).json({ error: 'Invalid model ID' });
|
|
259
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
260
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
261
|
+
if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
|
|
262
|
+
try {
|
|
263
|
+
await OllamaProvider.unloadModel(model);
|
|
264
|
+
daemon.broadcast({ type: 'ollama:model:unloaded', model });
|
|
265
|
+
daemon.audit.log('ollama.model.unload', { model });
|
|
266
|
+
res.json({ ok: true });
|
|
267
|
+
} catch (err) {
|
|
268
|
+
daemon.broadcast({ type: 'model:error', model, error: err.message });
|
|
269
|
+
res.status(500).json({ error: `Failed to unload model: ${err.message}` });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// --- Provider Management (install, login, set-path, verify) ---
|
|
274
|
+
|
|
275
|
+
const MANAGEABLE_PROVIDERS = new Set(['claude-code', 'codex', 'gemini']);
|
|
276
|
+
|
|
277
|
+
app.post('/api/providers/:id/install', (req, res) => {
|
|
278
|
+
const { id } = req.params;
|
|
279
|
+
if (!MANAGEABLE_PROVIDERS.has(id)) {
|
|
280
|
+
return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const INSTALL_PACKAGES = {
|
|
284
|
+
'claude-code': '@anthropic-ai/claude-code',
|
|
285
|
+
'codex': '@openai/codex',
|
|
286
|
+
'gemini': '@google/gemini-cli',
|
|
287
|
+
};
|
|
288
|
+
const pkg = INSTALL_PACKAGES[id];
|
|
289
|
+
|
|
290
|
+
res.setHeader('Content-Type', 'application/x-ndjson');
|
|
291
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
292
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
293
|
+
|
|
294
|
+
const write = (obj) => {
|
|
295
|
+
try { res.write(JSON.stringify(obj) + '\n'); } catch { /* client disconnected */ }
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
write({ status: 'installing', output: `Installing ${pkg}...`, progress: 0 });
|
|
299
|
+
|
|
300
|
+
const proc = spawn('bash', ['-lc', `npm install -g ${pkg}`], {
|
|
301
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
302
|
+
env: { ...process.env, NODE_ENV: undefined },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
let output = '';
|
|
306
|
+
let errOutput = '';
|
|
307
|
+
|
|
308
|
+
proc.stdout.on('data', (data) => {
|
|
309
|
+
output += data.toString();
|
|
310
|
+
write({ status: 'installing', output: data.toString().trim(), progress: 50 });
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
proc.stderr.on('data', (data) => {
|
|
314
|
+
errOutput += data.toString();
|
|
315
|
+
const line = data.toString().trim();
|
|
316
|
+
if (line) write({ status: 'installing', output: line, progress: 50 });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
proc.on('close', (code) => {
|
|
320
|
+
clearInstallCache();
|
|
321
|
+
const providerObj = getProvider(id);
|
|
322
|
+
const installed = providerObj ? providerObj.constructor.isInstalled() : false;
|
|
323
|
+
|
|
324
|
+
if (code === 0 && installed) {
|
|
325
|
+
write({ status: 'complete', output: `${pkg} installed successfully`, progress: 100, installed: true });
|
|
326
|
+
daemon.audit.log('provider.install', { provider: id, pkg, success: true });
|
|
327
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
328
|
+
} else {
|
|
329
|
+
const reason = code !== 0
|
|
330
|
+
? (errOutput || output).slice(-500)
|
|
331
|
+
: 'Install succeeded but provider binary not found in PATH';
|
|
332
|
+
write({ status: 'error', output: reason, progress: 100, installed: false });
|
|
333
|
+
daemon.audit.log('provider.install', { provider: id, pkg, success: false, code });
|
|
334
|
+
}
|
|
335
|
+
res.end();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
proc.on('error', (err) => {
|
|
339
|
+
write({ status: 'error', output: `Failed to start npm: ${err.message}`, progress: 100, installed: false });
|
|
340
|
+
res.end();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
req.on('close', () => {
|
|
344
|
+
try { proc.kill(); } catch { /* already exited */ }
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
app.post('/api/providers/:id/login', async (req, res) => {
|
|
349
|
+
const { id } = req.params;
|
|
350
|
+
if (!MANAGEABLE_PROVIDERS.has(id)) {
|
|
351
|
+
return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (id === 'gemini') {
|
|
355
|
+
return res.json({ status: 'not-supported', message: 'Gemini uses API key authentication. Set your key in Settings.' });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (id === 'claude-code') {
|
|
359
|
+
const providerObj = getProvider(id);
|
|
360
|
+
if (!providerObj || !providerObj.constructor.isInstalled()) {
|
|
361
|
+
return res.status(400).json({ error: 'Claude Code is not installed. Install it first.' });
|
|
362
|
+
}
|
|
363
|
+
daemon.audit.log('provider.login.started', { provider: id });
|
|
364
|
+
try {
|
|
365
|
+
const result = await ClaudeCodeProvider.startLogin();
|
|
366
|
+
clearInstallCache();
|
|
367
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
368
|
+
return res.json(result);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
return res.status(500).json({ status: 'error', error: err.message });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (id === 'codex') {
|
|
375
|
+
const providerObj = getProvider(id);
|
|
376
|
+
if (!providerObj || !providerObj.constructor.isInstalled()) {
|
|
377
|
+
return res.status(400).json({ error: 'Codex is not installed. Install it first.' });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const { method, key } = req.body || {};
|
|
381
|
+
|
|
382
|
+
if (key) {
|
|
383
|
+
daemon.audit.log('provider.login.started', { provider: id, method: 'api-key' });
|
|
384
|
+
try {
|
|
385
|
+
const result = await providerObj.constructor.onKeySet(key);
|
|
386
|
+
clearInstallCache();
|
|
387
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
388
|
+
return res.json({ status: result.ok ? 'authenticated' : 'error', ...result });
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return res.status(500).json({ status: 'error', error: err.message });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (method === 'chatgpt-plus') {
|
|
395
|
+
daemon.audit.log('provider.login.started', { provider: id, method: 'chatgpt-plus' });
|
|
396
|
+
return new Promise((resolve) => {
|
|
397
|
+
let responded = false;
|
|
398
|
+
const respond = (data, status) => {
|
|
399
|
+
if (responded) return;
|
|
400
|
+
responded = true;
|
|
401
|
+
clearInstallCache();
|
|
402
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
403
|
+
if (status) res.status(status).json(data);
|
|
404
|
+
else res.json(data);
|
|
405
|
+
resolve();
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const proc = spawn('codex', ['login'], {
|
|
409
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
410
|
+
shell: true,
|
|
411
|
+
});
|
|
412
|
+
proc.stdin.on('error', () => {});
|
|
413
|
+
let stdout = '';
|
|
414
|
+
let stderr = '';
|
|
415
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
416
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
417
|
+
|
|
418
|
+
const timeout = setTimeout(() => {
|
|
419
|
+
const urlMatch = (stdout + stderr).match(/https:\/\/\S+/);
|
|
420
|
+
respond(urlMatch
|
|
421
|
+
? { status: 'pending', url: urlMatch[0], browserOpened: true }
|
|
422
|
+
: { status: 'pending', message: 'Login started — check your browser', browserOpened: true });
|
|
423
|
+
}, 5000);
|
|
424
|
+
|
|
425
|
+
proc.on('close', (code) => {
|
|
426
|
+
clearTimeout(timeout);
|
|
427
|
+
if (code === 0) {
|
|
428
|
+
let hasKey = false;
|
|
429
|
+
try {
|
|
430
|
+
const authPath = resolve(homedir(), '.codex', 'auth.json');
|
|
431
|
+
if (existsSync(authPath)) {
|
|
432
|
+
const auth = JSON.parse(readFileSync(authPath, 'utf8'));
|
|
433
|
+
const token = auth.OPENAI_API_KEY
|
|
434
|
+
|| (auth.auth_mode === 'chatgpt' && auth.tokens?.id_token)
|
|
435
|
+
|| null;
|
|
436
|
+
if (token) {
|
|
437
|
+
daemon.credentials.setKey('codex', token);
|
|
438
|
+
hasKey = true;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} catch { /* auth.json missing or malformed — login still succeeded */ }
|
|
442
|
+
respond({ status: 'authenticated', hasKey });
|
|
443
|
+
} else {
|
|
444
|
+
respond({ status: 'error', error: stderr.slice(-200) || `Login failed (exit ${code})` });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
proc.on('error', (err) => {
|
|
449
|
+
clearTimeout(timeout);
|
|
450
|
+
respond({ status: 'error', error: err.message }, 500);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return res.status(400).json({ error: 'Provide either { key: "..." } or { method: "chatgpt-plus" }' });
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
app.post('/api/providers/:id/set-path', async (req, res) => {
|
|
460
|
+
const { id } = req.params;
|
|
461
|
+
if (!MANAGEABLE_PROVIDERS.has(id)) {
|
|
462
|
+
return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const { path: customPath } = req.body || {};
|
|
466
|
+
if (!customPath || typeof customPath !== 'string') {
|
|
467
|
+
return res.status(400).json({ error: 'path is required' });
|
|
468
|
+
}
|
|
469
|
+
if (customPath.length > 500) {
|
|
470
|
+
return res.status(400).json({ error: 'Path too long' });
|
|
471
|
+
}
|
|
472
|
+
if (!isAbsolute(customPath)) {
|
|
473
|
+
return res.status(400).json({ error: 'Path must be absolute' });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!existsSync(customPath)) {
|
|
477
|
+
return res.status(400).json({ error: `Path does not exist: ${customPath}` });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
const stat = statSync(customPath);
|
|
482
|
+
if (!stat.isFile()) {
|
|
483
|
+
return res.status(400).json({ error: 'Path must point to a file, not a directory' });
|
|
484
|
+
}
|
|
485
|
+
const mode = stat.mode;
|
|
486
|
+
const isExecutable = !!(mode & 0o111);
|
|
487
|
+
if (!isExecutable) {
|
|
488
|
+
return res.status(400).json({ error: 'File is not executable' });
|
|
489
|
+
}
|
|
490
|
+
} catch (err) {
|
|
491
|
+
return res.status(400).json({ error: `Cannot stat path: ${err.message}` });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!daemon.config.providerPaths) daemon.config.providerPaths = {};
|
|
495
|
+
daemon.config.providerPaths[id] = customPath;
|
|
496
|
+
|
|
497
|
+
const { saveConfig } = await import('../firstrun.js');
|
|
498
|
+
saveConfig(daemon.grooveDir, daemon.config);
|
|
499
|
+
|
|
500
|
+
setProviderPaths(daemon.config.providerPaths);
|
|
501
|
+
clearInstallCache();
|
|
502
|
+
|
|
503
|
+
daemon.audit.log('provider.setPath', { provider: id, path: customPath });
|
|
504
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
505
|
+
|
|
506
|
+
res.json({ ok: true, path: customPath });
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
app.post('/api/providers/:id/verify', async (req, res) => {
|
|
510
|
+
const { id } = req.params;
|
|
511
|
+
if (!MANAGEABLE_PROVIDERS.has(id)) {
|
|
512
|
+
return res.status(400).json({ error: `Invalid provider. Valid: ${[...MANAGEABLE_PROVIDERS].join(', ')}` });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
clearInstallCache();
|
|
516
|
+
const providerObj = getProvider(id);
|
|
517
|
+
if (!providerObj) {
|
|
518
|
+
return res.json({ installed: false, authenticated: false, version: null, error: 'Unknown provider' });
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const installed = providerObj.constructor.isInstalled();
|
|
522
|
+
let authenticated = false;
|
|
523
|
+
let version = null;
|
|
524
|
+
let error = null;
|
|
525
|
+
|
|
526
|
+
if (installed) {
|
|
527
|
+
const authStatus = providerObj.constructor.isAuthenticated?.();
|
|
528
|
+
authenticated = !!(authStatus?.authenticated);
|
|
529
|
+
|
|
530
|
+
const command = providerObj.constructor.command;
|
|
531
|
+
const customPath = getProviderPath(id);
|
|
532
|
+
const bin = customPath || command;
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
version = execFileSync(bin, ['--version'], {
|
|
536
|
+
encoding: 'utf8',
|
|
537
|
+
timeout: 5000,
|
|
538
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
539
|
+
shell: true,
|
|
540
|
+
}).trim();
|
|
541
|
+
} catch (err) {
|
|
542
|
+
version = null;
|
|
543
|
+
error = `Version check failed: ${err.message?.slice(0, 200) || 'unknown error'}`;
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
error = 'Provider not installed';
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
daemon.broadcast({ type: 'provider:status-changed', provider: id });
|
|
550
|
+
|
|
551
|
+
res.json({ installed, authenticated, version, error });
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// --- Local Models (GGUF via HuggingFace) ---
|
|
555
|
+
|
|
556
|
+
app.get('/api/models/installed', (req, res) => {
|
|
557
|
+
const installed = daemon.modelManager.getInstalled();
|
|
558
|
+
const llamaStatus = daemon.llamaServer.getStatus();
|
|
559
|
+
res.json({ models: installed, llamaServer: llamaStatus });
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
app.get('/api/models/search', async (req, res) => {
|
|
563
|
+
try {
|
|
564
|
+
const query = req.query.q || req.query.query || '';
|
|
565
|
+
if (!query) return res.status(400).json({ error: 'query parameter (q) is required' });
|
|
566
|
+
const results = await daemon.modelManager.search(query, {
|
|
567
|
+
limit: parseInt(req.query.limit) || 20,
|
|
568
|
+
});
|
|
569
|
+
res.json(results);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
res.status(500).json({ error: err.message });
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
app.get('/api/models/:repoId(*)/files', async (req, res) => {
|
|
576
|
+
try {
|
|
577
|
+
const files = await daemon.modelManager.getModelFiles(req.params.repoId);
|
|
578
|
+
res.json(files);
|
|
579
|
+
} catch (err) {
|
|
580
|
+
res.status(500).json({ error: err.message });
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
app.post('/api/models/download', async (req, res) => {
|
|
585
|
+
try {
|
|
586
|
+
const { repoId, filename } = req.body;
|
|
587
|
+
if (!repoId || !filename) return res.status(400).json({ error: 'repoId and filename are required' });
|
|
588
|
+
// Start download in background — progress via WebSocket
|
|
589
|
+
daemon.modelManager.download(repoId, filename).catch(() => {});
|
|
590
|
+
daemon.audit.log('model.download', { repoId, filename });
|
|
591
|
+
res.json({ started: true, filename, repoId });
|
|
592
|
+
} catch (err) {
|
|
593
|
+
res.status(400).json({ error: err.message });
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
app.post('/api/models/download/cancel', (req, res) => {
|
|
598
|
+
const { filename } = req.body;
|
|
599
|
+
if (!filename) return res.status(400).json({ error: 'filename is required' });
|
|
600
|
+
const cancelled = daemon.modelManager.cancelDownload(filename);
|
|
601
|
+
res.json({ cancelled });
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
app.get('/api/models/downloads', (req, res) => {
|
|
605
|
+
res.json(daemon.modelManager.getActiveDownloads());
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
app.delete('/api/models/:id', (req, res) => {
|
|
609
|
+
const deleted = daemon.modelManager.deleteModel(req.params.id);
|
|
610
|
+
if (deleted) {
|
|
611
|
+
daemon.audit.log('model.delete', { id: req.params.id });
|
|
612
|
+
res.json({ ok: true });
|
|
613
|
+
} else {
|
|
614
|
+
res.status(404).json({ error: 'Model not found' });
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
app.post('/api/models/:id/import-to-ollama', async (req, res) => {
|
|
619
|
+
const model = daemon.modelManager.getModel(req.params.id);
|
|
620
|
+
if (!model) return res.status(404).json({ error: 'Model not found' });
|
|
621
|
+
const ggufPath = daemon.modelManager.getModelPath(req.params.id);
|
|
622
|
+
if (!ggufPath) return res.status(404).json({ error: 'Model file not found on disk' });
|
|
623
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
624
|
+
|
|
625
|
+
const ollamaName = (model.id || model.filename.replace('.gguf', '')).toLowerCase().replace(/[^a-z0-9._-]/g, '-');
|
|
626
|
+
const modelfilePath = resolve(ggufPath + '.Modelfile');
|
|
627
|
+
try {
|
|
628
|
+
writeFileSync(modelfilePath, `FROM ${ggufPath}\n`);
|
|
629
|
+
const { execFileSync } = await import('child_process');
|
|
630
|
+
execFileSync('ollama', ['create', ollamaName, '-f', modelfilePath], { timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
631
|
+
try { unlinkSync(modelfilePath); } catch {}
|
|
632
|
+
daemon.audit.log('model.import-ollama', { id: model.id, ollamaName });
|
|
633
|
+
daemon.broadcast({ type: 'ollama:model:imported', model: ollamaName });
|
|
634
|
+
res.json({ ok: true, ollamaName });
|
|
635
|
+
} catch (err) {
|
|
636
|
+
try { unlinkSync(modelfilePath); } catch {}
|
|
637
|
+
res.status(500).json({ error: `Import failed: ${err.message}` });
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
app.get('/api/models/recommend', (req, res) => {
|
|
642
|
+
const ramGb = parseInt(req.query.ram) || 16;
|
|
643
|
+
const quant = daemon.modelManager.recommendQuantization('7B', ramGb);
|
|
644
|
+
res.json({ recommendedQuantization: quant, ramGb });
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
app.get('/api/models/recommended', (req, res) => {
|
|
648
|
+
const hardware = OllamaProvider.getSystemHardware();
|
|
649
|
+
const catalog = OllamaProvider.catalog;
|
|
650
|
+
// Filter to models that fit in RAM — same threshold as hardware recommendation
|
|
651
|
+
// Apple Silicon unified memory handles these well, no aggressive headroom needed
|
|
652
|
+
const recommended = catalog
|
|
653
|
+
.filter((m) => m.ramGb <= hardware.totalRamGb)
|
|
654
|
+
.sort((a, b) => b.ramGb - a.ramGb) // Biggest that fits = best quality
|
|
655
|
+
.slice(0, 12);
|
|
656
|
+
res.json({ models: recommended, hardware });
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// --- Ollama Running Models ---
|
|
660
|
+
|
|
661
|
+
app.get('/api/models/status', async (req, res) => {
|
|
662
|
+
const installed = OllamaProvider.isInstalled();
|
|
663
|
+
if (!installed) return res.json({ serverRunning: false, runningModels: [], installedModels: [], hardware: OllamaProvider.getSystemHardware() });
|
|
664
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
665
|
+
const runningModels = serverRunning ? OllamaProvider.getRunningModels() : [];
|
|
666
|
+
const installedModels = OllamaProvider.getInstalledModels();
|
|
667
|
+
const hardware = OllamaProvider.getSystemHardware();
|
|
668
|
+
res.json({ serverRunning, runningModels, installedModels, hardware });
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
app.get('/api/models/running', async (req, res) => {
|
|
672
|
+
if (!OllamaProvider.isInstalled()) return res.json([]);
|
|
673
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
674
|
+
if (!serverRunning) return res.json([]);
|
|
675
|
+
res.json(OllamaProvider.getRunningModels());
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
app.post('/api/models/:id/load', async (req, res) => {
|
|
679
|
+
const modelId = req.params.id;
|
|
680
|
+
if (!modelId) return res.status(400).json({ error: 'model id is required' });
|
|
681
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
682
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
683
|
+
if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
|
|
684
|
+
try {
|
|
685
|
+
const result = await OllamaProvider.loadModel(modelId);
|
|
686
|
+
daemon.broadcast({ type: 'model:loaded', model: modelId });
|
|
687
|
+
daemon.audit.log('model.load', { model: modelId });
|
|
688
|
+
res.json(result);
|
|
689
|
+
} catch (err) {
|
|
690
|
+
daemon.broadcast({ type: 'model:error', model: modelId, error: err.message });
|
|
691
|
+
res.status(500).json({ error: `Failed to load model: ${err.message}` });
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
app.post('/api/models/:id/unload', async (req, res) => {
|
|
696
|
+
const modelId = req.params.id;
|
|
697
|
+
if (!modelId) return res.status(400).json({ error: 'model id is required' });
|
|
698
|
+
if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
|
|
699
|
+
const serverRunning = await OllamaProvider.isServerRunning();
|
|
700
|
+
if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
|
|
701
|
+
try {
|
|
702
|
+
const result = await OllamaProvider.unloadModel(modelId);
|
|
703
|
+
daemon.broadcast({ type: 'model:unloaded', model: modelId });
|
|
704
|
+
daemon.audit.log('model.unload', { model: modelId });
|
|
705
|
+
res.json(result);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
daemon.broadcast({ type: 'model:error', model: modelId, error: err.message });
|
|
708
|
+
res.status(500).json({ error: `Failed to unload model: ${err.message}` });
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
app.get('/api/llama/status', (req, res) => {
|
|
713
|
+
res.json(daemon.llamaServer.getStatus());
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
app.get('/api/mlx/status', (req, res) => {
|
|
717
|
+
res.json(daemon.mlxServer.getStatus());
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
app.get('/api/lab/tools', (req, res) => {
|
|
721
|
+
res.json(daemon.modelLab.getInstalledTools());
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
app.post('/api/lab/tools/refresh', (req, res) => {
|
|
725
|
+
res.json(daemon.modelLab.refreshInstalledTools());
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// --- Credentials ---
|
|
729
|
+
|
|
730
|
+
app.get('/api/credentials', (req, res) => {
|
|
731
|
+
res.json(daemon.credentials.listProviders());
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
app.post('/api/credentials/:provider', async (req, res) => {
|
|
735
|
+
if (!req.body.key) return res.status(400).json({ error: 'key is required' });
|
|
736
|
+
daemon.credentials.setKey(req.params.provider, req.body.key);
|
|
737
|
+
daemon.audit.log('credential.set', { provider: req.params.provider });
|
|
738
|
+
|
|
739
|
+
// Provider-specific auth setup (e.g., Codex auto-login)
|
|
740
|
+
const provider = getProvider(req.params.provider);
|
|
741
|
+
let authResult = null;
|
|
742
|
+
if (provider?.constructor?.onKeySet) {
|
|
743
|
+
try { authResult = await provider.constructor.onKeySet(req.body.key); } catch { /* best effort */ }
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
res.json({ ok: true, masked: daemon.credentials.mask(req.body.key), auth: authResult });
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
app.delete('/api/credentials/:provider', (req, res) => {
|
|
750
|
+
daemon.credentials.deleteKey(req.params.provider);
|
|
751
|
+
daemon.audit.log('credential.delete', { provider: req.params.provider });
|
|
752
|
+
res.json({ ok: true });
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
}
|