groove-dev 0.27.103 → 0.27.106

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.
Files changed (39) hide show
  1. package/moe-training/client/index.js +1 -0
  2. package/moe-training/client/step-classifier.js +1 -1
  3. package/moe-training/client/trajectory-capture.js +52 -7
  4. package/moe-training/test/client/step-classifier.test.js +42 -0
  5. package/moe-training/test/client/trajectory-capture.test.js +199 -0
  6. package/node_modules/@groove-dev/cli/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/package.json +1 -1
  8. package/node_modules/@groove-dev/daemon/src/api.js +21 -7
  9. package/node_modules/@groove-dev/daemon/src/process.js +54 -0
  10. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +7 -7
  11. package/node_modules/@groove-dev/daemon/src/providers/codex.js +1 -1
  12. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +1 -1
  13. package/node_modules/@groove-dev/gui/dist/assets/{index-8gdXdRnq.js → index-BN7fQKaF.js} +23 -23
  14. package/node_modules/@groove-dev/gui/dist/assets/{index-C1ObKizg.css → index-QwgLRN8B.css} +1 -1
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/package.json +1 -1
  17. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +6 -0
  18. package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +57 -7
  19. package/node_modules/@groove-dev/gui/src/views/settings.jsx +35 -2
  20. package/node_modules/moe-training/client/index.js +1 -0
  21. package/node_modules/moe-training/client/step-classifier.js +1 -1
  22. package/node_modules/moe-training/client/trajectory-capture.js +52 -7
  23. package/node_modules/moe-training/test/client/step-classifier.test.js +42 -0
  24. package/node_modules/moe-training/test/client/trajectory-capture.test.js +199 -0
  25. package/package.json +1 -1
  26. package/packages/cli/package.json +1 -1
  27. package/packages/daemon/package.json +1 -1
  28. package/packages/daemon/src/api.js +21 -7
  29. package/packages/daemon/src/process.js +54 -0
  30. package/packages/daemon/src/providers/claude-code.js +7 -7
  31. package/packages/daemon/src/providers/codex.js +1 -1
  32. package/packages/daemon/src/providers/gemini.js +1 -1
  33. package/packages/gui/dist/assets/{index-8gdXdRnq.js → index-BN7fQKaF.js} +23 -23
  34. package/packages/gui/dist/assets/{index-C1ObKizg.css → index-QwgLRN8B.css} +1 -1
  35. package/packages/gui/dist/index.html +2 -2
  36. package/packages/gui/package.json +1 -1
  37. package/packages/gui/src/components/onboarding/setup-wizard.jsx +6 -0
  38. package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +57 -7
  39. package/packages/gui/src/views/settings.jsx +35 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.103",
