squads-cli 0.6.1 → 0.7.0
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/README.md +196 -1152
- package/dist/auth-YW3UPFSB.js +23 -0
- package/dist/autonomy-BWTVDEAT.js +102 -0
- package/dist/autonomy-BWTVDEAT.js.map +1 -0
- package/dist/chunk-3KCWNZWW.js +401 -0
- package/dist/chunk-3KCWNZWW.js.map +1 -0
- package/dist/chunk-67RO2HKR.js +174 -0
- package/dist/chunk-67RO2HKR.js.map +1 -0
- package/dist/chunk-7JVD7RD4.js +275 -0
- package/dist/chunk-7JVD7RD4.js.map +1 -0
- package/dist/chunk-BODLDQY7.js +452 -0
- package/dist/chunk-BODLDQY7.js.map +1 -0
- package/dist/chunk-FFFCFZ6A.js +121 -0
- package/dist/chunk-FFFCFZ6A.js.map +1 -0
- package/dist/chunk-FIWT2NMM.js +165 -0
- package/dist/chunk-FIWT2NMM.js.map +1 -0
- package/dist/chunk-L6GQCHDF.js +222 -0
- package/dist/chunk-L6GQCHDF.js.map +1 -0
- package/dist/{chunk-O7UV3FWI.js → chunk-LDM62TIX.js} +2 -2
- package/dist/chunk-LDM62TIX.js.map +1 -0
- package/dist/chunk-LOA3KWYJ.js +294 -0
- package/dist/chunk-LOA3KWYJ.js.map +1 -0
- package/dist/chunk-NA45DFXY.js +616 -0
- package/dist/chunk-NA45DFXY.js.map +1 -0
- package/dist/{chunk-4CMAEQQY.js → chunk-NQN6JPI7.js} +4 -3
- package/dist/chunk-NQN6JPI7.js.map +1 -0
- package/dist/chunk-OQJHPULO.js +103 -0
- package/dist/chunk-OQJHPULO.js.map +1 -0
- package/dist/chunk-QHNUMM4V.js +87 -0
- package/dist/chunk-QHNUMM4V.js.map +1 -0
- package/dist/chunk-RM6BWILN.js +74 -0
- package/dist/chunk-RM6BWILN.js.map +1 -0
- package/dist/chunk-WBR5J7EX.js +90 -0
- package/dist/chunk-WBR5J7EX.js.map +1 -0
- package/dist/chunk-Z2UKDBNL.js +162 -0
- package/dist/chunk-Z2UKDBNL.js.map +1 -0
- package/dist/cli.js +2151 -12594
- package/dist/cli.js.map +1 -1
- package/dist/context-M2A2DOFV.js +291 -0
- package/dist/context-M2A2DOFV.js.map +1 -0
- package/dist/context-feed-JMNW4GAM.js +391 -0
- package/dist/context-feed-JMNW4GAM.js.map +1 -0
- package/dist/cost-N37I4UTA.js +274 -0
- package/dist/cost-N37I4UTA.js.map +1 -0
- package/dist/create-554W5HNU.js +286 -0
- package/dist/create-554W5HNU.js.map +1 -0
- package/dist/daemon-XWPQPPPN.js +546 -0
- package/dist/daemon-XWPQPPPN.js.map +1 -0
- package/dist/dashboard-L7YKVQEB.js +945 -0
- package/dist/dashboard-L7YKVQEB.js.map +1 -0
- package/dist/dashboard-MFNRLCEE.js +794 -0
- package/dist/dashboard-MFNRLCEE.js.map +1 -0
- package/dist/doctor-RG75M5RO.js +346 -0
- package/dist/doctor-RG75M5RO.js.map +1 -0
- package/dist/env-config-KCLDBKYX.js +21 -0
- package/dist/exec-JQKBF7BL.js +197 -0
- package/dist/exec-JQKBF7BL.js.map +1 -0
- package/dist/feedback-KA2UYBZG.js +229 -0
- package/dist/feedback-KA2UYBZG.js.map +1 -0
- package/dist/github-UQTM5KMS.js +23 -0
- package/dist/goal-EOPC5ZCD.js +168 -0
- package/dist/goal-EOPC5ZCD.js.map +1 -0
- package/dist/health-3FZDOSR5.js +209 -0
- package/dist/health-3FZDOSR5.js.map +1 -0
- package/dist/history-TFVXJEDH.js +229 -0
- package/dist/history-TFVXJEDH.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/init-UOWTNMIE.js +747 -0
- package/dist/init-UOWTNMIE.js.map +1 -0
- package/dist/kpi-2SQ2WCVT.js +413 -0
- package/dist/kpi-2SQ2WCVT.js.map +1 -0
- package/dist/learn-6ERTERAO.js +269 -0
- package/dist/learn-6ERTERAO.js.map +1 -0
- package/dist/list-KSOMUBMB.js +92 -0
- package/dist/list-KSOMUBMB.js.map +1 -0
- package/dist/login-ST6PAXYE.js +155 -0
- package/dist/login-ST6PAXYE.js.map +1 -0
- package/dist/memory-3CSNKXIL.js +562 -0
- package/dist/memory-3CSNKXIL.js.map +1 -0
- package/dist/progress-FKG4V2VH.js +202 -0
- package/dist/progress-FKG4V2VH.js.map +1 -0
- package/dist/providers-66PDCORB.js +65 -0
- package/dist/providers-66PDCORB.js.map +1 -0
- package/dist/results-2MJFLWEO.js +224 -0
- package/dist/results-2MJFLWEO.js.map +1 -0
- package/dist/run-72OQLH5A.js +2685 -0
- package/dist/run-72OQLH5A.js.map +1 -0
- package/dist/session-6H67XPAQ.js +64 -0
- package/dist/session-6H67XPAQ.js.map +1 -0
- package/dist/{chunk-NHGLXN2F.js → sessions-GVQIMN4W.js} +23 -459
- package/dist/sessions-GVQIMN4W.js.map +1 -0
- package/dist/{squad-parser-4BI3G4RS.js → squad-parser-CM3HOIWM.js} +2 -2
- package/dist/squad-parser-CM3HOIWM.js.map +1 -0
- package/dist/stats-ONZI557Q.js +335 -0
- package/dist/stats-ONZI557Q.js.map +1 -0
- package/dist/status-FYH42FTB.js +346 -0
- package/dist/status-FYH42FTB.js.map +1 -0
- package/dist/sync-HJZJNXHW.js +800 -0
- package/dist/sync-HJZJNXHW.js.map +1 -0
- package/dist/update-B4WMUOPO.js +83 -0
- package/dist/update-B4WMUOPO.js.map +1 -0
- package/dist/{update-ALJKFFM7.js → update-L7FGHN6W.js} +2 -2
- package/dist/update-L7FGHN6W.js.map +1 -0
- package/package.json +18 -10
- package/dist/chunk-4CMAEQQY.js.map +0 -1
- package/dist/chunk-NHGLXN2F.js.map +0 -1
- package/dist/chunk-O7UV3FWI.js.map +0 -1
- package/dist/sessions-6PB7ALCE.js +0 -16
- /package/dist/{sessions-6PB7ALCE.js.map → auth-YW3UPFSB.js.map} +0 -0
- /package/dist/{squad-parser-4BI3G4RS.js.map → env-config-KCLDBKYX.js.map} +0 -0
- /package/dist/{update-ALJKFFM7.js.map → github-UQTM5KMS.js.map} +0 -0
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
calculateROIMetrics,
|
|
4
|
+
calculateSquadCostProjections,
|
|
5
|
+
fetchBridgeStats,
|
|
6
|
+
fetchClaudeCodeCapacity,
|
|
7
|
+
fetchCostSummary,
|
|
8
|
+
fetchInsights,
|
|
9
|
+
fetchNpmStats,
|
|
10
|
+
fetchQuotaInfo,
|
|
11
|
+
getPlanType,
|
|
12
|
+
isMaxPlan
|
|
13
|
+
} from "./chunk-NA45DFXY.js";
|
|
14
|
+
import "./chunk-LOA3KWYJ.js";
|
|
15
|
+
import {
|
|
16
|
+
cleanupStaleSessions,
|
|
17
|
+
getLiveSessionSummaryAsync
|
|
18
|
+
} from "./chunk-BODLDQY7.js";
|
|
19
|
+
import {
|
|
20
|
+
getActivitySparkline,
|
|
21
|
+
getGitHubStatsOptimized,
|
|
22
|
+
getMultiRepoGitStats
|
|
23
|
+
} from "./chunk-3KCWNZWW.js";
|
|
24
|
+
import {
|
|
25
|
+
Events,
|
|
26
|
+
track
|
|
27
|
+
} from "./chunk-L6GQCHDF.js";
|
|
28
|
+
import {
|
|
29
|
+
findSquadsDir,
|
|
30
|
+
hasLocalInfraConfig,
|
|
31
|
+
listSquads,
|
|
32
|
+
loadSquad
|
|
33
|
+
} from "./chunk-LDM62TIX.js";
|
|
34
|
+
import {
|
|
35
|
+
findMemoryDir
|
|
36
|
+
} from "./chunk-ZTQ7ISUR.js";
|
|
37
|
+
import {
|
|
38
|
+
checkForUpdate
|
|
39
|
+
} from "./chunk-NQN6JPI7.js";
|
|
40
|
+
import {
|
|
41
|
+
RESET,
|
|
42
|
+
barChart,
|
|
43
|
+
bold,
|
|
44
|
+
box,
|
|
45
|
+
colors,
|
|
46
|
+
gradient,
|
|
47
|
+
icons,
|
|
48
|
+
padEnd,
|
|
49
|
+
progressBar,
|
|
50
|
+
sparkline,
|
|
51
|
+
truncate,
|
|
52
|
+
writeLine
|
|
53
|
+
} from "./chunk-N7KDWU4W.js";
|
|
54
|
+
import "./chunk-7OCVIDC7.js";
|
|
55
|
+
|
|
56
|
+
// src/commands/dashboard.ts
|
|
57
|
+
import { readdirSync, existsSync, statSync } from "fs";
|
|
58
|
+
import { join } from "path";
|
|
59
|
+
|
|
60
|
+
// src/lib/db.ts
|
|
61
|
+
async function isDatabaseAvailable() {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
async function saveDashboardSnapshot(_snapshot) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
async function getDashboardHistory(_limit = 30) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
async function closeDatabase() {
|
|
71
|
+
}
|
|
72
|
+
async function getLatestBaseline() {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/commands/dashboard.ts
|
|
77
|
+
function getLastActivityDate(squadName) {
|
|
78
|
+
const memoryDir = findMemoryDir();
|
|
79
|
+
if (!memoryDir) return "unknown";
|
|
80
|
+
const squadMemory = join(memoryDir, squadName);
|
|
81
|
+
if (!existsSync(squadMemory)) return "\u2014";
|
|
82
|
+
let latestTime = 0;
|
|
83
|
+
try {
|
|
84
|
+
const agents = readdirSync(squadMemory, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
85
|
+
for (const agent of agents) {
|
|
86
|
+
const agentPath = join(squadMemory, agent.name);
|
|
87
|
+
const files = readdirSync(agentPath).filter((f) => f.endsWith(".md"));
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const filePath = join(agentPath, file);
|
|
90
|
+
const stats = statSync(filePath);
|
|
91
|
+
if (stats.mtimeMs > latestTime) {
|
|
92
|
+
latestTime = stats.mtimeMs;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
return "\u2014";
|
|
98
|
+
}
|
|
99
|
+
if (latestTime === 0) return "\u2014";
|
|
100
|
+
const ageMs = Date.now() - latestTime;
|
|
101
|
+
const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
102
|
+
if (ageDays === 0) return "today";
|
|
103
|
+
if (ageDays === 1) return "1d";
|
|
104
|
+
if (ageDays < 7) return `${ageDays}d`;
|
|
105
|
+
return `${Math.floor(ageDays / 7)}w`;
|
|
106
|
+
}
|
|
107
|
+
function collectSquadMetrics(squadNames, gitStats, ghStats) {
|
|
108
|
+
const squadData = [];
|
|
109
|
+
const repoSquadMap = {
|
|
110
|
+
website: ["agents-squads-web"],
|
|
111
|
+
product: ["squads-cli"],
|
|
112
|
+
engineering: ["hq", "squads-cli"],
|
|
113
|
+
research: ["research"],
|
|
114
|
+
intelligence: ["intelligence"],
|
|
115
|
+
customer: ["customer"],
|
|
116
|
+
finance: ["finance"],
|
|
117
|
+
company: ["company", "hq"],
|
|
118
|
+
marketing: ["marketing", "agents-squads-web"],
|
|
119
|
+
cli: ["squads-cli"]
|
|
120
|
+
};
|
|
121
|
+
for (const name of squadNames) {
|
|
122
|
+
const squad = loadSquad(name);
|
|
123
|
+
if (!squad) continue;
|
|
124
|
+
const lastActivity = getLastActivityDate(name);
|
|
125
|
+
const github = ghStats?.bySquad.get(name) || null;
|
|
126
|
+
let status = "active";
|
|
127
|
+
const activeGoals = squad.goals.filter((g) => !g.completed);
|
|
128
|
+
if (activeGoals.length === 0) {
|
|
129
|
+
status = "needs-goal";
|
|
130
|
+
} else if (lastActivity.includes("w") || lastActivity === "\u2014") {
|
|
131
|
+
status = "stale";
|
|
132
|
+
}
|
|
133
|
+
const totalGoals = squad.goals.length;
|
|
134
|
+
const completedGoals = squad.goals.filter((g) => g.completed).length;
|
|
135
|
+
const hasProgress = squad.goals.filter((g) => g.progress).length;
|
|
136
|
+
const goalProgress = totalGoals > 0 ? Math.round((completedGoals + hasProgress * 0.3) / totalGoals * 100) : 0;
|
|
137
|
+
let squadCommits = 0;
|
|
138
|
+
if (gitStats) {
|
|
139
|
+
for (const [repo, commits] of gitStats.commitsByRepo) {
|
|
140
|
+
if (repoSquadMap[name]?.includes(repo)) {
|
|
141
|
+
squadCommits += commits;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const githubStats = github || {
|
|
146
|
+
prsOpened: 0,
|
|
147
|
+
prsMerged: 0,
|
|
148
|
+
issuesClosed: 0,
|
|
149
|
+
issuesOpen: 0,
|
|
150
|
+
commits: 0,
|
|
151
|
+
recentIssues: [],
|
|
152
|
+
recentPRs: []
|
|
153
|
+
};
|
|
154
|
+
githubStats.commits = squadCommits;
|
|
155
|
+
squadData.push({
|
|
156
|
+
name,
|
|
157
|
+
mission: squad.mission,
|
|
158
|
+
goals: squad.goals,
|
|
159
|
+
lastActivity,
|
|
160
|
+
status,
|
|
161
|
+
github: githubStats,
|
|
162
|
+
goalProgress
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return squadData;
|
|
166
|
+
}
|
|
167
|
+
function calculateDashboardStats(squadData, ghStats) {
|
|
168
|
+
const activeSquads = squadData.filter((s) => s.status === "active").length;
|
|
169
|
+
const totalPRs = ghStats ? ghStats.prsMerged : 0;
|
|
170
|
+
const totalIssuesClosed = ghStats ? ghStats.issuesClosed : 0;
|
|
171
|
+
const totalIssuesOpen = ghStats ? ghStats.issuesOpen : 0;
|
|
172
|
+
const overallProgress = squadData.length > 0 ? Math.round(squadData.reduce((sum, s) => sum + s.goalProgress, 0) / squadData.length) : 0;
|
|
173
|
+
return {
|
|
174
|
+
activeSquads,
|
|
175
|
+
totalSquads: squadData.length,
|
|
176
|
+
totalPRs,
|
|
177
|
+
totalIssuesClosed,
|
|
178
|
+
totalIssuesOpen,
|
|
179
|
+
overallProgress
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function renderDashboardHeader(stats, sessionSummary, gitStats, ghStats) {
|
|
183
|
+
writeLine();
|
|
184
|
+
writeLine(` ${gradient("squads")} ${colors.dim}dashboard${RESET}`);
|
|
185
|
+
const updateInfo = checkForUpdate();
|
|
186
|
+
if (updateInfo.updateAvailable) {
|
|
187
|
+
writeLine(` ${colors.cyan}\u2B06${RESET} Update available: ${colors.dim}${updateInfo.currentVersion}${RESET} \u2192 ${colors.green}${updateInfo.latestVersion}${RESET} ${colors.dim}(run \`squads update\`)${RESET}`);
|
|
188
|
+
}
|
|
189
|
+
if (sessionSummary.totalSessions > 0) {
|
|
190
|
+
const sessionText = sessionSummary.totalSessions === 1 ? "session" : "sessions";
|
|
191
|
+
const squadText = sessionSummary.squadCount === 1 ? "squad" : "squads";
|
|
192
|
+
let toolInfo = "";
|
|
193
|
+
if (sessionSummary.byTool && Object.keys(sessionSummary.byTool).length > 0) {
|
|
194
|
+
const toolParts = Object.entries(sessionSummary.byTool).sort((a, b) => b[1] - a[1]).map(([tool, count]) => `${colors.dim}${tool}${RESET} ${colors.cyan}${count}${RESET}`);
|
|
195
|
+
toolInfo = ` ${colors.dim}(${RESET}${toolParts.join(` ${colors.dim}\xB7${RESET} `)}${colors.dim})${RESET}`;
|
|
196
|
+
}
|
|
197
|
+
writeLine(` ${colors.green}${icons.active}${RESET} ${colors.white}${sessionSummary.totalSessions}${RESET} active ${sessionText} ${colors.dim}across${RESET} ${colors.cyan}${sessionSummary.squadCount}${RESET} ${squadText}${toolInfo}`);
|
|
198
|
+
}
|
|
199
|
+
writeLine();
|
|
200
|
+
const statsParts = [`${colors.cyan}${stats.activeSquads}${RESET}/${stats.totalSquads} squads`];
|
|
201
|
+
if (ghStats) {
|
|
202
|
+
statsParts.push(`${colors.green}${stats.totalPRs}${RESET} PRs merged`);
|
|
203
|
+
statsParts.push(`${colors.purple}${stats.totalIssuesClosed}${RESET} closed`);
|
|
204
|
+
statsParts.push(`${colors.yellow}${stats.totalIssuesOpen}${RESET} open`);
|
|
205
|
+
} else {
|
|
206
|
+
statsParts.push(`${colors.cyan}${gitStats?.totalCommits || 0}${RESET} commits`);
|
|
207
|
+
statsParts.push(`${colors.dim}use -f for PRs/issues${RESET}`);
|
|
208
|
+
}
|
|
209
|
+
writeLine(` ${statsParts.join(` ${colors.dim}\u2502${RESET} `)}`);
|
|
210
|
+
writeLine();
|
|
211
|
+
writeLine(` ${progressBar(stats.overallProgress, 32)} ${colors.dim}${stats.overallProgress}% goal progress${RESET}`);
|
|
212
|
+
writeLine();
|
|
213
|
+
}
|
|
214
|
+
function renderSquadsTable(squadData) {
|
|
215
|
+
const w = { name: 13, commits: 9, prs: 5, issues: 8, goals: 7, bar: 10 };
|
|
216
|
+
const tableWidth = w.name + w.commits + w.prs + w.issues + w.goals + w.bar + 6;
|
|
217
|
+
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
218
|
+
writeLine(` ${colors.purple}${box.vertical}${RESET} ${bold}${padEnd("SQUAD", w.name)}${RESET}${bold}${padEnd("COMMITS", w.commits)}${RESET}${bold}${padEnd("PRs", w.prs)}${RESET}${bold}${padEnd("ISSUES", w.issues)}${RESET}${bold}${padEnd("GOALS", w.goals)}${RESET}${bold}PROGRESS${RESET} ${colors.purple}${box.vertical}${RESET}`);
|
|
219
|
+
writeLine(` ${colors.purple}${box.teeRight}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.teeLeft}${RESET}`);
|
|
220
|
+
const sortedSquads = [...squadData].sort((a, b) => {
|
|
221
|
+
const aActivity = (a.github?.commits || 0) + (a.github?.prsMerged || 0) * 5;
|
|
222
|
+
const bActivity = (b.github?.commits || 0) + (b.github?.prsMerged || 0) * 5;
|
|
223
|
+
return bActivity - aActivity;
|
|
224
|
+
});
|
|
225
|
+
for (const squad of sortedSquads) {
|
|
226
|
+
const gh = squad.github;
|
|
227
|
+
const commits = gh?.commits || 0;
|
|
228
|
+
const prs = gh?.prsMerged || 0;
|
|
229
|
+
const issuesClosed = gh?.issuesClosed || 0;
|
|
230
|
+
const issuesOpen = gh?.issuesOpen || 0;
|
|
231
|
+
const completedCount = squad.goals.filter((g) => g.completed).length;
|
|
232
|
+
const totalCount = squad.goals.length;
|
|
233
|
+
const commitColor = commits > 10 ? colors.green : commits > 0 ? colors.cyan : colors.dim;
|
|
234
|
+
const prColor = prs > 0 ? colors.green : colors.dim;
|
|
235
|
+
const issueColor = issuesClosed > 0 ? colors.green : colors.dim;
|
|
236
|
+
writeLine(` ${colors.purple}${box.vertical}${RESET} ${colors.cyan}${padEnd(squad.name, w.name)}${RESET}${commitColor}${padEnd(String(commits), w.commits)}${RESET}${prColor}${padEnd(String(prs), w.prs)}${RESET}${issueColor}${padEnd(`${issuesClosed}/${issuesOpen}`, w.issues)}${RESET}${padEnd(`${completedCount}/${totalCount}`, w.goals)}${progressBar(squad.goalProgress, 8)} ${colors.purple}${box.vertical}${RESET}`);
|
|
237
|
+
}
|
|
238
|
+
writeLine(` ${colors.purple}${box.bottomLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.bottomRight}${RESET}`);
|
|
239
|
+
writeLine();
|
|
240
|
+
}
|
|
241
|
+
function renderWorkingOn(gitStats) {
|
|
242
|
+
if (gitStats && gitStats.recentCommits && gitStats.recentCommits.length > 0) {
|
|
243
|
+
writeLine(` ${bold}Working On${RESET}`);
|
|
244
|
+
writeLine();
|
|
245
|
+
for (const commit of gitStats.recentCommits.slice(0, 3)) {
|
|
246
|
+
const shortHash = commit.hash.slice(0, 7);
|
|
247
|
+
const shortMsg = truncate(commit.message, 45);
|
|
248
|
+
writeLine(` ${colors.dim}${shortHash}${RESET} ${shortMsg} ${colors.dim}(${commit.repo})${RESET}`);
|
|
249
|
+
}
|
|
250
|
+
writeLine();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function renderGoalsSection(squadData) {
|
|
254
|
+
const allActiveGoals = squadData.flatMap(
|
|
255
|
+
(s) => s.goals.filter((g) => !g.completed).map((g) => ({
|
|
256
|
+
squad: s.name,
|
|
257
|
+
goal: g,
|
|
258
|
+
scope: inferScope(g.description)
|
|
259
|
+
}))
|
|
260
|
+
);
|
|
261
|
+
if (allActiveGoals.length > 0) {
|
|
262
|
+
const scopeOrder = { tactical: 0, operational: 1, strategic: 2 };
|
|
263
|
+
const sortedGoals = [...allActiveGoals].sort((a, b) => {
|
|
264
|
+
const scopeDiff = scopeOrder[a.scope] - scopeOrder[b.scope];
|
|
265
|
+
if (scopeDiff !== 0) return scopeDiff;
|
|
266
|
+
const aHasProgress = a.goal.progress ? 1 : 0;
|
|
267
|
+
const bHasProgress = b.goal.progress ? 1 : 0;
|
|
268
|
+
return bHasProgress - aHasProgress;
|
|
269
|
+
});
|
|
270
|
+
const scopeLabels = { tactical: "Next", operational: "In Progress", strategic: "Vision" };
|
|
271
|
+
const scopeIcons = { tactical: icons.active, operational: icons.progress, strategic: icons.empty };
|
|
272
|
+
writeLine(` ${bold}Goals${RESET} ${colors.dim}(tactical \u2192 strategic)${RESET}`);
|
|
273
|
+
writeLine();
|
|
274
|
+
const maxGoals = 5;
|
|
275
|
+
let lastScope = "";
|
|
276
|
+
for (const { squad, goal, scope } of sortedGoals.slice(0, maxGoals)) {
|
|
277
|
+
if (scope !== lastScope) {
|
|
278
|
+
const label = scopeLabels[scope];
|
|
279
|
+
const labelColor = scope === "tactical" ? colors.green : scope === "strategic" ? colors.purple : colors.cyan;
|
|
280
|
+
writeLine(` ${labelColor}${label}${RESET}`);
|
|
281
|
+
lastScope = scope;
|
|
282
|
+
}
|
|
283
|
+
const hasProgress = goal.progress && goal.progress.length > 0;
|
|
284
|
+
const icon = scopeIcons[scope];
|
|
285
|
+
writeLine(` ${icon} ${colors.dim}${squad}${RESET} ${truncate(goal.description, 48)}`);
|
|
286
|
+
if (hasProgress) {
|
|
287
|
+
writeLine(` ${colors.dim}\u2514${RESET} ${colors.green}${truncate(goal.progress, 52)}${RESET}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (allActiveGoals.length > maxGoals) {
|
|
291
|
+
writeLine(` ${colors.dim} +${allActiveGoals.length - maxGoals} more${RESET}`);
|
|
292
|
+
}
|
|
293
|
+
writeLine();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function renderDashboardFooter() {
|
|
297
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}<squad>${RESET} ${colors.dim}Execute a squad${RESET}`);
|
|
298
|
+
writeLine(` ${colors.dim}$${RESET} squads goal set ${colors.dim}Add a goal${RESET}`);
|
|
299
|
+
writeLine();
|
|
300
|
+
}
|
|
301
|
+
async function dashboardCommand(options = {}) {
|
|
302
|
+
await track(Events.CLI_DASHBOARD, { verbose: options.verbose, ceo: options.ceo, fast: options.fast });
|
|
303
|
+
const squadsDir = findSquadsDir();
|
|
304
|
+
if (!squadsDir) {
|
|
305
|
+
writeLine(`${colors.red}No .agents/squads directory found${RESET}`);
|
|
306
|
+
writeLine(`${colors.dim}Run \`squads init\` to create one.${RESET}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (options.ceo) {
|
|
310
|
+
await renderCeoReport(squadsDir);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const baseDir = findAgentsSquadsDir();
|
|
314
|
+
const squadNames = listSquads(squadsDir);
|
|
315
|
+
const skipGitHub = options.fast !== false;
|
|
316
|
+
const cache = await fetchDashboardData(baseDir, skipGitHub);
|
|
317
|
+
const squadData = collectSquadMetrics(squadNames, cache.gitStats, cache.ghStats);
|
|
318
|
+
const stats = calculateDashboardStats(squadData, cache.ghStats);
|
|
319
|
+
if (options.json) {
|
|
320
|
+
const goalCount2 = {
|
|
321
|
+
active: squadData.reduce((sum, s) => sum + s.goals.filter((g) => !g.completed).length, 0),
|
|
322
|
+
completed: squadData.reduce((sum, s) => sum + s.goals.filter((g) => g.completed).length, 0)
|
|
323
|
+
};
|
|
324
|
+
console.log(JSON.stringify({
|
|
325
|
+
ok: true,
|
|
326
|
+
command: "dash",
|
|
327
|
+
data: {
|
|
328
|
+
squads: squadData.map((s) => ({
|
|
329
|
+
name: s.name,
|
|
330
|
+
mission: s.mission,
|
|
331
|
+
status: s.status,
|
|
332
|
+
goalProgress: s.goalProgress,
|
|
333
|
+
lastActivity: s.lastActivity,
|
|
334
|
+
goals: s.goals
|
|
335
|
+
})),
|
|
336
|
+
stats,
|
|
337
|
+
goals: goalCount2,
|
|
338
|
+
sessions: cache.sessionSummary,
|
|
339
|
+
costs: cache.costs,
|
|
340
|
+
gitStats: cache.gitStats ? {
|
|
341
|
+
totalCommits: cache.gitStats.totalCommits,
|
|
342
|
+
repos: cache.gitStats.repos?.map((r) => ({ name: r.name, commits: r.commits }))
|
|
343
|
+
} : null
|
|
344
|
+
}
|
|
345
|
+
}, null, 2));
|
|
346
|
+
await closeDatabase();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
renderDashboardHeader(stats, cache.sessionSummary, cache.gitStats, cache.ghStats);
|
|
350
|
+
renderSquadsTable(squadData);
|
|
351
|
+
const goalCount = {
|
|
352
|
+
active: squadData.reduce((sum, s) => sum + s.goals.filter((g) => !g.completed).length, 0),
|
|
353
|
+
completed: squadData.reduce((sum, s) => sum + s.goals.filter((g) => g.completed).length, 0)
|
|
354
|
+
};
|
|
355
|
+
renderGitPerformanceCached(cache);
|
|
356
|
+
renderTokenEconomicsCached(cache, goalCount);
|
|
357
|
+
renderROICached(cache, goalCount);
|
|
358
|
+
renderQuotaCached(cache);
|
|
359
|
+
renderCapacityCached(cache);
|
|
360
|
+
renderInfrastructureCached(cache);
|
|
361
|
+
renderAcquisitionCached(cache);
|
|
362
|
+
renderHistoricalTrendsCached(cache);
|
|
363
|
+
renderInsightsCached(cache);
|
|
364
|
+
renderWorkingOn(cache.gitStats);
|
|
365
|
+
renderGoalsSection(squadData);
|
|
366
|
+
renderDashboardFooter();
|
|
367
|
+
saveSnapshotCached(squadData, cache, baseDir).catch(() => {
|
|
368
|
+
});
|
|
369
|
+
await closeDatabase();
|
|
370
|
+
}
|
|
371
|
+
async function fetchDashboardData(baseDir, skipGitHub) {
|
|
372
|
+
const timeout = (promise, ms, fallback) => Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(fallback), ms))]);
|
|
373
|
+
cleanupStaleSessions();
|
|
374
|
+
const [gitStats, ghStats, costs, bridgeStats, activity, dbAvailable, history, insights, sessionSummary, npmStats, quotaInfo, capacity, baseline] = await Promise.all([
|
|
375
|
+
// Git stats (local, parallel across repos)
|
|
376
|
+
baseDir ? getMultiRepoGitStats(baseDir, 30) : Promise.resolve(null),
|
|
377
|
+
// GitHub stats (network, ~20-30s) - skip by default for fast mode
|
|
378
|
+
skipGitHub ? Promise.resolve(null) : Promise.resolve(baseDir ? getGitHubStatsOptimized(baseDir, 30) : null),
|
|
379
|
+
// Langfuse costs (network, 2s timeout)
|
|
380
|
+
timeout(fetchCostSummary(100), 2e3, null),
|
|
381
|
+
// Bridge stats (local network, 2s timeout)
|
|
382
|
+
timeout(fetchBridgeStats(), 2e3, null),
|
|
383
|
+
// Activity sparkline (local, parallel across repos)
|
|
384
|
+
baseDir ? getActivitySparkline(baseDir, 14) : Promise.resolve([]),
|
|
385
|
+
// Database availability check (1.5s timeout)
|
|
386
|
+
timeout(isDatabaseAvailable(), 1500, false),
|
|
387
|
+
// Dashboard history (1.5s timeout)
|
|
388
|
+
timeout(getDashboardHistory(14).catch(() => []), 1500, []),
|
|
389
|
+
// Insights (2s timeout)
|
|
390
|
+
timeout(fetchInsights("week").catch(() => null), 2e3, null),
|
|
391
|
+
// Session summary (parallel lsof, ~1s)
|
|
392
|
+
getLiveSessionSummaryAsync(),
|
|
393
|
+
// NPM download stats (network, 2s timeout)
|
|
394
|
+
timeout(fetchNpmStats("squads-cli"), 2e3, null),
|
|
395
|
+
// Quota/autonomy info (local network, 2s timeout)
|
|
396
|
+
timeout(fetchQuotaInfo(), 2e3, null),
|
|
397
|
+
// Claude Code capacity (local file read, fast)
|
|
398
|
+
fetchClaudeCodeCapacity(),
|
|
399
|
+
// Latest baseline for ROI comparison (1.5s timeout)
|
|
400
|
+
timeout(getLatestBaseline().catch(() => null), 1500, null)
|
|
401
|
+
]);
|
|
402
|
+
const roiMetrics = calculateROIMetrics(costs, 0, gitStats?.totalCommits || 0, ghStats?.prsMerged || 0);
|
|
403
|
+
const squadProjections = calculateSquadCostProjections(bridgeStats, null);
|
|
404
|
+
return { gitStats, ghStats, costs, bridgeStats, activity, dbAvailable, history, insights, sessionSummary, npmStats, quotaInfo, capacity, baseline, roiMetrics, squadProjections };
|
|
405
|
+
}
|
|
406
|
+
function findAgentsSquadsDir() {
|
|
407
|
+
const parentDir = join(process.cwd(), "..");
|
|
408
|
+
if (existsSync(join(parentDir, "hq"))) {
|
|
409
|
+
return parentDir;
|
|
410
|
+
}
|
|
411
|
+
if (existsSync(join(process.cwd(), ".git"))) {
|
|
412
|
+
return process.cwd();
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
function formatK(n) {
|
|
417
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
|
418
|
+
if (n >= 1e3) return (n / 1e3).toFixed(0) + "k";
|
|
419
|
+
return String(n);
|
|
420
|
+
}
|
|
421
|
+
function renderGitPerformanceCached(cache) {
|
|
422
|
+
const { gitStats: stats, activity } = cache;
|
|
423
|
+
if (!stats || stats.totalCommits === 0) {
|
|
424
|
+
writeLine(` ${bold}Git Activity${RESET} ${colors.dim}(no commits in 30d)${RESET}`);
|
|
425
|
+
writeLine();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
writeLine(` ${bold}Git Activity${RESET} ${colors.dim}(30d)${RESET}`);
|
|
429
|
+
writeLine();
|
|
430
|
+
if (activity.length > 0) {
|
|
431
|
+
const spark = sparkline(activity);
|
|
432
|
+
writeLine(` ${colors.dim}Last 14d:${RESET} ${spark}`);
|
|
433
|
+
writeLine();
|
|
434
|
+
}
|
|
435
|
+
const metrics = [
|
|
436
|
+
`${colors.cyan}${stats.totalCommits}${RESET} commits`,
|
|
437
|
+
`${colors.green}${stats.avgCommitsPerDay}${RESET}/day`,
|
|
438
|
+
`${colors.purple}${stats.activeDays}${RESET} active days`
|
|
439
|
+
];
|
|
440
|
+
if (stats.peakDay) {
|
|
441
|
+
metrics.push(`${colors.yellow}${stats.peakDay.count}${RESET} peak ${colors.dim}(${stats.peakDay.date})${RESET}`);
|
|
442
|
+
}
|
|
443
|
+
writeLine(` ${metrics.join(` ${colors.dim}\u2502${RESET} `)}`);
|
|
444
|
+
writeLine();
|
|
445
|
+
const sortedRepos = Array.from(stats.commitsByRepo.entries()).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
446
|
+
if (sortedRepos.length > 0) {
|
|
447
|
+
const maxRepoCommits = sortedRepos[0][1];
|
|
448
|
+
for (const [repo, commits] of sortedRepos) {
|
|
449
|
+
const bar = barChart(commits, maxRepoCommits, 12);
|
|
450
|
+
writeLine(` ${colors.cyan}${padEnd(repo, 20)}${RESET}${bar} ${colors.dim}${commits}${RESET}`);
|
|
451
|
+
}
|
|
452
|
+
writeLine();
|
|
453
|
+
}
|
|
454
|
+
const sortedAuthors = Array.from(stats.commitsByAuthor.entries()).sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
455
|
+
if (sortedAuthors.length > 0) {
|
|
456
|
+
const authorLine = sortedAuthors.map(([author, count]) => `${colors.dim}${truncate(author, 15)}${RESET} ${colors.cyan}${count}${RESET}`).join(` ${colors.dim}\u2502${RESET} `);
|
|
457
|
+
writeLine(` ${colors.dim}By author:${RESET} ${authorLine}`);
|
|
458
|
+
writeLine();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function renderTokenEconomicsCached(cache, goalCount) {
|
|
462
|
+
const costs = cache.costs;
|
|
463
|
+
const stats = cache.bridgeStats;
|
|
464
|
+
const hasInfra = hasLocalInfraConfig();
|
|
465
|
+
const hasData = costs || stats;
|
|
466
|
+
writeLine(` ${bold}Token Economics${RESET}`);
|
|
467
|
+
writeLine();
|
|
468
|
+
const planType = getPlanType();
|
|
469
|
+
const tier = parseInt(process.env.ANTHROPIC_TIER || "0", 10);
|
|
470
|
+
if (planType === "unknown") {
|
|
471
|
+
writeLine(` ${colors.dim}\u25CB${RESET} ${bold}Plan${RESET} ${colors.yellow}not configured${RESET}`);
|
|
472
|
+
writeLine();
|
|
473
|
+
writeLine(` ${colors.dim}Set your Claude plan:${RESET}`);
|
|
474
|
+
writeLine(` ${colors.dim}$${RESET} export SQUADS_PLAN_TYPE=max ${colors.dim}# $200/mo flat${RESET}`);
|
|
475
|
+
writeLine(` ${colors.dim}$${RESET} export SQUADS_PLAN_TYPE=usage ${colors.dim}# pay-per-token${RESET}`);
|
|
476
|
+
writeLine();
|
|
477
|
+
} else {
|
|
478
|
+
const maxPlan = planType === "max";
|
|
479
|
+
const planIcon = maxPlan ? `${colors.purple}\u25C6${RESET}` : `${colors.dim}\u25CB${RESET}`;
|
|
480
|
+
const planLabel = maxPlan ? "Claude Max" : "Claude Pro";
|
|
481
|
+
const planCost = maxPlan ? "$200/mo flat" : "pay-per-token";
|
|
482
|
+
const tierDisplay = tier > 0 ? ` ${colors.dim}Tier ${tier}${RESET}` : "";
|
|
483
|
+
writeLine(` ${planIcon} ${bold}${planLabel}${RESET} ${colors.dim}${planCost}${RESET}${tierDisplay}`);
|
|
484
|
+
writeLine();
|
|
485
|
+
}
|
|
486
|
+
if (!hasInfra || !hasData) {
|
|
487
|
+
writeLine(` ${colors.dim}\u25CB${RESET} Track costs, tokens, and API usage`);
|
|
488
|
+
writeLine(` ${colors.dim}\u25CB${RESET} Monitor rate limits and budgets`);
|
|
489
|
+
writeLine();
|
|
490
|
+
writeLine(` ${colors.dim}Setup:${RESET} github.com/agents-squads/squads-cli#analytics`);
|
|
491
|
+
writeLine();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const todayTokens = stats ? stats.today.inputTokens + stats.today.outputTokens : 0;
|
|
495
|
+
const todayCalls = stats?.today.generations || costs?.totalCalls || 0;
|
|
496
|
+
const todayCost = stats?.today.costUsd || costs?.totalCost || 0;
|
|
497
|
+
writeLine(` ${colors.dim}Today${RESET}`);
|
|
498
|
+
writeLine(` ${colors.cyan}${formatK(todayTokens)}${RESET} tokens ${colors.dim}\u2502${RESET} ${colors.cyan}${todayCalls}${RESET} calls ${colors.dim}\u2502${RESET} ${colors.green}$${todayCost.toFixed(2)}${RESET}`);
|
|
499
|
+
if (stats?.week && stats.week.generations > 0) {
|
|
500
|
+
const weekTokens = (stats.week.inputTokens || 0) + (stats.week.outputTokens || 0);
|
|
501
|
+
writeLine(` ${colors.dim}Week${RESET} ${colors.purple}${formatK(weekTokens)}${RESET} tokens ${colors.dim}\u2502${RESET} ${colors.purple}${stats.week.generations}${RESET} calls ${colors.dim}\u2502${RESET} ${colors.purple}$${stats.week.costUsd.toFixed(2)}${RESET}`);
|
|
502
|
+
}
|
|
503
|
+
writeLine();
|
|
504
|
+
if (goalCount && goalCount.completed > 0 && todayTokens > 0) {
|
|
505
|
+
const tokensPerGoal = Math.round(todayTokens / goalCount.completed);
|
|
506
|
+
writeLine(` ${colors.dim}Efficiency${RESET}`);
|
|
507
|
+
writeLine(` ${colors.cyan}${formatK(tokensPerGoal)}${RESET} tokens/goal ${colors.dim}\u2502${RESET} ${colors.green}${goalCount.completed}${RESET} goals done`);
|
|
508
|
+
writeLine();
|
|
509
|
+
}
|
|
510
|
+
writeLine(` ${colors.dim}Rate Limits${RESET} ${colors.dim}(check /usage for real limits)${RESET}`);
|
|
511
|
+
const tierLimits = {
|
|
512
|
+
1: { rpm: 50, inputTpm: 3e4, outputTpm: 8e3 },
|
|
513
|
+
2: { rpm: 1e3, inputTpm: 45e4, outputTpm: 9e4 },
|
|
514
|
+
3: { rpm: 2e3, inputTpm: 8e5, outputTpm: 16e4 },
|
|
515
|
+
4: { rpm: 4e3, inputTpm: 2e6, outputTpm: 4e5 }
|
|
516
|
+
};
|
|
517
|
+
const limits = tierLimits[tier] || tierLimits[4];
|
|
518
|
+
const now = /* @__PURE__ */ new Date();
|
|
519
|
+
const minutesElapsed = Math.max(now.getHours() * 60 + now.getMinutes(), 1);
|
|
520
|
+
const callsPerMinute = todayCalls / minutesElapsed;
|
|
521
|
+
const tokensPerMinute = todayTokens / minutesElapsed;
|
|
522
|
+
const rpmPct = callsPerMinute / limits.rpm * 100;
|
|
523
|
+
const tpmPct = tokensPerMinute / (limits.inputTpm + limits.outputTpm) * 100;
|
|
524
|
+
const rpmBar = progressBar(Math.min(rpmPct, 100), 10);
|
|
525
|
+
const tpmBar = progressBar(Math.min(tpmPct, 100), 10);
|
|
526
|
+
const rpmColor = rpmPct > 75 ? colors.red : rpmPct > 50 ? colors.yellow : colors.green;
|
|
527
|
+
const tpmColor = tpmPct > 75 ? colors.red : tpmPct > 50 ? colors.yellow : colors.green;
|
|
528
|
+
writeLine(` RPM ${rpmBar} ${rpmColor}${callsPerMinute.toFixed(1)}${RESET}${colors.dim}/${limits.rpm}${RESET}`);
|
|
529
|
+
writeLine(` TPM ${tpmBar} ${tpmColor}${formatK(Math.round(tokensPerMinute))}${RESET}${colors.dim}/${formatK(limits.inputTpm + limits.outputTpm)}${RESET}`);
|
|
530
|
+
const rpmAvailable = Math.max(0, limits.rpm - callsPerMinute);
|
|
531
|
+
const tpmAvailable = Math.max(0, limits.inputTpm + limits.outputTpm - tokensPerMinute);
|
|
532
|
+
if (rpmAvailable > 100 && tpmAvailable > 1e4) {
|
|
533
|
+
writeLine(` ${colors.green}\u25CF${RESET} ${colors.dim}Capacity for autonomous triggers${RESET}`);
|
|
534
|
+
} else if (rpmPct > 75 || tpmPct > 75) {
|
|
535
|
+
writeLine(` ${colors.yellow}\u26A0${RESET} ${colors.yellow}Rate limits constrained${RESET}`);
|
|
536
|
+
}
|
|
537
|
+
writeLine();
|
|
538
|
+
}
|
|
539
|
+
function renderQuotaCached(cache) {
|
|
540
|
+
const quota = cache.quotaInfo;
|
|
541
|
+
if (!quota || !quota.monthlyQuota || quota.monthlyQuota === 0) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const monthlyUsed = quota.monthlyUsed || 0;
|
|
545
|
+
const monthlyQuota = quota.monthlyQuota;
|
|
546
|
+
const roiMultiplier = monthlyUsed / monthlyQuota;
|
|
547
|
+
const roiDisplay = roiMultiplier >= 1 ? `${roiMultiplier.toFixed(1)}x` : `${(roiMultiplier * 100).toFixed(0)}%`;
|
|
548
|
+
const utilizationColor = roiMultiplier >= 2 ? colors.green : roiMultiplier >= 1 ? colors.cyan : roiMultiplier >= 0.5 ? colors.yellow : colors.dim;
|
|
549
|
+
const roiIcon = roiMultiplier >= 3 ? "\u{1F680}" : roiMultiplier >= 2 ? "\u{1F389}" : roiMultiplier >= 1 ? "\u2713" : "";
|
|
550
|
+
writeLine(` ${bold}Subscription ROI${RESET} ${colors.dim}(autonomy: ${quota.autonomyScore}% ${quota.confidenceLevel})${RESET}`);
|
|
551
|
+
writeLine();
|
|
552
|
+
const barWidth = 24;
|
|
553
|
+
const fillPct = Math.min(roiMultiplier, 1);
|
|
554
|
+
const filled = Math.round(fillPct * barWidth);
|
|
555
|
+
const overflowIndicator = roiMultiplier > 1 ? `${utilizationColor}+${RESET}` : "";
|
|
556
|
+
const bar = `${utilizationColor}${"\u2588".repeat(filled)}${colors.dim}${"\u2591".repeat(barWidth - filled)}${RESET}${overflowIndicator}`;
|
|
557
|
+
writeLine(` ${bar} ${utilizationColor}${roiDisplay}${RESET} ${roiIcon}`);
|
|
558
|
+
writeLine(` ${colors.green}$${monthlyUsed.toFixed(2)}${RESET} ${colors.dim}consumed${RESET} ${colors.dim}/${RESET} $${monthlyQuota}${colors.dim}/mo subscription${RESET}`);
|
|
559
|
+
if (roiMultiplier >= 2) {
|
|
560
|
+
writeLine(` ${colors.green}Excellent value${RESET} ${colors.dim}- ${roiDisplay} return on $${monthlyQuota} subscription${RESET}`);
|
|
561
|
+
} else if (roiMultiplier >= 1) {
|
|
562
|
+
writeLine(` ${colors.cyan}Good utilization${RESET} ${colors.dim}- maximizing subscription value${RESET}`);
|
|
563
|
+
} else {
|
|
564
|
+
const potentialValue = monthlyQuota - monthlyUsed;
|
|
565
|
+
writeLine(` ${colors.yellow}Room to grow${RESET} ${colors.dim}- $${potentialValue.toFixed(0)} of unused capacity${RESET}`);
|
|
566
|
+
writeLine(` ${colors.dim}Tip: Run more routines with${RESET} ${colors.cyan}squads run <squad>${RESET}`);
|
|
567
|
+
}
|
|
568
|
+
if (quota.learningCount > 0) {
|
|
569
|
+
writeLine(` ${colors.dim}Learnings:${RESET} ${colors.purple}${quota.learningCount}${RESET} ${colors.dim}captured${RESET}`);
|
|
570
|
+
}
|
|
571
|
+
writeLine();
|
|
572
|
+
}
|
|
573
|
+
function renderCapacityCached(cache) {
|
|
574
|
+
const cap = cache.capacity;
|
|
575
|
+
if (!cap) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const formatTokens = (n) => {
|
|
579
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
580
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(0)}k`;
|
|
581
|
+
return n.toString();
|
|
582
|
+
};
|
|
583
|
+
writeLine(` ${bold}Subscription Capacity${RESET} ${colors.dim}(from Claude Code)${RESET}`);
|
|
584
|
+
writeLine();
|
|
585
|
+
const weeklyPct = cap.weeklyCapacityPct;
|
|
586
|
+
const weeklyColor = weeklyPct > 80 ? colors.red : weeklyPct > 60 ? colors.yellow : colors.green;
|
|
587
|
+
const weeklyBarWidth = 20;
|
|
588
|
+
const weeklyFilled = Math.min(Math.round(weeklyPct / 100 * weeklyBarWidth), weeklyBarWidth);
|
|
589
|
+
const weeklyBar = `${weeklyColor}${"\u2588".repeat(weeklyFilled)}${colors.dim}${"\u2591".repeat(weeklyBarWidth - weeklyFilled)}${RESET}`;
|
|
590
|
+
writeLine(` ${colors.dim}Weekly:${RESET} ${weeklyBar} ${weeklyColor}${weeklyPct}%${RESET} ${colors.dim}(resets ${cap.weeklyResetDate})${RESET}`);
|
|
591
|
+
writeLine(` ${colors.dim}${formatTokens(cap.weeklyTokensUsed)} / ${formatTokens(cap.weeklyTokensLimit)} tokens${RESET}`);
|
|
592
|
+
const opusPct = cap.weeklyTokensUsed > 0 ? Math.round(cap.opusTokensUsed / cap.weeklyTokensUsed * 100) : 0;
|
|
593
|
+
const sonnetPct = cap.weeklyTokensUsed > 0 ? Math.round(cap.sonnetTokensUsed / cap.weeklyTokensUsed * 100) : 0;
|
|
594
|
+
writeLine(` ${colors.dim}opus${RESET} ${colors.purple}${opusPct}%${RESET} ${colors.dim}sonnet${RESET} ${colors.cyan}${sonnetPct}%${RESET}`);
|
|
595
|
+
if (cap.sessionCapacityPct > 10) {
|
|
596
|
+
const sessionPct = cap.sessionCapacityPct;
|
|
597
|
+
const sessionColor = sessionPct > 80 ? colors.red : sessionPct > 60 ? colors.yellow : colors.green;
|
|
598
|
+
writeLine(` ${colors.dim}Session:${RESET} ${sessionColor}${sessionPct}%${RESET} ${colors.dim}(resets ${cap.sessionResetTime})${RESET}`);
|
|
599
|
+
}
|
|
600
|
+
const headroom = 100 - weeklyPct;
|
|
601
|
+
if (headroom > 50) {
|
|
602
|
+
writeLine(` ${colors.green}\u25CF${RESET} ${colors.dim}${headroom}% headroom for autonomous agents${RESET}`);
|
|
603
|
+
} else if (headroom > 20) {
|
|
604
|
+
writeLine(` ${colors.yellow}\u25CF${RESET} ${colors.dim}${headroom}% remaining - monitor usage${RESET}`);
|
|
605
|
+
} else {
|
|
606
|
+
writeLine(` ${colors.red}\u25CF${RESET} ${colors.dim}Low capacity - consider Sonnet for routine tasks${RESET}`);
|
|
607
|
+
}
|
|
608
|
+
writeLine();
|
|
609
|
+
}
|
|
610
|
+
function renderInfrastructureCached(cache) {
|
|
611
|
+
const stats = cache.bridgeStats;
|
|
612
|
+
const hasInfra = hasLocalInfraConfig();
|
|
613
|
+
if (!hasInfra || !stats) {
|
|
614
|
+
writeLine(` ${bold}Infrastructure${RESET} ${colors.dim}(not connected)${RESET}`);
|
|
615
|
+
writeLine();
|
|
616
|
+
writeLine(` ${colors.dim}\u25CB${RESET} postgres ${colors.dim}\u25CB${RESET} redis ${colors.dim}\u25CB${RESET} otel`);
|
|
617
|
+
writeLine();
|
|
618
|
+
writeLine(` ${colors.dim}Setup:${RESET} github.com/agents-squads/squads-cli#infrastructure`);
|
|
619
|
+
writeLine();
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
writeLine(` ${bold}Infrastructure${RESET} ${colors.dim}(${stats.source})${RESET}`);
|
|
623
|
+
writeLine();
|
|
624
|
+
const pgStatus = stats.health.postgres === "connected" ? `${colors.green}\u25CF${RESET}` : `${colors.red}\u25CF${RESET}`;
|
|
625
|
+
const redisStatus = stats.health.redis === "connected" ? `${colors.green}\u25CF${RESET}` : stats.health.redis === "disabled" ? `${colors.dim}\u25CB${RESET}` : `${colors.red}\u25CF${RESET}`;
|
|
626
|
+
const otelWorking = stats.health.postgres === "connected" && stats.today.generations > 0;
|
|
627
|
+
const otelStatus = otelWorking ? `${colors.green}\u25CF${RESET}` : `${colors.dim}\u25CB${RESET}`;
|
|
628
|
+
writeLine(` ${pgStatus} postgres ${redisStatus} redis ${otelStatus} otel`);
|
|
629
|
+
writeLine();
|
|
630
|
+
if (stats.today.generations > 0 || stats.today.costUsd > 0) {
|
|
631
|
+
const maxPlan = isMaxPlan();
|
|
632
|
+
const costColor = maxPlan ? colors.green : stats.budget.usedPct > 80 ? colors.red : stats.budget.usedPct > 50 ? colors.yellow : colors.green;
|
|
633
|
+
const costDisplay = maxPlan ? `${costColor}$${stats.today.costUsd.toFixed(2)}${RESET}` : `${costColor}$${stats.today.costUsd.toFixed(2)}${RESET}${colors.dim}/$${stats.budget.daily}${RESET}`;
|
|
634
|
+
writeLine(` ${colors.dim}Today:${RESET} ${colors.cyan}${stats.today.generations}${RESET}${colors.dim} calls${RESET} ${costDisplay} ${colors.dim}${formatK(stats.today.inputTokens)}+${formatK(stats.today.outputTokens)} tokens${RESET}`);
|
|
635
|
+
if (stats.byModel && stats.byModel.length > 0) {
|
|
636
|
+
const modelLine = stats.byModel.map((m) => {
|
|
637
|
+
const shortName = m.model.includes("opus") ? "opus" : m.model.includes("sonnet") ? "sonnet" : m.model.includes("haiku") ? "haiku" : m.model.slice(0, 10);
|
|
638
|
+
return `${colors.dim}${shortName}${RESET} ${colors.cyan}${m.generations}${RESET}`;
|
|
639
|
+
}).join(" ");
|
|
640
|
+
writeLine(` ${colors.dim}Models:${RESET} ${modelLine}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (stats.week && stats.week.generations > 0) {
|
|
644
|
+
const weekModelLine = stats.week.byModel?.map((m) => {
|
|
645
|
+
const shortName = m.model.includes("opus") ? "opus" : m.model.includes("sonnet") ? "sonnet" : m.model.includes("haiku") ? "haiku" : m.model.slice(0, 10);
|
|
646
|
+
return `${colors.dim}${shortName}${RESET} ${colors.purple}$${m.costUsd.toFixed(0)}${RESET}`;
|
|
647
|
+
}).join(" ") || "";
|
|
648
|
+
writeLine(` ${colors.dim}Week:${RESET} ${colors.cyan}${stats.week.generations}${RESET}${colors.dim} calls${RESET} ${colors.purple}$${stats.week.costUsd.toFixed(2)}${RESET} ${weekModelLine}`);
|
|
649
|
+
}
|
|
650
|
+
writeLine();
|
|
651
|
+
}
|
|
652
|
+
function renderAcquisitionCached(cache) {
|
|
653
|
+
const npmPackage = process.env.SQUADS_NPM_PACKAGE;
|
|
654
|
+
if (!npmPackage) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const npm = cache.npmStats;
|
|
658
|
+
if (!npm) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
writeLine(` ${bold}Acquisition${RESET} ${colors.dim}(npm)${RESET}`);
|
|
662
|
+
writeLine();
|
|
663
|
+
const trendIcon = npm.weekOverWeek >= 0 ? `${colors.green}\u2191${RESET}` : `${colors.red}\u2193${RESET}`;
|
|
664
|
+
const trendColor = npm.weekOverWeek >= 0 ? colors.green : colors.red;
|
|
665
|
+
writeLine(` ${colors.cyan}${npm.downloads.lastWeek}${RESET} installs/week ${trendIcon} ${trendColor}${Math.abs(npm.weekOverWeek)}%${RESET} ${colors.dim}wow${RESET}`);
|
|
666
|
+
writeLine(` ${colors.dim}Today${RESET} ${npm.downloads.lastDay} ${colors.dim}\u2502${RESET} ${colors.dim}Month${RESET} ${npm.downloads.lastMonth}`);
|
|
667
|
+
writeLine();
|
|
668
|
+
}
|
|
669
|
+
async function saveSnapshotCached(squadData, cache, _baseDir) {
|
|
670
|
+
if (!cache.dbAvailable) return;
|
|
671
|
+
const { gitStats, ghStats, costs } = cache;
|
|
672
|
+
const squadsData = squadData.map((s) => ({
|
|
673
|
+
name: s.name,
|
|
674
|
+
commits: s.github?.commits || 0,
|
|
675
|
+
prsOpened: s.github?.prsOpened || 0,
|
|
676
|
+
prsMerged: s.github?.prsMerged || 0,
|
|
677
|
+
issuesClosed: s.github?.issuesClosed || 0,
|
|
678
|
+
issuesOpen: s.github?.issuesOpen || 0,
|
|
679
|
+
goalsActive: s.goals.filter((g) => !g.completed).length,
|
|
680
|
+
goalsTotal: s.goals.length,
|
|
681
|
+
progress: s.goalProgress
|
|
682
|
+
}));
|
|
683
|
+
const authorsData = gitStats ? Array.from(gitStats.commitsByAuthor.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([name, commits]) => ({ name, commits })) : [];
|
|
684
|
+
const reposData = gitStats ? Array.from(gitStats.commitsByRepo.entries()).sort((a, b) => b[1] - a[1]).map(([name, commits]) => ({ name, commits })) : [];
|
|
685
|
+
const totalInputTokens = costs?.bySquad.reduce((sum, s) => sum + s.inputTokens, 0) || 0;
|
|
686
|
+
const totalOutputTokens = costs?.bySquad.reduce((sum, s) => sum + s.outputTokens, 0) || 0;
|
|
687
|
+
const overallProgress = squadData.length > 0 ? Math.round(squadData.reduce((sum, s) => sum + s.goalProgress, 0) / squadData.length) : 0;
|
|
688
|
+
const snapshot = {
|
|
689
|
+
totalSquads: squadData.length,
|
|
690
|
+
totalCommits: gitStats?.totalCommits || 0,
|
|
691
|
+
totalPrsMerged: ghStats?.prsMerged || 0,
|
|
692
|
+
totalIssuesClosed: ghStats?.issuesClosed || 0,
|
|
693
|
+
totalIssuesOpen: ghStats?.issuesOpen || 0,
|
|
694
|
+
goalProgressPct: overallProgress,
|
|
695
|
+
costUsd: costs?.totalCost || 0,
|
|
696
|
+
dailyBudgetUsd: costs?.dailyBudget || 0,
|
|
697
|
+
// 0 = not configured (no hardcoded defaults)
|
|
698
|
+
inputTokens: totalInputTokens,
|
|
699
|
+
outputTokens: totalOutputTokens,
|
|
700
|
+
commits30d: gitStats?.totalCommits || 0,
|
|
701
|
+
avgCommitsPerDay: gitStats?.avgCommitsPerDay || 0,
|
|
702
|
+
activeDays: gitStats?.activeDays || 0,
|
|
703
|
+
peakCommits: gitStats?.peakDay?.count || 0,
|
|
704
|
+
peakDate: gitStats?.peakDay?.date || null,
|
|
705
|
+
squadsData,
|
|
706
|
+
authorsData,
|
|
707
|
+
reposData
|
|
708
|
+
};
|
|
709
|
+
const saveTimeout = new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
710
|
+
await Promise.race([saveDashboardSnapshot(snapshot), saveTimeout]);
|
|
711
|
+
}
|
|
712
|
+
var TACTICAL_KEYWORDS = ["fix", "add", "update", "implement", "run", "test", "deploy", "merge", "review", "debug"];
|
|
713
|
+
var STRATEGIC_KEYWORDS = ["revenue", "establish", "build", "create", "define", "design", "launch", "ship", "foundation", "strategy"];
|
|
714
|
+
function inferScope(goal) {
|
|
715
|
+
const lower = goal.toLowerCase();
|
|
716
|
+
if (TACTICAL_KEYWORDS.some((k) => lower.includes(k))) return "tactical";
|
|
717
|
+
if (STRATEGIC_KEYWORDS.some((k) => lower.includes(k))) return "strategic";
|
|
718
|
+
return "operational";
|
|
719
|
+
}
|
|
720
|
+
function inferPriority(goal) {
|
|
721
|
+
const lower = goal.toLowerCase();
|
|
722
|
+
if (STRATEGIC_KEYWORDS.slice(0, 3).some((k) => lower.includes(k))) return "P0";
|
|
723
|
+
if (TACTICAL_KEYWORDS.slice(0, 4).some((k) => lower.includes(k))) return "P1";
|
|
724
|
+
return "P2";
|
|
725
|
+
}
|
|
726
|
+
async function renderCeoReport(squadsDir) {
|
|
727
|
+
const squadNames = listSquads(squadsDir);
|
|
728
|
+
const allGoals = [];
|
|
729
|
+
const blockers = [];
|
|
730
|
+
let activeSquads = 0;
|
|
731
|
+
let staleSquads = 0;
|
|
732
|
+
for (const name of squadNames) {
|
|
733
|
+
const squad = loadSquad(name);
|
|
734
|
+
if (!squad) continue;
|
|
735
|
+
const lastActivity = getLastActivityDate(name);
|
|
736
|
+
const activeGoals = squad.goals.filter((g) => !g.completed);
|
|
737
|
+
if (activeGoals.length === 0) {
|
|
738
|
+
blockers.push(`${name}: No active goals`);
|
|
739
|
+
} else if (lastActivity.includes("w") || lastActivity === "\u2014") {
|
|
740
|
+
blockers.push(`${name}: Stale (${lastActivity})`);
|
|
741
|
+
staleSquads++;
|
|
742
|
+
} else {
|
|
743
|
+
activeSquads++;
|
|
744
|
+
}
|
|
745
|
+
for (const goal of activeGoals) {
|
|
746
|
+
allGoals.push({
|
|
747
|
+
squad: name,
|
|
748
|
+
goal,
|
|
749
|
+
priority: inferPriority(goal.description)
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
allGoals.sort((a, b) => {
|
|
754
|
+
const order = { P0: 0, P1: 1, P2: 2 };
|
|
755
|
+
return order[a.priority] - order[b.priority];
|
|
756
|
+
});
|
|
757
|
+
const p0Goals = allGoals.filter((g) => g.priority === "P0");
|
|
758
|
+
const p1Goals = allGoals.filter((g) => g.priority === "P1");
|
|
759
|
+
writeLine();
|
|
760
|
+
writeLine(` ${gradient("squads")} ${colors.dim}CEO Report${RESET}`);
|
|
761
|
+
writeLine(` ${colors.dim}${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}${RESET}`);
|
|
762
|
+
writeLine();
|
|
763
|
+
const w = { label: 20, value: 12 };
|
|
764
|
+
const tableWidth = w.label + w.value + 4;
|
|
765
|
+
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
766
|
+
writeLine(` ${colors.purple}${box.vertical}${RESET} ${bold}${padEnd("METRIC", w.label)}${RESET}${bold}VALUE${RESET} ${colors.purple}${box.vertical}${RESET}`);
|
|
767
|
+
writeLine(` ${colors.purple}${box.teeRight}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.teeLeft}${RESET}`);
|
|
768
|
+
writeLine(` ${colors.purple}${box.vertical}${RESET} ${padEnd("Active Squads", w.label)}${colors.green}${padEnd(`${activeSquads}/${squadNames.length}`, w.value)}${RESET}${colors.purple}${box.vertical}${RESET}`);
|
|
769
|
+
writeLine(` ${colors.purple}${box.vertical}${RESET} ${padEnd("P0 Goals", w.label)}${colors.red}${padEnd(String(p0Goals.length), w.value)}${RESET}${colors.purple}${box.vertical}${RESET}`);
|
|
770
|
+
writeLine(` ${colors.purple}${box.vertical}${RESET} ${padEnd("P1 Goals", w.label)}${colors.yellow}${padEnd(String(p1Goals.length), w.value)}${RESET}${colors.purple}${box.vertical}${RESET}`);
|
|
771
|
+
writeLine(` ${colors.purple}${box.vertical}${RESET} ${padEnd("Blockers", w.label)}${blockers.length > 0 ? colors.red : colors.green}${padEnd(String(blockers.length), w.value)}${RESET}${colors.purple}${box.vertical}${RESET}`);
|
|
772
|
+
const costs = await fetchCostSummary(100);
|
|
773
|
+
if (costs) {
|
|
774
|
+
const spendStr = `$${costs.totalCost.toFixed(2)} / $${costs.dailyBudget}`;
|
|
775
|
+
writeLine(` ${colors.purple}${box.vertical}${RESET} ${padEnd("Daily Spend", w.label)}${colors.green}${padEnd(spendStr, w.value)}${RESET}${colors.purple}${box.vertical}${RESET}`);
|
|
776
|
+
}
|
|
777
|
+
writeLine(` ${colors.purple}${box.bottomLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.bottomRight}${RESET}`);
|
|
778
|
+
writeLine();
|
|
779
|
+
if (p0Goals.length > 0) {
|
|
780
|
+
writeLine(` ${bold}${colors.red}P0${RESET} ${bold}Priorities${RESET} ${colors.dim}(revenue/launch critical)${RESET}`);
|
|
781
|
+
writeLine();
|
|
782
|
+
for (const { squad, goal } of p0Goals.slice(0, 5)) {
|
|
783
|
+
writeLine(` ${icons.error} ${colors.cyan}${squad}${RESET} ${goal.description}`);
|
|
784
|
+
if (goal.progress) {
|
|
785
|
+
writeLine(` ${colors.dim}\u2514 ${goal.progress}${RESET}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
writeLine();
|
|
789
|
+
}
|
|
790
|
+
if (p1Goals.length > 0) {
|
|
791
|
+
writeLine(` ${bold}${colors.yellow}P1${RESET} ${bold}Important${RESET} ${colors.dim}(tracking/foundations)${RESET}`);
|
|
792
|
+
writeLine();
|
|
793
|
+
for (const { squad, goal } of p1Goals.slice(0, 3)) {
|
|
794
|
+
writeLine(` ${icons.warning} ${colors.cyan}${squad}${RESET} ${truncate(goal.description, 50)}`);
|
|
795
|
+
}
|
|
796
|
+
if (p1Goals.length > 3) {
|
|
797
|
+
writeLine(` ${colors.dim} +${p1Goals.length - 3} more${RESET}`);
|
|
798
|
+
}
|
|
799
|
+
writeLine();
|
|
800
|
+
}
|
|
801
|
+
if (blockers.length > 0) {
|
|
802
|
+
writeLine(` ${bold}Blockers${RESET}`);
|
|
803
|
+
writeLine();
|
|
804
|
+
for (const blocker of blockers.slice(0, 3)) {
|
|
805
|
+
writeLine(` ${icons.error} ${colors.red}${blocker}${RESET}`);
|
|
806
|
+
}
|
|
807
|
+
writeLine();
|
|
808
|
+
}
|
|
809
|
+
writeLine(` ${bold}Next Steps${RESET}`);
|
|
810
|
+
writeLine();
|
|
811
|
+
if (p0Goals.length > 0) {
|
|
812
|
+
writeLine(` ${icons.active} Focus on P0: ${colors.cyan}${p0Goals[0].squad}${RESET} - ${truncate(p0Goals[0].goal.description, 40)}`);
|
|
813
|
+
}
|
|
814
|
+
if (blockers.length > 0) {
|
|
815
|
+
writeLine(` ${icons.warning} Unblock: ${colors.yellow}${blockers[0]}${RESET}`);
|
|
816
|
+
}
|
|
817
|
+
if (staleSquads > 0) {
|
|
818
|
+
writeLine(` ${icons.progress} Revive ${staleSquads} stale squad(s)`);
|
|
819
|
+
}
|
|
820
|
+
writeLine();
|
|
821
|
+
writeLine(` ${colors.dim}$${RESET} squads dash ${colors.dim}Full operational view${RESET}`);
|
|
822
|
+
writeLine(` ${colors.dim}$${RESET} squads goal list ${colors.dim}All active goals${RESET}`);
|
|
823
|
+
writeLine();
|
|
824
|
+
}
|
|
825
|
+
function renderHistoricalTrendsCached(cache) {
|
|
826
|
+
if (!cache.dbAvailable) return;
|
|
827
|
+
const history = cache.history;
|
|
828
|
+
if (history.length < 2) return;
|
|
829
|
+
writeLine(` ${bold}Usage Trends${RESET} ${colors.dim}(${history.length}d history)${RESET}`);
|
|
830
|
+
writeLine();
|
|
831
|
+
const dailyCosts = history.map((h) => h.costUsd).reverse();
|
|
832
|
+
const costSparkStr = sparkline(dailyCosts);
|
|
833
|
+
const totalSpend = dailyCosts.reduce((sum, c) => sum + c, 0);
|
|
834
|
+
const avgDaily = totalSpend / dailyCosts.length;
|
|
835
|
+
writeLine(` ${colors.dim}Cost:${RESET} ${costSparkStr} ${colors.green}$${totalSpend.toFixed(2)}${RESET} total ${colors.dim}($${avgDaily.toFixed(2)}/day avg)${RESET}`);
|
|
836
|
+
const inputTokens = history.map((h) => h.inputTokens).reverse();
|
|
837
|
+
const totalInput = inputTokens.reduce((sum, t) => sum + t, 0);
|
|
838
|
+
const tokenSparkStr = sparkline(inputTokens);
|
|
839
|
+
writeLine(` ${colors.dim}Tokens:${RESET} ${tokenSparkStr} ${colors.cyan}${formatK(totalInput)}${RESET} input ${colors.dim}(${formatK(Math.round(totalInput / inputTokens.length))}/day)${RESET}`);
|
|
840
|
+
const goalProgress = history.map((h) => h.goalProgressPct).reverse();
|
|
841
|
+
const latestProgress = goalProgress[goalProgress.length - 1] || 0;
|
|
842
|
+
const earliestProgress = goalProgress[0] || 0;
|
|
843
|
+
const progressDelta = latestProgress - earliestProgress;
|
|
844
|
+
const progressColor = progressDelta > 0 ? colors.green : progressDelta < 0 ? colors.red : colors.dim;
|
|
845
|
+
const progressSign = progressDelta > 0 ? "+" : "";
|
|
846
|
+
writeLine(` ${colors.dim}Goals:${RESET} ${sparkline(goalProgress)} ${colors.purple}${latestProgress}%${RESET} ${progressColor}${progressSign}${progressDelta.toFixed(0)}%${RESET}${colors.dim} vs start${RESET}`);
|
|
847
|
+
writeLine();
|
|
848
|
+
}
|
|
849
|
+
function renderInsightsCached(cache) {
|
|
850
|
+
const insights = cache.insights;
|
|
851
|
+
if (!insights || insights.source === "none" || insights.taskMetrics.length === 0) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
writeLine(` ${bold}Agent Insights${RESET} ${colors.dim}(${insights.days}d)${RESET}`);
|
|
855
|
+
writeLine();
|
|
856
|
+
const totals = insights.taskMetrics.reduce(
|
|
857
|
+
(acc, t) => ({
|
|
858
|
+
tasks: acc.tasks + t.tasksTotal,
|
|
859
|
+
completed: acc.completed + t.tasksCompleted,
|
|
860
|
+
failed: acc.failed + t.tasksFailed,
|
|
861
|
+
retries: acc.retries + t.totalRetries,
|
|
862
|
+
withRetries: acc.withRetries + t.tasksWithRetries
|
|
863
|
+
}),
|
|
864
|
+
{ tasks: 0, completed: 0, failed: 0, retries: 0, withRetries: 0 }
|
|
865
|
+
);
|
|
866
|
+
if (totals.tasks > 0) {
|
|
867
|
+
const successRate = totals.tasks > 0 ? (totals.completed / totals.tasks * 100).toFixed(0) : "0";
|
|
868
|
+
const successColor = parseInt(successRate) >= 80 ? colors.green : parseInt(successRate) >= 60 ? colors.yellow : colors.red;
|
|
869
|
+
writeLine(` ${colors.dim}Tasks:${RESET} ${colors.green}${totals.completed}${RESET}${colors.dim}/${totals.tasks} completed${RESET} ${successColor}${successRate}%${RESET}${colors.dim} success${RESET} ${colors.red}${totals.failed}${RESET}${colors.dim} failed${RESET}`);
|
|
870
|
+
if (totals.retries > 0) {
|
|
871
|
+
const retryRate = totals.tasks > 0 ? (totals.withRetries / totals.tasks * 100).toFixed(0) : "0";
|
|
872
|
+
const retryColor = parseInt(retryRate) > 30 ? colors.red : parseInt(retryRate) > 15 ? colors.yellow : colors.green;
|
|
873
|
+
writeLine(` ${colors.dim}Retries:${RESET} ${retryColor}${totals.retries}${RESET}${colors.dim} total${RESET} ${retryColor}${retryRate}%${RESET}${colors.dim} of tasks needed retry${RESET}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
writeLine();
|
|
877
|
+
}
|
|
878
|
+
function renderROICached(cache, goalCount) {
|
|
879
|
+
const { costs, bridgeStats, baseline, squadProjections } = cache;
|
|
880
|
+
if (!costs && !bridgeStats) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const roiMetrics = calculateROIMetrics(
|
|
884
|
+
costs,
|
|
885
|
+
goalCount.completed,
|
|
886
|
+
cache.gitStats?.totalCommits || 0,
|
|
887
|
+
cache.ghStats?.prsMerged || 0
|
|
888
|
+
);
|
|
889
|
+
writeLine(` ${bold}ROI & Projections${RESET}`);
|
|
890
|
+
writeLine();
|
|
891
|
+
if (roiMetrics.totalCostUsd > 0) {
|
|
892
|
+
const metricsLine = [];
|
|
893
|
+
if (roiMetrics.costPerGoal > 0) {
|
|
894
|
+
metricsLine.push(`${colors.cyan}$${roiMetrics.costPerGoal.toFixed(2)}${RESET}${colors.dim}/goal${RESET}`);
|
|
895
|
+
}
|
|
896
|
+
if (roiMetrics.costPerPR > 0) {
|
|
897
|
+
metricsLine.push(`${colors.cyan}$${roiMetrics.costPerPR.toFixed(2)}${RESET}${colors.dim}/PR${RESET}`);
|
|
898
|
+
}
|
|
899
|
+
if (roiMetrics.costPerCommit > 0) {
|
|
900
|
+
metricsLine.push(`${colors.cyan}$${roiMetrics.costPerCommit.toFixed(2)}${RESET}${colors.dim}/commit${RESET}`);
|
|
901
|
+
}
|
|
902
|
+
if (metricsLine.length > 0) {
|
|
903
|
+
writeLine(` ${colors.dim}Cost/Output:${RESET} ${metricsLine.join(` ${colors.dim}|${RESET} `)}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (roiMetrics.roiMultiplier > 0) {
|
|
907
|
+
const roiColor = roiMetrics.roiMultiplier >= 3 ? colors.green : roiMetrics.roiMultiplier >= 1 ? colors.cyan : colors.yellow;
|
|
908
|
+
writeLine(` ${colors.dim}Est. ROI:${RESET} ${roiColor}${roiMetrics.roiMultiplier.toFixed(1)}x${RESET} ${colors.dim}($${roiMetrics.estimatedValueUsd.toFixed(0)} value / $${roiMetrics.totalCostUsd.toFixed(2)} cost)${RESET}`);
|
|
909
|
+
}
|
|
910
|
+
writeLine();
|
|
911
|
+
writeLine(` ${colors.dim}Projections${RESET}`);
|
|
912
|
+
const dailyBudget = costs?.dailyBudget || 200;
|
|
913
|
+
const projColor = roiMetrics.dailyProjectedCost > dailyBudget ? colors.red : roiMetrics.dailyProjectedCost > dailyBudget * 0.8 ? colors.yellow : colors.green;
|
|
914
|
+
writeLine(` ${colors.dim}Daily:${RESET} ${projColor}~$${roiMetrics.dailyProjectedCost.toFixed(2)}${RESET} ${colors.dim}Weekly:${RESET} ${colors.cyan}~$${roiMetrics.weeklyProjectedCost.toFixed(0)}${RESET} ${colors.dim}Monthly:${RESET} ${colors.purple}~$${roiMetrics.monthlyProjectedCost.toFixed(0)}${RESET}`);
|
|
915
|
+
if (squadProjections.length > 0) {
|
|
916
|
+
writeLine();
|
|
917
|
+
writeLine(` ${colors.dim}By Squad (projected monthly)${RESET}`);
|
|
918
|
+
const topSquads = squadProjections.sort((a, b) => b.projectedMonthlyCost - a.projectedMonthlyCost).slice(0, 4);
|
|
919
|
+
for (const sq of topSquads) {
|
|
920
|
+
const trendIcon = sq.costTrend === "increasing" ? `${colors.red}\u2191${RESET}` : sq.costTrend === "decreasing" ? `${colors.green}\u2193${RESET}` : `${colors.dim}-${RESET}`;
|
|
921
|
+
writeLine(` ${colors.cyan}${padEnd(sq.squad, 14)}${RESET}${colors.dim}~$${sq.projectedMonthlyCost.toFixed(0)}/mo${RESET} ${trendIcon}`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (baseline) {
|
|
925
|
+
writeLine();
|
|
926
|
+
writeLine(` ${colors.dim}vs Baseline${RESET} ${colors.dim}(${baseline.name})${RESET}`);
|
|
927
|
+
const costDelta = roiMetrics.totalCostUsd - baseline.costUsd;
|
|
928
|
+
const goalsDelta = goalCount.completed - baseline.goalsCompleted;
|
|
929
|
+
const costPerGoalBefore = baseline.goalsCompleted > 0 ? baseline.costUsd / baseline.goalsCompleted : 0;
|
|
930
|
+
const costPerGoalAfter = goalCount.completed > 0 ? roiMetrics.totalCostUsd / goalCount.completed : 0;
|
|
931
|
+
const efficiencyChange = costPerGoalBefore > 0 ? (costPerGoalBefore - costPerGoalAfter) / costPerGoalBefore * 100 : 0;
|
|
932
|
+
const costColor = costDelta > 0 ? colors.red : colors.green;
|
|
933
|
+
const goalsColor = goalsDelta >= 0 ? colors.green : colors.red;
|
|
934
|
+
const effColor = efficiencyChange > 0 ? colors.green : efficiencyChange < 0 ? colors.red : colors.dim;
|
|
935
|
+
writeLine(` ${colors.dim}Cost:${RESET} ${costColor}${costDelta >= 0 ? "+" : ""}$${costDelta.toFixed(2)}${RESET} ${colors.dim}Goals:${RESET} ${goalsColor}${goalsDelta >= 0 ? "+" : ""}${goalsDelta}${RESET} ${colors.dim}Efficiency:${RESET} ${effColor}${efficiencyChange >= 0 ? "+" : ""}${efficiencyChange.toFixed(0)}%${RESET}`);
|
|
936
|
+
} else {
|
|
937
|
+
writeLine();
|
|
938
|
+
writeLine(` ${colors.dim}No baseline set. Capture one with:${RESET} squads baseline`);
|
|
939
|
+
}
|
|
940
|
+
writeLine();
|
|
941
|
+
}
|
|
942
|
+
export {
|
|
943
|
+
dashboardCommand
|
|
944
|
+
};
|
|
945
|
+
//# sourceMappingURL=dashboard-L7YKVQEB.js.map
|