groove-dev 0.27.110 → 0.27.112
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/EMBEDDING_SERVICE_BUILD_PLAN.md +200 -0
- package/TRAINING_DATA_v2.md +9 -0
- package/moe-training/client/consent.js +47 -55
- package/moe-training/client/domain-tagger.js +3 -1
- package/moe-training/client/trajectory-capture.js +3 -2
- package/moe-training/shared/constants.js +1 -0
- package/moe-training/test/client/consent.test.js +23 -20
- package/moe-training/test/client/domain-tagger.test.js +6 -4
- 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 +24 -42
- package/node_modules/@groove-dev/daemon/src/index.js +8 -10
- package/node_modules/@groove-dev/gui/dist/assets/{index-B8JomvGM.js → index-CHu5w3i3.js} +1 -1
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +1 -1
- package/node_modules/moe-training/client/consent.js +47 -55
- package/node_modules/moe-training/client/domain-tagger.js +3 -1
- package/node_modules/moe-training/client/trajectory-capture.js +3 -2
- package/node_modules/moe-training/shared/constants.js +1 -0
- package/node_modules/moe-training/test/client/consent.test.js +23 -20
- package/node_modules/moe-training/test/client/domain-tagger.test.js +6 -4
- 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 +24 -42
- package/packages/daemon/src/index.js +8 -10
- package/packages/gui/dist/assets/{index-B8JomvGM.js → index-CHu5w3i3.js} +1 -1
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/stores/groove.js +1 -1
- package/TRAINING_DATA.md +0 -12
- package/codex/browser-racing-game/README.md +0 -45
- package/codex/browser-racing-game/dist/assets/index-D-sGTraQ.js +0 -47
- package/codex/browser-racing-game/dist/assets/index-S75nJv69.css +0 -1
- package/codex/browser-racing-game/dist/index.html +0 -14
- package/codex/browser-racing-game/index.html +0 -13
- package/codex/browser-racing-game/package-lock.json +0 -841
- package/codex/browser-racing-game/package.json +0 -15
- package/codex/browser-racing-game/src/app.css +0 -359
- package/codex/browser-racing-game/src/main.ts +0 -913
- package/codex/browser-racing-game/tsconfig.json +0 -20
- package/codex/browser-racing-game/vite.config.ts +0 -12
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-CHu5w3i3.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-CFF1Lrnz.js">
|
|
@@ -2702,7 +2702,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2702
2702
|
set({ trainingOptIn: enabled });
|
|
2703
2703
|
if (!enabled) set({ trainingStats: null });
|
|
2704
2704
|
} catch (e) {
|
|
2705
|
-
get().addToast('error', 'Failed to update training preference');
|
|
2705
|
+
get().addToast('error', 'Failed to update training preference', e.body?.detail || e.message);
|
|
2706
2706
|
}
|
|
2707
2707
|
},
|
|
2708
2708
|
|
|
@@ -1,44 +1,46 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
-
import Database from 'better-sqlite3';
|
|
4
3
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
5
|
import { join } from 'node:path';
|
|
7
6
|
import { homedir } from 'node:os';
|
|
8
7
|
import { CURRENT_CONSENT_VERSION } from '../shared/constants.js';
|
|
9
8
|
|
|
9
|
+
function ensureDir(filePath) {
|
|
10
|
+
const dir = filePath.replace(/[/\\][^/\\]+$/, '');
|
|
11
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readJSON(filePath) {
|
|
15
|
+
if (!existsSync(filePath)) return null;
|
|
16
|
+
try { return JSON.parse(readFileSync(filePath, 'utf-8')); } catch { return null; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeJSON(filePath, data) {
|
|
20
|
+
ensureDir(filePath);
|
|
21
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
export class ConsentManager {
|
|
11
|
-
constructor(
|
|
12
|
-
this.
|
|
13
|
-
const dir = this._dbPath.replace(/[/\\][^/\\]+$/, '');
|
|
14
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
15
|
-
this._db = new Database(this._dbPath);
|
|
16
|
-
this._db.pragma('journal_mode = WAL');
|
|
17
|
-
this._db.exec(`
|
|
18
|
-
CREATE TABLE IF NOT EXISTS consent_history (
|
|
19
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
20
|
-
user_id TEXT NOT NULL,
|
|
21
|
-
opted_in INTEGER NOT NULL,
|
|
22
|
-
consent_version TEXT NOT NULL,
|
|
23
|
-
metadata TEXT,
|
|
24
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
25
|
-
)
|
|
26
|
-
`);
|
|
25
|
+
constructor(consentPath) {
|
|
26
|
+
this._path = consentPath || join(homedir(), '.groove', 'consent.json');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
recordConsent(userId, optedIn, consentVersion
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
recordConsent(userId, optedIn, consentVersion) {
|
|
30
|
+
const data = {
|
|
31
|
+
user_id: userId,
|
|
32
|
+
opted_in: !!optedIn,
|
|
33
|
+
consent_version: consentVersion,
|
|
34
|
+
updated_at: new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
writeJSON(this._path, data);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
isOptedIn(userId) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (row.consent_version !== CURRENT_CONSENT_VERSION) return false;
|
|
41
|
-
return row.opted_in === 1;
|
|
40
|
+
const data = readJSON(this._path);
|
|
41
|
+
if (!data) return false;
|
|
42
|
+
if (data.consent_version !== CURRENT_CONSENT_VERSION) return false;
|
|
43
|
+
return data.opted_in === true;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
revokeConsent(userId) {
|
|
@@ -46,34 +48,28 @@ export class ConsentManager {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
getOptedInCount() {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
AND consent_version = ?
|
|
53
|
-
AND id = (SELECT MAX(id) FROM consent_history ch2 WHERE ch2.user_id = ch1.user_id)
|
|
54
|
-
`).get(CURRENT_CONSENT_VERSION);
|
|
55
|
-
return row?.cnt || 0;
|
|
51
|
+
const data = readJSON(this._path);
|
|
52
|
+
if (!data || !data.opted_in || data.consent_version !== CURRENT_CONSENT_VERSION) return 0;
|
|
53
|
+
return 1;
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
getConsentHistory(userId) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
const data = readJSON(this._path);
|
|
58
|
+
if (!data) return [];
|
|
59
|
+
return [{
|
|
60
|
+
user_id: data.user_id,
|
|
61
|
+
opted_in: data.opted_in,
|
|
62
|
+
consent_version: data.consent_version,
|
|
63
|
+
created_at: data.updated_at,
|
|
64
|
+
metadata: null,
|
|
65
|
+
}];
|
|
67
66
|
}
|
|
68
67
|
|
|
69
|
-
close() {
|
|
70
|
-
this._db.close();
|
|
71
|
-
}
|
|
68
|
+
close() {}
|
|
72
69
|
|
|
73
70
|
static getOrCreateUserId(userIdPath) {
|
|
74
71
|
const filePath = userIdPath || join(homedir(), '.groove', 'user_id');
|
|
75
|
-
|
|
76
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
72
|
+
ensureDir(filePath);
|
|
77
73
|
if (existsSync(filePath)) {
|
|
78
74
|
return readFileSync(filePath, 'utf-8').trim();
|
|
79
75
|
}
|
|
@@ -82,15 +78,11 @@ export class ConsentManager {
|
|
|
82
78
|
return uid;
|
|
83
79
|
}
|
|
84
80
|
|
|
85
|
-
static isCaptureEnabled(userIdPath,
|
|
81
|
+
static isCaptureEnabled(userIdPath, consentPath) {
|
|
86
82
|
const filePath = userIdPath || join(homedir(), '.groove', 'user_id');
|
|
87
83
|
if (!existsSync(filePath)) return false;
|
|
84
|
+
const manager = new ConsentManager(consentPath);
|
|
88
85
|
const userId = readFileSync(filePath, 'utf-8').trim();
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
return manager.isOptedIn(userId);
|
|
92
|
-
} finally {
|
|
93
|
-
manager.close();
|
|
94
|
-
}
|
|
86
|
+
return manager.isOptedIn(userId);
|
|
95
87
|
}
|
|
96
88
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
+
import { EMBEDDING_SERVICE_URL } from '../shared/constants.js';
|
|
4
|
+
|
|
3
5
|
const DEFAULT_MODEL = 'sentence-transformers/all-MiniLM-L6-v2';
|
|
4
6
|
const DEFAULT_TOP_K = 3;
|
|
5
7
|
|
|
@@ -171,7 +173,7 @@ const DOMAIN_TAXONOMY = {
|
|
|
171
173
|
|
|
172
174
|
export class DomainTagger {
|
|
173
175
|
constructor(options = {}) {
|
|
174
|
-
this._serviceUrl = options.serviceUrl
|
|
176
|
+
this._serviceUrl = options.serviceUrl !== undefined ? options.serviceUrl : EMBEDDING_SERVICE_URL;
|
|
175
177
|
this._registryUrl = options.registryUrl || process.env.LEAF_REGISTRY_URL || null;
|
|
176
178
|
this._registry = options.registry || null;
|
|
177
179
|
this._model = options.model || DEFAULT_MODEL;
|
|
@@ -298,9 +298,10 @@ export class TrajectoryCapture {
|
|
|
298
298
|
|
|
299
299
|
if (this._domainTagger) {
|
|
300
300
|
const role = ctx.metadata.agent_role || '';
|
|
301
|
-
const firstPrompt = ctx.allSteps.find((s) => s.type === 'thought')?.content || '';
|
|
302
301
|
const thoughtSteps = ctx.allSteps.filter((s) => s.type === 'thought');
|
|
303
|
-
const
|
|
302
|
+
const firstPrompt = thoughtSteps[0]?.content || '';
|
|
303
|
+
const remainingThoughts = thoughtSteps.slice(1);
|
|
304
|
+
const routingText = DomainTagger.buildRoutingText(role, firstPrompt, remainingThoughts);
|
|
304
305
|
ctx.metadata.domain_tags = await this._domainTagger.tag(routingText);
|
|
305
306
|
ctx.metadata.session_embedding = await this._domainTagger.embed(routingText);
|
|
306
307
|
}
|
|
@@ -39,3 +39,4 @@ export const TRAINING_EXCLUSION_REASONS = ['too_few_steps', 'no_actions', 'no_ob
|
|
|
39
39
|
export const USER_MESSAGE_MAX_CHARS = 2000;
|
|
40
40
|
|
|
41
41
|
export const CENTRAL_COMMAND_URL = process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai';
|
|
42
|
+
export const EMBEDDING_SERVICE_URL = process.env.EMBEDDING_SERVICE_URL || `${CENTRAL_COMMAND_URL}/v1/embed`;
|
|
@@ -8,12 +8,12 @@ import { tmpdir } from 'node:os';
|
|
|
8
8
|
import { ConsentManager } from '../../client/consent.js';
|
|
9
9
|
|
|
10
10
|
describe('ConsentManager', () => {
|
|
11
|
-
let tmpDir,
|
|
11
|
+
let tmpDir, consentPath, manager;
|
|
12
12
|
|
|
13
13
|
beforeEach(() => {
|
|
14
14
|
tmpDir = mkdtempSync(join(tmpdir(), 'consent-test-'));
|
|
15
|
-
|
|
16
|
-
manager = new ConsentManager(
|
|
15
|
+
consentPath = join(tmpDir, 'consent.json');
|
|
16
|
+
manager = new ConsentManager(consentPath);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
afterEach(() => {
|
|
@@ -43,19 +43,24 @@ describe('ConsentManager', () => {
|
|
|
43
43
|
|
|
44
44
|
it('getOptedInCount counts correctly', () => {
|
|
45
45
|
manager.recordConsent('user1', true, '1.0');
|
|
46
|
-
manager.
|
|
47
|
-
manager.recordConsent('user3', false, '1.0');
|
|
48
|
-
assert.equal(manager.getOptedInCount(), 2);
|
|
46
|
+
assert.equal(manager.getOptedInCount(), 1);
|
|
49
47
|
});
|
|
50
48
|
|
|
51
|
-
it('getConsentHistory returns
|
|
52
|
-
manager.recordConsent('user1', true, '1.0'
|
|
53
|
-
manager.revokeConsent('user1');
|
|
49
|
+
it('getConsentHistory returns current state', () => {
|
|
50
|
+
manager.recordConsent('user1', true, '1.0');
|
|
54
51
|
const history = manager.getConsentHistory('user1');
|
|
55
|
-
assert.equal(history.length,
|
|
52
|
+
assert.equal(history.length, 1);
|
|
56
53
|
assert.equal(history[0].opted_in, true);
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('consent.json is written with 0o600 permissions', () => {
|
|
57
|
+
manager.recordConsent('user1', true, '1.0');
|
|
58
|
+
assert.ok(existsSync(consentPath));
|
|
59
|
+
const data = JSON.parse(readFileSync(consentPath, 'utf-8'));
|
|
60
|
+
assert.equal(data.opted_in, true);
|
|
61
|
+
assert.equal(data.consent_version, '1.0');
|
|
62
|
+
assert.equal(data.user_id, 'user1');
|
|
63
|
+
assert.ok(data.updated_at);
|
|
59
64
|
});
|
|
60
65
|
});
|
|
61
66
|
|
|
@@ -96,26 +101,24 @@ describe('ConsentManager.isCaptureEnabled', () => {
|
|
|
96
101
|
});
|
|
97
102
|
|
|
98
103
|
it('returns false when no user_id file exists', () => {
|
|
99
|
-
const result = ConsentManager.isCaptureEnabled(join(tmpDir, 'no_file'), join(tmpDir, '
|
|
104
|
+
const result = ConsentManager.isCaptureEnabled(join(tmpDir, 'no_file'), join(tmpDir, 'consent.json'));
|
|
100
105
|
assert.equal(result, false);
|
|
101
106
|
});
|
|
102
107
|
|
|
103
108
|
it('returns false when user not opted in', () => {
|
|
104
109
|
const uidPath = join(tmpDir, 'user_id');
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
const result = ConsentManager.isCaptureEnabled(uidPath, dbPath);
|
|
110
|
+
ConsentManager.getOrCreateUserId(uidPath);
|
|
111
|
+
const result = ConsentManager.isCaptureEnabled(uidPath, join(tmpDir, 'consent.json'));
|
|
108
112
|
assert.equal(result, false);
|
|
109
113
|
});
|
|
110
114
|
|
|
111
115
|
it('returns true when user is opted in', () => {
|
|
112
116
|
const uidPath = join(tmpDir, 'user_id');
|
|
113
117
|
const uid = ConsentManager.getOrCreateUserId(uidPath);
|
|
114
|
-
const
|
|
115
|
-
const mgr = new ConsentManager(
|
|
118
|
+
const consentPath = join(tmpDir, 'consent.json');
|
|
119
|
+
const mgr = new ConsentManager(consentPath);
|
|
116
120
|
mgr.recordConsent(uid, true, '1.0');
|
|
117
|
-
|
|
118
|
-
const result = ConsentManager.isCaptureEnabled(uidPath, dbPath);
|
|
121
|
+
const result = ConsentManager.isCaptureEnabled(uidPath, consentPath);
|
|
119
122
|
assert.equal(result, true);
|
|
120
123
|
});
|
|
121
124
|
});
|
|
@@ -8,7 +8,7 @@ describe('DomainTagger', () => {
|
|
|
8
8
|
let tagger;
|
|
9
9
|
|
|
10
10
|
beforeEach(async () => {
|
|
11
|
-
tagger = new DomainTagger();
|
|
11
|
+
tagger = new DomainTagger({ serviceUrl: null });
|
|
12
12
|
await tagger.init();
|
|
13
13
|
});
|
|
14
14
|
|
|
@@ -176,7 +176,7 @@ describe('DomainTagger', () => {
|
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
it('returns null when not initialized', async () => {
|
|
179
|
-
const uninit = new DomainTagger();
|
|
179
|
+
const uninit = new DomainTagger({ serviceUrl: null });
|
|
180
180
|
const result = await uninit.tag('Build a Python Django app');
|
|
181
181
|
assert.equal(result, null);
|
|
182
182
|
});
|
|
@@ -203,6 +203,7 @@ describe('DomainTagger', () => {
|
|
|
203
203
|
|
|
204
204
|
it('accepts registry option and falls back to keyword mode without embedding service', async () => {
|
|
205
205
|
const registryTagger = new DomainTagger({
|
|
206
|
+
serviceUrl: null,
|
|
206
207
|
registry: [
|
|
207
208
|
{ id: 'quantum_computing', domain_description: 'Quantum computing, qubits, quantum gates' },
|
|
208
209
|
{ id: 'bioinformatics', domain_description: 'Biology, genomics, DNA analysis' },
|
|
@@ -215,6 +216,7 @@ describe('DomainTagger', () => {
|
|
|
215
216
|
|
|
216
217
|
it('accepts registryUrl option and falls back to keyword mode when unavailable', async () => {
|
|
217
218
|
const registryTagger = new DomainTagger({
|
|
219
|
+
serviceUrl: null,
|
|
218
220
|
registryUrl: 'http://localhost:99999/api/leaves',
|
|
219
221
|
});
|
|
220
222
|
await registryTagger.init();
|
|
@@ -223,7 +225,7 @@ describe('DomainTagger', () => {
|
|
|
223
225
|
});
|
|
224
226
|
|
|
225
227
|
it('uses expanded taxonomy — has 40 domains by default', async () => {
|
|
226
|
-
const defaultTagger = new DomainTagger();
|
|
228
|
+
const defaultTagger = new DomainTagger({ serviceUrl: null });
|
|
227
229
|
await defaultTagger.init();
|
|
228
230
|
assert.ok(defaultTagger._domains.length >= 35, `Expected 35+ domains, got ${defaultTagger._domains.length}`);
|
|
229
231
|
});
|
|
@@ -239,7 +241,7 @@ describe('DomainTagger', () => {
|
|
|
239
241
|
});
|
|
240
242
|
|
|
241
243
|
it('embed() returns null when not initialized', async () => {
|
|
242
|
-
const uninit = new DomainTagger();
|
|
244
|
+
const uninit = new DomainTagger({ serviceUrl: null });
|
|
243
245
|
const result = await uninit.embed('Build a Python app');
|
|
244
246
|
assert.equal(result, null);
|
|
245
247
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.112",
|
|
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)",
|
|
@@ -112,7 +112,7 @@ export function createApi(app, daemon) {
|
|
|
112
112
|
res.setHeader('Content-Security-Policy', "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; frame-ancestors 'self'");
|
|
113
113
|
} else {
|
|
114
114
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
115
|
-
res.setHeader('Content-Security-Policy', "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self' ws://localhost:* ws://127.0.0.1:* http://localhost:* http://127.0.0.1:*; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-src 'self'
|
|
115
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self' ws://localhost:* ws://127.0.0.1:* http://localhost:* http://127.0.0.1:*; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-src 'self' http://127.0.0.1:* http://localhost:*; frame-ancestors 'none'");
|
|
116
116
|
}
|
|
117
117
|
next();
|
|
118
118
|
});
|
|
@@ -4855,9 +4855,13 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4855
4855
|
|
|
4856
4856
|
app.get('/api/training/status', (req, res) => {
|
|
4857
4857
|
let userId = null;
|
|
4858
|
-
|
|
4858
|
+
let optedIn = false;
|
|
4859
|
+
try {
|
|
4860
|
+
userId = ConsentManager.getOrCreateUserId();
|
|
4861
|
+
optedIn = ConsentManager.isCaptureEnabled();
|
|
4862
|
+
} catch (e) { /* */ }
|
|
4859
4863
|
res.json({
|
|
4860
|
-
optedIn
|
|
4864
|
+
optedIn,
|
|
4861
4865
|
userId: userId ? userId.substring(0, 8) + '...' : null,
|
|
4862
4866
|
captureActive: !!daemon.trajectoryCapture,
|
|
4863
4867
|
sessionsCaptured: daemon.state.get('training_sessions_captured') || 0,
|
|
@@ -4869,39 +4873,23 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4869
4873
|
const { enabled } = req.body;
|
|
4870
4874
|
if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'enabled must be boolean' });
|
|
4871
4875
|
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
try {
|
|
4878
|
-
const userId = ConsentManager.getOrCreateUserId();
|
|
4879
|
-
const consent = new ConsentManager();
|
|
4880
|
-
try {
|
|
4881
|
-
consent.recordConsent(userId, true, '1.0');
|
|
4882
|
-
} finally {
|
|
4883
|
-
consent.close();
|
|
4884
|
-
}
|
|
4876
|
+
try {
|
|
4877
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4878
|
+
const consent = new ConsentManager();
|
|
4879
|
+
if (enabled) {
|
|
4880
|
+
consent.recordConsent(userId, true, '1.0');
|
|
4885
4881
|
await daemon._initTrajectoryCapture();
|
|
4886
4882
|
daemon.state.set('training_enrolled_at', new Date().toISOString());
|
|
4887
|
-
}
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
if (daemon.trajectoryCapture) {
|
|
4893
|
-
try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
|
|
4894
|
-
daemon.trajectoryCapture = null;
|
|
4895
|
-
}
|
|
4896
|
-
try {
|
|
4897
|
-
const userId = ConsentManager.getOrCreateUserId();
|
|
4898
|
-
const consent = new ConsentManager();
|
|
4899
|
-
try {
|
|
4900
|
-
consent.revokeConsent(userId);
|
|
4901
|
-
} finally {
|
|
4902
|
-
consent.close();
|
|
4883
|
+
} else {
|
|
4884
|
+
consent.revokeConsent(userId);
|
|
4885
|
+
if (daemon.trajectoryCapture) {
|
|
4886
|
+
try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
|
|
4887
|
+
daemon.trajectoryCapture = null;
|
|
4903
4888
|
}
|
|
4904
|
-
}
|
|
4889
|
+
}
|
|
4890
|
+
} catch (e) {
|
|
4891
|
+
console.error('[training/opt-in] Failed to update data sharing:', e);
|
|
4892
|
+
return res.status(500).json({ error: 'Failed to update data sharing', detail: e.message });
|
|
4905
4893
|
}
|
|
4906
4894
|
|
|
4907
4895
|
daemon.broadcast({ type: 'training:status', data: { optedIn: enabled, captureActive: !!daemon.trajectoryCapture } });
|
|
@@ -4911,18 +4899,13 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4911
4899
|
|
|
4912
4900
|
app.post('/api/training/opt-in/delete', async (req, res) => {
|
|
4913
4901
|
try {
|
|
4914
|
-
|
|
4915
|
-
const
|
|
4916
|
-
|
|
4902
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4903
|
+
const consent = new ConsentManager();
|
|
4904
|
+
consent.revokeConsent(userId);
|
|
4917
4905
|
if (daemon.trajectoryCapture) {
|
|
4918
4906
|
try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
|
|
4919
4907
|
daemon.trajectoryCapture = null;
|
|
4920
4908
|
}
|
|
4921
|
-
try {
|
|
4922
|
-
const userId = ConsentManager.getOrCreateUserId();
|
|
4923
|
-
const consent = new ConsentManager();
|
|
4924
|
-
try { consent.revokeConsent(userId); } finally { consent.close(); }
|
|
4925
|
-
} catch (e) { /* */ }
|
|
4926
4909
|
daemon.broadcast({ type: 'training:status', data: { optedIn: false, captureActive: false } });
|
|
4927
4910
|
if (daemon.audit) daemon.audit.log('training.delete', {});
|
|
4928
4911
|
res.json({ ok: true, deleted: true });
|
|
@@ -4948,7 +4931,6 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4948
4931
|
'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
|
|
4949
4932
|
'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
|
|
4950
4933
|
'onboardingDismissed', 'defaultModel', 'defaultChatProvider', 'defaultChatModel',
|
|
4951
|
-
'training_opt_in',
|
|
4952
4934
|
];
|
|
4953
4935
|
for (const key of Object.keys(req.body)) {
|
|
4954
4936
|
if (!ALLOWED_KEYS.includes(key)) {
|
|
@@ -666,17 +666,15 @@ export class Daemon {
|
|
|
666
666
|
}
|
|
667
667
|
|
|
668
668
|
async _initTrajectoryCapture() {
|
|
669
|
-
if (!this.config.training_opt_in) return;
|
|
670
669
|
try {
|
|
671
|
-
if (ConsentManager.isCaptureEnabled())
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
670
|
+
if (!ConsentManager.isCaptureEnabled()) return;
|
|
671
|
+
const pkgPath = new URL('../package.json', import.meta.url);
|
|
672
|
+
const version = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
|
|
673
|
+
this.trajectoryCapture = new TrajectoryCapture({
|
|
674
|
+
centralCommandUrl: process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai',
|
|
675
|
+
grooveVersion: version,
|
|
676
|
+
});
|
|
677
|
+
this.trajectoryCapture.init();
|
|
680
678
|
} catch (e) {
|
|
681
679
|
// Training capture is never critical
|
|
682
680
|
}
|