groove-dev 0.27.101 → 0.27.102

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/CLAUDE.md CHANGED
@@ -263,3 +263,10 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
263
263
  - Dashboard: routing donut, cache panel, context health gauges
264
264
  - Monitor/QC agent mode (stay active, loop)
265
265
  - Distribution: demo video, HN launch, Twitter content
266
+
267
+ <!-- GROOVE:START -->
268
+ ## GROOVE Orchestration (auto-injected)
269
+ Active agents: 0
270
+ See AGENTS_REGISTRY.md for full agent state.
271
+ **Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
272
+ <!-- GROOVE:END -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.101",
3
+ "version": "0.27.102",
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.101",
3
+ "version": "0.27.102",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  // kills the previous one. Previews are also killed on team delete and on
12
12
  // daemon shutdown.
13
13
 
14
- import { spawn as cpSpawn } from 'child_process';
14
+ import { spawn as cpSpawn, execSync } from 'child_process';
15
15
  import { resolve, extname } from 'path';
16
16
  import { existsSync, readFileSync, statSync } from 'fs';
17
17
  import { createServer } from 'http';
@@ -101,11 +101,55 @@ export class PreviewService {
101
101
  return result;
102
102
  }
103
103
 
104
+ const installResult = this._ensureDependencies(teamId, baseDir);
105
+ if (installResult?.failed) {
106
+ this.daemon.audit?.log('preview.failed', { teamId, reason: installResult.reason });
107
+ return { launched: false, reason: installResult.reason };
108
+ }
109
+
104
110
  let result;
105
111
  if (preview.kind === 'static-html') {
106
- result = await this._launchStatic(teamId, baseDir, preview);
112
+ if (this._needsBuild(baseDir, preview)) {
113
+ const buildResult = this._runBuild(teamId, baseDir);
114
+ if (buildResult?.failed) {
115
+ this.daemon.audit?.log('preview.failed', { teamId, reason: buildResult.reason });
116
+ return { launched: false, reason: buildResult.reason };
117
+ }
118
+ const distDir = resolve(baseDir, 'dist');
119
+ if (existsSync(distDir)) {
120
+ result = await this._launchStatic(teamId, distDir, { ...preview, openPath: preview.openPath || 'index.html' });
121
+ } else {
122
+ result = await this._launchStatic(teamId, baseDir, preview);
123
+ }
124
+ } else {
125
+ result = await this._launchStatic(teamId, baseDir, preview);
126
+ }
107
127
  } else if (preview.kind === 'dev-server') {
128
+ if (this._needsPreBuild(baseDir)) {
129
+ const preBuild = this._runBuild(teamId, baseDir);
130
+ if (preBuild?.failed) {
131
+ this.daemon.audit?.log('preview.prebuild-failed', { teamId, reason: preBuild.reason });
132
+ }
133
+ }
108
134
  result = await this._launchDevServer(teamId, baseDir, preview);
135
+ // Fallback: if dev-server failed, try building and serving statically
136
+ if (!result.launched) {
137
+ const pkgPath = resolve(baseDir, 'package.json');
138
+ if (existsSync(pkgPath)) {
139
+ try {
140
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
141
+ if (pkg.scripts?.build) {
142
+ const buildResult = this._runBuild(teamId, baseDir);
143
+ if (!buildResult?.failed) {
144
+ const distDir = resolve(baseDir, 'dist');
145
+ if (existsSync(resolve(distDir, 'index.html'))) {
146
+ result = await this._launchStatic(teamId, distDir, { ...preview, openPath: 'index.html' });
147
+ }
148
+ }
149
+ }
150
+ } catch { /* fallback failed, keep original error */ }
151
+ }
152
+ }
109
153
  } else {
110
154
  result = { launched: false, reason: `unknown_kind: ${preview.kind}` };
111
155
  }
@@ -118,6 +162,99 @@ export class PreviewService {
118
162
  return result;
119
163
  }
120
164
 
