groove-dev 0.27.112 → 0.27.113
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/TRAINING_DATA_v3.md +11 -0
- package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +44 -0
- package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +1 -0
- package/codex-test/offroad-nitro-racer/dist/index.html +23 -0
- package/codex-test/offroad-nitro-racer/index.html +21 -0
- package/codex-test/offroad-nitro-racer/package-lock.json +841 -0
- package/codex-test/offroad-nitro-racer/package.json +15 -0
- package/codex-test/offroad-nitro-racer/src/game/AI.ts +28 -0
- package/codex-test/offroad-nitro-racer/src/game/Audio.ts +63 -0
- package/codex-test/offroad-nitro-racer/src/game/Car.ts +247 -0
- package/codex-test/offroad-nitro-racer/src/game/Effects.ts +62 -0
- package/codex-test/offroad-nitro-racer/src/game/Game.ts +229 -0
- package/codex-test/offroad-nitro-racer/src/game/Input.ts +45 -0
- package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +224 -0
- package/codex-test/offroad-nitro-racer/src/game/Track.ts +158 -0
- package/codex-test/offroad-nitro-racer/src/game/UI.ts +96 -0
- package/codex-test/offroad-nitro-racer/src/game/math.ts +42 -0
- package/codex-test/offroad-nitro-racer/src/main.ts +24 -0
- package/codex-test/offroad-nitro-racer/src/style.css +291 -0
- package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +1 -0
- package/codex-test/offroad-nitro-racer/tsconfig.json +18 -0
- package/codex-test/offroad-nitro-racer/vite.config.ts +7 -0
- package/moe-training/client/parsers/codex.js +3 -3
- package/moe-training/client/parsers/gemini.js +2 -2
- package/moe-training/client/step-classifier.js +2 -2
- package/moe-training/test/client/step-classifier.test.js +63 -7
- 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 +51 -15
- package/node_modules/@groove-dev/daemon/src/index.js +22 -8
- package/node_modules/@groove-dev/gui/dist/assets/{index-CHu5w3i3.js → index-BYh6iHqL.js} +3 -3
- 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/components/preview/preview-workspace.jsx +3 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +15 -0
- package/node_modules/moe-training/client/parsers/codex.js +3 -3
- package/node_modules/moe-training/client/parsers/gemini.js +2 -2
- package/node_modules/moe-training/client/step-classifier.js +2 -2
- package/node_modules/moe-training/test/client/step-classifier.test.js +63 -7
- 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 +51 -15
- package/packages/daemon/src/index.js +22 -8
- package/packages/gui/dist/assets/{index-CHu5w3i3.js → index-BYh6iHqL.js} +3 -3
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/preview/preview-workspace.jsx +3 -1
- package/packages/gui/src/stores/groove.js +15 -0
- package/TRAINING_DATA_v2.md +0 -9
|
@@ -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-BYh6iHqL.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">
|
|
@@ -237,7 +237,9 @@ export function PreviewWorkspace({ embedded = false }) {
|
|
|
237
237
|
return <EmptyPreview />;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
const iframeSrc = previewState.
|
|
240
|
+
const iframeSrc = previewState.teamId
|
|
241
|
+
? `/api/preview/${previewState.teamId}/proxy/`
|
|
242
|
+
: previewState.url;
|
|
241
243
|
|
|
242
244
|
const deviceWidth = DEVICE_WIDTHS[previewState.deviceSize] || '100%';
|
|
243
245
|
const isFullWidth = previewState.deviceSize === 'desktop';
|
|
@@ -224,6 +224,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
224
224
|
get().fetchBetaStatus();
|
|
225
225
|
get().fetchNetworkInstallStatus();
|
|
226
226
|
get().fetchTrainingStatus();
|
|
227
|
+
get().fetchActivePreviews();
|
|
227
228
|
if (!get().onboardingComplete) get().fetchOnboardingStatus();
|
|
228
229
|
if (window.groove?.auth?.onSubscriptionStatus) {
|
|
229
230
|
window.groove.auth.onSubscriptionStatus((data) => {
|
|
@@ -1261,6 +1262,20 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1261
1262
|
|
|
1262
1263
|
// ── Preview ──────────────────────────────────────────────
|
|
1263
1264
|
|
|
1265
|
+
async fetchActivePreviews() {
|
|
1266
|
+
try {
|
|
1267
|
+
const data = await api.get('/preview');
|
|
1268
|
+
const previews = data.previews || [];
|
|
1269
|
+
if (previews.length > 0) {
|
|
1270
|
+
const p = previews.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0))[0];
|
|
1271
|
+
set({
|
|
1272
|
+
previewState: { url: `/api/preview/${p.teamId}/proxy/`, teamId: p.teamId, kind: p.kind, deviceSize: 'desktop', screenshotMode: false },
|
|
1273
|
+
showPreviewInAgents: true,
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
} catch {}
|
|
1277
|
+
},
|
|
1278
|
+
|
|
1264
1279
|
openPreview(url, teamId, kind) {
|
|
1265
1280
|
set({ previewState: { url, teamId, kind, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], showPreviewInAgents: true });
|
|
1266
1281
|
},
|
|
@@ -57,15 +57,15 @@ export class CodexParser {
|
|
|
57
57
|
if (item.type === 'command_execution') {
|
|
58
58
|
const rawOutput = item.aggregated_output || '';
|
|
59
59
|
if (item.exit_code !== 0) {
|
|
60
|
-
return { type: 'error', content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
|
|
60
|
+
return { type: 'error', is_error: true, content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
|
|
61
61
|
}
|
|
62
62
|
const obs = truncateObservation(rawOutput);
|
|
63
|
-
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
63
|
+
return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
64
64
|
}
|
|
65
65
|
if (item.type === 'file_edit' || item.type === 'file_write' || item.type === 'file_read') {
|
|
66
66
|
const rawOutput = item.output || item.content || '';
|
|
67
67
|
const obs = truncateObservation(rawOutput);
|
|
68
|
-
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
68
|
+
return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
69
69
|
}
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
@@ -64,11 +64,11 @@ export class GeminiParser {
|
|
|
64
64
|
const contentParts = Array.isArray(rawContent) ? rawContent : (typeof rawContent === 'string' ? [{ text: rawContent }] : rawContent ? [rawContent] : []);
|
|
65
65
|
const rawText = contentParts.map((p) => p.text || '').join('');
|
|
66
66
|
const obs = truncateObservation(rawText);
|
|
67
|
-
return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
67
|
+
return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
case 'error': {
|
|
71
|
-
return { type: 'error', content: jsonEvent.message || 'Unknown error' };
|
|
71
|
+
return { type: 'error', is_error: true, content: jsonEvent.message || 'Unknown error' };
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
case 'agent_end': {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
|
|
3
|
-
const ERROR_SIGNAL_RE =
|
|
3
|
+
const ERROR_SIGNAL_RE = /(?:^|\n)\s*(?:error[ :\[]\S|Error[ :\[]\S|ERROR[ :\[]\S)|(?:exit code [1-9]|non-zero exit|ENOENT|EACCES|EPERM|Command failed)|(?:(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError):\s)|(?:Cannot find module|Module not found|ModuleNotFoundError|ImportError:)|(?:FATAL|PANIC|Traceback \(most recent)|(?:error TS\d{4}:)/;
|
|
4
4
|
const FIX_SIGNAL_RE = /\b(?:fix|correcting|I see the issue|let me fix|the (?:issue|problem|bug) (?:is|was)|instead I should|my mistake)\b/i;
|
|
5
5
|
|
|
6
6
|
const CORRECTION_RE = /\b(?:no[,. ](?:that|not|don't|wrong)|that'?s (?:not|wrong|incorrect)|don'?t do that|stop (?:doing|that)|instead (?:of|do)|undo|revert|go back|try (?:again|differently)|you (?:broke|missed|forgot))\b/i;
|
|
@@ -54,7 +54,7 @@ export class StepClassifier {
|
|
|
54
54
|
|
|
55
55
|
const content = step.content || '';
|
|
56
56
|
|
|
57
|
-
if (
|
|
57
|
+
if (step.type === 'observation' && step.is_error !== false && ERROR_SIGNAL_RE.test(content)) {
|
|
58
58
|
step.type = 'error';
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -132,11 +132,11 @@ describe('StepClassifier', () => {
|
|
|
132
132
|
assert.equal(StepClassifier.countUserInterventions(steps), 0);
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
it('reclassifies action
|
|
135
|
+
it('never reclassifies action to error', () => {
|
|
136
136
|
const classifier = new StepClassifier();
|
|
137
137
|
const step = { type: 'action', content: 'Command failed with exit code 1' };
|
|
138
138
|
const result = classifier.onStep(step);
|
|
139
|
-
assert.equal(result.type, '
|
|
139
|
+
assert.equal(result.type, 'action');
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
it('reclassifies observation with error content to error', () => {
|
|
@@ -215,19 +215,75 @@ describe('StepClassifier', () => {
|
|
|
215
215
|
assert.equal(result.type, 'error');
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
-
it('
|
|
218
|
+
it('does not reclassify observation containing bare word "error" in source code', () => {
|
|
219
219
|
const classifier = new StepClassifier();
|
|
220
|
-
const step = { type: '
|
|
220
|
+
const step = { type: 'observation', content: 'function handleError(err) { console.error(err); }' };
|
|
221
221
|
const result = classifier.onStep(step);
|
|
222
|
-
assert.equal(result.type, '
|
|
222
|
+
assert.equal(result.type, 'observation');
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
it('
|
|
225
|
+
it('does not reclassify observation with "0 errors" or "found 0 vulnerabilities"', () => {
|
|
226
226
|
const classifier = new StepClassifier();
|
|
227
|
-
const step = { type: '
|
|
227
|
+
const step = { type: 'observation', content: 'Build succeeded\n0 errors, 0 warnings\nfound 0 vulnerabilities' };
|
|
228
|
+
const result = classifier.onStep(step);
|
|
229
|
+
assert.equal(result.type, 'observation');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('does not reclassify observation reading a file that mentions exceptions', () => {
|
|
233
|
+
const classifier = new StepClassifier();
|
|
234
|
+
const step = { type: 'observation', content: '{"scripts": {"build": "tsc && vite build"}, "name": "my-app"}' };
|
|
235
|
+
const result = classifier.onStep(step);
|
|
236
|
+
assert.equal(result.type, 'observation');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('reclassifies observation with real TypeScript build error', () => {
|
|
240
|
+
const classifier = new StepClassifier();
|
|
241
|
+
const step = { type: 'observation', content: 'src/main.ts(1,8): error TS2882: Cannot find module' };
|
|
242
|
+
const result = classifier.onStep(step);
|
|
243
|
+
assert.equal(result.type, 'error');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('reclassifies observation with Python traceback', () => {
|
|
247
|
+
const classifier = new StepClassifier();
|
|
248
|
+
const step = { type: 'observation', content: 'Traceback (most recent call last):\n File "main.py", line 5' };
|
|
249
|
+
const result = classifier.onStep(step);
|
|
250
|
+
assert.equal(result.type, 'error');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('reclassifies observation with actual TypeError message', () => {
|
|
254
|
+
const classifier = new StepClassifier();
|
|
255
|
+
const step = { type: 'observation', content: 'TypeError: Cannot read properties of undefined (reading "map")' };
|
|
256
|
+
const result = classifier.onStep(step);
|
|
257
|
+
assert.equal(result.type, 'error');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('reclassifies observation with exit code failure', () => {
|
|
261
|
+
const classifier = new StepClassifier();
|
|
262
|
+
const step = { type: 'observation', content: 'Process exited with exit code 1' };
|
|
263
|
+
const result = classifier.onStep(step);
|
|
264
|
+
assert.equal(result.type, 'error');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('reclassifies observation with ModuleNotFoundError', () => {
|
|
268
|
+
const classifier = new StepClassifier();
|
|
269
|
+
const step = { type: 'observation', content: 'ModuleNotFoundError: No module named requests' };
|
|
228
270
|
const result = classifier.onStep(step);
|
|
229
271
|
assert.equal(result.type, 'error');
|
|
230
272
|
});
|
|
273
|
+
|
|
274
|
+
it('preserves action type even with error keywords', () => {
|
|
275
|
+
const classifier = new StepClassifier();
|
|
276
|
+
const step = { type: 'action', content: 'Command failed with exit code 1' };
|
|
277
|
+
const result = classifier.onStep(step);
|
|
278
|
+
assert.equal(result.type, 'action');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('preserves action type regardless of is_error flag', () => {
|
|
282
|
+
const classifier = new StepClassifier();
|
|
283
|
+
const step = { type: 'action', content: 'ENOENT: no such file', is_error: true };
|
|
284
|
+
const result = classifier.onStep(step);
|
|
285
|
+
assert.equal(result.type, 'action');
|
|
286
|
+
});
|
|
231
287
|
});
|
|
232
288
|
|
|
233
289
|
describe('StepClassifier.classifyIntent', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.113",
|
|
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)",
|
|
@@ -3855,6 +3855,48 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3855
3855
|
// --- Preview Proxy (same-origin iframe support) ---
|
|
3856
3856
|
// Forwards HTTP requests to the dev server so the GUI can iframe the preview
|
|
3857
3857
|
// without cross-origin issues. WebSocket upgrade for HMR is handled in index.js.
|
|
3858
|
+
|
|
3859
|
+
function rewriteAbsoluteUrls(body, proxyBase) {
|
|
3860
|
+
let out = body;
|
|
3861
|
+
// HTML attributes: src, href, action, poster
|
|
3862
|
+
out = out.replace(/((?:src|href|action|poster)\s*=\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3863
|
+
// JS imports: from '/' and import('/')
|
|
3864
|
+
out = out.replace(/(from\s+(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3865
|
+
out = out.replace(/(import\s*\(\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3866
|
+
// CSS url()
|
|
3867
|
+
out = out.replace(/(url\s*\(\s*(["']?))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
|
|
3868
|
+
return out;
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
const REWRITABLE_TYPES = ['text/html', 'application/javascript', 'text/javascript', 'text/css'];
|
|
3872
|
+
|
|
3873
|
+
function handleProxyResponse(proxyRes, res, proxyBase) {
|
|
3874
|
+
const fwdHeaders = { ...proxyRes.headers };
|
|
3875
|
+
delete fwdHeaders['content-security-policy'];
|
|
3876
|
+
delete fwdHeaders['x-frame-options'];
|
|
3877
|
+
|
|
3878
|
+
const ct = (fwdHeaders['content-type'] || '').toLowerCase();
|
|
3879
|
+
const shouldRewrite = REWRITABLE_TYPES.some((t) => ct.includes(t));
|
|
3880
|
+
|
|
3881
|
+
if (!shouldRewrite) {
|
|
3882
|
+
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3883
|
+
proxyRes.pipe(res);
|
|
3884
|
+
return;
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
const chunks = [];
|
|
3888
|
+
proxyRes.on('data', (c) => chunks.push(c));
|
|
3889
|
+
proxyRes.on('end', () => {
|
|
3890
|
+
let body = Buffer.concat(chunks).toString('utf8');
|
|
3891
|
+
body = rewriteAbsoluteUrls(body, proxyBase);
|
|
3892
|
+
const buf = Buffer.from(body, 'utf8');
|
|
3893
|
+
fwdHeaders['content-length'] = buf.length;
|
|
3894
|
+
delete fwdHeaders['transfer-encoding'];
|
|
3895
|
+
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3896
|
+
res.end(buf);
|
|
3897
|
+
});
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3858
3900
|
app.all('/api/preview/:teamId/proxy/*', (req, res) => {
|
|
3859
3901
|
const entry = daemon.preview?.get(req.params.teamId);
|
|
3860
3902
|
if (!entry || !entry.url) return res.status(404).json({ error: 'No active preview for this team' });
|
|
@@ -3865,9 +3907,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3865
3907
|
const proxyPath = req.params[0] || '';
|
|
3866
3908
|
const search = req.url.includes('?') ? '?' + req.url.split('?').slice(1).join('?') : '';
|
|
3867
3909
|
const fullPath = '/' + proxyPath + search;
|
|
3910
|
+
const proxyBase = `/api/preview/${req.params.teamId}/proxy`;
|
|
3868
3911
|
|
|
3869
3912
|
const headers = { ...req.headers };
|
|
3870
3913
|
delete headers.host;
|
|
3914
|
+
delete headers['accept-encoding'];
|
|
3871
3915
|
headers.host = targetUrl.host;
|
|
3872
3916
|
|
|
3873
3917
|
const proxyReq = httpRequest({
|
|
@@ -3876,13 +3920,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3876
3920
|
path: fullPath,
|
|
3877
3921
|
method: req.method,
|
|
3878
3922
|
headers,
|
|
3879
|
-
}, (proxyRes) =>
|
|
3880
|
-
const fwdHeaders = { ...proxyRes.headers };
|
|
3881
|
-
delete fwdHeaders['content-security-policy'];
|
|
3882
|
-
delete fwdHeaders['x-frame-options'];
|
|
3883
|
-
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3884
|
-
proxyRes.pipe(res);
|
|
3885
|
-
});
|
|
3923
|
+
}, (proxyRes) => handleProxyResponse(proxyRes, res, proxyBase));
|
|
3886
3924
|
|
|
3887
3925
|
proxyReq.on('error', (err) => {
|
|
3888
3926
|
if (!res.headersSent) res.status(502).json({ error: `Proxy error: ${err.message}` });
|
|
@@ -3899,9 +3937,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3899
3937
|
try { targetUrl = new URL(entry.devUrl || entry.url); } catch { return res.status(500).json({ error: 'Invalid preview URL' }); }
|
|
3900
3938
|
|
|
3901
3939
|
const search = req.url.includes('?') ? '?' + req.url.split('?').slice(1).join('?') : '';
|
|
3940
|
+
const proxyBase = `/api/preview/${req.params.teamId}/proxy`;
|
|
3902
3941
|
|
|
3903
3942
|
const headers = { ...req.headers };
|
|
3904
3943
|
delete headers.host;
|
|
3944
|
+
delete headers['accept-encoding'];
|
|
3905
3945
|
headers.host = targetUrl.host;
|
|
3906
3946
|
|
|
3907
3947
|
const proxyReq = httpRequest({
|
|
@@ -3910,13 +3950,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3910
3950
|
path: '/' + search,
|
|
3911
3951
|
method: req.method,
|
|
3912
3952
|
headers,
|
|
3913
|
-
}, (proxyRes) =>
|
|
3914
|
-
const fwdHeaders = { ...proxyRes.headers };
|
|
3915
|
-
delete fwdHeaders['content-security-policy'];
|
|
3916
|
-
delete fwdHeaders['x-frame-options'];
|
|
3917
|
-
res.writeHead(proxyRes.statusCode, fwdHeaders);
|
|
3918
|
-
proxyRes.pipe(res);
|
|
3919
|
-
});
|
|
3953
|
+
}, (proxyRes) => handleProxyResponse(proxyRes, res, proxyBase));
|
|
3920
3954
|
|
|
3921
3955
|
proxyReq.on('error', (err) => {
|
|
3922
3956
|
if (!res.headersSent) res.status(502).json({ error: `Proxy error: ${err.message}` });
|
|
@@ -6183,13 +6217,15 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
6183
6217
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
6184
6218
|
env: tagEnv,
|
|
6185
6219
|
});
|
|
6220
|
+
daemon._networkCheckProc = proc;
|
|
6186
6221
|
let stdout = '';
|
|
6187
6222
|
let stderr = '';
|
|
6188
6223
|
proc.stdout.on('data', (c) => { stdout += c.toString(); });
|
|
6189
6224
|
proc.stderr.on('data', (c) => { stderr += c.toString(); });
|
|
6190
6225
|
const timeout = setTimeout(() => { safeKill(proc, 'SIGTERM'); }, 10_000);
|
|
6191
|
-
proc.on('error', () => { clearTimeout(timeout); resolvePromise(null); });
|
|
6226
|
+
proc.on('error', () => { clearTimeout(timeout); daemon._networkCheckProc = null; resolvePromise(null); });
|
|
6192
6227
|
proc.on('close', (code) => {
|
|
6228
|
+
daemon._networkCheckProc = null;
|
|
6193
6229
|
clearTimeout(timeout);
|
|
6194
6230
|
if (code !== 0) return resolvePromise(null);
|
|
6195
6231
|
const tags = [];
|
|
@@ -290,11 +290,11 @@ export class Daemon {
|
|
|
290
290
|
});
|
|
291
291
|
|
|
292
292
|
// Debounced file I/O for registry changes (at most once per 2s)
|
|
293
|
-
|
|
293
|
+
this._registryIoTimer = null;
|
|
294
294
|
const _debouncedRegistryIo = () => {
|
|
295
|
-
if (_registryIoTimer) return;
|
|
296
|
-
_registryIoTimer = setTimeout(() => {
|
|
297
|
-
_registryIoTimer = null;
|
|
295
|
+
if (this._registryIoTimer) return;
|
|
296
|
+
this._registryIoTimer = setTimeout(() => {
|
|
297
|
+
this._registryIoTimer = null;
|
|
298
298
|
this.introducer.writeRegistryFile(this.projectDir);
|
|
299
299
|
this.introducer.injectGrooveSection(this.projectDir);
|
|
300
300
|
}, 2000);
|
|
@@ -779,6 +779,11 @@ export class Daemon {
|
|
|
779
779
|
if (this._stateSaveInterval) clearInterval(this._stateSaveInterval);
|
|
780
780
|
if (this._classifierInterval) clearInterval(this._classifierInterval);
|
|
781
781
|
if (this._subscriptionPollInterval) clearInterval(this._subscriptionPollInterval);
|
|
782
|
+
if (this._registryIoTimer) clearTimeout(this._registryIoTimer);
|
|
783
|
+
if (this._networkCheckProc) {
|
|
784
|
+
try { this._networkCheckProc.kill(); } catch { /* already exited */ }
|
|
785
|
+
this._networkCheckProc = null;
|
|
786
|
+
}
|
|
782
787
|
|
|
783
788
|
// Clean up file watchers and terminal sessions
|
|
784
789
|
this.fileWatcher.unwatchAll();
|
|
@@ -823,10 +828,19 @@ export class Daemon {
|
|
|
823
828
|
|
|
824
829
|
// Close server
|
|
825
830
|
return new Promise((resolvePromise) => {
|
|
826
|
-
this.
|
|
827
|
-
this.
|
|
828
|
-
|
|
829
|
-
|
|
831
|
+
this.federationWss.close(() => {
|
|
832
|
+
this.wss.close(() => {
|
|
833
|
+
this.server.close(() => {
|
|
834
|
+
// Unref lingering handles (idle fetch/undici TLS pool connections,
|
|
835
|
+
// closed servers) so they don't prevent process exit in tests.
|
|
836
|
+
for (const h of process._getActiveHandles()) {
|
|
837
|
+
if (typeof h.unref === 'function' && h !== process.stdout && h !== process.stderr) {
|
|
838
|
+
h.unref();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
console.log('GROOVE daemon stopped.');
|
|
842
|
+
resolvePromise();
|
|
843
|
+
});
|
|
830
844
|
});
|
|
831
845
|
});
|
|
832
846
|
});
|