@cluesmith/codev 1.4.0 → 1.4.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/bin/af.js +0 -0
- package/bin/codev.js +0 -0
- package/bin/consult.js +0 -0
- package/bin/generate-image.js +0 -0
- package/dist/agent-farm/servers/dashboard-server.js +487 -5
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +10 -0
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -0
- package/dist/commands/init.js.map +1 -1
- package/package.json +1 -1
- package/skeleton/templates/projectlist-archive.md +21 -0
- package/skeleton/templates/projectlist.md +17 -0
- package/templates/dashboard/css/activity.css +151 -0
- package/templates/dashboard/css/dialogs.css +149 -0
- package/templates/dashboard/css/files.css +530 -0
- package/templates/dashboard/css/layout.css +124 -0
- package/templates/dashboard/css/projects.css +501 -0
- package/templates/dashboard/css/statusbar.css +23 -0
- package/templates/dashboard/css/tabs.css +314 -0
- package/templates/dashboard/css/utilities.css +50 -0
- package/templates/dashboard/css/variables.css +45 -0
- package/templates/dashboard/index.html +158 -0
- package/templates/dashboard/js/activity.js +238 -0
- package/templates/dashboard/js/dialogs.js +328 -0
- package/templates/dashboard/js/files.js +436 -0
- package/templates/dashboard/js/main.js +487 -0
- package/templates/dashboard/js/projects.js +544 -0
- package/templates/dashboard/js/state.js +91 -0
- package/templates/dashboard/js/tabs.js +500 -0
- package/templates/dashboard/js/utils.js +57 -0
- package/templates/dashboard-split.html +1186 -171
- package/templates/open.html +7 -2
package/bin/af.js
CHANGED
|
File without changes
|
package/bin/codev.js
CHANGED
|
File without changes
|
package/bin/consult.js
CHANGED
|
File without changes
|
package/bin/generate-image.js
CHANGED
|
File without changes
|
|
@@ -7,9 +7,11 @@ import http from 'node:http';
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import net from 'node:net';
|
|
10
|
-
import { spawn, execSync } from 'node:child_process';
|
|
10
|
+
import { spawn, execSync, exec } from 'node:child_process';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
11
12
|
import { randomUUID } from 'node:crypto';
|
|
12
13
|
import { fileURLToPath } from 'node:url';
|
|
14
|
+
const execAsync = promisify(exec);
|
|
13
15
|
import { Command } from 'commander';
|
|
14
16
|
import { loadState, getAnnotations, addAnnotation, removeAnnotation, getUtils, tryAddUtil, removeUtil, getBuilder, getBuilders, removeBuilder, upsertBuilder, clearState, } from '../state.js';
|
|
15
17
|
import { spawnTtyd } from '../utils/shell.js';
|
|
@@ -89,7 +91,9 @@ function findTemplatePath(filename, required = false) {
|
|
|
89
91
|
return null;
|
|
90
92
|
}
|
|
91
93
|
const projectRoot = findProjectRoot();
|
|
92
|
-
|
|
94
|
+
// Use modular dashboard template (Spec 0060)
|
|
95
|
+
const templatePath = findTemplatePath('dashboard/index.html', true);
|
|
96
|
+
// Keep legacy paths for backwards compatibility (will be removed in cleanup)
|
|
93
97
|
const legacyTemplatePath = findTemplatePath('dashboard.html', true);
|
|
94
98
|
// Clean up dead processes from state (called on state load)
|
|
95
99
|
function cleanupDeadProcesses() {
|
|
@@ -351,13 +355,14 @@ function spawnWorktreeBuilder(builderPort, state) {
|
|
|
351
355
|
// Create git branch and worktree
|
|
352
356
|
execSync(`git branch "${branchName}" HEAD`, { cwd: projectRoot, stdio: 'ignore' });
|
|
353
357
|
execSync(`git worktree add "${worktreePath}" "${branchName}"`, { cwd: projectRoot, stdio: 'ignore' });
|
|
354
|
-
// Get builder command from config or use default
|
|
358
|
+
// Get builder command from config or use default shell
|
|
355
359
|
const configPath = path.resolve(projectRoot, 'codev', 'config.json');
|
|
356
|
-
|
|
360
|
+
const defaultShell = process.env.SHELL || 'bash';
|
|
361
|
+
let builderCommand = defaultShell;
|
|
357
362
|
if (fs.existsSync(configPath)) {
|
|
358
363
|
try {
|
|
359
364
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
360
|
-
builderCommand = config?.shell?.builder ||
|
|
365
|
+
builderCommand = config?.shell?.builder || defaultShell;
|
|
361
366
|
}
|
|
362
367
|
catch {
|
|
363
368
|
// Use default
|
|
@@ -500,6 +505,393 @@ function getOpenServerPath() {
|
|
|
500
505
|
}
|
|
501
506
|
return { script: jsPath, useTsx: false };
|
|
502
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Escape a string for safe use in shell commands
|
|
510
|
+
* Handles special characters that could cause command injection
|
|
511
|
+
*/
|
|
512
|
+
function escapeShellArg(str) {
|
|
513
|
+
// Single-quote the string and escape any single quotes within it
|
|
514
|
+
return "'" + str.replace(/'/g, "'\\''") + "'";
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Get today's git commits from all branches for the current user
|
|
518
|
+
*/
|
|
519
|
+
async function getGitCommits(projectRoot) {
|
|
520
|
+
try {
|
|
521
|
+
const { stdout: authorRaw } = await execAsync('git config user.name', { cwd: projectRoot });
|
|
522
|
+
const author = authorRaw.trim();
|
|
523
|
+
if (!author)
|
|
524
|
+
return [];
|
|
525
|
+
// Escape author name to prevent command injection
|
|
526
|
+
const safeAuthor = escapeShellArg(author);
|
|
527
|
+
// Get commits from all branches since midnight
|
|
528
|
+
const { stdout: output } = await execAsync(`git log --all --since="midnight" --author=${safeAuthor} --format="%H|%s|%aI|%D"`, { cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
529
|
+
if (!output.trim())
|
|
530
|
+
return [];
|
|
531
|
+
return output.trim().split('\n').filter(Boolean).map(line => {
|
|
532
|
+
const parts = line.split('|');
|
|
533
|
+
const hash = parts[0] || '';
|
|
534
|
+
const message = parts[1] || '';
|
|
535
|
+
const time = parts[2] || '';
|
|
536
|
+
const refs = parts.slice(3).join('|'); // refs might contain |
|
|
537
|
+
// Extract branch name from refs
|
|
538
|
+
let branch = 'unknown';
|
|
539
|
+
const headMatch = refs.match(/HEAD -> ([^,]+)/);
|
|
540
|
+
const branchMatch = refs.match(/([^,\s]+)$/);
|
|
541
|
+
if (headMatch) {
|
|
542
|
+
branch = headMatch[1];
|
|
543
|
+
}
|
|
544
|
+
else if (branchMatch && branchMatch[1]) {
|
|
545
|
+
branch = branchMatch[1];
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
hash: hash.slice(0, 7),
|
|
549
|
+
message: message.slice(0, 100), // Truncate long messages
|
|
550
|
+
time,
|
|
551
|
+
branch,
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
catch (err) {
|
|
556
|
+
console.error('Error getting git commits:', err.message);
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Get unique files modified today
|
|
562
|
+
*/
|
|
563
|
+
async function getModifiedFiles(projectRoot) {
|
|
564
|
+
try {
|
|
565
|
+
const { stdout: authorRaw } = await execAsync('git config user.name', { cwd: projectRoot });
|
|
566
|
+
const author = authorRaw.trim();
|
|
567
|
+
if (!author)
|
|
568
|
+
return [];
|
|
569
|
+
// Escape author name to prevent command injection
|
|
570
|
+
const safeAuthor = escapeShellArg(author);
|
|
571
|
+
const { stdout: output } = await execAsync(`git log --all --since="midnight" --author=${safeAuthor} --name-only --format=""`, { cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 });
|
|
572
|
+
if (!output.trim())
|
|
573
|
+
return [];
|
|
574
|
+
const files = [...new Set(output.trim().split('\n').filter(Boolean))];
|
|
575
|
+
return files.sort();
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
console.error('Error getting modified files:', err.message);
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Get GitHub PRs created or merged today via gh CLI
|
|
584
|
+
* Combines PRs created today AND PRs merged today (which may have been created earlier)
|
|
585
|
+
*/
|
|
586
|
+
async function getGitHubPRs(projectRoot) {
|
|
587
|
+
try {
|
|
588
|
+
// Use local time for the date (spec says "today" means local machine time)
|
|
589
|
+
const now = new Date();
|
|
590
|
+
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
591
|
+
// Fetch PRs created today AND PRs merged today in parallel
|
|
592
|
+
const [createdResult, mergedResult] = await Promise.allSettled([
|
|
593
|
+
execAsync(`gh pr list --author "@me" --state all --search "created:>=${today}" --json number,title,state,url`, { cwd: projectRoot, timeout: 15000 }),
|
|
594
|
+
execAsync(`gh pr list --author "@me" --state merged --search "merged:>=${today}" --json number,title,state,url`, { cwd: projectRoot, timeout: 15000 }),
|
|
595
|
+
]);
|
|
596
|
+
const prsMap = new Map();
|
|
597
|
+
// Process PRs created today
|
|
598
|
+
if (createdResult.status === 'fulfilled' && createdResult.value.stdout.trim()) {
|
|
599
|
+
const prs = JSON.parse(createdResult.value.stdout);
|
|
600
|
+
for (const pr of prs) {
|
|
601
|
+
prsMap.set(pr.number, {
|
|
602
|
+
number: pr.number,
|
|
603
|
+
title: pr.title.slice(0, 100),
|
|
604
|
+
state: pr.state,
|
|
605
|
+
url: pr.url,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// Process PRs merged today (may overlap with created, deduped by Map)
|
|
610
|
+
if (mergedResult.status === 'fulfilled' && mergedResult.value.stdout.trim()) {
|
|
611
|
+
const prs = JSON.parse(mergedResult.value.stdout);
|
|
612
|
+
for (const pr of prs) {
|
|
613
|
+
prsMap.set(pr.number, {
|
|
614
|
+
number: pr.number,
|
|
615
|
+
title: pr.title.slice(0, 100),
|
|
616
|
+
state: pr.state,
|
|
617
|
+
url: pr.url,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return Array.from(prsMap.values());
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
// gh CLI might not be available or authenticated
|
|
625
|
+
console.error('Error getting GitHub PRs:', err.message);
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Get builder activity from state.db for today
|
|
631
|
+
* Note: state.json doesn't track timestamps, so we can only report current builders
|
|
632
|
+
* without duration. They'll be counted as activity points, not time intervals.
|
|
633
|
+
*/
|
|
634
|
+
function getBuilderActivity() {
|
|
635
|
+
try {
|
|
636
|
+
const builders = getBuilders();
|
|
637
|
+
// Return current builders without time tracking (state.json lacks timestamps)
|
|
638
|
+
// Time tracking will rely primarily on git commits
|
|
639
|
+
return builders.map(b => ({
|
|
640
|
+
id: b.id,
|
|
641
|
+
status: b.status || 'unknown',
|
|
642
|
+
startTime: '', // Unknown - not tracked in state.json
|
|
643
|
+
endTime: undefined,
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
console.error('Error getting builder activity:', err.message);
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Detect project status changes in projectlist.md today
|
|
653
|
+
* Handles YAML format inside Markdown fenced code blocks
|
|
654
|
+
*/
|
|
655
|
+
async function getProjectChanges(projectRoot) {
|
|
656
|
+
try {
|
|
657
|
+
const projectlistPath = path.join(projectRoot, 'codev/projectlist.md');
|
|
658
|
+
if (!fs.existsSync(projectlistPath))
|
|
659
|
+
return [];
|
|
660
|
+
// Get the first commit hash from today that touched projectlist.md
|
|
661
|
+
const { stdout: firstCommitOutput } = await execAsync(`git log --since="midnight" --format=%H -- codev/projectlist.md | tail -1`, { cwd: projectRoot });
|
|
662
|
+
if (!firstCommitOutput.trim())
|
|
663
|
+
return [];
|
|
664
|
+
// Get diff of projectlist.md from that commit's parent to HEAD
|
|
665
|
+
let diff;
|
|
666
|
+
try {
|
|
667
|
+
const { stdout } = await execAsync(`git diff ${firstCommitOutput.trim()}^..HEAD -- codev/projectlist.md`, { cwd: projectRoot, maxBuffer: 1024 * 1024 });
|
|
668
|
+
diff = stdout;
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
return [];
|
|
672
|
+
}
|
|
673
|
+
if (!diff.trim())
|
|
674
|
+
return [];
|
|
675
|
+
// Parse status changes from diff
|
|
676
|
+
// Format is YAML inside Markdown code blocks:
|
|
677
|
+
// - id: "0058"
|
|
678
|
+
// title: "File Search Autocomplete"
|
|
679
|
+
// status: implementing
|
|
680
|
+
const changes = [];
|
|
681
|
+
const lines = diff.split('\n');
|
|
682
|
+
let currentId = '';
|
|
683
|
+
let currentTitle = '';
|
|
684
|
+
let oldStatus = '';
|
|
685
|
+
let newStatus = '';
|
|
686
|
+
for (const line of lines) {
|
|
687
|
+
// Track current project context from YAML id field
|
|
688
|
+
// Match lines like: " - id: \"0058\"" or "+ - id: \"0058\""
|
|
689
|
+
const idMatch = line.match(/^[+-]?\s*-\s*id:\s*["']?(\d{4})["']?/);
|
|
690
|
+
if (idMatch) {
|
|
691
|
+
// If we have a pending status change from previous project, emit it
|
|
692
|
+
if (oldStatus && newStatus && currentId) {
|
|
693
|
+
changes.push({
|
|
694
|
+
id: currentId,
|
|
695
|
+
title: currentTitle,
|
|
696
|
+
oldStatus,
|
|
697
|
+
newStatus,
|
|
698
|
+
});
|
|
699
|
+
oldStatus = '';
|
|
700
|
+
newStatus = '';
|
|
701
|
+
}
|
|
702
|
+
currentId = idMatch[1];
|
|
703
|
+
currentTitle = ''; // Will be filled by title line
|
|
704
|
+
}
|
|
705
|
+
// Track title (comes after id in YAML)
|
|
706
|
+
// Match lines like: " title: \"File Search Autocomplete\""
|
|
707
|
+
const titleMatch = line.match(/^[+-]?\s*title:\s*["']?([^"']+)["']?/);
|
|
708
|
+
if (titleMatch && currentId) {
|
|
709
|
+
currentTitle = titleMatch[1].trim();
|
|
710
|
+
}
|
|
711
|
+
// Track status changes
|
|
712
|
+
// Match lines like: "- status: implementing" or "+ status: implemented"
|
|
713
|
+
const statusMatch = line.match(/^([+-])\s*status:\s*(\w+)/);
|
|
714
|
+
if (statusMatch) {
|
|
715
|
+
const [, modifier, status] = statusMatch;
|
|
716
|
+
if (modifier === '-') {
|
|
717
|
+
oldStatus = status;
|
|
718
|
+
}
|
|
719
|
+
else if (modifier === '+') {
|
|
720
|
+
newStatus = status;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// Emit final pending change if exists
|
|
725
|
+
if (oldStatus && newStatus && currentId) {
|
|
726
|
+
changes.push({
|
|
727
|
+
id: currentId,
|
|
728
|
+
title: currentTitle,
|
|
729
|
+
oldStatus,
|
|
730
|
+
newStatus,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
return changes;
|
|
734
|
+
}
|
|
735
|
+
catch (err) {
|
|
736
|
+
console.error('Error getting project changes:', err.message);
|
|
737
|
+
return [];
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Merge overlapping time intervals
|
|
742
|
+
*/
|
|
743
|
+
function mergeIntervals(intervals) {
|
|
744
|
+
if (intervals.length === 0)
|
|
745
|
+
return [];
|
|
746
|
+
// Sort by start time
|
|
747
|
+
const sorted = [...intervals].sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
748
|
+
const merged = [{ ...sorted[0] }];
|
|
749
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
750
|
+
const last = merged[merged.length - 1];
|
|
751
|
+
const current = sorted[i];
|
|
752
|
+
// If overlapping or within 2 hours, merge
|
|
753
|
+
const gapMs = current.start.getTime() - last.end.getTime();
|
|
754
|
+
const twoHoursMs = 2 * 60 * 60 * 1000;
|
|
755
|
+
if (gapMs <= twoHoursMs) {
|
|
756
|
+
last.end = new Date(Math.max(last.end.getTime(), current.end.getTime()));
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
merged.push({ ...current });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return merged;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Calculate active time from commits and builder activity
|
|
766
|
+
*/
|
|
767
|
+
function calculateTimeTracking(commits, builders) {
|
|
768
|
+
const intervals = [];
|
|
769
|
+
const fiveMinutesMs = 5 * 60 * 1000;
|
|
770
|
+
// Add commit timestamps (treat each as 5-minute interval)
|
|
771
|
+
for (const commit of commits) {
|
|
772
|
+
if (commit.time) {
|
|
773
|
+
const time = new Date(commit.time);
|
|
774
|
+
if (!isNaN(time.getTime())) {
|
|
775
|
+
intervals.push({
|
|
776
|
+
start: time,
|
|
777
|
+
end: new Date(time.getTime() + fiveMinutesMs),
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// Add builder sessions
|
|
783
|
+
for (const builder of builders) {
|
|
784
|
+
if (builder.startTime) {
|
|
785
|
+
const start = new Date(builder.startTime);
|
|
786
|
+
const end = builder.endTime ? new Date(builder.endTime) : new Date();
|
|
787
|
+
if (!isNaN(start.getTime()) && !isNaN(end.getTime())) {
|
|
788
|
+
intervals.push({ start, end });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (intervals.length === 0) {
|
|
793
|
+
return {
|
|
794
|
+
activeMinutes: 0,
|
|
795
|
+
firstActivity: '',
|
|
796
|
+
lastActivity: '',
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const merged = mergeIntervals(intervals);
|
|
800
|
+
const totalMinutes = merged.reduce((sum, interval) => sum + (interval.end.getTime() - interval.start.getTime()) / (1000 * 60), 0);
|
|
801
|
+
return {
|
|
802
|
+
activeMinutes: Math.round(totalMinutes),
|
|
803
|
+
firstActivity: merged[0].start.toISOString(),
|
|
804
|
+
lastActivity: merged[merged.length - 1].end.toISOString(),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Find the consult CLI path
|
|
809
|
+
* Returns the path to the consult binary, checking multiple locations
|
|
810
|
+
*/
|
|
811
|
+
function findConsultPath() {
|
|
812
|
+
// When running from dist/, check relative paths
|
|
813
|
+
// dist/agent-farm/servers/ -> ../../../bin/consult.js
|
|
814
|
+
const distPath = path.join(__dirname, '../../../bin/consult.js');
|
|
815
|
+
if (fs.existsSync(distPath)) {
|
|
816
|
+
return distPath;
|
|
817
|
+
}
|
|
818
|
+
// When running from src/ with tsx, check src-relative paths
|
|
819
|
+
// src/agent-farm/servers/ -> ../../../bin/consult.js (won't exist, it's .ts in src)
|
|
820
|
+
// But bin/ is at packages/codev/bin/consult.js, so it should still work
|
|
821
|
+
// Fall back to npx consult (works if @cluesmith/codev is installed)
|
|
822
|
+
return 'npx consult';
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Generate AI summary via consult CLI
|
|
826
|
+
*/
|
|
827
|
+
async function generateAISummary(data) {
|
|
828
|
+
// Build prompt with commit messages and file names only (security: no full diffs)
|
|
829
|
+
const hours = Math.floor(data.timeTracking.activeMinutes / 60);
|
|
830
|
+
const mins = data.timeTracking.activeMinutes % 60;
|
|
831
|
+
const prompt = `Summarize this developer's activity today for a standup report.
|
|
832
|
+
|
|
833
|
+
Commits (${data.commits.length}):
|
|
834
|
+
${data.commits.slice(0, 20).map(c => `- ${c.message}`).join('\n') || '(none)'}
|
|
835
|
+
${data.commits.length > 20 ? `... and ${data.commits.length - 20} more` : ''}
|
|
836
|
+
|
|
837
|
+
PRs: ${data.prs.map(p => `#${p.number} ${p.title} (${p.state})`).join(', ') || 'None'}
|
|
838
|
+
|
|
839
|
+
Files modified: ${data.files.length} files
|
|
840
|
+
${data.files.slice(0, 10).join(', ')}${data.files.length > 10 ? ` ... and ${data.files.length - 10} more` : ''}
|
|
841
|
+
|
|
842
|
+
Project status changes:
|
|
843
|
+
${data.projectChanges.map(p => `- ${p.id} ${p.title}: ${p.oldStatus} → ${p.newStatus}`).join('\n') || '(none)'}
|
|
844
|
+
|
|
845
|
+
Active time: ~${hours}h ${mins}m
|
|
846
|
+
|
|
847
|
+
Write a brief, professional summary (2-3 sentences) focusing on accomplishments. Be concise and suitable for a standup or status report.`;
|
|
848
|
+
try {
|
|
849
|
+
// Use consult CLI to generate summary
|
|
850
|
+
const consultCmd = findConsultPath();
|
|
851
|
+
const safePrompt = escapeShellArg(prompt);
|
|
852
|
+
// Use async exec with timeout
|
|
853
|
+
const { stdout } = await execAsync(`${consultCmd} --model gemini general ${safePrompt}`, { timeout: 60000, maxBuffer: 1024 * 1024 });
|
|
854
|
+
return stdout.trim();
|
|
855
|
+
}
|
|
856
|
+
catch (err) {
|
|
857
|
+
console.error('AI summary generation failed:', err.message);
|
|
858
|
+
return '';
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Collect all activity data for today
|
|
863
|
+
*/
|
|
864
|
+
async function collectActivitySummary(projectRoot) {
|
|
865
|
+
// Collect data from all sources in parallel - these are now truly async
|
|
866
|
+
const [commits, files, prs, builders, projectChanges] = await Promise.all([
|
|
867
|
+
getGitCommits(projectRoot),
|
|
868
|
+
getModifiedFiles(projectRoot),
|
|
869
|
+
getGitHubPRs(projectRoot),
|
|
870
|
+
Promise.resolve(getBuilderActivity()), // This one is sync (reads from state)
|
|
871
|
+
getProjectChanges(projectRoot),
|
|
872
|
+
]);
|
|
873
|
+
const timeTracking = calculateTimeTracking(commits, builders);
|
|
874
|
+
// Generate AI summary (skip if no activity)
|
|
875
|
+
let aiSummary = '';
|
|
876
|
+
if (commits.length > 0 || prs.length > 0) {
|
|
877
|
+
aiSummary = await generateAISummary({
|
|
878
|
+
commits,
|
|
879
|
+
prs,
|
|
880
|
+
files,
|
|
881
|
+
timeTracking,
|
|
882
|
+
projectChanges,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
commits,
|
|
887
|
+
prs,
|
|
888
|
+
builders,
|
|
889
|
+
projectChanges,
|
|
890
|
+
files,
|
|
891
|
+
timeTracking,
|
|
892
|
+
aiSummary: aiSummary || undefined,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
503
895
|
// Use split template as main, legacy is already loaded via findTemplatePath
|
|
504
896
|
const finalTemplatePath = templatePath;
|
|
505
897
|
// Security: Validate request origin
|
|
@@ -1144,6 +1536,96 @@ const server = http.createServer(async (req, res) => {
|
|
|
1144
1536
|
res.end(JSON.stringify(tree));
|
|
1145
1537
|
return;
|
|
1146
1538
|
}
|
|
1539
|
+
// API: Get daily activity summary (Spec 0059)
|
|
1540
|
+
if (req.method === 'GET' && url.pathname === '/api/activity-summary') {
|
|
1541
|
+
try {
|
|
1542
|
+
const activitySummary = await collectActivitySummary(projectRoot);
|
|
1543
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1544
|
+
res.end(JSON.stringify(activitySummary));
|
|
1545
|
+
}
|
|
1546
|
+
catch (err) {
|
|
1547
|
+
console.error('Activity summary error:', err);
|
|
1548
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1549
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1550
|
+
}
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
// API: Hot reload check (Spec 0060)
|
|
1554
|
+
// Returns modification times for all dashboard CSS/JS files
|
|
1555
|
+
if (req.method === 'GET' && url.pathname === '/api/hot-reload') {
|
|
1556
|
+
try {
|
|
1557
|
+
const dashboardDir = path.join(__dirname, '../../../templates/dashboard');
|
|
1558
|
+
const cssDir = path.join(dashboardDir, 'css');
|
|
1559
|
+
const jsDir = path.join(dashboardDir, 'js');
|
|
1560
|
+
const mtimes = {};
|
|
1561
|
+
// Collect CSS file modification times
|
|
1562
|
+
if (fs.existsSync(cssDir)) {
|
|
1563
|
+
for (const file of fs.readdirSync(cssDir)) {
|
|
1564
|
+
if (file.endsWith('.css')) {
|
|
1565
|
+
const stat = fs.statSync(path.join(cssDir, file));
|
|
1566
|
+
mtimes[`css/${file}`] = stat.mtimeMs;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
// Collect JS file modification times
|
|
1571
|
+
if (fs.existsSync(jsDir)) {
|
|
1572
|
+
for (const file of fs.readdirSync(jsDir)) {
|
|
1573
|
+
if (file.endsWith('.js')) {
|
|
1574
|
+
const stat = fs.statSync(path.join(jsDir, file));
|
|
1575
|
+
mtimes[`js/${file}`] = stat.mtimeMs;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1580
|
+
res.end(JSON.stringify({ mtimes }));
|
|
1581
|
+
}
|
|
1582
|
+
catch (err) {
|
|
1583
|
+
console.error('Hot reload check error:', err);
|
|
1584
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1585
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1586
|
+
}
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
// Serve dashboard CSS files
|
|
1590
|
+
if (req.method === 'GET' && url.pathname.startsWith('/dashboard/css/')) {
|
|
1591
|
+
const filename = url.pathname.replace('/dashboard/css/', '');
|
|
1592
|
+
// Validate filename to prevent path traversal
|
|
1593
|
+
if (!filename || filename.includes('..') || filename.includes('/') || !filename.endsWith('.css')) {
|
|
1594
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1595
|
+
res.end('Invalid filename');
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const cssPath = path.join(__dirname, '../../../templates/dashboard/css', filename);
|
|
1599
|
+
if (fs.existsSync(cssPath)) {
|
|
1600
|
+
const content = fs.readFileSync(cssPath, 'utf-8');
|
|
1601
|
+
res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8' });
|
|
1602
|
+
res.end(content);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1606
|
+
res.end('CSS file not found');
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
// Serve dashboard JS files
|
|
1610
|
+
if (req.method === 'GET' && url.pathname.startsWith('/dashboard/js/')) {
|
|
1611
|
+
const filename = url.pathname.replace('/dashboard/js/', '');
|
|
1612
|
+
// Validate filename to prevent path traversal
|
|
1613
|
+
if (!filename || filename.includes('..') || filename.includes('/') || !filename.endsWith('.js')) {
|
|
1614
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
1615
|
+
res.end('Invalid filename');
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const jsPath = path.join(__dirname, '../../../templates/dashboard/js', filename);
|
|
1619
|
+
if (fs.existsSync(jsPath)) {
|
|
1620
|
+
const content = fs.readFileSync(jsPath, 'utf-8');
|
|
1621
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
|
|
1622
|
+
res.end(content);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
1626
|
+
res.end('JS file not found');
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1147
1629
|
// Serve dashboard
|
|
1148
1630
|
if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
1149
1631
|
try {
|