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
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3
|
+
color: #fff7ed;
|
|
4
|
+
background: #140c08;
|
|
5
|
+
font-synthesis: none;
|
|
6
|
+
text-rendering: optimizeLegibility;
|
|
7
|
+
-webkit-font-smoothing: antialiased;
|
|
8
|
+
--amber: #f6b23c;
|
|
9
|
+
--cyan: #32f5ff;
|
|
10
|
+
--red: #f43f5e;
|
|
11
|
+
--panel: rgba(30, 18, 12, 0.82);
|
|
12
|
+
--panel-border: rgba(255, 240, 210, 0.22);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
* {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
html,
|
|
20
|
+
body,
|
|
21
|
+
#app {
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
margin: 0;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
body {
|
|
29
|
+
min-width: 320px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
button {
|
|
33
|
+
font: inherit;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.shell {
|
|
37
|
+
position: relative;
|
|
38
|
+
width: 100vw;
|
|
39
|
+
height: 100vh;
|
|
40
|
+
isolation: isolate;
|
|
41
|
+
background: radial-gradient(circle at 50% 50%, #a7642c 0%, #3b2115 72%);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
canvas {
|
|
45
|
+
position: absolute;
|
|
46
|
+
inset: 0;
|
|
47
|
+
width: 100%;
|
|
48
|
+
height: 100%;
|
|
49
|
+
image-rendering: auto;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.overlay {
|
|
53
|
+
position: absolute;
|
|
54
|
+
inset: 0;
|
|
55
|
+
z-index: 2;
|
|
56
|
+
pointer-events: none;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.overlay--active {
|
|
60
|
+
display: grid;
|
|
61
|
+
place-items: center;
|
|
62
|
+
padding: 24px;
|
|
63
|
+
pointer-events: auto;
|
|
64
|
+
background:
|
|
65
|
+
radial-gradient(circle at center, rgba(255, 170, 70, 0.12), transparent 38%),
|
|
66
|
+
linear-gradient(135deg, rgba(13, 8, 5, 0.52), rgba(13, 8, 5, 0.78));
|
|
67
|
+
backdrop-filter: blur(3px);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.panel {
|
|
71
|
+
width: min(620px, 94vw);
|
|
72
|
+
border: 2px solid var(--panel-border);
|
|
73
|
+
border-radius: 28px;
|
|
74
|
+
padding: 28px;
|
|
75
|
+
background: var(--panel);
|
|
76
|
+
box-shadow: 0 30px 90px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
|
77
|
+
text-align: center;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.panel.hero {
|
|
81
|
+
transform: rotate(-1deg);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.panel.compact {
|
|
85
|
+
width: min(420px, 92vw);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.eyebrow {
|
|
89
|
+
margin: 0 0 10px;
|
|
90
|
+
color: var(--cyan);
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
font-weight: 900;
|
|
93
|
+
letter-spacing: 0.22em;
|
|
94
|
+
text-transform: uppercase;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
h1,
|
|
98
|
+
h2 {
|
|
99
|
+
margin: 0;
|
|
100
|
+
line-height: 0.9;
|
|
101
|
+
text-transform: uppercase;
|
|
102
|
+
text-shadow: 0 5px 0 #5a230f, 0 12px 30px rgba(0, 0, 0, 0.55);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
h1 {
|
|
106
|
+
font-size: clamp(48px, 9vw, 92px);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
h2 {
|
|
110
|
+
font-size: clamp(38px, 7vw, 64px);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.lede,
|
|
114
|
+
.panel p:not(.eyebrow) {
|
|
115
|
+
color: #ffe7c5;
|
|
116
|
+
font-size: 16px;
|
|
117
|
+
line-height: 1.6;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.controls-grid {
|
|
121
|
+
display: grid;
|
|
122
|
+
grid-template-columns: 1fr 1.2fr;
|
|
123
|
+
gap: 10px 16px;
|
|
124
|
+
margin: 24px auto;
|
|
125
|
+
max-width: 390px;
|
|
126
|
+
text-align: left;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.controls-grid span,
|
|
130
|
+
.result-row span {
|
|
131
|
+
color: #ffd8a3;
|
|
132
|
+
font-size: 12px;
|
|
133
|
+
font-weight: 800;
|
|
134
|
+
letter-spacing: 0.12em;
|
|
135
|
+
text-transform: uppercase;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.controls-grid strong,
|
|
139
|
+
.result-row strong {
|
|
140
|
+
color: #fff;
|
|
141
|
+
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.primary {
|
|
145
|
+
display: inline-flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
min-height: 48px;
|
|
149
|
+
padding: 0 22px;
|
|
150
|
+
border: 0;
|
|
151
|
+
border-radius: 999px;
|
|
152
|
+
color: #241006;
|
|
153
|
+
background: linear-gradient(180deg, #ffe08a, var(--amber));
|
|
154
|
+
box-shadow: 0 7px 0 #8b4314, 0 18px 30px rgba(0, 0, 0, 0.3);
|
|
155
|
+
font-weight: 1000;
|
|
156
|
+
letter-spacing: 0.06em;
|
|
157
|
+
text-transform: uppercase;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.result-row {
|
|
161
|
+
display: flex;
|
|
162
|
+
justify-content: space-between;
|
|
163
|
+
gap: 20px;
|
|
164
|
+
margin: 16px auto;
|
|
165
|
+
max-width: 340px;
|
|
166
|
+
padding: 14px 16px;
|
|
167
|
+
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
168
|
+
border-radius: 16px;
|
|
169
|
+
background: rgba(0, 0, 0, 0.2);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.hud {
|
|
173
|
+
position: absolute;
|
|
174
|
+
left: 18px;
|
|
175
|
+
right: 18px;
|
|
176
|
+
display: flex;
|
|
177
|
+
gap: 10px;
|
|
178
|
+
align-items: center;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.hud--top {
|
|
183
|
+
top: 16px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.hud-card {
|
|
187
|
+
min-width: 112px;
|
|
188
|
+
padding: 10px 12px;
|
|
189
|
+
border: 1px solid rgba(255, 235, 204, 0.24);
|
|
190
|
+
border-radius: 16px;
|
|
191
|
+
background: rgba(21, 12, 8, 0.68);
|
|
192
|
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.22);
|
|
193
|
+
backdrop-filter: blur(8px);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.hud-card span {
|
|
197
|
+
display: block;
|
|
198
|
+
color: #ffd8a3;
|
|
199
|
+
font-size: 10px;
|
|
200
|
+
font-weight: 900;
|
|
201
|
+
letter-spacing: 0.14em;
|
|
202
|
+
text-transform: uppercase;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.hud-card strong {
|
|
206
|
+
display: block;
|
|
207
|
+
margin-top: 2px;
|
|
208
|
+
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
209
|
+
font-size: clamp(16px, 3vw, 22px);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.nitro-stack {
|
|
213
|
+
position: absolute;
|
|
214
|
+
right: 22px;
|
|
215
|
+
bottom: 22px;
|
|
216
|
+
display: flex;
|
|
217
|
+
gap: 8px;
|
|
218
|
+
padding: 10px;
|
|
219
|
+
border-radius: 999px;
|
|
220
|
+
background: rgba(14, 10, 8, 0.68);
|
|
221
|
+
border: 1px solid rgba(255, 255, 255, 0.16);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.nitro-stack i {
|
|
225
|
+
width: 48px;
|
|
226
|
+
height: 16px;
|
|
227
|
+
border: 2px solid rgba(255, 255, 255, 0.35);
|
|
228
|
+
border-radius: 999px;
|
|
229
|
+
background: rgba(255, 255, 255, 0.12);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.nitro-stack i.full {
|
|
233
|
+
border-color: #a5f3fc;
|
|
234
|
+
background: linear-gradient(90deg, #0891b2, var(--cyan));
|
|
235
|
+
box-shadow: 0 0 18px rgba(50, 245, 255, 0.75);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.countdown {
|
|
239
|
+
position: absolute;
|
|
240
|
+
inset: 0;
|
|
241
|
+
display: grid;
|
|
242
|
+
place-items: center;
|
|
243
|
+
color: #fff;
|
|
244
|
+
font-size: clamp(84px, 20vw, 210px);
|
|
245
|
+
font-weight: 1000;
|
|
246
|
+
text-shadow: 0 10px 0 #6c2c10, 0 0 60px rgba(255, 181, 67, 0.8);
|
|
247
|
+
animation: pop 0.45s ease both;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.toast {
|
|
251
|
+
position: absolute;
|
|
252
|
+
left: 50%;
|
|
253
|
+
bottom: 34px;
|
|
254
|
+
transform: translateX(-50%);
|
|
255
|
+
padding: 12px 18px;
|
|
256
|
+
border-radius: 999px;
|
|
257
|
+
color: #261208;
|
|
258
|
+
background: #ffe08a;
|
|
259
|
+
box-shadow: 0 8px 0 #8b4314;
|
|
260
|
+
font-weight: 1000;
|
|
261
|
+
text-transform: uppercase;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@keyframes pop {
|
|
265
|
+
from {
|
|
266
|
+
transform: scale(0.7) rotate(-4deg);
|
|
267
|
+
opacity: 0;
|
|
268
|
+
}
|
|
269
|
+
to {
|
|
270
|
+
transform: scale(1) rotate(0deg);
|
|
271
|
+
opacity: 1;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@media (max-width: 720px) {
|
|
276
|
+
.hud {
|
|
277
|
+
left: 8px;
|
|
278
|
+
right: 8px;
|
|
279
|
+
flex-wrap: wrap;
|
|
280
|
+
justify-content: flex-start;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.hud-card {
|
|
284
|
+
min-width: calc(50% - 6px);
|
|
285
|
+
padding: 8px 10px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.nitro-stack i {
|
|
289
|
+
width: 34px;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noUnusedLocals": false,
|
|
15
|
+
"noUnusedParameters": false
|
|
16
|
+
},
|
|
17
|
+
"include": ["src"]
|
|
18
|
+
}
|
|
@@ -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', () => {
|
|
@@ -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
|
});
|