3
+ "version": "0.27.106",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -131,7 +131,13 @@ function ProjectFolderStep({ selectedDir, onSelectDir }) {
131
131
  const remoteHomedir = useGrooveStore((s) => s.remoteHomedir);
132
132
  const [browsing, setBrowsing] = useState(false);
133
133
 
134
+ const isRemote = new URLSearchParams(window.location.search).has('instance');
135
+
134
136
  async function handleBrowse() {
137
+ if (isRemote) {
138
+ setBrowsing(true);
139
+ return;
140
+ }
135
141
  const dir = await selectFolder({
136
142
  title: 'Choose your project folder',
137
143
  defaultPath: selectedDir || undefined,
@@ -145,9 +145,9 @@ function InstallStep({ providerId, meta }) {
145
145
 
146
146
  {hasError && (
147
147
  <div className="space-y-3">
148
- {typeof hasError === 'string' && hasError.length > 40 && (
149
- <div className="p-3 bg-surface-0 border border-border-subtle rounded-md max-h-24 overflow-y-auto">
150
- <p className="text-2xs font-mono text-danger/80 whitespace-pre-wrap break-all">{hasError}</p>
148
+ {typeof hasError === 'string' && (
149
+ <div className="p-3 bg-danger/5 border border-danger/20 rounded-md max-h-32 overflow-y-auto">
150
+ <p className="text-2xs font-mono text-danger whitespace-pre-wrap break-all">{hasError}</p>
151
151
  </div>
152
152
  )}
153
153
  <p className="text-xs text-text-2 font-sans">Check that npm is in your PATH and try again.</p>
@@ -172,6 +172,8 @@ function AuthenticateStep({ providerId, meta, onSaveKey }) {
172
172
  const [showKey, setShowKey] = useState(false);
173
173
  const [saving, setSaving] = useState(false);
174
174
  const [loginStarted, setLoginStarted] = useState(false);
175
+ const [verifying, setVerifying] = useState(false);
176
+ const [verifyError, setVerifyError] = useState('');
175
177
  const [authMode, setAuthMode] = useState(meta.authType === 'subscription' ? 'subscription' : 'apikey');
176
178
 
177
179
  async function handleSaveKey() {
@@ -261,9 +263,33 @@ function AuthenticateStep({ providerId, meta, onSaveKey }) {
261
263
  <ExternalLink size={12} className="text-accent" />
262
264
  <span className="text-xs text-accent font-sans">Sign-in opened in your browser</span>
263
265
  </div>
264
- <Button variant="primary" size="sm" onClick={onSaveKey} className="gap-1.5">
265
- <Check size={11} /> I've signed in
266
+ <Button
267
+ variant="primary"
268
+ size="sm"
269
+ disabled={verifying}
270
+ onClick={async () => {
271
+ setVerifying(true);
272
+ setVerifyError('');
273
+ try {
274
+ const res = await api.post(`/providers/${providerId}/verify`);
275
+ if (res.authenticated) {
276
+ onSaveKey();
277
+ } else {
278
+ setVerifyError('Authentication not detected yet. Please complete sign-in in your browser and try again.');
279
+ }
280
+ } catch {
281
+ setVerifyError('Authentication not detected yet. Please complete sign-in in your browser and try again.');
282
+ } finally {
283
+ setVerifying(false);
284
+ }
285
+ }}
286
+ className="gap-1.5"
287
+ >
288
+ {verifying ? <Loader2 size={11} className="animate-spin" /> : <Check size={11} />} I've signed in
266
289
  </Button>
290
+ {verifyError && (
291
+ <p className="text-2xs text-danger font-sans">{verifyError}</p>
292
+ )}
267
293
  </div>
268
294
  )}
269
295
  <p className="text-2xs text-text-4 font-sans">
@@ -287,9 +313,33 @@ function AuthenticateStep({ providerId, meta, onSaveKey }) {
287
313
  <ExternalLink size={12} className="text-accent" />
288
314
  <span className="text-xs text-accent font-sans">Sign-in opened in your browser</span>
289
315
  </div>
290
- <Button variant="primary" size="sm" onClick={onSaveKey} className="gap-1.5">
291
- <Check size={11} /> I've signed in
316
+ <Button
317
+ variant="primary"
318
+ size="sm"
319
+ disabled={verifying}
320
+ onClick={async () => {
321
+ setVerifying(true);
322
+ setVerifyError('');
323
+ try {
324
+ const res = await api.post(`/providers/codex/verify`);
325
+ if (res.authenticated) {
326
+ onSaveKey();
327
+ } else {
328
+ setVerifyError('Authentication not detected yet. Please complete sign-in in your browser and try again.');
329
+ }
330
+ } catch {
331
+ setVerifyError('Authentication not detected yet. Please complete sign-in in your browser and try again.');
332
+ } finally {
333
+ setVerifying(false);
334
+ }
335
+ }}
336
+ className="gap-1.5"
337
+ >
338
+ {verifying ? <Loader2 size={11} className="animate-spin" /> : <Check size={11} />} I've signed in
292
339
  </Button>
340
+ {verifyError && (
341
+ <p className="text-2xs text-danger font-sans">{verifyError}</p>
342
+ )}
293
343
  </div>
294
344
  )}
295
345
  </div>
@@ -454,10 +454,43 @@ function ProviderCard({ provider, onKeyChange }) {
454
454
  <Button
455
455
  variant="primary"
456
456
  size="sm"
457
- onClick={() => { setLoginPending(false); if (onKeyChange) onKeyChange(); }}
457
+ disabled={checking}
458
+ onClick={async () => {
459
+ if (provider.id === 'codex') {
460
+ setChecking(true);
461
+ try {
462
+ const res = await api.post(`/providers/codex/verify`);
463
+ if (res.authenticated) {
464
+ setLoginPending(false);
465
+ if (onKeyChange) onKeyChange();
466
+ } else {
467
+ addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
468
+ }
469
+ } catch {
470
+ addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
471
+ } finally {
472
+ setChecking(false);
473
+ }
474
+ } else {
475
+ setChecking(true);
476
+ try {
477
+ const res = await api.post(`/providers/claude-code/verify`);
478
+ if (res.authenticated) {
479
+ setLoginPending(false);
480
+ if (onKeyChange) onKeyChange();
481
+ } else {
482
+ addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
483
+ }
484
+ } catch {
485
+ addToast('error', 'Authentication not detected yet. Please complete sign-in in your browser and try again.');
486
+ } finally {
487
+ setChecking(false);
488
+ }
489
+ }
490
+ }}
458
491
  className="w-full h-8 text-xs gap-1.5"
459
492
  >
460
- <Check size={12} /> I've signed in
493
+ {checking ? <Loader2 size={12} className="animate-spin" /> : <Check size={12} />} I've signed in
461
494
  </Button>
462
495
  <button
463
496
  onClick={() => setLoginPending(false)}
@@ -8,3 +8,4 @@ export { TransmissionQueue } from './transmission-queue.js';
8
8
  export { EnvelopeBuilder } from './envelope-builder.js';
9
9
  export { StepClassifier } from './step-classifier.js';
10
10
  export { getParser } from './parsers/index.js';
11
+ export { DomainTagger } from './domain-tagger.js';
@@ -38,7 +38,7 @@ export class StepClassifier {
38
38
 
39
39
  const content = step.content || '';
40
40
 
41
- if ((step.type === 'action' || step.type === 'observation') && ERROR_SIGNAL_RE.test(content)) {
41
+ if ((step.type === 'action' || step.type === 'observation') && step.is_error !== false && ERROR_SIGNAL_RE.test(content)) {
42
42
  step.type = 'error';
43
43
  }
44
44
 
@@ -8,6 +8,7 @@ import { StepClassifier } from './step-classifier.js';
8
8
  import { EnvelopeBuilder } from './envelope-builder.js';
9
9
  import { SessionAttestation } from './session-attestation.js';
10
10
  import { TransmissionQueue } from './transmission-queue.js';
11
+ import { DomainTagger } from './domain-tagger.js';
11
12
  import {
12
13
  CHUNK_TIMEOUT_MS,
13
14
  CENTRAL_COMMAND_URL,
@@ -33,7 +34,7 @@ export class TrajectoryCapture {
33
34
  this._contexts = new Map();
34
35
  }
35
36
 
36
- init() {
37
+ async init() {
37
38
  if (!ConsentManager.isCaptureEnabled()) {
38
39
  this._enabled = false;
39
40
  return;
@@ -43,6 +44,8 @@ export class TrajectoryCapture {
43
44
  this._attestation = new SessionAttestation(this._centralCommandUrl);
44
45
  this._transmissionQueue = new TransmissionQueue(this._centralCommandUrl);
45
46
  this._transmissionQueue.start();
47
+ this._domainTagger = new DomainTagger();
48
+ await this._domainTagger.init();
46
49
  this._offlineRetryTimer = setInterval(() => {
47
50
  this._retryOfflineQueue();
48
51
  }, OFFLINE_RETRY_INTERVAL_MS);
@@ -126,10 +129,6 @@ export class TrajectoryCapture {
126
129
  if (resolved) ctx.metadata.model_engine = resolved;
127
130
  }
128
131
 
129
- const tokens = ctx.parser.extractTokens(jsonEvent);
130
- if (tokens) {
131
- ctx.totalTokens += (tokens.input || 0) + (tokens.output || 0);
132
- }
133
132
  }
134
133
 
135
134
  onUserMessage(agentId, text) {
@@ -155,6 +154,42 @@ export class TrajectoryCapture {
155
154
  this._processStep(agentId, ctx, classified);
156
155
  }
157
156
 
157
+ onParsedOutput(agentId, output) {
158
+ if (!this._enabled) return;
159
+ const ctx = this._contexts.get(agentId);
160
+ if (!ctx || !output || !output.type) return;
161
+
162
+ if (output.type === 'activity') {
163
+ if (output.subtype === 'assistant') {
164
+ this._processStep(agentId, ctx, { type: 'thought', content: output.data || '' });
165
+ } else if (output.subtype === 'tool_use' && Array.isArray(output.data)) {
166
+ for (const item of output.data) {
167
+ this._processStep(agentId, ctx, {
168
+ type: 'action',
169
+ tool: item.name || '',
170
+ arguments: item.input || {},
171
+ content: `Using ${item.name || 'tool'}`,
172
+ });
173
+ }
174
+ } else if (output.subtype === 'tool_result' && Array.isArray(output.data)) {
175
+ for (const item of output.data) {
176
+ const isError = item.success === false;
177
+ this._processStep(agentId, ctx, {
178
+ type: isError ? 'error' : 'observation',
179
+ content: item.output || '',
180
+ tool: item.name || '',
181
+ is_error: isError,
182
+ });
183
+ }
184
+ }
185
+ } else if (output.type === 'result') {
186
+ this._processStep(agentId, ctx, {
187
+ type: 'resolution',
188
+ content: typeof output.data === 'string' ? output.data : '',
189
+ });
190
+ }
191
+ }
192
+
158
193
  async onAgentComplete(agentId, outcome) {
159
194
  await this._closeAgent(agentId, outcome?.status || 'SUCCESS', outcome);
160
195
  }
@@ -198,6 +233,7 @@ export class TrajectoryCapture {
198
233
  ...ev,
199
234
  };
200
235
 
236
+ ctx.totalTokens += ev.token_count;
201
237
  if (ev.type === 'error') ctx.errorsEncountered++;
202
238
  ctx.allSteps.push(step);
203
239
 
@@ -254,6 +290,14 @@ export class TrajectoryCapture {
254
290
  const userInterventions = StepClassifier.countUserInterventions(ctx.allSteps);
255
291
  const durationSeconds = Math.round((Date.now() - ctx.startTime) / 1000);
256
292
 
293
+ if (this._domainTagger) {
294
+ const role = ctx.metadata.agent_role || '';
295
+ const firstPrompt = ctx.allSteps.find((s) => s.type === 'thought')?.content || '';
296
+ const thoughtSteps = ctx.allSteps.filter((s) => s.type === 'thought');
297
+ const routingText = DomainTagger.buildRoutingText(role, firstPrompt, thoughtSteps);
298
+ ctx.metadata.domain_tags = await this._domainTagger.tag(routingText);
299
+ }
300
+
257
301
  const { tier, reason: tierReason } = this._computeQualityTier(ctx, status, userInterventions);
258
302
  const { eligible, exclusionReason } = this._computeTrainingEligibility(ctx, durationSeconds);
259
303
 
@@ -296,8 +340,9 @@ export class TrajectoryCapture {
296
340
 
297
341
  _computeQualityTier(ctx, status, userInterventions) {
298
342
  const quality = ctx.metadata.session_quality;
299
- if (quality >= TIER_A_MIN_QUALITY && ctx.errorsEncountered === 0 && userInterventions === 0 && status === 'SUCCESS') {
300
- return { tier: 'TIER_A', reason: 'high_quality_no_errors' };
343
+ if (quality >= TIER_A_MIN_QUALITY && (ctx.errorsEncountered === 0 || ctx.errorsEncountered <= ctx.errorsRecovered) && userInterventions === 0 && status === 'SUCCESS') {
344
+ const reason = ctx.errorsEncountered > 0 ? 'high_quality_errors_recovered' : 'high_quality_no_errors';
345
+ return { tier: 'TIER_A', reason };
301
346
  }
302
347
  if (status !== 'SUCCESS') {
303
348
  return { tier: 'TIER_C', reason: 'non_success_status' };
@@ -132,4 +132,46 @@ describe('StepClassifier', () => {
132
132
  assert.ok(result);
133
133
  assert.equal(result.type, 'action');
134
134
  });
135
+
136
+ it('preserves observation type when is_error is false despite error keywords', () => {
137
+ const classifier = new StepClassifier();
138
+ const step = { type: 'observation', content: 'Cannot find module foo', is_error: false };
139
+ const result = classifier.onStep(step);
140
+ assert.equal(result.type, 'observation');
141
+ });
142
+
143
+ it('preserves observation type when is_error:false and content has TypeError', () => {
144
+ const classifier = new StepClassifier();
145
+ const step = { type: 'observation', content: 'TypeError: something failed', is_error: false };
146
+ const result = classifier.onStep(step);
147
+ assert.equal(result.type, 'observation');
148
+ });
149
+
150
+ it('still reclassifies observation to error when is_error is true', () => {
151
+ const classifier = new StepClassifier();
152
+ const step = { type: 'observation', content: 'Command failed with exit code 1', is_error: true };
153
+ const result = classifier.onStep(step);
154
+ assert.equal(result.type, 'error');
155
+ });
156
+
157
+ it('still reclassifies observation to error when is_error is undefined', () => {
158
+ const classifier = new StepClassifier();
159
+ const step = { type: 'observation', content: 'ENOENT: no such file or directory' };
160
+ const result = classifier.onStep(step);
161
+ assert.equal(result.type, 'error');
162
+ });
163
+
164
+ it('preserves action type when is_error is false', () => {
165
+ const classifier = new StepClassifier();
166
+ const step = { type: 'action', content: 'Command failed with exit code 1', is_error: false };
167
+ const result = classifier.onStep(step);
168
+ assert.equal(result.type, 'action');
169
+ });
170
+
171
+ it('still reclassifies action to error when is_error is not set', () => {
172
+ const classifier = new StepClassifier();
173
+ const step = { type: 'action', content: 'Command failed with exit code 1' };
174
+ const result = classifier.onStep(step);
175
+ assert.equal(result.type, 'error');
176
+ });
135
177
  });
@@ -292,6 +292,101 @@ describe('TrajectoryCapture — user feedback emission', () => {
292
292
  });
293
293
  });
294
294
 
295
+ describe('TrajectoryCapture — token counting via _processStep', () => {
296
+ it('accumulates token_count from every step type', () => {
297
+ const tc = makeTc();
298
+ tc._scrubber = { scrub: (s) => s };
299
+ const ctx = makeCtx();
300
+ ctx.totalTokens = 0;
301
+ ctx.stepCount = 0;
302
+ ctx.allSteps = [];
303
+ ctx.builder = { addStep: () => null };
304
+ ctx.classifier = {
305
+ onStep: (s) => s,
306
+ };
307
+
308
+ tc._processStep('agent-1', ctx, { type: 'thought', content: 'thinking about it', token_count: 50 });
309
+ tc._processStep('agent-1', ctx, { type: 'action', content: 'run test', token_count: 30 });
310
+ tc._processStep('agent-1', ctx, { type: 'observation', content: 'test passed', token_count: 100 });
311
+ tc._processStep('agent-1', ctx, { type: 'thought', content: 'next step', token_count: 20 });
312
+
313
+ assert.equal(ctx.totalTokens, 200);
314
+ assert.equal(ctx.stepCount, 4);
315
+ });
316
+
317
+ it('estimates token_count when not provided', () => {
318
+ const tc = makeTc();
319
+ tc._scrubber = { scrub: (s) => s };
320
+ const ctx = makeCtx();
321
+ ctx.totalTokens = 0;
322
+ ctx.stepCount = 0;
323
+ ctx.allSteps = [];
324
+ ctx.builder = { addStep: () => null };
325
+ ctx.classifier = {
326
+ onStep: (s) => s,
327
+ };
328
+
329
+ tc._processStep('agent-1', ctx, { type: 'thought', content: 'a'.repeat(100) });
330
+ assert.equal(ctx.totalTokens, 25);
331
+ });
332
+
333
+ it('does not double-count tokens from onStdoutLine', () => {
334
+ const tc = makeTc();
335
+ tc._scrubber = { scrub: (s) => s };
336
+ tc._enabled = true;
337
+
338
+ const ctx = makeCtx();
339
+ ctx.totalTokens = 0;
340
+ ctx.stepCount = 0;
341
+ ctx.allSteps = [];
342
+ ctx.builder = { addStep: () => null };
343
+ ctx.classifier = {
344
+ onStep: (s) => s,
345
+ };
346
+ ctx.parser = {
347
+ parseEvent: () => ({ type: 'thought', content: 'hello', token_count: 10 }),
348
+ extractModel: () => null,
349
+ };
350
+ tc._contexts.set('agent-x', ctx);
351
+
352
+ tc.onStdoutLine('agent-x', '{"type":"assistant"}');
353
+ assert.equal(ctx.totalTokens, 10);
354
+ });
355
+ });
356
+
357
+ describe('TrajectoryCapture — TIER_A with recovered errors', () => {
358
+ it('TIER_A when all errors are recovered', () => {
359
+ const tc = makeTc();
360
+ const ctx = makeCtx({ quality: 80, errorsEncountered: 2, errorsRecovered: 2 });
361
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
362
+ assert.equal(result.tier, 'TIER_A');
363
+ assert.equal(result.reason, 'high_quality_errors_recovered');
364
+ });
365
+
366
+ it('TIER_A when errors recovered exceed errors encountered', () => {
367
+ const tc = makeTc();
368
+ const ctx = makeCtx({ quality: 75, errorsEncountered: 1, errorsRecovered: 2 });
369
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
370
+ assert.equal(result.tier, 'TIER_A');
371
+ assert.equal(result.reason, 'high_quality_errors_recovered');
372
+ });
373
+
374
+ it('not TIER_A when errors exceed recoveries', () => {
375
+ const tc = makeTc();
376
+ const ctx = makeCtx({ quality: 80, errorsEncountered: 3, errorsRecovered: 1 });
377
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
378
+ assert.notEqual(result.tier, 'TIER_A');
379
+ });
380
+
381
+ it('TIER_A with zero errors still uses original reason', () => {
382
+ const tc = makeTc();
383
+ const ctx = makeCtx({ quality: 80, errorsEncountered: 0 });
384
+ const result = tc._computeQualityTier(ctx, 'SUCCESS', 0);
385
+ assert.equal(result.tier, 'TIER_A');
386
+ assert.equal(result.reason, 'high_quality_no_errors');
387
+ });
388
+ });
389
+
295
390
  describe('TrajectoryCapture — _computeQuality', () => {
296
391
  it('base score is 50', () => {
297
392
  const tc = makeTc();
@@ -343,3 +438,107 @@ describe('TrajectoryCapture — _computeQuality', () => {
343
438
  assert.equal(quality, 100);
344
439
  });
345
440
  });
441
+
442
+ describe('TrajectoryCapture — onParsedOutput', () => {
443
+ function makeEnabledTc() {
444
+ const tc = makeTc();
445
+ tc._enabled = true;
446
+ tc._scrubber = { scrub: (s) => s };
447
+ const ctx = makeCtx();
448
+ ctx.totalTokens = 0;
449
+ ctx.stepCount = 0;
450
+ ctx.allSteps = [];
451
+ ctx.builder = { addStep: () => null };
452
+ ctx.classifier = { onStep: (s) => s };
453
+ tc._contexts.set('agent-loop-1', ctx);
454
+ return { tc, ctx };
455
+ }
456
+
457
+ it('converts assistant activity to thought step', () => {
458
+ const { tc, ctx } = makeEnabledTc();
459
+ tc.onParsedOutput('agent-loop-1', { type: 'activity', subtype: 'assistant', data: 'I will fix the bug' });
460
+ assert.equal(ctx.stepCount, 1);
461
+ assert.equal(ctx.allSteps[0].type, 'thought');
462
+ assert.equal(ctx.allSteps[0].content, 'I will fix the bug');
463
+ });
464
+
465
+ it('converts tool_use activity to action step', () => {
466
+ const { tc, ctx } = makeEnabledTc();
467
+ tc.onParsedOutput('agent-loop-1', {
468
+ type: 'activity', subtype: 'tool_use',
469
+ data: [{ type: 'tool_use', name: 'Edit', input: { path: 'foo.js' } }],
470
+ });
471
+ assert.equal(ctx.stepCount, 1);
472
+ assert.equal(ctx.allSteps[0].type, 'action');
473
+ assert.equal(ctx.allSteps[0].tool, 'Edit');
474
+ });
475
+
476
+ it('converts successful tool_result to observation step', () => {
477
+ const { tc, ctx } = makeEnabledTc();
478
+ tc.onParsedOutput('agent-loop-1', {
479
+ type: 'activity', subtype: 'tool_result',
480
+ data: [{ type: 'tool_result', name: 'Bash', success: true, output: 'tests passed' }],
481
+ });
482
+ assert.equal(ctx.stepCount, 1);
483
+ assert.equal(ctx.allSteps[0].type, 'observation');
484
+ assert.equal(ctx.allSteps[0].content, 'tests passed');
485
+ });
486
+
487
+ it('converts failed tool_result to error step', () => {
488
+ const { tc, ctx } = makeEnabledTc();
489
+ tc.onParsedOutput('agent-loop-1', {
490
+ type: 'activity', subtype: 'tool_result',
491
+ data: [{ type: 'tool_result', name: 'Bash', success: false, output: 'command not found' }],
492
+ });
493
+ assert.equal(ctx.stepCount, 1);
494
+ assert.equal(ctx.allSteps[0].type, 'error');
495
+ assert.equal(ctx.allSteps[0].is_error, true);
496
+ });
497
+
498
+ it('converts result to resolution step', () => {
499
+ const { tc, ctx } = makeEnabledTc();
500
+ tc.onParsedOutput('agent-loop-1', { type: 'result', subtype: 'assistant', data: 'Task complete' });
501
+ assert.equal(ctx.stepCount, 1);
502
+ assert.equal(ctx.allSteps[0].type, 'resolution');
503
+ assert.equal(ctx.allSteps[0].content, 'Task complete');
504
+ });
505
+
506
+ it('ignores stream activity (partial deltas)', () => {
507
+ const { tc, ctx } = makeEnabledTc();
508
+ tc.onParsedOutput('agent-loop-1', { type: 'activity', subtype: 'stream', data: 'partial' });
509
+ assert.equal(ctx.stepCount, 0);
510
+ });
511
+
512
+ it('ignores token-only activity', () => {
513
+ const { tc, ctx } = makeEnabledTc();
514
+ tc.onParsedOutput('agent-loop-1', { type: 'activity', tokensUsed: 500, inputTokens: 400, outputTokens: 100 });
515
+ assert.equal(ctx.stepCount, 0);
516
+ });
517
+
518
+ it('silently returns for unknown agent', () => {
519
+ const { tc } = makeEnabledTc();
520
+ tc.onParsedOutput('unknown-agent', { type: 'activity', subtype: 'assistant', data: 'hello' });
521
+ });
522
+
523
+ it('silently returns when disabled', () => {
524
+ const { tc, ctx } = makeEnabledTc();
525
+ tc._enabled = false;
526
+ tc.onParsedOutput('agent-loop-1', { type: 'activity', subtype: 'assistant', data: 'hello' });
527
+ assert.equal(ctx.stepCount, 0);
528
+ });
529
+
530
+ it('accumulates tokens across multiple outputs', () => {
531
+ const { tc, ctx } = makeEnabledTc();
532
+ tc.onParsedOutput('agent-loop-1', { type: 'activity', subtype: 'assistant', data: 'thinking about the problem' });
533
+ tc.onParsedOutput('agent-loop-1', {
534
+ type: 'activity', subtype: 'tool_use',
535
+ data: [{ type: 'tool_use', name: 'Bash', input: { command: 'ls' } }],
536
+ });
537
+ tc.onParsedOutput('agent-loop-1', {
538
+ type: 'activity', subtype: 'tool_result',
539
+ data: [{ type: 'tool_result', name: 'Bash', success: true, output: 'file1.js\nfile2.js' }],
540
+ });
541
+ assert.equal(ctx.stepCount, 3);
542
+ assert.ok(ctx.totalTokens > 0);
543
+ });
544
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.103",
3
+ "version": "0.27.106",
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)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.103",
3
+ "version": "0.27.106",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.103",
3
+ "version": "0.27.106",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -631,9 +631,8 @@ export function createApi(app, daemon) {
631
631
 
632
632
  write({ status: 'installing', output: `Installing ${pkg}...`, progress: 0 });
633
633
 
634
- const proc = spawn('npm', ['install', '-g', pkg], {
634
+ const proc = spawn('bash', ['-lc', `npm install -g ${pkg}`], {
635
635
  stdio: ['ignore', 'pipe', 'pipe'],
636
- shell: true,
637
636
  env: { ...process.env, NODE_ENV: undefined },
638
637
  });
639
638
 
@@ -758,9 +757,25 @@ export function createApi(app, daemon) {
758
757
 
759
758
  proc.on('close', (code) => {
760
759
  clearTimeout(timeout);
761
- respond(code === 0
762
- ? { status: 'authenticated' }
763
- : { status: 'error', error: stderr.slice(-200) || `Login failed (exit ${code})` });
760
+ if (code === 0) {
761
+ let hasKey = false;
762
+ try {
763
+ const authPath = resolve(homedir(), '.codex', 'auth.json');
764
+ if (existsSync(authPath)) {
765
+ const auth = JSON.parse(readFileSync(authPath, 'utf8'));
766
+ const token = auth.OPENAI_API_KEY
767
+ || (auth.auth_mode === 'chatgpt' && auth.tokens?.id_token)
768
+ || null;
769
+ if (token) {
770
+ daemon.credentials.setKey('codex', token);
771
+ hasKey = true;
772
+ }
773
+ }
774
+ } catch { /* auth.json missing or malformed — login still succeeded */ }
775
+ respond({ status: 'authenticated', hasKey });
776
+ } else {
777
+ respond({ status: 'error', error: stderr.slice(-200) || `Login failed (exit ${code})` });
778
+ }
764
779
  });
765
780
 
766
781
  proc.on('error', (err) => {
@@ -4713,9 +4728,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
4713
4728
 
4714
4729
  write({ status: 'installing', output: `Installing ${pkg}...`, progress: 0 });
4715
4730
 
4716
- const proc = spawn('npm', ['install', '-g', pkg], {
4731
+ const proc = spawn('bash', ['-lc', `npm install -g ${pkg}`], {
4717
4732
  stdio: ['ignore', 'pipe', 'pipe'],
4718
- shell: true,
4719
4733
  env: { ...process.env, NODE_ENV: undefined },
4720
4734
  });
4721
4735