165
+ _ensureDependencies(teamId, baseDir) {
166
+ const pkgPath = resolve(baseDir, 'package.json');
167
+ const nodeModules = resolve(baseDir, 'node_modules');
168
+ if (!existsSync(pkgPath) || existsSync(nodeModules)) return null;
169
+ try {
170
+ console.log(`[Groove:Preview] Running npm install in ${baseDir}`);
171
+ this.daemon.audit?.log('preview.npm-install', { teamId, baseDir });
172
+ execSync('npm install', { cwd: baseDir, timeout: 120_000, stdio: 'pipe' });
173
+ return null;
174
+ } catch (err) {
175
+ return { failed: true, reason: `npm install failed: ${err.message?.slice(0, 300)}` };
176
+ }
177
+ }
178
+
179
+ _needsBuild(baseDir, preview) {
180
+ const pkgPath = resolve(baseDir, 'package.json');
181
+ let hasBuildScript = false;
182
+ try {
183
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
184
+ hasBuildScript = !!pkg.scripts?.build;
185
+ } catch { /* no package.json or malformed */ }
186
+
187
+ const distDir = resolve(baseDir, 'dist');
188
+ const distExists = existsSync(distDir);
189
+
190
+ // Primary: build script exists and dist/ doesn't
191
+ if (hasBuildScript && !distExists) return true;
192
+
193
+ // Stale check: dist/ exists but package.json is newer than dist/index.html
194
+ if (hasBuildScript && distExists) {
195
+ const distIndex = resolve(distDir, 'index.html');
196
+ if (existsSync(distIndex) && existsSync(pkgPath)) {
197
+ try {
198
+ const distMtime = statSync(distIndex).mtimeMs;
199
+ const pkgMtime = statSync(pkgPath).mtimeMs;
200
+ if (pkgMtime > distMtime) return true;
201
+ } catch { /* ignore stat errors */ }
202
+ }
203
+ }
204
+
205
+ // Secondary: entry file references .tsx/.jsx sources (needs transpilation)
206
+ const openPath = (preview.openPath || 'index.html').replace(/^\/+/, '');
207
+ const entryFile = resolve(baseDir, openPath);
208
+ if (existsSync(entryFile)) {
209
+ try {
210
+ const html = readFileSync(entryFile, 'utf8');
211
+ if (/src=["'][^"']*\.(tsx?|jsx)["']/i.test(html)) return true;
212
+ } catch { /* ignore */ }
213
+ }
214
+
215
+ // Entry file missing — check if a build might create it
216
+ if (!existsSync(entryFile) && hasBuildScript) {
217
+ const frameworkConfigs = ['vite.config', 'next.config', 'webpack.config'];
218
+ for (const cfg of frameworkConfigs) {
219
+ for (const ext of ['.js', '.ts', '.mjs', '.cjs']) {
220
+ if (existsSync(resolve(baseDir, cfg + ext))) return true;
221
+ }
222
+ }
223
+ }
224
+
225
+ return false;
226
+ }
227
+
228
+ _needsPreBuild(baseDir) {
229
+ const pkgPath = resolve(baseDir, 'package.json');
230
+ if (!existsSync(pkgPath)) return false;
231
+ try {
232
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
233
+ const startScript = pkg.scripts?.start || '';
234
+ if (/\bnext\s+start\b/.test(startScript)) return true;
235
+ if (/\bserve\b/.test(startScript) && !pkg.scripts?.dev) return true;
236
+ if (/\bhttp-server\b/.test(startScript)) return true;
237
+ } catch { /* ignore */ }
238
+ return false;
239
+ }
240
+
241
+ _runBuild(teamId, baseDir) {
242
+ const pkgPath = resolve(baseDir, 'package.json');
243
+ if (!existsSync(pkgPath)) return { failed: true, reason: 'no package.json for build' };
244
+ try {
245
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
246
+ if (!pkg.scripts?.build) return { failed: true, reason: 'no build script' };
247
+ } catch { return { failed: true, reason: 'malformed package.json' }; }
248
+ try {
249
+ console.log(`[Groove:Preview] Running npm run build in ${baseDir}`);
250
+ this.daemon.audit?.log('preview.build', { teamId, baseDir });
251
+ execSync('npm run build', { cwd: baseDir, timeout: 120_000, stdio: 'pipe' });
252
+ return null;
253
+ } catch (err) {
254
+ return { failed: true, reason: `build failed: ${err.message?.slice(0, 300)}` };
255
+ }
256
+ }
257
+
121
258
  _launchStatic(teamId, baseDir, preview) {
122
259
  const openPath = (preview.openPath || 'index.html').replace(/^\/+/, '');
123
260
  const entryFile = resolve(baseDir, openPath);
@@ -130,6 +267,15 @@ export class PreviewService {
130
267
  const filePath = resolve(baseDir, rel);
131
268
  if (!filePath.startsWith(baseDir)) { res.statusCode = 403; return res.end(); }
132
269
  if (!existsSync(filePath) || !statSync(filePath).isFile()) {
270
+ // SPA fallback: serve index.html for HTML requests (client-side routing)
271
+ const acceptsHtml = (req.headers.accept || '').includes('text/html');
272
+ if (acceptsHtml) {
273
+ const fallback = resolve(baseDir, openPath);
274
+ if (existsSync(fallback) && statSync(fallback).isFile()) {
275
+ res.setHeader('Content-Type', 'text/html');
276
+ return res.end(readFileSync(fallback));
277
+ }
278
+ }
133
279
  res.statusCode = 404; return res.end('Not found');
134
280
  }
135
281
  res.setHeader('Content-Type', mimeLookup(extname(filePath)) || 'application/octet-stream');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.101",
3
+ "version": "0.27.102",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.101",
3
+ "version": "0.27.102",
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.101",
3
+ "version": "0.27.102",
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.101",
3
+ "version": "0.27.102",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  // kills the previous one. Previews are also killed on team delete and on
12
12
  // daemon shutdown.
13
13
 
14
- import { spawn as cpSpawn } from 'child_process';
14
+ import { spawn as cpSpawn, execSync } from 'child_process';
15
15
  import { resolve, extname } from 'path';
16
16
  import { existsSync, readFileSync, statSync } from 'fs';
17
17
  import { createServer } from 'http';
@@ -101,11 +101,55 @@ export class PreviewService {
101
101
  return result;
102
102
  }
103
103
 
104
+ const installResult = this._ensureDependencies(teamId, baseDir);
105
+ if (installResult?.failed) {
106
+ this.daemon.audit?.log('preview.failed', { teamId, reason: installResult.reason });
107
+ return { launched: false, reason: installResult.reason };
108
+ }
109
+
104
110
  let result;
105
111
  if (preview.kind === 'static-html') {
106
- result = await this._launchStatic(teamId, baseDir, preview);
112
+ if (this._needsBuild(baseDir, preview)) {
113
+ const buildResult = this._runBuild(teamId, baseDir);
114
+ if (buildResult?.failed) {
115
+ this.daemon.audit?.log('preview.failed', { teamId, reason: buildResult.reason });
116
+ return { launched: false, reason: buildResult.reason };
117
+ }
118
+ const distDir = resolve(baseDir, 'dist');
119
+ if (existsSync(distDir)) {
120
+ result = await this._launchStatic(teamId, distDir, { ...preview, openPath: preview.openPath || 'index.html' });
121
+ } else {
122
+ result = await this._launchStatic(teamId, baseDir, preview);
123
+ }
124
+ } else {
125
+ result = await this._launchStatic(teamId, baseDir, preview);
126
+ }
107
127
  } else if (preview.kind === 'dev-server') {
128
+ if (this._needsPreBuild(baseDir)) {
129
+ const preBuild = this._runBuild(teamId, baseDir);
130
+ if (preBuild?.failed) {
131
+ this.daemon.audit?.log('preview.prebuild-failed', { teamId, reason: preBuild.reason });
132
+ }
133
+ }
108
134
  result = await this._launchDevServer(teamId, baseDir, preview);
135
+ // Fallback: if dev-server failed, try building and serving statically
136
+ if (!result.launched) {
137
+ const pkgPath = resolve(baseDir, 'package.json');
138
+ if (existsSync(pkgPath)) {
139
+ try {
140
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
141
+ if (pkg.scripts?.build) {
142
+ const buildResult = this._runBuild(teamId, baseDir);
143
+ if (!buildResult?.failed) {
144
+ const distDir = resolve(baseDir, 'dist');
145
+ if (existsSync(resolve(distDir, 'index.html'))) {
146
+ result = await this._launchStatic(teamId, distDir, { ...preview, openPath: 'index.html' });
147
+ }
148
+ }
149
+ }
150
+ } catch { /* fallback failed, keep original error */ }
151
+ }
152
+ }
109
153
  } else {
110
154
  result = { launched: false, reason: `unknown_kind: ${preview.kind}` };
111
155
  }
@@ -118,6 +162,99 @@ export class PreviewService {
118
162
  return result;
119
163
  }
120
164
 
165
+ _ensureDependencies(teamId, baseDir) {
166
+ const pkgPath = resolve(baseDir, 'package.json');
167
+ const nodeModules = resolve(baseDir, 'node_modules');
168
+ if (!existsSync(pkgPath) || existsSync(nodeModules)) return null;
169
+ try {
170
+ console.log(`[Groove:Preview] Running npm install in ${baseDir}`);
171
+ this.daemon.audit?.log('preview.npm-install', { teamId, baseDir });
172
+ execSync('npm install', { cwd: baseDir, timeout: 120_000, stdio: 'pipe' });
173
+ return null;
174
+ } catch (err) {
175
+ return { failed: true, reason: `npm install failed: ${err.message?.slice(0, 300)}` };
176
+ }
177
+ }
178
+
179
+ _needsBuild(baseDir, preview) {
180
+ const pkgPath = resolve(baseDir, 'package.json');
181
+ let hasBuildScript = false;
182
+ try {
183
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
184
+ hasBuildScript = !!pkg.scripts?.build;
185
+ } catch { /* no package.json or malformed */ }
186
+
187
+ const distDir = resolve(baseDir, 'dist');
188
+ const distExists = existsSync(distDir);
189
+
190
+ // Primary: build script exists and dist/ doesn't
191
+ if (hasBuildScript && !distExists) return true;
192
+
193
+ // Stale check: dist/ exists but package.json is newer than dist/index.html
194
+ if (hasBuildScript && distExists) {
195
+ const distIndex = resolve(distDir, 'index.html');
196
+ if (existsSync(distIndex) && existsSync(pkgPath)) {
197
+ try {
198
+ const distMtime = statSync(distIndex).mtimeMs;
199
+ const pkgMtime = statSync(pkgPath).mtimeMs;
200
+ if (pkgMtime > distMtime) return true;
201
+ } catch { /* ignore stat errors */ }
202
+ }
203
+ }
204
+
205
+ // Secondary: entry file references .tsx/.jsx sources (needs transpilation)
206
+ const openPath = (preview.openPath || 'index.html').replace(/^\/+/, '');
207
+ const entryFile = resolve(baseDir, openPath);
208
+ if (existsSync(entryFile)) {
209
+ try {
210
+ const html = readFileSync(entryFile, 'utf8');
211
+ if (/src=["'][^"']*\.(tsx?|jsx)["']/i.test(html)) return true;
212
+ } catch { /* ignore */ }
213
+ }
214
+
215
+ // Entry file missing — check if a build might create it
216
+ if (!existsSync(entryFile) && hasBuildScript) {
217
+ const frameworkConfigs = ['vite.config', 'next.config', 'webpack.config'];
218
+ for (const cfg of frameworkConfigs) {
219
+ for (const ext of ['.js', '.ts', '.mjs', '.cjs']) {
220
+ if (existsSync(resolve(baseDir, cfg + ext))) return true;
221
+ }
222
+ }
223
+ }
224
+
225
+ return false;
226
+ }
227
+
228
+ _needsPreBuild(baseDir) {
229
+ const pkgPath = resolve(baseDir, 'package.json');
230
+ if (!existsSync(pkgPath)) return false;
231
+ try {
232
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
233
+ const startScript = pkg.scripts?.start || '';
234
+ if (/\bnext\s+start\b/.test(startScript)) return true;
235
+ if (/\bserve\b/.test(startScript) && !pkg.scripts?.dev) return true;
236
+ if (/\bhttp-server\b/.test(startScript)) return true;
237
+ } catch { /* ignore */ }
238
+ return false;
239
+ }
240
+
241
+ _runBuild(teamId, baseDir) {
242
+ const pkgPath = resolve(baseDir, 'package.json');
243
+ if (!existsSync(pkgPath)) return { failed: true, reason: 'no package.json for build' };
244
+ try {
245
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
246
+ if (!pkg.scripts?.build) return { failed: true, reason: 'no build script' };
247
+ } catch { return { failed: true, reason: 'malformed package.json' }; }
248
+ try {
249
+ console.log(`[Groove:Preview] Running npm run build in ${baseDir}`);
250
+ this.daemon.audit?.log('preview.build', { teamId, baseDir });
251
+ execSync('npm run build', { cwd: baseDir, timeout: 120_000, stdio: 'pipe' });
252
+ return null;
253
+ } catch (err) {
254
+ return { failed: true, reason: `build failed: ${err.message?.slice(0, 300)}` };
255
+ }
256
+ }
257
+
121
258
  _launchStatic(teamId, baseDir, preview) {
122
259
  const openPath = (preview.openPath || 'index.html').replace(/^\/+/, '');
123
260
  const entryFile = resolve(baseDir, openPath);
@@ -130,6 +267,15 @@ export class PreviewService {
130
267
  const filePath = resolve(baseDir, rel);
131
268
  if (!filePath.startsWith(baseDir)) { res.statusCode = 403; return res.end(); }
132
269
  if (!existsSync(filePath) || !statSync(filePath).isFile()) {
270
+ // SPA fallback: serve index.html for HTML requests (client-side routing)
271
+ const acceptsHtml = (req.headers.accept || '').includes('text/html');
272
+ if (acceptsHtml) {
273
+ const fallback = resolve(baseDir, openPath);
274
+ if (existsSync(fallback) && statSync(fallback).isFile()) {
275
+ res.setHeader('Content-Type', 'text/html');
276
+ return res.end(readFileSync(fallback));
277
+ }
278
+ }
133
279
  res.statusCode = 404; return res.end('Not found');
134
280
  }
135
281
  res.setHeader('Content-Type', mimeLookup(extname(filePath)) || 'application/octet-stream');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.101",
3
+ "version": "0.27.102",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",