groove-dev 0.27.152 → 0.27.154
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/journalist.js +52 -21
- package/node_modules/@groove-dev/daemon/src/keeper.js +37 -2
- package/node_modules/@groove-dev/daemon/src/llama-server.js +96 -3
- package/node_modules/@groove-dev/daemon/src/model-manager.js +52 -10
- package/node_modules/@groove-dev/daemon/src/routes/coordination.js +16 -0
- package/node_modules/@groove-dev/daemon/src/routes/files.js +71 -2
- package/node_modules/@groove-dev/daemon/src/routes/providers.js +11 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BTLb6zTD.js +1015 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Diw6wDPU.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/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +252 -44
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +51 -3
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +9 -2
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +40 -3
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +23 -8
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +8 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +9 -1
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +24 -5
- package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +13 -0
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +87 -44
- package/node_modules/@groove-dev/gui/src/views/models.jsx +15 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/journalist.js +52 -21
- package/packages/daemon/src/keeper.js +37 -2
- package/packages/daemon/src/llama-server.js +96 -3
- package/packages/daemon/src/model-manager.js +52 -10
- package/packages/daemon/src/routes/coordination.js +16 -0
- package/packages/daemon/src/routes/files.js +71 -2
- package/packages/daemon/src/routes/providers.js +11 -0
- package/packages/gui/dist/assets/index-BTLb6zTD.js +1015 -0
- package/packages/gui/dist/assets/index-Diw6wDPU.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +252 -44
- package/packages/gui/src/components/agents/agent-file-tree.jsx +51 -3
- package/packages/gui/src/components/agents/spawn-wizard.jsx +9 -2
- package/packages/gui/src/components/editor/file-tree.jsx +40 -3
- package/packages/gui/src/components/lab/runtime-config.jsx +23 -8
- package/packages/gui/src/components/settings/quick-connect.jsx +8 -1
- package/packages/gui/src/stores/groove.js +9 -1
- package/packages/gui/src/stores/slices/agents-slice.js +24 -5
- package/packages/gui/src/stores/slices/providers-slice.js +13 -0
- package/packages/gui/src/views/memory.jsx +87 -44
- package/packages/gui/src/views/models.jsx +15 -2
- package/node_modules/@groove-dev/gui/dist/assets/index-CEkPsSAm.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-CReKPWhY.js +0 -1011
- package/packages/gui/dist/assets/index-CEkPsSAm.css +0 -1
- package/packages/gui/dist/assets/index-CReKPWhY.js +0 -1011
|
@@ -644,20 +644,14 @@ export class Journalist {
|
|
|
644
644
|
proc.stdin.write(stdinData);
|
|
645
645
|
proc.stdin.end();
|
|
646
646
|
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
647
|
-
const timer = setTimeout(() => { proc.kill(); reject(new Error('Headless timeout')); },
|
|
647
|
+
const timer = setTimeout(() => { proc.kill(); reject(new Error('Headless timeout')); }, 120_000);
|
|
648
648
|
proc.on('exit', (code) => {
|
|
649
649
|
clearTimeout(timer);
|
|
650
650
|
if (code !== 0) return reject(new Error(`Headless exited with code ${code}`));
|
|
651
651
|
this._recordHeadlessUsage(stdout, trackAs, modelId);
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const json = JSON.parse(line);
|
|
656
|
-
if (json.result) return resolve(json.result);
|
|
657
|
-
if (json.content?.[0]?.text) return resolve(json.content[0].text);
|
|
658
|
-
} catch { /* not json */ }
|
|
659
|
-
}
|
|
660
|
-
resolve(stdout.trim());
|
|
652
|
+
const extracted = this._parseHeadlessOutput(stdout);
|
|
653
|
+
if (extracted) return resolve(extracted);
|
|
654
|
+
reject(new Error('Headless produced no usable output'));
|
|
661
655
|
});
|
|
662
656
|
return;
|
|
663
657
|
}
|
|
@@ -666,24 +660,61 @@ export class Journalist {
|
|
|
666
660
|
env: { ...process.env, ...env },
|
|
667
661
|
cwd: this.daemon.projectDir,
|
|
668
662
|
maxBuffer: 1024 * 1024 * 5,
|
|
669
|
-
timeout:
|
|
663
|
+
timeout: 120_000,
|
|
670
664
|
}, (err, stdout, stderr) => {
|
|
671
665
|
if (err) return reject(err);
|
|
672
666
|
this._recordHeadlessUsage(stdout, trackAs, modelId);
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const data = JSON.parse(line);
|
|
677
|
-
if (data.type === 'result' && data.result) {
|
|
678
|
-
return resolve(data.result);
|
|
679
|
-
}
|
|
680
|
-
} catch { /* skip */ }
|
|
681
|
-
}
|
|
682
|
-
resolve(stdout);
|
|
667
|
+
const extracted = this._parseHeadlessOutput(stdout);
|
|
668
|
+
if (extracted) return resolve(extracted);
|
|
669
|
+
reject(new Error('Headless produced no usable output'));
|
|
683
670
|
});
|
|
684
671
|
});
|
|
685
672
|
}
|
|
686
673
|
|
|
674
|
+
_parseHeadlessOutput(stdout) {
|
|
675
|
+
const lines = stdout.split('\n');
|
|
676
|
+
let resultText = '';
|
|
677
|
+
let assistantText = '';
|
|
678
|
+
let codexText = '';
|
|
679
|
+
let networkText = '';
|
|
680
|
+
|
|
681
|
+
for (const line of lines) {
|
|
682
|
+
try {
|
|
683
|
+
const json = JSON.parse(line);
|
|
684
|
+
|
|
685
|
+
// Claude/Gemini stream-json: {"type":"result","result":"..."}
|
|
686
|
+
if (typeof json.result === 'string' && json.result.trim()) {
|
|
687
|
+
resultText = json.result;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Claude/Gemini assistant message: {"type":"assistant","message":{"content":[{"type":"text","text":"..."}]}}
|
|
691
|
+
const msgContent = json.message?.content?.[0]?.text || json.content?.[0]?.text;
|
|
692
|
+
if (msgContent && msgContent.trim()) {
|
|
693
|
+
assistantText = msgContent;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Codex --json: {"type":"item.completed","item":{"type":"agent_message","text":"..."}}
|
|
697
|
+
if (json.type === 'item.completed' && json.item?.type === 'agent_message' && json.item.text?.trim()) {
|
|
698
|
+
codexText = json.item.text;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Groove Network: {"type":"done|complete|result","text":"..."}
|
|
702
|
+
if ((json.type === 'done' || json.type === 'complete') && typeof json.text === 'string' && json.text.trim()) {
|
|
703
|
+
networkText = json.text;
|
|
704
|
+
}
|
|
705
|
+
} catch { /* not json */ }
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (resultText) return resultText;
|
|
709
|
+
if (codexText) return codexText;
|
|
710
|
+
if (networkText) return networkText;
|
|
711
|
+
if (assistantText) return assistantText;
|
|
712
|
+
// Ollama / plain-text providers: raw text output (no JSON)
|
|
713
|
+
const plain = stdout.trim();
|
|
714
|
+
if (plain && !plain.startsWith('{') && plain.length > 20) return plain;
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
|
|
687
718
|
parseSynthesisResult(text, agents) {
|
|
688
719
|
// Parse the structured output from AI
|
|
689
720
|
const sections = {
|
|
@@ -58,7 +58,7 @@ export class Keeper {
|
|
|
58
58
|
|
|
59
59
|
save(tag, content) {
|
|
60
60
|
if (!tag || typeof tag !== 'string') throw new Error('Tag is required');
|
|
61
|
-
if (content === undefined || content === null) throw new Error('Content is required');
|
|
61
|
+
if (content === undefined || content === null || !String(content).trim()) throw new Error('Content is required');
|
|
62
62
|
const normalized = this._normalize(tag);
|
|
63
63
|
if (!normalized) throw new Error('Tag is required');
|
|
64
64
|
const filePath = this._tagToPath(normalized);
|
|
@@ -129,7 +129,7 @@ export class Keeper {
|
|
|
129
129
|
update(tag, content) {
|
|
130
130
|
const normalized = this._normalize(tag);
|
|
131
131
|
if (!normalized) throw new Error('Tag is required');
|
|
132
|
-
if (content === undefined || content === null) throw new Error('Content is required');
|
|
132
|
+
if (content === undefined || content === null || !String(content).trim()) throw new Error('Content is required');
|
|
133
133
|
const filePath = this._tagToPath(normalized);
|
|
134
134
|
if (!existsSync(filePath)) throw new Error(`Memory #${normalized} does not exist`);
|
|
135
135
|
writeFileSync(filePath, String(content));
|
|
@@ -153,6 +153,41 @@ export class Keeper {
|
|
|
153
153
|
return true;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
move(oldTag, newTag) {
|
|
157
|
+
const oldNorm = this._normalize(oldTag);
|
|
158
|
+
const newNorm = this._normalize(newTag);
|
|
159
|
+
if (!oldNorm || !newNorm) throw new Error('Both old and new tags are required');
|
|
160
|
+
if (oldNorm === newNorm) return this._index[oldNorm];
|
|
161
|
+
const oldPath = this._tagToPath(oldNorm);
|
|
162
|
+
if (!existsSync(oldPath)) throw new Error(`Memory #${oldNorm} does not exist`);
|
|
163
|
+
if (this._index[newNorm]) throw new Error(`Memory #${newNorm} already exists`);
|
|
164
|
+
const content = readFileSync(oldPath, 'utf8');
|
|
165
|
+
const newPath = this._tagToPath(newNorm);
|
|
166
|
+
this._ensureParentDir(newPath);
|
|
167
|
+
writeFileSync(newPath, content);
|
|
168
|
+
unlinkSync(oldPath);
|
|
169
|
+
this._index[newNorm] = { ...this._index[oldNorm], tag: newNorm, updatedAt: new Date().toISOString() };
|
|
170
|
+
delete this._index[oldNorm];
|
|
171
|
+
// Move children too (e.g. moving "a" to "b/a" also moves "a/child" to "b/a/child")
|
|
172
|
+
const prefix = oldNorm + '/';
|
|
173
|
+
for (const tag of Object.keys(this._index)) {
|
|
174
|
+
if (tag.startsWith(prefix)) {
|
|
175
|
+
const childSuffix = tag.slice(prefix.length);
|
|
176
|
+
const childNewTag = newNorm + '/' + childSuffix;
|
|
177
|
+
const childOldPath = this._tagToPath(tag);
|
|
178
|
+
const childNewPath = this._tagToPath(childNewTag);
|
|
179
|
+
const childContent = readFileSync(childOldPath, 'utf8');
|
|
180
|
+
this._ensureParentDir(childNewPath);
|
|
181
|
+
writeFileSync(childNewPath, childContent);
|
|
182
|
+
unlinkSync(childOldPath);
|
|
183
|
+
this._index[childNewTag] = { ...this._index[tag], tag: childNewTag, updatedAt: new Date().toISOString() };
|
|
184
|
+
delete this._index[tag];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
this._saveIndex();
|
|
188
|
+
return { tag: newNorm, ...this._index[newNorm] };
|
|
189
|
+
}
|
|
190
|
+
|
|
156
191
|
// ── Doc (AI-generated) ───────────────────────────────────
|
|
157
192
|
|
|
158
193
|
saveDoc(tag, content) {
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
// Each model gets its own server on a unique port.
|
|
6
6
|
// Auto-starts when an agent needs a GGUF model, auto-stops when idle.
|
|
7
7
|
|
|
8
|
-
import { spawn, execSync } from 'child_process';
|
|
8
|
+
import { spawn, execSync, execFileSync } from 'child_process';
|
|
9
|
+
import { existsSync, mkdirSync, chmodSync } from 'fs';
|
|
10
|
+
import { resolve } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
9
12
|
|
|
10
13
|
const BASE_PORT = 8081;
|
|
11
14
|
const MAX_SERVERS = 5;
|
|
@@ -25,10 +28,98 @@ export class LlamaServerManager {
|
|
|
25
28
|
execSync('which llama-server', { stdio: 'ignore' });
|
|
26
29
|
return true;
|
|
27
30
|
} catch {
|
|
28
|
-
|
|
31
|
+
// Check common manual install locations
|
|
32
|
+
const paths = [
|
|
33
|
+
resolve(homedir(), '.local', 'bin', 'llama-server'),
|
|
34
|
+
resolve(homedir(), '.groove', 'bin', 'llama-server'),
|
|
35
|
+
'/usr/local/bin/llama-server',
|
|
36
|
+
];
|
|
37
|
+
return paths.some(p => existsSync(p));
|
|
29
38
|
}
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
static getLlamaServerPath() {
|
|
42
|
+
try {
|
|
43
|
+
return execSync('which llama-server', { stdio: 'pipe', encoding: 'utf8' }).trim();
|
|
44
|
+
} catch {
|
|
45
|
+
const paths = [
|
|
46
|
+
resolve(homedir(), '.local', 'bin', 'llama-server'),
|
|
47
|
+
resolve(homedir(), '.groove', 'bin', 'llama-server'),
|
|
48
|
+
'/usr/local/bin/llama-server',
|
|
49
|
+
];
|
|
50
|
+
return paths.find(p => existsSync(p)) || 'llama-server';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static async install() {
|
|
55
|
+
const platform = process.platform;
|
|
56
|
+
|
|
57
|
+
if (platform === 'darwin') {
|
|
58
|
+
try {
|
|
59
|
+
execSync('which brew', { stdio: 'ignore' });
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error('Homebrew not found. Install it from https://brew.sh then retry.');
|
|
62
|
+
}
|
|
63
|
+
execSync('brew install llama.cpp', { stdio: 'pipe', timeout: 600000 });
|
|
64
|
+
return { method: 'brew', path: execSync('which llama-server', { encoding: 'utf8', stdio: 'pipe' }).trim() };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (platform === 'linux') {
|
|
68
|
+
const installDir = resolve(homedir(), '.local', 'bin');
|
|
69
|
+
mkdirSync(installDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
|
|
72
|
+
const hasCuda = (() => { try { execSync('which nvidia-smi', { stdio: 'ignore' }); return true; } catch { return false; } })();
|
|
73
|
+
|
|
74
|
+
const resp = await fetch('https://api.github.com/repos/ggml-org/llama.cpp/releases/latest', {
|
|
75
|
+
headers: { 'User-Agent': 'groove-dev' },
|
|
76
|
+
});
|
|
77
|
+
if (!resp.ok) throw new Error(`GitHub API error: ${resp.status}`);
|
|
78
|
+
const release = await resp.json();
|
|
79
|
+
|
|
80
|
+
const suffix = hasCuda ? `ubuntu-${arch}-cuda` : `ubuntu-${arch}`;
|
|
81
|
+
let asset = release.assets.find(a => a.name.includes(suffix) && a.name.endsWith('.zip'));
|
|
82
|
+
if (!asset && hasCuda) {
|
|
83
|
+
asset = release.assets.find(a => a.name.includes(`ubuntu-${arch}`) && a.name.endsWith('.zip'));
|
|
84
|
+
}
|
|
85
|
+
if (!asset) {
|
|
86
|
+
asset = release.assets.find(a => a.name.includes('ubuntu') && a.name.includes(arch) && a.name.endsWith('.zip'));
|
|
87
|
+
}
|
|
88
|
+
if (!asset) throw new Error(`No pre-built binary found for linux-${arch}. Build from source: https://github.com/ggml-org/llama.cpp#build`);
|
|
89
|
+
|
|
90
|
+
const tmpZip = `/tmp/groove-llama-${Date.now()}.zip`;
|
|
91
|
+
const tmpDir = `/tmp/groove-llama-extract-${Date.now()}`;
|
|
92
|
+
|
|
93
|
+
execSync(`curl -fSL "${asset.browser_download_url}" -o "${tmpZip}"`, { stdio: 'pipe', timeout: 600000 });
|
|
94
|
+
execSync(`unzip -o "${tmpZip}" -d "${tmpDir}"`, { stdio: 'pipe', timeout: 60000 });
|
|
95
|
+
|
|
96
|
+
const findResult = execSync(`find "${tmpDir}" -name llama-server -type f`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
97
|
+
const binPath = findResult.split('\n')[0];
|
|
98
|
+
if (!binPath) throw new Error('llama-server binary not found in release archive');
|
|
99
|
+
|
|
100
|
+
const destPath = resolve(installDir, 'llama-server');
|
|
101
|
+
execSync(`cp "${binPath}" "${destPath}"`, { stdio: 'pipe' });
|
|
102
|
+
chmodSync(destPath, 0o755);
|
|
103
|
+
|
|
104
|
+
// Copy shared libraries if present
|
|
105
|
+
try {
|
|
106
|
+
const libDir = resolve(binPath, '..', '..', 'lib');
|
|
107
|
+
if (existsSync(libDir)) {
|
|
108
|
+
const userLibDir = resolve(homedir(), '.local', 'lib');
|
|
109
|
+
mkdirSync(userLibDir, { recursive: true });
|
|
110
|
+
execSync(`cp -r "${libDir}/"* "${userLibDir}/"`, { stdio: 'pipe' });
|
|
111
|
+
}
|
|
112
|
+
} catch { /* libs are optional */ }
|
|
113
|
+
|
|
114
|
+
// Cleanup
|
|
115
|
+
try { execSync(`rm -rf "${tmpZip}" "${tmpDir}"`, { stdio: 'ignore' }); } catch { /* best-effort */ }
|
|
116
|
+
|
|
117
|
+
return { method: 'github-release', path: destPath, cuda: hasCuda, release: release.tag_name };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error(`Automatic install not supported on ${platform}. Install llama-server manually: https://github.com/ggml-org/llama.cpp#build`);
|
|
121
|
+
}
|
|
122
|
+
|
|
32
123
|
// --- Server Lifecycle ---
|
|
33
124
|
|
|
34
125
|
/**
|
|
@@ -74,9 +165,11 @@ export class LlamaServerManager {
|
|
|
74
165
|
args.push('--flash-attn', 'auto');
|
|
75
166
|
}
|
|
76
167
|
|
|
77
|
-
const
|
|
168
|
+
const serverBin = LlamaServerManager.getLlamaServerPath();
|
|
169
|
+
const proc = spawn(serverBin, args, {
|
|
78
170
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
79
171
|
detached: false,
|
|
172
|
+
env: { ...process.env, LD_LIBRARY_PATH: [resolve(homedir(), '.local', 'lib'), process.env.LD_LIBRARY_PATH].filter(Boolean).join(':') },
|
|
80
173
|
});
|
|
81
174
|
|
|
82
175
|
if (!proc.pid) {
|
|
@@ -69,7 +69,6 @@ export class ModelManager {
|
|
|
69
69
|
async search(query, { limit = 20, sort = 'downloads' } = {}) {
|
|
70
70
|
const params = new URLSearchParams({
|
|
71
71
|
search: query,
|
|
72
|
-
filter: 'gguf',
|
|
73
72
|
sort,
|
|
74
73
|
direction: '-1',
|
|
75
74
|
limit: String(limit),
|
|
@@ -83,15 +82,20 @@ export class ModelManager {
|
|
|
83
82
|
if (!res.ok) throw new Error(`HuggingFace API error: ${res.status}`);
|
|
84
83
|
const models = await res.json();
|
|
85
84
|
|
|
86
|
-
return models.map((m) =>
|
|
87
|
-
id
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
return models.map((m) => {
|
|
86
|
+
const id = m.modelId || m.id;
|
|
87
|
+
const tags = m.tags || [];
|
|
88
|
+
return {
|
|
89
|
+
id,
|
|
90
|
+
name: id.split('/').pop() || id,
|
|
91
|
+
author: id.split('/')[0] || '',
|
|
92
|
+
downloads: m.downloads || 0,
|
|
93
|
+
likes: m.likes || 0,
|
|
94
|
+
tags,
|
|
95
|
+
lastModified: m.lastModified,
|
|
96
|
+
recommendedRuntimes: inferRuntimes(id, tags),
|
|
97
|
+
};
|
|
98
|
+
});
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
async getModelFiles(repoId) {
|
|
@@ -409,3 +413,41 @@ function classifyTier(params, quant) {
|
|
|
409
413
|
if (billions >= 10) return 'medium';
|
|
410
414
|
return 'light';
|
|
411
415
|
}
|
|
416
|
+
|
|
417
|
+
function inferRuntimes(repoId, tags) {
|
|
418
|
+
const lower = repoId.toLowerCase();
|
|
419
|
+
const tagSet = new Set(tags.map((t) => t.toLowerCase()));
|
|
420
|
+
const runtimes = new Set();
|
|
421
|
+
|
|
422
|
+
// GGUF → llama.cpp and (implicitly) Ollama
|
|
423
|
+
if (tagSet.has('gguf') || lower.includes('-gguf') || lower.includes('_gguf')) {
|
|
424
|
+
runtimes.add('llama.cpp');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// MLX-optimized models
|
|
428
|
+
if (tagSet.has('mlx') || lower.includes('-mlx') || lower.includes('_mlx')) {
|
|
429
|
+
runtimes.add('MLX');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// GPTQ / AWQ quantized → vLLM handles these well
|
|
433
|
+
if (tagSet.has('gptq') || tagSet.has('awq') || lower.includes('-gptq') || lower.includes('-awq')) {
|
|
434
|
+
runtimes.add('vLLM');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// SafeTensors / standard transformer weights → vLLM, TGI, MLX
|
|
438
|
+
if (tagSet.has('safetensors') || tagSet.has('transformers')) {
|
|
439
|
+
runtimes.add('vLLM');
|
|
440
|
+
runtimes.add('TGI');
|
|
441
|
+
if (!runtimes.has('MLX')) runtimes.add('MLX');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// If nothing matched, infer from general model traits
|
|
445
|
+
if (runtimes.size === 0) {
|
|
446
|
+
if (tagSet.has('pytorch') || tagSet.has('tf') || tagSet.has('jax')) {
|
|
447
|
+
runtimes.add('vLLM');
|
|
448
|
+
runtimes.add('TGI');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return [...runtimes];
|
|
453
|
+
}
|
|
@@ -249,6 +249,19 @@ export function registerCoordinationRoutes(app, daemon) {
|
|
|
249
249
|
}
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
+
app.post('/api/keeper/move', (req, res) => {
|
|
253
|
+
try {
|
|
254
|
+
const { oldTag, newTag } = req.body || {};
|
|
255
|
+
if (!oldTag || !newTag) return res.status(400).json({ error: 'oldTag and newTag are required' });
|
|
256
|
+
const item = daemon.keeper.move(oldTag, newTag);
|
|
257
|
+
daemon.audit.log('keeper.move', { oldTag, newTag: item.tag });
|
|
258
|
+
daemon.broadcast({ type: 'keeper:moved', oldTag, item });
|
|
259
|
+
res.json(item);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
res.status(err.message.includes('does not exist') ? 404 : 400).json({ error: err.message });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
252
265
|
app.delete('/api/keeper/link/:tag(*)', (req, res) => {
|
|
253
266
|
try {
|
|
254
267
|
const { docPath } = req.body || {};
|
|
@@ -289,6 +302,9 @@ export function registerCoordinationRoutes(app, daemon) {
|
|
|
289
302
|
} else {
|
|
290
303
|
doc = `# ${tag}\n\n*Auto-generated document from conversation*\n\n${transcript.slice(0, 5000)}`;
|
|
291
304
|
}
|
|
305
|
+
if (!doc || !doc.trim()) {
|
|
306
|
+
return res.status(502).json({ error: 'AI synthesis returned empty content — try again' });
|
|
307
|
+
}
|
|
292
308
|
const item = daemon.keeper.saveDoc(tag, doc);
|
|
293
309
|
daemon.audit.log('keeper.doc', { tag: item.tag, agentId });
|
|
294
310
|
daemon.broadcast({ type: 'keeper:saved', item });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { resolve, sep, isAbsolute } from 'path';
|
|
2
|
+
import { resolve, sep, isAbsolute, basename } from 'path';
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream, realpathSync } from 'fs';
|
|
4
4
|
import { execFile, execFileSync } from 'child_process';
|
|
5
5
|
import { homedir } from 'os';
|
|
@@ -331,6 +331,53 @@ export function registerFileRoutes(app, daemon) {
|
|
|
331
331
|
}
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
+
// Download a file (serves raw with Content-Disposition)
|
|
335
|
+
app.get('/api/files/download', (req, res) => {
|
|
336
|
+
const relPath = req.query.path;
|
|
337
|
+
const result = validateFilePath(relPath, getEditorRoot(daemon));
|
|
338
|
+
if (result.error) return res.status(400).json({ error: result.error });
|
|
339
|
+
if (!existsSync(result.fullPath)) return res.status(404).json({ error: 'File not found' });
|
|
340
|
+
|
|
341
|
+
const stat = statSync(result.fullPath);
|
|
342
|
+
if (stat.isDirectory()) return res.status(400).json({ error: 'Cannot download a directory' });
|
|
343
|
+
|
|
344
|
+
const name = basename(result.fullPath);
|
|
345
|
+
const mime = mimeLookup(name) || 'application/octet-stream';
|
|
346
|
+
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(name)}"`);
|
|
347
|
+
res.setHeader('Content-Type', mime);
|
|
348
|
+
res.setHeader('Content-Length', stat.size);
|
|
349
|
+
createReadStream(result.fullPath).pipe(res);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Upload files (base64-encoded) to a target directory
|
|
353
|
+
app.post('/api/files/upload', (req, res) => {
|
|
354
|
+
const { dir = '', files } = req.body;
|
|
355
|
+
const rootDir = getEditorRoot(daemon);
|
|
356
|
+
if (!rootDir) return res.status(400).json({ error: 'Editor root not set' });
|
|
357
|
+
if (!Array.isArray(files) || files.length === 0) return res.status(400).json({ error: 'files[] required' });
|
|
358
|
+
if (files.length > 50) return res.status(400).json({ error: 'Max 50 files per upload' });
|
|
359
|
+
|
|
360
|
+
const uploaded = [];
|
|
361
|
+
for (const file of files) {
|
|
362
|
+
if (!file.name || !file.content) continue;
|
|
363
|
+
const safeName = String(file.name).replace(/\.\./g, '').replace(/\//g, '_');
|
|
364
|
+
if (!safeName) continue;
|
|
365
|
+
const relPath = dir ? `${dir}/${safeName}` : safeName;
|
|
366
|
+
const result = validateFilePath(relPath, rootDir);
|
|
367
|
+
if (result.error) continue;
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const parentDir = resolve(result.fullPath, '..');
|
|
371
|
+
mkdirSync(parentDir, { recursive: true });
|
|
372
|
+
const buf = Buffer.from(file.content, 'base64');
|
|
373
|
+
writeFileSync(result.fullPath, buf);
|
|
374
|
+
daemon.audit.log('file.upload', { path: relPath, size: buf.length });
|
|
375
|
+
uploaded.push({ path: relPath, size: buf.length });
|
|
376
|
+
} catch { /* skip failed files */ }
|
|
377
|
+
}
|
|
378
|
+
res.json({ uploaded, total: uploaded.length });
|
|
379
|
+
});
|
|
380
|
+
|
|
334
381
|
// Create a new file
|
|
335
382
|
app.post('/api/files/create', (req, res) => {
|
|
336
383
|
const { path: relPath, content = '' } = req.body;
|
|
@@ -595,9 +642,31 @@ export function registerFileRoutes(app, daemon) {
|
|
|
595
642
|
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
596
643
|
const rawFiles = daemon.registry.getFilesTouched(req.params.id);
|
|
597
644
|
const rootDir = agent.workingDir || daemon.projectDir;
|
|
645
|
+
|
|
646
|
+
// Build git diff numstat for line-level +/- counts
|
|
647
|
+
let numstatMap = {};
|
|
648
|
+
const writtenPaths = rawFiles.filter(f => f.writes > 0).map(f => f.path);
|
|
649
|
+
if (writtenPaths.length > 0) {
|
|
650
|
+
try {
|
|
651
|
+
const out = execFileSync('git', ['diff', '--numstat', '--', ...writtenPaths], {
|
|
652
|
+
cwd: rootDir, timeout: 10000, maxBuffer: 2 * 1024 * 1024,
|
|
653
|
+
}).toString();
|
|
654
|
+
for (const line of out.split('\n')) {
|
|
655
|
+
const m = line.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
|
|
656
|
+
if (m) {
|
|
657
|
+
numstatMap[m[3]] = {
|
|
658
|
+
additions: m[1] === '-' ? 0 : Number(m[1]),
|
|
659
|
+
deletions: m[2] === '-' ? 0 : Number(m[2]),
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
} catch { /* git not available or not a repo */ }
|
|
664
|
+
}
|
|
665
|
+
|
|
598
666
|
const files = rawFiles.map(f => {
|
|
599
667
|
const fullPath = isAbsolute(f.path) ? f.path : resolve(rootDir, f.path);
|
|
600
|
-
|
|
668
|
+
const stats = numstatMap[f.path] || null;
|
|
669
|
+
return { ...f, exists: existsSync(fullPath), additions: stats?.additions ?? null, deletions: stats?.deletions ?? null };
|
|
601
670
|
});
|
|
602
671
|
res.json({ files, total: files.length });
|
|
603
672
|
});
|
|
@@ -713,6 +713,17 @@ export function registerProviderRoutes(app, daemon) {
|
|
|
713
713
|
res.json(daemon.llamaServer.getStatus());
|
|
714
714
|
});
|
|
715
715
|
|
|
716
|
+
app.post('/api/llama/install', async (req, res) => {
|
|
717
|
+
try {
|
|
718
|
+
const { LlamaServerManager } = await import('../llama-server.js');
|
|
719
|
+
const result = await LlamaServerManager.install();
|
|
720
|
+
daemon.modelLab.refreshInstalledTools();
|
|
721
|
+
res.json({ success: true, ...result });
|
|
722
|
+
} catch (err) {
|
|
723
|
+
res.status(500).json({ error: err.message });
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
716
727
|
app.get('/api/mlx/status', (req, res) => {
|
|
717
728
|
res.json(daemon.mlxServer.getStatus());
|
|
718
729
|
});
|