groove-dev 0.27.60 → 0.27.61
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 +69 -52
- package/node_modules/@groove-dev/daemon/src/conversations.js +75 -31
- package/node_modules/@groove-dev/daemon/src/journalist.js +1 -0
- package/node_modules/@groove-dev/daemon/src/process.js +17 -7
- package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +63 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +55 -0
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +53 -0
- package/node_modules/@groove-dev/daemon/src/providers/local.js +44 -0
- package/node_modules/@groove-dev/daemon/src/providers/ollama.js +44 -0
- package/node_modules/@groove-dev/daemon/src/rotator.js +4 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-DD6taBMp.css → index-B3AqeyS4.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-DcnRqlqB.js → index-DWao9glo.js} +178 -178
- 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/chat/chat-view.jsx +3 -2
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +13 -7
- package/node_modules/@groove-dev/gui/src/components/ui/update-modal.jsx +70 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +27 -6
- 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 +69 -52
- package/packages/daemon/src/conversations.js +75 -31
- package/packages/daemon/src/journalist.js +1 -0
- package/packages/daemon/src/process.js +17 -7
- package/packages/daemon/src/providers/base.js +4 -0
- package/packages/daemon/src/providers/claude-code.js +63 -0
- package/packages/daemon/src/providers/codex.js +55 -0
- package/packages/daemon/src/providers/gemini.js +53 -0
- package/packages/daemon/src/providers/local.js +44 -0
- package/packages/daemon/src/providers/ollama.js +44 -0
- package/packages/daemon/src/rotator.js +4 -0
- package/packages/gui/dist/assets/{index-DD6taBMp.css → index-B3AqeyS4.css} +1 -1
- package/packages/gui/dist/assets/{index-DcnRqlqB.js → index-DWao9glo.js} +178 -178
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/chat/chat-view.jsx +3 -2
- package/packages/gui/src/components/chat/model-picker.jsx +1 -1
- package/packages/gui/src/components/layout/status-bar.jsx +13 -7
- package/packages/gui/src/components/ui/update-modal.jsx +70 -0
- package/packages/gui/src/stores/groove.js +27 -6
|
@@ -7,6 +7,26 @@ import { resolve } from 'path';
|
|
|
7
7
|
import { homedir } from 'os';
|
|
8
8
|
import { Provider } from './base.js';
|
|
9
9
|
|
|
10
|
+
async function parseSSEStream(response, onEvent) {
|
|
11
|
+
const reader = response.body.getReader();
|
|
12
|
+
const decoder = new TextDecoder();
|
|
13
|
+
let buffer = '';
|
|
14
|
+
while (true) {
|
|
15
|
+
const { done, value } = await reader.read();
|
|
16
|
+
if (done) break;
|
|
17
|
+
buffer += decoder.decode(value, { stream: true });
|
|
18
|
+
const lines = buffer.split('\n');
|
|
19
|
+
buffer = lines.pop();
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
if (line.startsWith('data: ')) {
|
|
22
|
+
const data = line.slice(6);
|
|
23
|
+
if (data === '[DONE]') { onEvent({ done: true }); return; }
|
|
24
|
+
try { onEvent(JSON.parse(data)); } catch { /* skip malformed */ }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
10
30
|
export class CodexProvider extends Provider {
|
|
11
31
|
static name = 'codex';
|
|
12
32
|
static displayName = 'Codex';
|
|
@@ -128,6 +148,41 @@ export class CodexProvider extends Provider {
|
|
|
128
148
|
return (inputTokens / 1000) * model.pricing.input + (outputTokens / 1000) * model.pricing.output;
|
|
129
149
|
}
|
|
130
150
|
|
|
151
|
+
streamChat(messages, model, apiKey, onChunk, onDone, onError) {
|
|
152
|
+
if (!apiKey) return null;
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
let finished = false;
|
|
155
|
+
const finish = () => { if (!finished) { finished = true; onDone(); } };
|
|
156
|
+
fetch('https://api.openai.com/v1/chat/completions', {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: {
|
|
159
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
},
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
model: model || 'gpt-5.4-mini',
|
|
164
|
+
messages,
|
|
165
|
+
stream: true,
|
|
166
|
+
}),
|
|
167
|
+
signal: controller.signal,
|
|
168
|
+
}).then((res) => {
|
|
169
|
+
if (!res.ok) {
|
|
170
|
+
return res.text().then((t) => { throw new Error(`OpenAI API ${res.status}: ${t.slice(0, 200)}`); });
|
|
171
|
+
}
|
|
172
|
+
return parseSSEStream(res, (event) => {
|
|
173
|
+
if (event.done) { finish(); return; }
|
|
174
|
+
const content = event.choices?.[0]?.delta?.content;
|
|
175
|
+
if (content) onChunk(content);
|
|
176
|
+
});
|
|
177
|
+
}).then(() => {
|
|
178
|
+
finish();
|
|
179
|
+
}).catch((err) => {
|
|
180
|
+
if (err.name === 'AbortError') return;
|
|
181
|
+
onError(err);
|
|
182
|
+
});
|
|
183
|
+
return controller;
|
|
184
|
+
}
|
|
185
|
+
|
|
131
186
|
parseOutput(line) {
|
|
132
187
|
const trimmed = line.trim();
|
|
133
188
|
if (!trimmed) return null;
|
|
@@ -4,6 +4,26 @@
|
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { Provider } from './base.js';
|
|
6
6
|
|
|
7
|
+
async function parseSSEStream(response, onEvent) {
|
|
8
|
+
const reader = response.body.getReader();
|
|
9
|
+
const decoder = new TextDecoder();
|
|
10
|
+
let buffer = '';
|
|
11
|
+
while (true) {
|
|
12
|
+
const { done, value } = await reader.read();
|
|
13
|
+
if (done) break;
|
|
14
|
+
buffer += decoder.decode(value, { stream: true });
|
|
15
|
+
const lines = buffer.split('\n');
|
|
16
|
+
buffer = lines.pop();
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
if (line.startsWith('data: ')) {
|
|
19
|
+
const data = line.slice(6);
|
|
20
|
+
if (data === '[DONE]') { onEvent({ done: true }); return; }
|
|
21
|
+
try { onEvent(JSON.parse(data)); } catch { /* skip malformed */ }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
export class GeminiProvider extends Provider {
|
|
8
28
|
static name = 'gemini';
|
|
9
29
|
static displayName = 'Gemini CLI';
|
|
@@ -68,6 +88,39 @@ export class GeminiProvider extends Provider {
|
|
|
68
88
|
return (inputTokens / 1000) * model.pricing.input + (outputTokens / 1000) * model.pricing.output;
|
|
69
89
|
}
|
|
70
90
|
|
|
91
|
+
streamChat(messages, model, apiKey, onChunk, onDone, onError) {
|
|
92
|
+
if (!apiKey) return null;
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
let finished = false;
|
|
95
|
+
const finish = () => { if (!finished) { finished = true; onDone(); } };
|
|
96
|
+
const m = model || 'gemini-2.5-flash';
|
|
97
|
+
const contents = messages.map((msg) => ({
|
|
98
|
+
role: msg.role === 'assistant' ? 'model' : 'user',
|
|
99
|
+
parts: [{ text: msg.content }],
|
|
100
|
+
}));
|
|
101
|
+
fetch(`https://generativelanguage.googleapis.com/v1beta/models/${m}:streamGenerateContent?alt=sse&key=${apiKey}`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: JSON.stringify({ contents }),
|
|
105
|
+
signal: controller.signal,
|
|
106
|
+
}).then((res) => {
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
return res.text().then((t) => { throw new Error(`Gemini API ${res.status}: ${t.slice(0, 200)}`); });
|
|
109
|
+
}
|
|
110
|
+
return parseSSEStream(res, (event) => {
|
|
111
|
+
if (event.done) { finish(); return; }
|
|
112
|
+
const text = event.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
113
|
+
if (text) onChunk(text);
|
|
114
|
+
});
|
|
115
|
+
}).then(() => {
|
|
116
|
+
finish();
|
|
117
|
+
}).catch((err) => {
|
|
118
|
+
if (err.name === 'AbortError') return;
|
|
119
|
+
onError(err);
|
|
120
|
+
});
|
|
121
|
+
return controller;
|
|
122
|
+
}
|
|
123
|
+
|
|
71
124
|
parseOutput(line) {
|
|
72
125
|
const trimmed = line.trim();
|
|
73
126
|
if (!trimmed) return null;
|
|
@@ -194,6 +194,50 @@ export class LocalProvider extends Provider {
|
|
|
194
194
|
return false; // Needs rotation for model switch
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
streamChat(messages, model, apiKey, onChunk, onDone, onError) {
|
|
198
|
+
const controller = new AbortController();
|
|
199
|
+
let finished = false;
|
|
200
|
+
const finish = () => { if (!finished) { finished = true; onDone(); } };
|
|
201
|
+
fetch('http://localhost:11434/api/chat', {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: { 'Content-Type': 'application/json' },
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
model: model || 'qwen2.5-coder:7b',
|
|
206
|
+
messages,
|
|
207
|
+
stream: true,
|
|
208
|
+
}),
|
|
209
|
+
signal: controller.signal,
|
|
210
|
+
}).then(async (res) => {
|
|
211
|
+
if (!res.ok) {
|
|
212
|
+
const t = await res.text();
|
|
213
|
+
throw new Error(`Ollama API ${res.status}: ${t.slice(0, 200)}`);
|
|
214
|
+
}
|
|
215
|
+
const reader = res.body.getReader();
|
|
216
|
+
const decoder = new TextDecoder();
|
|
217
|
+
let buffer = '';
|
|
218
|
+
while (true) {
|
|
219
|
+
const { done, value } = await reader.read();
|
|
220
|
+
if (done) break;
|
|
221
|
+
buffer += decoder.decode(value, { stream: true });
|
|
222
|
+
const lines = buffer.split('\n');
|
|
223
|
+
buffer = lines.pop();
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
if (!line.trim()) continue;
|
|
226
|
+
try {
|
|
227
|
+
const json = JSON.parse(line);
|
|
228
|
+
if (json.done) { finish(); return; }
|
|
229
|
+
if (json.message?.content) onChunk(json.message.content);
|
|
230
|
+
} catch { /* skip malformed */ }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
finish();
|
|
234
|
+
}).catch((err) => {
|
|
235
|
+
if (err.name === 'AbortError') return;
|
|
236
|
+
onError(err);
|
|
237
|
+
});
|
|
238
|
+
return controller;
|
|
239
|
+
}
|
|
240
|
+
|
|
197
241
|
parseOutput(line) {
|
|
198
242
|
const trimmed = (line || '').trim();
|
|
199
243
|
if (!trimmed) return null;
|
|
@@ -275,6 +275,50 @@ export class OllamaProvider extends Provider {
|
|
|
275
275
|
return false; // Needs rotation for model switch
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
streamChat(messages, model, apiKey, onChunk, onDone, onError) {
|
|
279
|
+
const controller = new AbortController();
|
|
280
|
+
let finished = false;
|
|
281
|
+
const finish = () => { if (!finished) { finished = true; onDone(); } };
|
|
282
|
+
fetch('http://localhost:11434/api/chat', {
|
|
283
|
+
method: 'POST',
|
|
284
|
+
headers: { 'Content-Type': 'application/json' },
|
|
285
|
+
body: JSON.stringify({
|
|
286
|
+
model: model || 'llama3.1:8b',
|
|
287
|
+
messages,
|
|
288
|
+
stream: true,
|
|
289
|
+
}),
|
|
290
|
+
signal: controller.signal,
|
|
291
|
+
}).then(async (res) => {
|
|
292
|
+
if (!res.ok) {
|
|
293
|
+
const t = await res.text();
|
|
294
|
+
throw new Error(`Ollama API ${res.status}: ${t.slice(0, 200)}`);
|
|
295
|
+
}
|
|
296
|
+
const reader = res.body.getReader();
|
|
297
|
+
const decoder = new TextDecoder();
|
|
298
|
+
let buffer = '';
|
|
299
|
+
while (true) {
|
|
300
|
+
const { done, value } = await reader.read();
|
|
301
|
+
if (done) break;
|
|
302
|
+
buffer += decoder.decode(value, { stream: true });
|
|
303
|
+
const lines = buffer.split('\n');
|
|
304
|
+
buffer = lines.pop();
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
if (!line.trim()) continue;
|
|
307
|
+
try {
|
|
308
|
+
const json = JSON.parse(line);
|
|
309
|
+
if (json.done) { finish(); return; }
|
|
310
|
+
if (json.message?.content) onChunk(json.message.content);
|
|
311
|
+
} catch { /* skip malformed */ }
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
finish();
|
|
315
|
+
}).catch((err) => {
|
|
316
|
+
if (err.name === 'AbortError') return;
|
|
317
|
+
onError(err);
|
|
318
|
+
});
|
|
319
|
+
return controller;
|
|
320
|
+
}
|
|
321
|
+
|
|
278
322
|
parseOutput(line) {
|
|
279
323
|
const trimmed = line.trim();
|
|
280
324
|
if (!trimmed) return null;
|
|
@@ -299,6 +299,10 @@ export class Rotator extends EventEmitter {
|
|
|
299
299
|
brief = brief + '\n\n## User Instruction\n\n' + options.additionalPrompt;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
if (agent.role === 'planner' && !brief.includes('PLANNING ONLY')) {
|
|
303
|
+
brief = 'CRITICAL: You are a PLANNING ONLY agent. Do NOT implement code. Route all work to your team via .groove/recommended-team.json.\n\n' + brief;
|
|
304
|
+
}
|
|
305
|
+
|
|
302
306
|
// Persist to Layer 7 handoff chain so future rotations have causal continuity
|
|
303
307
|
if (this.daemon.memory) {
|
|
304
308
|
this.daemon.memory.appendHandoffBrief(agent.role, {
|