groove-dev 0.27.119 → 0.27.121
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/moe-training/client/trajectory-capture.js +55 -0
- package/moe-training/test/client/trajectory-capture.test.js +63 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/start.js +2 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +30 -10
- package/node_modules/@groove-dev/daemon/src/conversations.js +54 -32
- package/node_modules/@groove-dev/daemon/src/introducer.js +45 -20
- package/node_modules/@groove-dev/daemon/src/process.js +47 -1
- package/node_modules/@groove-dev/daemon/src/teams.js +33 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-DT6Jbf_q.css → index-BLd3MON8.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-BxPCaxlC.js → index-bmkBX18f.js} +1721 -1721
- 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-config.jsx +3 -41
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +4 -43
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +8 -10
- package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +8 -23
- package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +54 -143
- package/node_modules/@groove-dev/gui/src/components/ui/data-sharing-modal.jsx +7 -57
- package/node_modules/@groove-dev/gui/src/stores/groove.js +13 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +50 -84
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +61 -1
- package/node_modules/moe-training/client/trajectory-capture.js +55 -0
- package/node_modules/moe-training/test/client/trajectory-capture.test.js +63 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/start.js +2 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +30 -10
- package/packages/daemon/src/conversations.js +54 -32
- package/packages/daemon/src/introducer.js +45 -20
- package/packages/daemon/src/process.js +47 -1
- package/packages/daemon/src/teams.js +33 -0
- package/packages/gui/dist/assets/{index-DT6Jbf_q.css → index-BLd3MON8.css} +1 -1
- package/packages/gui/dist/assets/{index-BxPCaxlC.js → index-bmkBX18f.js} +1721 -1721
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-config.jsx +3 -41
- package/packages/gui/src/components/agents/spawn-wizard.jsx +4 -43
- package/packages/gui/src/components/layout/status-bar.jsx +8 -10
- package/packages/gui/src/components/onboarding/setup-wizard.jsx +8 -23
- package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +54 -143
- package/packages/gui/src/components/ui/data-sharing-modal.jsx +7 -57
- package/packages/gui/src/stores/groove.js +13 -0
- package/packages/gui/src/views/settings.jsx +50 -84
- package/packages/gui/src/views/teams.jsx +61 -1
|
@@ -62,10 +62,8 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
62
62
|
const [customPathOpen, setCustomPathOpen] = useState(false);
|
|
63
63
|
const [customPath, setCustomPath] = useState('');
|
|
64
64
|
const [savingPath, setSavingPath] = useState(false);
|
|
65
|
-
const [loginPending, setLoginPending] = useState(false);
|
|
66
65
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
67
66
|
const installProgress = useGrooveStore((s) => s.providerInstallProgress[provider.id]);
|
|
68
|
-
const loginProvider = useGrooveStore((s) => s.loginProvider);
|
|
69
67
|
const setProviderPath = useGrooveStore((s) => s.setProviderPath);
|
|
70
68
|
const verifyProvider = useGrooveStore((s) => s.verifyProvider);
|
|
71
69
|
const installProvider = useGrooveStore((s) => s.installProvider);
|
|
@@ -100,15 +98,6 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
async function handleLogin(body) {
|
|
104
|
-
try {
|
|
105
|
-
setLoginPending(true);
|
|
106
|
-
await loginProvider(provider.id, body);
|
|
107
|
-
} catch {
|
|
108
|
-
setLoginPending(false);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
101
|
async function handleSavePath() {
|
|
113
102
|
if (!customPath.trim()) return;
|
|
114
103
|
setSavingPath(true);
|
|
@@ -321,15 +310,30 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
321
310
|
{effectivelyInstalled && !isReady && !settingKey && !isInstalling && (
|
|
322
311
|
<div className="flex flex-col gap-3 flex-1">
|
|
323
312
|
{/* ── Claude Code auth ── */}
|
|
324
|
-
{provider.id === 'claude-code' &&
|
|
313
|
+
{provider.id === 'claude-code' && (
|
|
325
314
|
<>
|
|
326
|
-
<div className="space-y-
|
|
327
|
-
<p className="text-xs text-text-1 font-sans font-medium">Sign in
|
|
328
|
-
<
|
|
315
|
+
<div className="space-y-2">
|
|
316
|
+
<p className="text-xs text-text-1 font-sans font-medium">Sign in via terminal</p>
|
|
317
|
+
<div className="space-y-1.5">
|
|
318
|
+
<div className="flex items-start gap-2">
|
|
319
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">1</span>
|
|
320
|
+
<p className="text-2xs text-text-2 font-sans flex items-center gap-1">
|
|
321
|
+
<Terminal size={10} className="text-text-3 flex-shrink-0" />
|
|
322
|
+
Open the Groove terminal below
|
|
323
|
+
</p>
|
|
324
|
+
</div>
|
|
325
|
+
<div className="flex items-start gap-2">
|
|
326
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">2</span>
|
|
327
|
+
<p className="text-2xs text-text-2 font-sans">
|
|
328
|
+
Run: <code className="font-mono text-accent bg-surface-4 px-1.5 py-0.5 rounded text-2xs">claude</code>
|
|
329
|
+
</p>
|
|
330
|
+
</div>
|
|
331
|
+
<div className="flex items-start gap-2">
|
|
332
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">3</span>
|
|
333
|
+
<p className="text-2xs text-text-2 font-sans">Follow the prompts to sign in with your Anthropic account</p>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
329
336
|
</div>
|
|
330
|
-
<Button variant="primary" size="sm" onClick={() => handleLogin()} className="w-full h-9 text-xs gap-1.5">
|
|
331
|
-
<ExternalLink size={12} /> Sign In
|
|
332
|
-
</Button>
|
|
333
337
|
<button
|
|
334
338
|
onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
|
|
335
339
|
className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans text-center"
|
|
@@ -340,15 +344,36 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
340
344
|
)}
|
|
341
345
|
|
|
342
346
|
{/* ── Codex auth ── */}
|
|
343
|
-
{provider.id === 'codex' &&
|
|
347
|
+
{provider.id === 'codex' && (
|
|
344
348
|
<>
|
|
345
|
-
<div className="space-y-
|
|
346
|
-
<p className="text-xs text-text-1 font-sans font-medium">Sign in
|
|
347
|
-
<
|
|
349
|
+
<div className="space-y-2">
|
|
350
|
+
<p className="text-xs text-text-1 font-sans font-medium">Sign in via terminal</p>
|
|
351
|
+
<div className="space-y-1.5">
|
|
352
|
+
<div className="flex items-start gap-2">
|
|
353
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">1</span>
|
|
354
|
+
<p className="text-2xs text-text-2 font-sans flex items-center gap-1">
|
|
355
|
+
<Terminal size={10} className="text-text-3 flex-shrink-0" />
|
|
356
|
+
Open the Groove terminal below
|
|
357
|
+
</p>
|
|
358
|
+
</div>
|
|
359
|
+
<div className="flex items-start gap-2">
|
|
360
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">2</span>
|
|
361
|
+
<p className="text-2xs text-text-2 font-sans">
|
|
362
|
+
Run: <code className="font-mono text-accent bg-surface-4 px-1.5 py-0.5 rounded text-2xs">npm i -g @openai/codex</code> (if not installed)
|
|
363
|
+
</p>
|
|
364
|
+
</div>
|
|
365
|
+
<div className="flex items-start gap-2">
|
|
366
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">3</span>
|
|
367
|
+
<p className="text-2xs text-text-2 font-sans">
|
|
368
|
+
Run: <code className="font-mono text-accent bg-surface-4 px-1.5 py-0.5 rounded text-2xs">codex login</code>
|
|
369
|
+
</p>
|
|
370
|
+
</div>
|
|
371
|
+
<div className="flex items-start gap-2">
|
|
372
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">4</span>
|
|
373
|
+
<p className="text-2xs text-text-2 font-sans">Follow the prompts to authenticate</p>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
348
376
|
</div>
|
|
349
|
-
<Button variant="primary" size="sm" onClick={() => handleLogin({ method: 'chatgpt-plus' })} className="w-full h-9 text-xs gap-1.5">
|
|
350
|
-
<ExternalLink size={12} /> Sign In
|
|
351
|
-
</Button>
|
|
352
377
|
<button
|
|
353
378
|
onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
|
|
354
379
|
className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans text-center"
|
|
@@ -442,65 +467,6 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
442
467
|
</>
|
|
443
468
|
)}
|
|
444
469
|
|
|
445
|
-
{/* ── Any provider: login pending state ── */}
|
|
446
|
-
{(provider.id === 'claude-code' || provider.id === 'codex') && loginPending && (
|
|
447
|
-
<div className="flex flex-col gap-3">
|
|
448
|
-
<div className="flex items-center gap-2 p-3 bg-accent/5 border border-accent/15 rounded-md">
|
|
449
|
-
<Loader2 size={14} className="text-accent animate-spin" />
|
|
450
|
-
<div>
|
|
451
|
-
<p className="text-xs text-accent font-sans font-medium">Check your browser</p>
|
|
452
|
-
<p className="text-2xs text-text-3 font-sans">Complete the sign-in in the browser window that opened.</p>
|
|
453
|
-
</div>
|
|
454
|
-
</div>
|
|
455
|
-
<Button
|
|
456
|
-
variant="primary"
|
|
457
|
-
size="sm"
|
|
458
|
-
disabled={checking}
|
|
459
|
-
onClick={async () => {
|
|
460
|
-
if (provider.id === 'codex') {
|
|
461
|
-
setChecking(true);
|
|
462
|
-
try {
|
|
463
|
-
const res = await api.post(`/providers/codex/verify`);
|
|
464
|
-
if (res.authenticated) {
|
|
465
|
-
setLoginPending(false);
|
|
466
|
-
if (onKeyChange) onKeyChange();
|
|
467
|
-
} else {
|
|
468
|
-
addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
|
|
469
|
-
}
|
|
470
|
-
} catch {
|
|
471
|
-
addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
|
|
472
|
-
} finally {
|
|
473
|
-
setChecking(false);
|
|
474
|
-
}
|
|
475
|
-
} else {
|
|
476
|
-
setChecking(true);
|
|
477
|
-
try {
|
|
478
|
-
const res = await api.post(`/providers/claude-code/verify`);
|
|
479
|
-
if (res.authenticated) {
|
|
480
|
-
setLoginPending(false);
|
|
481
|
-
if (onKeyChange) onKeyChange();
|
|
482
|
-
} else {
|
|
483
|
-
addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
|
|
484
|
-
}
|
|
485
|
-
} catch {
|
|
486
|
-
addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
|
|
487
|
-
} finally {
|
|
488
|
-
setChecking(false);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}}
|
|
492
|
-
className="w-full h-8 text-xs gap-1.5"
|
|
493
|
-
>
|
|
494
|
-
{checking ? <Loader2 size={12} className="animate-spin" /> : <Check size={12} />} I've signed in
|
|
495
|
-
</Button>
|
|
496
|
-
<button
|
|
497
|
-
onClick={() => setLoginPending(false)}
|
|
498
|
-
className="text-2xs text-text-4 hover:text-text-2 cursor-pointer font-sans text-center"
|
|
499
|
-
>
|
|
500
|
-
Cancel
|
|
501
|
-
</button>
|
|
502
|
-
</div>
|
|
503
|
-
)}
|
|
504
470
|
</div>
|
|
505
471
|
)}
|
|
506
472
|
|
|
@@ -9,10 +9,11 @@ import { api } from '../lib/api';
|
|
|
9
9
|
import { useToast } from '../lib/hooks/use-toast';
|
|
10
10
|
import { fmtNum, fmtDollar, timeAgo, fmtUptime } from '../lib/format';
|
|
11
11
|
import { cn } from '../lib/cn';
|
|
12
|
+
import { Dialog, DialogContent } from '../components/ui/dialog';
|
|
12
13
|
import {
|
|
13
14
|
Clock, CheckCircle, XCircle, AlertTriangle, ShieldCheck, ShieldX,
|
|
14
15
|
Users, Folder, Cpu, Trash2, Play, Pause, LayoutDashboard, ListChecks, Calendar,
|
|
15
|
-
Archive, RotateCcw, ChevronRight,
|
|
16
|
+
Archive, RotateCcw, ChevronRight, ArrowUpCircle,
|
|
16
17
|
} from 'lucide-react';
|
|
17
18
|
import { TeamRemovalDialog, PurgeConfirmDialog } from '../components/teams/team-removal-dialog';
|
|
18
19
|
|
|
@@ -28,9 +29,11 @@ function TeamsDashboard() {
|
|
|
28
29
|
const fetchArchivedTeams = useGrooveStore((s) => s.fetchArchivedTeams);
|
|
29
30
|
const restoreTeam = useGrooveStore((s) => s.restoreTeam);
|
|
30
31
|
const purgeTeam = useGrooveStore((s) => s.purgeTeam);
|
|
32
|
+
const promoteTeam = useGrooveStore((s) => s.promoteTeam);
|
|
31
33
|
|
|
32
34
|
const [archiveConfirm, setArchiveConfirm] = useState(null);
|
|
33
35
|
const [purgeConfirm, setPurgeConfirm] = useState(null);
|
|
36
|
+
const [promoteConfirm, setPromoteConfirm] = useState(null);
|
|
34
37
|
const [archivedOpen, setArchivedOpen] = useState(false);
|
|
35
38
|
|
|
36
39
|
useEffect(() => { fetchArchivedTeams(); }, []);
|
|
@@ -84,6 +87,15 @@ function TeamsDashboard() {
|
|
|
84
87
|
</div>
|
|
85
88
|
)}
|
|
86
89
|
</div>
|
|
90
|
+
{team.mode !== 'production' && (
|
|
91
|
+
<button
|
|
92
|
+
onClick={() => setPromoteConfirm(team)}
|
|
93
|
+
className="p-1.5 text-text-4 hover:text-success rounded transition-colors cursor-pointer"
|
|
94
|
+
title="Promote to production"
|
|
95
|
+
>
|
|
96
|
+
<ArrowUpCircle size={13} />
|
|
97
|
+
</button>
|
|
98
|
+
)}
|
|
87
99
|
<button
|
|
88
100
|
onClick={() => {
|
|
89
101
|
if (teamAgents.some((a) => a.status === 'running' || a.status === 'starting')) {
|
|
@@ -195,10 +207,58 @@ function TeamsDashboard() {
|
|
|
195
207
|
onOpenChange={(open) => !open && setPurgeConfirm(null)}
|
|
196
208
|
onPurge={purgeTeam}
|
|
197
209
|
/>
|
|
210
|
+
|
|
211
|
+
<PromoteConfirmDialog
|
|
212
|
+
team={promoteConfirm}
|
|
213
|
+
open={!!promoteConfirm}
|
|
214
|
+
onOpenChange={(open) => !open && setPromoteConfirm(null)}
|
|
215
|
+
onPromote={promoteTeam}
|
|
216
|
+
/>
|
|
198
217
|
</div>
|
|
199
218
|
);
|
|
200
219
|
}
|
|
201
220
|
|
|
221
|
+
function PromoteConfirmDialog({ team, open, onOpenChange, onPromote }) {
|
|
222
|
+
const [promoting, setPromoting] = useState(false);
|
|
223
|
+
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (!open) setPromoting(false);
|
|
226
|
+
}, [open]);
|
|
227
|
+
|
|
228
|
+
async function handleConfirm() {
|
|
229
|
+
setPromoting(true);
|
|
230
|
+
try {
|
|
231
|
+
await onPromote(team?.id);
|
|
232
|
+
onOpenChange(false);
|
|
233
|
+
} catch {
|
|
234
|
+
setPromoting(false);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
240
|
+
<DialogContent title="Promote to Production" description="Promote this team to production mode">
|
|
241
|
+
<div className="px-5 py-4 space-y-3">
|
|
242
|
+
<p className="text-sm text-text-1 font-sans">
|
|
243
|
+
Promote <span className="font-semibold text-text-0">{team?.name}</span> to production?
|
|
244
|
+
</p>
|
|
245
|
+
<p className="text-xs text-text-3 font-sans">
|
|
246
|
+
This will move files from the team directory into the project directory.
|
|
247
|
+
The team will switch to production mode and files will persist when the team is removed.
|
|
248
|
+
</p>
|
|
249
|
+
</div>
|
|
250
|
+
<div className="px-5 py-3 border-t border-border-subtle flex justify-end gap-2">
|
|
251
|
+
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>Cancel</Button>
|
|
252
|
+
<Button variant="primary" size="sm" disabled={promoting} onClick={handleConfirm} className="gap-1.5">
|
|
253
|
+
<ArrowUpCircle size={12} />
|
|
254
|
+
{promoting ? 'Promoting...' : 'Promote'}
|
|
255
|
+
</Button>
|
|
256
|
+
</div>
|
|
257
|
+
</DialogContent>
|
|
258
|
+
</Dialog>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
202
262
|
function Stat({ label, value, color }) {
|
|
203
263
|
return (
|
|
204
264
|
<div className="text-center">
|
|
@@ -128,6 +128,61 @@ export class TrajectoryCapture {
|
|
|
128
128
|
await this._attestation.openSession(sessionId, metadata);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
onChatTurnStart(conversationId, provider, model, message) {
|
|
132
|
+
if (!this._enabled) return null;
|
|
133
|
+
|
|
134
|
+
const agentId = `chat-api-${conversationId}-${Date.now()}`;
|
|
135
|
+
const sessionId = `sess_${randomUUID()}`;
|
|
136
|
+
const contributorId = ConsentManager.getOrCreateUserId();
|
|
137
|
+
const metadata = {
|
|
138
|
+
model_engine: model,
|
|
139
|
+
provider,
|
|
140
|
+
agent_role: 'chat',
|
|
141
|
+
agent_id: agentId,
|
|
142
|
+
task_complexity: 'medium',
|
|
143
|
+
team_size: 1,
|
|
144
|
+
session_quality: 0,
|
|
145
|
+
groove_version: this._grooveVersion,
|
|
146
|
+
leaf_context: null,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const builder = new EnvelopeBuilder(sessionId, contributorId, metadata);
|
|
150
|
+
const classifier = new StepClassifier();
|
|
151
|
+
|
|
152
|
+
const ctx = {
|
|
153
|
+
sessionId,
|
|
154
|
+
parser: null,
|
|
155
|
+
builder,
|
|
156
|
+
classifier,
|
|
157
|
+
metadata,
|
|
158
|
+
stepCount: 0,
|
|
159
|
+
chunkCount: 0,
|
|
160
|
+
totalTokens: 0,
|
|
161
|
+
errorsEncountered: 0,
|
|
162
|
+
errorsRecovered: 0,
|
|
163
|
+
filesModified: 0,
|
|
164
|
+
coordinationEvents: 0,
|
|
165
|
+
startTime: Date.now(),
|
|
166
|
+
chunkTimer: null,
|
|
167
|
+
allSteps: [],
|
|
168
|
+
revisionRounds: 0,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
this._contexts.set(agentId, ctx);
|
|
172
|
+
|
|
173
|
+
if (message && typeof message === 'string' && message.trim()) {
|
|
174
|
+
this._processStep(agentId, ctx, {
|
|
175
|
+
type: 'instruction',
|
|
176
|
+
content: message.slice(0, USER_MESSAGE_MAX_CHARS),
|
|
177
|
+
source: 'user',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this._attestation.openSession(sessionId, metadata).catch(() => {});
|
|
182
|
+
|
|
183
|
+
return agentId;
|
|
184
|
+
}
|
|
185
|
+
|
|
131
186
|
onStdoutLine(agentId, jsonLine) {
|
|
132
187
|
if (!this._enabled) return;
|
|
133
188
|
const ctx = this._contexts.get(agentId);
|
|
@@ -354,6 +354,69 @@ describe('TrajectoryCapture — planner/conversational eligibility', () => {
|
|
|
354
354
|
});
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
+
describe('TrajectoryCapture — API chat capture via onChatTurnStart', () => {
|
|
358
|
+
function makeChatTc() {
|
|
359
|
+
const tc = makeTc();
|
|
360
|
+
tc._enabled = true;
|
|
361
|
+
tc._scrubber = { scrub: (s) => s };
|
|
362
|
+
tc._attestation = { openSession: async () => {}, closeSession: async () => {}, signEnvelope: (sid, e) => e };
|
|
363
|
+
tc._transmissionQueue = { enqueue: () => {}, waitForDrain: async () => {} };
|
|
364
|
+
tc._domainTagger = null;
|
|
365
|
+
return tc;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
it('returns a synthetic agent ID and creates context', () => {
|
|
369
|
+
const tc = makeChatTc();
|
|
370
|
+
const agentId = tc.onChatTurnStart('conv-123', 'claude-code', 'opus', 'What is React?');
|
|
371
|
+
assert.ok(agentId);
|
|
372
|
+
assert.ok(agentId.startsWith('chat-api-conv-123-'));
|
|
373
|
+
const ctx = tc._contexts.get(agentId);
|
|
374
|
+
assert.ok(ctx);
|
|
375
|
+
assert.equal(ctx.metadata.agent_role, 'chat');
|
|
376
|
+
assert.equal(ctx.metadata.provider, 'claude-code');
|
|
377
|
+
assert.equal(ctx.metadata.model_engine, 'opus');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('records the user message as an instruction step', () => {
|
|
381
|
+
const tc = makeChatTc();
|
|
382
|
+
const agentId = tc.onChatTurnStart('conv-456', 'claude-code', 'opus', 'Explain hooks');
|
|
383
|
+
const ctx = tc._contexts.get(agentId);
|
|
384
|
+
assert.equal(ctx.stepCount, 1);
|
|
385
|
+
assert.equal(ctx.allSteps[0].type, 'instruction');
|
|
386
|
+
assert.ok(ctx.allSteps[0].content.includes('Explain hooks'));
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('works with onParsedOutput and onAgentComplete', async () => {
|
|
390
|
+
const tc = makeChatTc();
|
|
391
|
+
const agentId = tc.onChatTurnStart('conv-789', 'claude-code', 'opus', 'Tell me about React');
|
|
392
|
+
|
|
393
|
+
tc.onParsedOutput(agentId, { type: 'activity', subtype: 'assistant', data: 'React is a UI library' });
|
|
394
|
+
tc.onParsedOutput(agentId, { type: 'result', data: 'React is a UI library' });
|
|
395
|
+
|
|
396
|
+
const ctx = tc._contexts.get(agentId);
|
|
397
|
+
assert.equal(ctx.stepCount, 3);
|
|
398
|
+
assert.equal(ctx.allSteps[1].type, 'thought');
|
|
399
|
+
assert.equal(ctx.allSteps[2].type, 'resolution');
|
|
400
|
+
|
|
401
|
+
await tc.onAgentComplete(agentId, { status: 'SUCCESS' });
|
|
402
|
+
assert.equal(tc._contexts.has(agentId), false);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('returns null when disabled', () => {
|
|
406
|
+
const tc = makeChatTc();
|
|
407
|
+
tc._enabled = false;
|
|
408
|
+
const agentId = tc.onChatTurnStart('conv-000', 'claude-code', 'opus', 'Hello');
|
|
409
|
+
assert.equal(agentId, null);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('context has no parser (not needed for API chat)', () => {
|
|
413
|
+
const tc = makeChatTc();
|
|
414
|
+
const agentId = tc.onChatTurnStart('conv-nop', 'claude-code', 'opus', 'Hello');
|
|
415
|
+
const ctx = tc._contexts.get(agentId);
|
|
416
|
+
assert.equal(ctx.parser, null);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
357
420
|
describe('TrajectoryCapture — initial prompt capture', () => {
|
|
358
421
|
function makeSpawnTc() {
|
|
359
422
|
const tc = makeTc();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.121",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
5
|
import { resolve } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
6
7
|
import { Daemon } from '@groove-dev/daemon';
|
|
7
8
|
import chalk from 'chalk';
|
|
8
9
|
import { runSetupWizard, saveKeysViaDaemon } from '../setup.js';
|
|
9
10
|
|
|
10
11
|
export async function start(options) {
|
|
11
|
-
const grooveDir = process.env.GROOVE_DIR || resolve(
|
|
12
|
+
const grooveDir = process.env.GROOVE_DIR || resolve(homedir(), '.groove');
|
|
12
13
|
const isFirstRun = !existsSync(resolve(grooveDir, 'config.json'));
|
|
13
14
|
|
|
14
15
|
// ── First-run interactive wizard ────────────────────────────
|
|
@@ -1147,6 +1147,16 @@ export function createApi(app, daemon) {
|
|
|
1147
1147
|
}
|
|
1148
1148
|
});
|
|
1149
1149
|
|
|
1150
|
+
app.post('/api/teams/:id/promote', (req, res) => {
|
|
1151
|
+
try {
|
|
1152
|
+
const team = daemon.teams.promote(req.params.id);
|
|
1153
|
+
daemon.audit.log('team.promote', { id: team.id, name: team.name });
|
|
1154
|
+
res.json(team);
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
res.status(400).json({ error: err.message });
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1150
1160
|
// --- Conversations ---
|
|
1151
1161
|
|
|
1152
1162
|
app.get('/api/conversations', (req, res) => {
|
|
@@ -2957,8 +2967,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2957
2967
|
|
|
2958
2968
|
try {
|
|
2959
2969
|
const stat = statSync(result.fullPath);
|
|
2960
|
-
if (stat.size >
|
|
2961
|
-
return res.status(400).json({ error: 'File too large (>
|
|
2970
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
2971
|
+
return res.status(400).json({ error: 'File too large (>50MB)' });
|
|
2962
2972
|
}
|
|
2963
2973
|
|
|
2964
2974
|
// Binary detection: check first 8KB for null bytes
|
|
@@ -2990,8 +3000,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2990
3000
|
if (typeof content !== 'string') {
|
|
2991
3001
|
return res.status(400).json({ error: 'content must be a string' });
|
|
2992
3002
|
}
|
|
2993
|
-
if (content.length >
|
|
2994
|
-
return res.status(400).json({ error: 'Content too large (>
|
|
3003
|
+
if (content.length > 50 * 1024 * 1024) {
|
|
3004
|
+
return res.status(400).json({ error: 'Content too large (>50MB)' });
|
|
2995
3005
|
}
|
|
2996
3006
|
|
|
2997
3007
|
try {
|
|
@@ -3435,6 +3445,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3435
3445
|
const plannerAgent = found.agentId ? daemon.registry.get(found.agentId) : null;
|
|
3436
3446
|
const baseDir = plannerAgent?.workingDir || daemon.config?.defaultWorkingDir || daemon.projectDir;
|
|
3437
3447
|
const plannerProvider = plannerAgent?.provider || undefined;
|
|
3448
|
+
const plannerModel = plannerAgent?.model || undefined;
|
|
3438
3449
|
|
|
3439
3450
|
// Use the planner's teamId so launched agents join the correct team.
|
|
3440
3451
|
// Priority: explicit from frontend > agent that wrote the file > most recent planner > default
|
|
@@ -3477,6 +3488,15 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3477
3488
|
});
|
|
3478
3489
|
}
|
|
3479
3490
|
|
|
3491
|
+
function resolveProviderAndModel(cfgProvider, cfgModel, fallbackProvider, fallbackModel) {
|
|
3492
|
+
const provider = cfgProvider || plannerProvider || daemon.config?.defaultProvider || fallbackProvider || undefined;
|
|
3493
|
+
if (cfgModel) return { provider, model: cfgModel };
|
|
3494
|
+
if (!cfgProvider && plannerProvider && plannerProvider !== daemon.config?.defaultProvider) {
|
|
3495
|
+
return { provider, model: plannerModel || 'auto' };
|
|
3496
|
+
}
|
|
3497
|
+
return { provider, model: daemon.config?.defaultModel || fallbackModel || 'auto' };
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3480
3500
|
// Team-level overrides from the pre-planner config panel
|
|
3481
3501
|
const teamProvider = req.body?.teamProvider || undefined;
|
|
3482
3502
|
const teamModel = req.body?.teamModel || undefined;
|
|
@@ -3500,10 +3520,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3500
3520
|
|
|
3501
3521
|
// Safety net: if planner forgot the QC agent, auto-add one
|
|
3502
3522
|
if (phase2.length === 0 && phase1.length >= 2) {
|
|
3523
|
+
const { provider: qcProvider, model: qcModel } = resolveProviderAndModel(teamProvider, teamModel);
|
|
3503
3524
|
phase2 = [{
|
|
3504
3525
|
name: 'qc-agent',
|
|
3505
3526
|
role: 'fullstack', phase: 2, scope: [],
|
|
3506
|
-
provider:
|
|
3527
|
+
provider: qcProvider,
|
|
3528
|
+
model: qcModel,
|
|
3507
3529
|
prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests, and verify the project builds cleanly (npm run build). Do NOT start long-running dev servers — just verify the build succeeds. Commit all changes. IMPORTANT: Do NOT delete files from other projects or directories outside this project.',
|
|
3508
3530
|
}];
|
|
3509
3531
|
}
|
|
@@ -3556,8 +3578,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3556
3578
|
role: existing.role,
|
|
3557
3579
|
scope: normalizeScope(config.scope || existing.scope || [], existing.workingDir || projectWorkingDir),
|
|
3558
3580
|
prompt,
|
|
3559
|
-
|
|
3560
|
-
model: config.model || existing.model || daemon.config?.defaultModel || 'auto',
|
|
3581
|
+
...resolveProviderAndModel(config.provider, config.model, existing.provider, existing.model),
|
|
3561
3582
|
permission: config.permission || existing.permission || 'auto',
|
|
3562
3583
|
workingDir: existing.workingDir || projectWorkingDir,
|
|
3563
3584
|
name: existing.name,
|
|
@@ -3581,8 +3602,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3581
3602
|
role: config.role,
|
|
3582
3603
|
scope: normalizeScope(config.scope || [], config.workingDir || projectWorkingDir),
|
|
3583
3604
|
prompt,
|
|
3584
|
-
|
|
3585
|
-
model: config.model || daemon.config?.defaultModel || 'auto',
|
|
3605
|
+
...resolveProviderAndModel(config.provider, config.model),
|
|
3586
3606
|
permission: config.permission || 'auto',
|
|
3587
3607
|
workingDir: config.workingDir || projectWorkingDir,
|
|
3588
3608
|
name: config.name || undefined,
|
|
@@ -3624,7 +3644,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3624
3644
|
waitFor: phase1Ids,
|
|
3625
3645
|
agents: phase2.map((c) => ({
|
|
3626
3646
|
role: c.role, scope: c.scope || [], prompt: c.prompt || '',
|
|
3627
|
-
|
|
3647
|
+
...resolveProviderAndModel(c.provider, c.model),
|
|
3628
3648
|
permission: c.permission || 'auto',
|
|
3629
3649
|
reasoningEffort: c.reasoningEffort, temperature: c.temperature, verbosity: c.verbosity,
|
|
3630
3650
|
workingDir: c.workingDir || projectWorkingDir,
|