groove-dev 0.25.1 → 0.25.2
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/node_modules/@groove-dev/daemon/src/index.js +12 -0
- package/node_modules/@groove-dev/daemon/src/indexer.js +198 -1
- package/node_modules/@groove-dev/daemon/src/journalist.js +17 -0
- package/package.json +1 -1
- package/packages/daemon/src/index.js +12 -0
- package/packages/daemon/src/indexer.js +198 -1
- package/packages/daemon/src/journalist.js +17 -0
|
@@ -304,6 +304,18 @@ export class Daemon {
|
|
|
304
304
|
// Scan codebase for workspace/structure awareness
|
|
305
305
|
this.indexer.scan();
|
|
306
306
|
|
|
307
|
+
// Generate init map if none exists — baseline for all agents and journalist
|
|
308
|
+
const initMapCreated = this.indexer.generateInitMap();
|
|
309
|
+
if (initMapCreated) {
|
|
310
|
+
console.log('[Groove] Init map generated — GROOVE_PROJECT_MAP.md');
|
|
311
|
+
// Seed journalist with the init map so it maintains it from here
|
|
312
|
+
this.journalist.seedFromInitMap();
|
|
313
|
+
// Record the init scan as a cold-start skip — the first planner
|
|
314
|
+
// will read the map instead of spending 8K+ tokens scanning
|
|
315
|
+
this.tokens.recordColdStartSkipped();
|
|
316
|
+
this.audit.log('init.map', { stats: this.indexer.getStatus().stats });
|
|
317
|
+
}
|
|
318
|
+
|
|
307
319
|
resolvePromise(this);
|
|
308
320
|
});
|
|
309
321
|
});
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// thousands of tokens exploring the file tree.
|
|
7
7
|
|
|
8
8
|
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync } from 'fs';
|
|
9
|
-
import { resolve, relative, basename, join } from 'path';
|
|
9
|
+
import { resolve, relative, basename, join, extname } from 'path';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
10
11
|
|
|
11
12
|
const IGNORE_DIRS = new Set([
|
|
12
13
|
'node_modules', '.git', '.groove', 'dist', 'build', '.next', '.nuxt',
|
|
@@ -321,4 +322,200 @@ export class CodebaseIndexer {
|
|
|
321
322
|
stats: this.index?.stats || null,
|
|
322
323
|
};
|
|
323
324
|
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Generate a comprehensive init map from the scan results.
|
|
328
|
+
* Writes GROOVE_PROJECT_MAP.md as the baseline that the Journalist
|
|
329
|
+
* maintains from this point forward. Only runs if no map exists yet.
|
|
330
|
+
* Returns true if a new map was generated.
|
|
331
|
+
*/
|
|
332
|
+
generateInitMap() {
|
|
333
|
+
const rootDir = this.daemon.projectDir;
|
|
334
|
+
const mapPath = resolve(rootDir, 'GROOVE_PROJECT_MAP.md');
|
|
335
|
+
|
|
336
|
+
// Don't overwrite an existing journalist-maintained map
|
|
337
|
+
if (existsSync(mapPath)) return false;
|
|
338
|
+
if (!this.index) return false;
|
|
339
|
+
|
|
340
|
+
const { workspaces, keyFiles, stats, projectName, tree } = this.index;
|
|
341
|
+
const lines = [];
|
|
342
|
+
|
|
343
|
+
lines.push(`# GROOVE Project Map`);
|
|
344
|
+
lines.push('');
|
|
345
|
+
lines.push(`*Auto-generated by GROOVE init scan. The Journalist maintains this map going forward.*`);
|
|
346
|
+
lines.push('');
|
|
347
|
+
|
|
348
|
+
// ── Project overview
|
|
349
|
+
lines.push(`## Project: ${projectName}`);
|
|
350
|
+
lines.push('');
|
|
351
|
+
lines.push(`- **Files:** ${stats.totalFiles}`);
|
|
352
|
+
lines.push(`- **Directories:** ${stats.totalDirs}`);
|
|
353
|
+
lines.push(`- **Scanned:** ${new Date().toISOString()}`);
|
|
354
|
+
|
|
355
|
+
// ── Tech stack detection
|
|
356
|
+
const techStack = this._detectTechStack(rootDir);
|
|
357
|
+
if (techStack.length > 0) {
|
|
358
|
+
lines.push('');
|
|
359
|
+
lines.push(`## Tech Stack`);
|
|
360
|
+
lines.push('');
|
|
361
|
+
for (const tech of techStack) {
|
|
362
|
+
lines.push(`- ${tech}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Workspaces
|
|
367
|
+
if (workspaces.length > 0) {
|
|
368
|
+
lines.push('');
|
|
369
|
+
lines.push(`## Workspaces (${workspaces.length})`);
|
|
370
|
+
lines.push('');
|
|
371
|
+
for (const ws of workspaces) {
|
|
372
|
+
lines.push(`- \`${ws.path}/\` — ${ws.name} (${ws.files} files, ${ws.dirs} subdirs)`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── Directory structure (depth 0-2)
|
|
377
|
+
lines.push('');
|
|
378
|
+
lines.push(`## Structure`);
|
|
379
|
+
lines.push('');
|
|
380
|
+
const shallow = tree.filter((n) => n.depth <= 2 && n.children.length > 0);
|
|
381
|
+
for (const node of shallow) {
|
|
382
|
+
const indent = ' '.repeat(node.depth);
|
|
383
|
+
const name = node.path === '.' ? projectName : node.path.split('/').pop();
|
|
384
|
+
lines.push(`${indent}- \`${name}/\` (${node.files} files)`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Key files
|
|
388
|
+
if (keyFiles.length > 0) {
|
|
389
|
+
lines.push('');
|
|
390
|
+
lines.push(`## Key Files`);
|
|
391
|
+
lines.push('');
|
|
392
|
+
for (const f of keyFiles.slice(0, 40)) {
|
|
393
|
+
lines.push(`- ${f}`);
|
|
394
|
+
}
|
|
395
|
+
if (keyFiles.length > 40) {
|
|
396
|
+
lines.push(`- *(+${keyFiles.length - 40} more)*`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Entry points
|
|
401
|
+
const entryPoints = this._detectEntryPoints(rootDir);
|
|
402
|
+
if (entryPoints.length > 0) {
|
|
403
|
+
lines.push('');
|
|
404
|
+
lines.push(`## Entry Points`);
|
|
405
|
+
lines.push('');
|
|
406
|
+
for (const ep of entryPoints) {
|
|
407
|
+
lines.push(`- \`${ep}\``);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Git info
|
|
412
|
+
const gitInfo = this._getGitInfo(rootDir);
|
|
413
|
+
if (gitInfo) {
|
|
414
|
+
lines.push('');
|
|
415
|
+
lines.push(`## Git`);
|
|
416
|
+
lines.push('');
|
|
417
|
+
lines.push(`- **Branch:** ${gitInfo.branch}`);
|
|
418
|
+
if (gitInfo.recentCommits.length > 0) {
|
|
419
|
+
lines.push(`- **Recent commits:**`);
|
|
420
|
+
for (const c of gitInfo.recentCommits) {
|
|
421
|
+
lines.push(` - ${c}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── File type breakdown
|
|
427
|
+
const breakdown = this._fileTypeBreakdown(rootDir);
|
|
428
|
+
if (breakdown.length > 0) {
|
|
429
|
+
lines.push('');
|
|
430
|
+
lines.push(`## File Types`);
|
|
431
|
+
lines.push('');
|
|
432
|
+
for (const { ext, count } of breakdown.slice(0, 15)) {
|
|
433
|
+
lines.push(`- \`${ext}\` — ${count} files`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const content = lines.join('\n') + '\n';
|
|
438
|
+
try {
|
|
439
|
+
writeFileSync(mapPath, content);
|
|
440
|
+
return true;
|
|
441
|
+
} catch {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Detect tech stack from config files */
|
|
447
|
+
_detectTechStack(rootDir) {
|
|
448
|
+
const stack = [];
|
|
449
|
+
const pkg = this.readJson(resolve(rootDir, 'package.json'));
|
|
450
|
+
if (pkg) {
|
|
451
|
+
if (pkg.dependencies?.react || pkg.devDependencies?.react) stack.push('React');
|
|
452
|
+
if (pkg.dependencies?.next || pkg.devDependencies?.next) stack.push('Next.js');
|
|
453
|
+
if (pkg.dependencies?.vue || pkg.devDependencies?.vue) stack.push('Vue');
|
|
454
|
+
if (pkg.dependencies?.svelte || pkg.devDependencies?.svelte) stack.push('Svelte');
|
|
455
|
+
if (pkg.dependencies?.express || pkg.devDependencies?.express) stack.push('Express');
|
|
456
|
+
if (pkg.dependencies?.fastify || pkg.devDependencies?.fastify) stack.push('Fastify');
|
|
457
|
+
if (pkg.dependencies?.tailwindcss || pkg.devDependencies?.tailwindcss) stack.push('Tailwind CSS');
|
|
458
|
+
if (pkg.dependencies?.typescript || pkg.devDependencies?.typescript) stack.push('TypeScript');
|
|
459
|
+
if (pkg.dependencies?.vite || pkg.devDependencies?.vite) stack.push('Vite');
|
|
460
|
+
if (pkg.dependencies?.prisma || pkg.devDependencies?.prisma) stack.push('Prisma');
|
|
461
|
+
if (pkg.type === 'module') stack.push('ESM');
|
|
462
|
+
}
|
|
463
|
+
if (existsSync(resolve(rootDir, 'Cargo.toml'))) stack.push('Rust');
|
|
464
|
+
if (existsSync(resolve(rootDir, 'go.mod'))) stack.push('Go');
|
|
465
|
+
if (existsSync(resolve(rootDir, 'pyproject.toml'))) stack.push('Python');
|
|
466
|
+
if (existsSync(resolve(rootDir, 'Dockerfile'))) stack.push('Docker');
|
|
467
|
+
return stack;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Detect common entry point files */
|
|
471
|
+
_detectEntryPoints(rootDir) {
|
|
472
|
+
const candidates = [
|
|
473
|
+
'src/index.ts', 'src/index.js', 'src/index.tsx', 'src/index.jsx',
|
|
474
|
+
'src/main.ts', 'src/main.js', 'src/main.tsx', 'src/main.jsx',
|
|
475
|
+
'src/app.ts', 'src/app.js', 'src/app.tsx', 'src/app.jsx',
|
|
476
|
+
'src/App.tsx', 'src/App.jsx',
|
|
477
|
+
'app/layout.tsx', 'app/page.tsx', 'pages/index.tsx', 'pages/index.js',
|
|
478
|
+
'index.ts', 'index.js', 'index.html',
|
|
479
|
+
'server.ts', 'server.js', 'main.go', 'main.rs', 'main.py',
|
|
480
|
+
];
|
|
481
|
+
return candidates.filter((f) => existsSync(resolve(rootDir, f)));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Get git branch and recent commits */
|
|
485
|
+
_getGitInfo(rootDir) {
|
|
486
|
+
try {
|
|
487
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: rootDir, encoding: 'utf8', timeout: 5000 }).trim();
|
|
488
|
+
const log = execSync('git log --oneline -5 2>/dev/null', { cwd: rootDir, encoding: 'utf8', timeout: 5000 }).trim();
|
|
489
|
+
return {
|
|
490
|
+
branch,
|
|
491
|
+
recentCommits: log ? log.split('\n') : [],
|
|
492
|
+
};
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Count files by extension */
|
|
499
|
+
_fileTypeBreakdown(rootDir) {
|
|
500
|
+
const counts = {};
|
|
501
|
+
const walk = (dir, depth) => {
|
|
502
|
+
if (depth > 3) return;
|
|
503
|
+
try {
|
|
504
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
505
|
+
for (const e of entries) {
|
|
506
|
+
if (e.name.startsWith('.')) continue;
|
|
507
|
+
if (e.isDirectory()) {
|
|
508
|
+
if (!IGNORE_DIRS.has(e.name)) walk(resolve(dir, e.name), depth + 1);
|
|
509
|
+
} else if (e.isFile()) {
|
|
510
|
+
const ext = extname(e.name) || '(no ext)';
|
|
511
|
+
counts[ext] = (counts[ext] || 0) + 1;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} catch { /* */ }
|
|
515
|
+
};
|
|
516
|
+
walk(rootDir, 0);
|
|
517
|
+
return Object.entries(counts)
|
|
518
|
+
.map(([ext, count]) => ({ ext, count }))
|
|
519
|
+
.sort((a, b) => b.count - a.count);
|
|
520
|
+
}
|
|
324
521
|
}
|
|
@@ -29,6 +29,23 @@ export class Journalist {
|
|
|
29
29
|
this.interval = setInterval(() => this.cycle(), intervalMs);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Seed the journalist with the init map generated by the indexer.
|
|
34
|
+
* Sets it as the initial synthesis so agents read it immediately.
|
|
35
|
+
*/
|
|
36
|
+
seedFromInitMap() {
|
|
37
|
+
const mapPath = resolve(this.daemon.projectDir, 'GROOVE_PROJECT_MAP.md');
|
|
38
|
+
if (!existsSync(mapPath)) return;
|
|
39
|
+
try {
|
|
40
|
+
const content = readFileSync(mapPath, 'utf8');
|
|
41
|
+
this.lastSynthesis = {
|
|
42
|
+
projectMap: content,
|
|
43
|
+
decisions: '',
|
|
44
|
+
summary: 'Init scan: project structure mapped from filesystem analysis.',
|
|
45
|
+
};
|
|
46
|
+
} catch { /* non-fatal */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
stop() {
|
|
33
50
|
if (this.interval) {
|
|
34
51
|
clearInterval(this.interval);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. 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.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -304,6 +304,18 @@ export class Daemon {
|
|
|
304
304
|
// Scan codebase for workspace/structure awareness
|
|
305
305
|
this.indexer.scan();
|
|
306
306
|
|
|
307
|
+
// Generate init map if none exists — baseline for all agents and journalist
|
|
308
|
+
const initMapCreated = this.indexer.generateInitMap();
|
|
309
|
+
if (initMapCreated) {
|
|
310
|
+
console.log('[Groove] Init map generated — GROOVE_PROJECT_MAP.md');
|
|
311
|
+
// Seed journalist with the init map so it maintains it from here
|
|
312
|
+
this.journalist.seedFromInitMap();
|
|
313
|
+
// Record the init scan as a cold-start skip — the first planner
|
|
314
|
+
// will read the map instead of spending 8K+ tokens scanning
|
|
315
|
+
this.tokens.recordColdStartSkipped();
|
|
316
|
+
this.audit.log('init.map', { stats: this.indexer.getStatus().stats });
|
|
317
|
+
}
|
|
318
|
+
|
|
307
319
|
resolvePromise(this);
|
|
308
320
|
});
|
|
309
321
|
});
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// thousands of tokens exploring the file tree.
|
|
7
7
|
|
|
8
8
|
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync } from 'fs';
|
|
9
|
-
import { resolve, relative, basename, join } from 'path';
|
|
9
|
+
import { resolve, relative, basename, join, extname } from 'path';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
10
11
|
|
|
11
12
|
const IGNORE_DIRS = new Set([
|
|
12
13
|
'node_modules', '.git', '.groove', 'dist', 'build', '.next', '.nuxt',
|
|
@@ -321,4 +322,200 @@ export class CodebaseIndexer {
|
|
|
321
322
|
stats: this.index?.stats || null,
|
|
322
323
|
};
|
|
323
324
|
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Generate a comprehensive init map from the scan results.
|
|
328
|
+
* Writes GROOVE_PROJECT_MAP.md as the baseline that the Journalist
|
|
329
|
+
* maintains from this point forward. Only runs if no map exists yet.
|
|
330
|
+
* Returns true if a new map was generated.
|
|
331
|
+
*/
|
|
332
|
+
generateInitMap() {
|
|
333
|
+
const rootDir = this.daemon.projectDir;
|
|
334
|
+
const mapPath = resolve(rootDir, 'GROOVE_PROJECT_MAP.md');
|
|
335
|
+
|
|
336
|
+
// Don't overwrite an existing journalist-maintained map
|
|
337
|
+
if (existsSync(mapPath)) return false;
|
|
338
|
+
if (!this.index) return false;
|
|
339
|
+
|
|
340
|
+
const { workspaces, keyFiles, stats, projectName, tree } = this.index;
|
|
341
|
+
const lines = [];
|
|
342
|
+
|
|
343
|
+
lines.push(`# GROOVE Project Map`);
|
|
344
|
+
lines.push('');
|
|
345
|
+
lines.push(`*Auto-generated by GROOVE init scan. The Journalist maintains this map going forward.*`);
|
|
346
|
+
lines.push('');
|
|
347
|
+
|
|
348
|
+
// ── Project overview
|
|
349
|
+
lines.push(`## Project: ${projectName}`);
|
|
350
|
+
lines.push('');
|
|
351
|
+
lines.push(`- **Files:** ${stats.totalFiles}`);
|
|
352
|
+
lines.push(`- **Directories:** ${stats.totalDirs}`);
|
|
353
|
+
lines.push(`- **Scanned:** ${new Date().toISOString()}`);
|
|
354
|
+
|
|
355
|
+
// ── Tech stack detection
|
|
356
|
+
const techStack = this._detectTechStack(rootDir);
|
|
357
|
+
if (techStack.length > 0) {
|
|
358
|
+
lines.push('');
|
|
359
|
+
lines.push(`## Tech Stack`);
|
|
360
|
+
lines.push('');
|
|
361
|
+
for (const tech of techStack) {
|
|
362
|
+
lines.push(`- ${tech}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Workspaces
|
|
367
|
+
if (workspaces.length > 0) {
|
|
368
|
+
lines.push('');
|
|
369
|
+
lines.push(`## Workspaces (${workspaces.length})`);
|
|
370
|
+
lines.push('');
|
|
371
|
+
for (const ws of workspaces) {
|
|
372
|
+
lines.push(`- \`${ws.path}/\` — ${ws.name} (${ws.files} files, ${ws.dirs} subdirs)`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── Directory structure (depth 0-2)
|
|
377
|
+
lines.push('');
|
|
378
|
+
lines.push(`## Structure`);
|
|
379
|
+
lines.push('');
|
|
380
|
+
const shallow = tree.filter((n) => n.depth <= 2 && n.children.length > 0);
|
|
381
|
+
for (const node of shallow) {
|
|
382
|
+
const indent = ' '.repeat(node.depth);
|
|
383
|
+
const name = node.path === '.' ? projectName : node.path.split('/').pop();
|
|
384
|
+
lines.push(`${indent}- \`${name}/\` (${node.files} files)`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Key files
|
|
388
|
+
if (keyFiles.length > 0) {
|
|
389
|
+
lines.push('');
|
|
390
|
+
lines.push(`## Key Files`);
|
|
391
|
+
lines.push('');
|
|
392
|
+
for (const f of keyFiles.slice(0, 40)) {
|
|
393
|
+
lines.push(`- ${f}`);
|
|
394
|
+
}
|
|
395
|
+
if (keyFiles.length > 40) {
|
|
396
|
+
lines.push(`- *(+${keyFiles.length - 40} more)*`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── Entry points
|
|
401
|
+
const entryPoints = this._detectEntryPoints(rootDir);
|
|
402
|
+
if (entryPoints.length > 0) {
|
|
403
|
+
lines.push('');
|
|
404
|
+
lines.push(`## Entry Points`);
|
|
405
|
+
lines.push('');
|
|
406
|
+
for (const ep of entryPoints) {
|
|
407
|
+
lines.push(`- \`${ep}\``);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Git info
|
|
412
|
+
const gitInfo = this._getGitInfo(rootDir);
|
|
413
|
+
if (gitInfo) {
|
|
414
|
+
lines.push('');
|
|
415
|
+
lines.push(`## Git`);
|
|
416
|
+
lines.push('');
|
|
417
|
+
lines.push(`- **Branch:** ${gitInfo.branch}`);
|
|
418
|
+
if (gitInfo.recentCommits.length > 0) {
|
|
419
|
+
lines.push(`- **Recent commits:**`);
|
|
420
|
+
for (const c of gitInfo.recentCommits) {
|
|
421
|
+
lines.push(` - ${c}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── File type breakdown
|
|
427
|
+
const breakdown = this._fileTypeBreakdown(rootDir);
|
|
428
|
+
if (breakdown.length > 0) {
|
|
429
|
+
lines.push('');
|
|
430
|
+
lines.push(`## File Types`);
|
|
431
|
+
lines.push('');
|
|
432
|
+
for (const { ext, count } of breakdown.slice(0, 15)) {
|
|
433
|
+
lines.push(`- \`${ext}\` — ${count} files`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const content = lines.join('\n') + '\n';
|
|
438
|
+
try {
|
|
439
|
+
writeFileSync(mapPath, content);
|
|
440
|
+
return true;
|
|
441
|
+
} catch {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Detect tech stack from config files */
|
|
447
|
+
_detectTechStack(rootDir) {
|
|
448
|
+
const stack = [];
|
|
449
|
+
const pkg = this.readJson(resolve(rootDir, 'package.json'));
|
|
450
|
+
if (pkg) {
|
|
451
|
+
if (pkg.dependencies?.react || pkg.devDependencies?.react) stack.push('React');
|
|
452
|
+
if (pkg.dependencies?.next || pkg.devDependencies?.next) stack.push('Next.js');
|
|
453
|
+
if (pkg.dependencies?.vue || pkg.devDependencies?.vue) stack.push('Vue');
|
|
454
|
+
if (pkg.dependencies?.svelte || pkg.devDependencies?.svelte) stack.push('Svelte');
|
|
455
|
+
if (pkg.dependencies?.express || pkg.devDependencies?.express) stack.push('Express');
|
|
456
|
+
if (pkg.dependencies?.fastify || pkg.devDependencies?.fastify) stack.push('Fastify');
|
|
457
|
+
if (pkg.dependencies?.tailwindcss || pkg.devDependencies?.tailwindcss) stack.push('Tailwind CSS');
|
|
458
|
+
if (pkg.dependencies?.typescript || pkg.devDependencies?.typescript) stack.push('TypeScript');
|
|
459
|
+
if (pkg.dependencies?.vite || pkg.devDependencies?.vite) stack.push('Vite');
|
|
460
|
+
if (pkg.dependencies?.prisma || pkg.devDependencies?.prisma) stack.push('Prisma');
|
|
461
|
+
if (pkg.type === 'module') stack.push('ESM');
|
|
462
|
+
}
|
|
463
|
+
if (existsSync(resolve(rootDir, 'Cargo.toml'))) stack.push('Rust');
|
|
464
|
+
if (existsSync(resolve(rootDir, 'go.mod'))) stack.push('Go');
|
|
465
|
+
if (existsSync(resolve(rootDir, 'pyproject.toml'))) stack.push('Python');
|
|
466
|
+
if (existsSync(resolve(rootDir, 'Dockerfile'))) stack.push('Docker');
|
|
467
|
+
return stack;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Detect common entry point files */
|
|
471
|
+
_detectEntryPoints(rootDir) {
|
|
472
|
+
const candidates = [
|
|
473
|
+
'src/index.ts', 'src/index.js', 'src/index.tsx', 'src/index.jsx',
|
|
474
|
+
'src/main.ts', 'src/main.js', 'src/main.tsx', 'src/main.jsx',
|
|
475
|
+
'src/app.ts', 'src/app.js', 'src/app.tsx', 'src/app.jsx',
|
|
476
|
+
'src/App.tsx', 'src/App.jsx',
|
|
477
|
+
'app/layout.tsx', 'app/page.tsx', 'pages/index.tsx', 'pages/index.js',
|
|
478
|
+
'index.ts', 'index.js', 'index.html',
|
|
479
|
+
'server.ts', 'server.js', 'main.go', 'main.rs', 'main.py',
|
|
480
|
+
];
|
|
481
|
+
return candidates.filter((f) => existsSync(resolve(rootDir, f)));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Get git branch and recent commits */
|
|
485
|
+
_getGitInfo(rootDir) {
|
|
486
|
+
try {
|
|
487
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: rootDir, encoding: 'utf8', timeout: 5000 }).trim();
|
|
488
|
+
const log = execSync('git log --oneline -5 2>/dev/null', { cwd: rootDir, encoding: 'utf8', timeout: 5000 }).trim();
|
|
489
|
+
return {
|
|
490
|
+
branch,
|
|
491
|
+
recentCommits: log ? log.split('\n') : [],
|
|
492
|
+
};
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Count files by extension */
|
|
499
|
+
_fileTypeBreakdown(rootDir) {
|
|
500
|
+
const counts = {};
|
|
501
|
+
const walk = (dir, depth) => {
|
|
502
|
+
if (depth > 3) return;
|
|
503
|
+
try {
|
|
504
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
505
|
+
for (const e of entries) {
|
|
506
|
+
if (e.name.startsWith('.')) continue;
|
|
507
|
+
if (e.isDirectory()) {
|
|
508
|
+
if (!IGNORE_DIRS.has(e.name)) walk(resolve(dir, e.name), depth + 1);
|
|
509
|
+
} else if (e.isFile()) {
|
|
510
|
+
const ext = extname(e.name) || '(no ext)';
|
|
511
|
+
counts[ext] = (counts[ext] || 0) + 1;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} catch { /* */ }
|
|
515
|
+
};
|
|
516
|
+
walk(rootDir, 0);
|
|
517
|
+
return Object.entries(counts)
|
|
518
|
+
.map(([ext, count]) => ({ ext, count }))
|
|
519
|
+
.sort((a, b) => b.count - a.count);
|
|
520
|
+
}
|
|
324
521
|
}
|
|
@@ -29,6 +29,23 @@ export class Journalist {
|
|
|
29
29
|
this.interval = setInterval(() => this.cycle(), intervalMs);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Seed the journalist with the init map generated by the indexer.
|
|
34
|
+
* Sets it as the initial synthesis so agents read it immediately.
|
|
35
|
+
*/
|
|
36
|
+
seedFromInitMap() {
|
|
37
|
+
const mapPath = resolve(this.daemon.projectDir, 'GROOVE_PROJECT_MAP.md');
|
|
38
|
+
if (!existsSync(mapPath)) return;
|
|
39
|
+
try {
|
|
40
|
+
const content = readFileSync(mapPath, 'utf8');
|
|
41
|
+
this.lastSynthesis = {
|
|
42
|
+
projectMap: content,
|
|
43
|
+
decisions: '',
|
|
44
|
+
summary: 'Init scan: project structure mapped from filesystem analysis.',
|
|
45
|
+
};
|
|
46
|
+
} catch { /* non-fatal */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
stop() {
|
|
33
50
|
if (this.interval) {
|
|
34
51
|
clearInterval(this.interval);
|