squads-cli 0.1.2 → 0.2.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 +162 -21
- package/dist/cli.js +2102 -652
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
10
|
import { config } from "dotenv";
|
|
11
|
-
import { existsSync as
|
|
12
|
-
import { join as
|
|
13
|
-
import { homedir as
|
|
11
|
+
import { existsSync as existsSync17 } from "fs";
|
|
12
|
+
import { join as join17 } from "path";
|
|
13
|
+
import { homedir as homedir6 } from "os";
|
|
14
14
|
import { Command } from "commander";
|
|
15
15
|
import chalk3 from "chalk";
|
|
16
16
|
|
|
@@ -100,7 +100,7 @@ var SQUAD_LABELS = {
|
|
|
100
100
|
company: ["company", "strategy"],
|
|
101
101
|
marketing: ["marketing", "content", "seo"]
|
|
102
102
|
};
|
|
103
|
-
function
|
|
103
|
+
function getGitHubStatsOptimized(basePath, days = 30) {
|
|
104
104
|
const stats = {
|
|
105
105
|
prsOpened: 0,
|
|
106
106
|
prsMerged: 0,
|
|
@@ -121,74 +121,81 @@ function getGitHubStats(basePath, days = 30) {
|
|
|
121
121
|
}
|
|
122
122
|
const repos = ["hq", "agents-squads-web"];
|
|
123
123
|
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
124
|
+
const results = [];
|
|
124
125
|
for (const repo of repos) {
|
|
125
126
|
const repoPath = join(basePath, repo);
|
|
126
127
|
if (!existsSync(repoPath)) continue;
|
|
127
128
|
try {
|
|
128
|
-
const
|
|
129
|
-
`gh pr list --state all --json number,title,createdAt,mergedAt,labels --limit
|
|
130
|
-
{ cwd: repoPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
129
|
+
const output = execSync(
|
|
130
|
+
`echo '{"prs":' && gh pr list --state all --json number,title,createdAt,mergedAt,labels --limit 50 2>/dev/null && echo ',"issues":' && gh issue list --state all --json number,title,state,closedAt,labels --limit 50 2>/dev/null && echo '}'`,
|
|
131
|
+
{ cwd: repoPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 1e4 }
|
|
131
132
|
);
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
133
|
+
const prsMatch = output.match(/"prs":(\[.*?\]),"issues":/s);
|
|
134
|
+
const issuesMatch = output.match(/"issues":(\[.*?\])\s*\}/s);
|
|
135
|
+
const prs = prsMatch ? JSON.parse(prsMatch[1]) : [];
|
|
136
|
+
const issues = issuesMatch ? JSON.parse(issuesMatch[1]) : [];
|
|
137
|
+
results.push({ repo, prs, issues });
|
|
138
|
+
} catch {
|
|
139
|
+
try {
|
|
140
|
+
const prsOutput = execSync(
|
|
141
|
+
`gh pr list --state all --json number,title,createdAt,mergedAt,labels --limit 50 2>/dev/null`,
|
|
142
|
+
{ cwd: repoPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
|
|
143
|
+
);
|
|
144
|
+
const issuesOutput = execSync(
|
|
145
|
+
`gh issue list --state all --json number,title,state,closedAt,labels --limit 50 2>/dev/null`,
|
|
146
|
+
{ cwd: repoPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
|
|
147
|
+
);
|
|
148
|
+
results.push({
|
|
149
|
+
repo,
|
|
150
|
+
prs: JSON.parse(prsOutput || "[]"),
|
|
151
|
+
issues: JSON.parse(issuesOutput || "[]")
|
|
152
|
+
});
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const { repo, prs, issues } of results) {
|
|
158
|
+
for (const pr of prs) {
|
|
159
|
+
const created = new Date(pr.createdAt);
|
|
160
|
+
if (created < new Date(since)) continue;
|
|
161
|
+
stats.prsOpened++;
|
|
162
|
+
if (pr.mergedAt) stats.prsMerged++;
|
|
163
|
+
const squad = detectSquadFromPR(pr, repo);
|
|
164
|
+
const squadStats = stats.bySquad.get(squad);
|
|
165
|
+
if (squadStats) {
|
|
166
|
+
squadStats.prsOpened++;
|
|
167
|
+
if (pr.mergedAt) squadStats.prsMerged++;
|
|
168
|
+
if (squadStats.recentPRs.length < 3) {
|
|
169
|
+
squadStats.recentPRs.push({
|
|
170
|
+
title: pr.title,
|
|
171
|
+
number: pr.number,
|
|
172
|
+
merged: !!pr.mergedAt
|
|
173
|
+
});
|
|
150
174
|
}
|
|
151
175
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (issue.state === "CLOSED") {
|
|
161
|
-
const closed = new Date(issue.closedAt);
|
|
162
|
-
if (closed >= new Date(since)) {
|
|
163
|
-
stats.issuesClosed++;
|
|
164
|
-
if (squadStats) {
|
|
165
|
-
squadStats.issuesClosed++;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
} else {
|
|
169
|
-
stats.issuesOpen++;
|
|
176
|
+
}
|
|
177
|
+
for (const issue of issues) {
|
|
178
|
+
const squad = detectSquadFromIssue(issue, repo);
|
|
179
|
+
const squadStats = stats.bySquad.get(squad);
|
|
180
|
+
if (issue.state === "CLOSED") {
|
|
181
|
+
const closed = new Date(issue.closedAt || 0);
|
|
182
|
+
if (closed >= new Date(since)) {
|
|
183
|
+
stats.issuesClosed++;
|
|
170
184
|
if (squadStats) {
|
|
171
|
-
squadStats.
|
|
172
|
-
if (squadStats.recentIssues.length < 3) {
|
|
173
|
-
squadStats.recentIssues.push({
|
|
174
|
-
title: issue.title,
|
|
175
|
-
number: issue.number,
|
|
176
|
-
state: issue.state
|
|
177
|
-
});
|
|
178
|
-
}
|
|
185
|
+
squadStats.issuesClosed++;
|
|
179
186
|
}
|
|
180
187
|
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
const gitStats = getMultiRepoGitStats(basePath, days);
|
|
186
|
-
for (const [repo, commits] of gitStats.commitsByRepo) {
|
|
187
|
-
for (const [squad, repos2] of Object.entries(SQUAD_REPO_MAP)) {
|
|
188
|
-
if (repos2.includes(repo)) {
|
|
189
|
-
const squadStats = stats.bySquad.get(squad);
|
|
188
|
+
} else {
|
|
189
|
+
stats.issuesOpen++;
|
|
190
190
|
if (squadStats) {
|
|
191
|
-
squadStats.
|
|
191
|
+
squadStats.issuesOpen++;
|
|
192
|
+
if (squadStats.recentIssues.length < 3) {
|
|
193
|
+
squadStats.recentIssues.push({
|
|
194
|
+
title: issue.title,
|
|
195
|
+
number: issue.number,
|
|
196
|
+
state: issue.state
|
|
197
|
+
});
|
|
198
|
+
}
|
|
192
199
|
}
|
|
193
200
|
}
|
|
194
201
|
}
|
|
@@ -341,7 +348,8 @@ import { randomUUID } from "crypto";
|
|
|
341
348
|
var TELEMETRY_DIR = join2(homedir(), ".squads-cli");
|
|
342
349
|
var CONFIG_PATH = join2(TELEMETRY_DIR, "telemetry.json");
|
|
343
350
|
var EVENTS_PATH = join2(TELEMETRY_DIR, "events.json");
|
|
344
|
-
var
|
|
351
|
+
var DEFAULT_TELEMETRY_ENDPOINT = "https://qzayrjwxiznkqfvmjxyy.supabase.co/functions/v1/telemetry-ingest";
|
|
352
|
+
var TELEMETRY_ENDPOINT = process.env.SQUADS_TELEMETRY_URL || (process.env.SQUADS_BRIDGE_URL ? `${process.env.SQUADS_BRIDGE_URL}/api/telemetry` : DEFAULT_TELEMETRY_ENDPOINT);
|
|
345
353
|
var eventQueue = [];
|
|
346
354
|
var flushScheduled = false;
|
|
347
355
|
function ensureDir() {
|
|
@@ -1428,22 +1436,32 @@ async function runSquad(squad, squadsDir, options) {
|
|
|
1428
1436
|
}
|
|
1429
1437
|
}
|
|
1430
1438
|
} else {
|
|
1431
|
-
|
|
1432
|
-
(
|
|
1433
|
-
);
|
|
1434
|
-
if (orchestrator) {
|
|
1435
|
-
const agentPath = join5(squadsDir, squad.name, `${orchestrator.name}.md`);
|
|
1439
|
+
if (options.agent) {
|
|
1440
|
+
const agentPath = join5(squadsDir, squad.name, `${options.agent}.md`);
|
|
1436
1441
|
if (existsSync5(agentPath)) {
|
|
1437
|
-
await runAgent(
|
|
1442
|
+
await runAgent(options.agent, agentPath, squad.name, options);
|
|
1443
|
+
} else {
|
|
1444
|
+
writeLine(` ${icons.error} ${colors.red}Agent ${options.agent} not found${RESET}`);
|
|
1445
|
+
return;
|
|
1438
1446
|
}
|
|
1439
1447
|
} else {
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1448
|
+
const orchestrator = squad.agents.find(
|
|
1449
|
+
(a) => a.name.includes("lead") || a.trigger === "Manual"
|
|
1450
|
+
);
|
|
1451
|
+
if (orchestrator) {
|
|
1452
|
+
const agentPath = join5(squadsDir, squad.name, `${orchestrator.name}.md`);
|
|
1453
|
+
if (existsSync5(agentPath)) {
|
|
1454
|
+
await runAgent(orchestrator.name, agentPath, squad.name, options);
|
|
1455
|
+
}
|
|
1456
|
+
} else {
|
|
1457
|
+
writeLine(` ${colors.dim}No pipeline defined. Available agents:${RESET}`);
|
|
1458
|
+
for (const agent of squad.agents) {
|
|
1459
|
+
writeLine(` ${icons.empty} ${colors.cyan}${agent.name}${RESET} ${colors.dim}${agent.role}${RESET}`);
|
|
1460
|
+
}
|
|
1461
|
+
writeLine();
|
|
1462
|
+
writeLine(` ${colors.dim}Run a specific agent:${RESET}`);
|
|
1463
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET} --agent ${colors.cyan}<name>${RESET}`);
|
|
1443
1464
|
}
|
|
1444
|
-
writeLine();
|
|
1445
|
-
writeLine(` ${colors.dim}Run a specific agent:${RESET}`);
|
|
1446
|
-
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET} --agent ${colors.cyan}<name>${RESET}`);
|
|
1447
1465
|
}
|
|
1448
1466
|
}
|
|
1449
1467
|
writeLine();
|
|
@@ -1485,18 +1503,16 @@ After completion:
|
|
|
1485
1503
|
3. Report what was accomplished`;
|
|
1486
1504
|
const claudeAvailable = await checkClaudeCliAvailable();
|
|
1487
1505
|
if (options.execute && claudeAvailable) {
|
|
1488
|
-
spinner.text = `
|
|
1506
|
+
spinner.text = `Launching ${agentName} as background task...`;
|
|
1489
1507
|
try {
|
|
1490
|
-
const result = await executeWithClaude(prompt, options.verbose);
|
|
1491
|
-
spinner.succeed(`Agent ${agentName}
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
if (result.length > 500) writeLine(` ${colors.dim}... (truncated)${RESET}`);
|
|
1497
|
-
}
|
|
1508
|
+
const result = await executeWithClaude(prompt, options.verbose, options.timeout || 30);
|
|
1509
|
+
spinner.succeed(`Agent ${agentName} launched`);
|
|
1510
|
+
writeLine(` ${colors.dim}${result}${RESET}`);
|
|
1511
|
+
writeLine();
|
|
1512
|
+
writeLine(` ${colors.dim}Monitor:${RESET} squads workers`);
|
|
1513
|
+
writeLine(` ${colors.dim}Memory:${RESET} squads memory show ${squadName}`);
|
|
1498
1514
|
} catch (error) {
|
|
1499
|
-
spinner.fail(`Agent ${agentName} failed`);
|
|
1515
|
+
spinner.fail(`Agent ${agentName} failed to launch`);
|
|
1500
1516
|
updateExecutionStatus(squadName, agentName, "failed", String(error));
|
|
1501
1517
|
writeLine(` ${colors.red}${String(error)}${RESET}`);
|
|
1502
1518
|
}
|
|
@@ -1509,13 +1525,10 @@ After completion:
|
|
|
1509
1525
|
writeLine(` ${colors.dim}Install: npm install -g @anthropic-ai/claude-code${RESET}`);
|
|
1510
1526
|
}
|
|
1511
1527
|
writeLine();
|
|
1512
|
-
writeLine(` ${colors.dim}To
|
|
1513
|
-
writeLine(` ${colors.dim}$${RESET}
|
|
1514
|
-
writeLine();
|
|
1515
|
-
writeLine(` ${colors.dim}Or run with --execute flag:${RESET}`);
|
|
1516
|
-
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squadName}${RESET} --execute`);
|
|
1528
|
+
writeLine(` ${colors.dim}To launch as background task:${RESET}`);
|
|
1529
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squadName}${RESET} -a ${colors.cyan}${agentName}${RESET} --execute`);
|
|
1517
1530
|
writeLine();
|
|
1518
|
-
writeLine(` ${colors.dim}Or
|
|
1531
|
+
writeLine(` ${colors.dim}Or run interactively:${RESET}`);
|
|
1519
1532
|
writeLine(` ${colors.dim}$${RESET} Run the ${colors.cyan}${agentName}${RESET} agent from ${agentPath}`);
|
|
1520
1533
|
}
|
|
1521
1534
|
}
|
|
@@ -1526,48 +1539,51 @@ async function checkClaudeCliAvailable() {
|
|
|
1526
1539
|
check.on("error", () => resolve(false));
|
|
1527
1540
|
});
|
|
1528
1541
|
}
|
|
1529
|
-
async function executeWithClaude(prompt, verbose) {
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
reject(err);
|
|
1565
|
-
});
|
|
1566
|
-
setTimeout(() => {
|
|
1567
|
-
claude.kill();
|
|
1568
|
-
reject(new Error("Execution timed out after 5 minutes"));
|
|
1569
|
-
}, 5 * 60 * 1e3);
|
|
1542
|
+
async function executeWithClaude(prompt, verbose, timeoutMinutes = 30) {
|
|
1543
|
+
const userConfigPath = join5(process.env.HOME || "", ".claude.json");
|
|
1544
|
+
const squadMatch = prompt.match(/squad (\w+)/);
|
|
1545
|
+
const agentMatch = prompt.match(/(\w+) agent/);
|
|
1546
|
+
const squadName = squadMatch?.[1] || "unknown";
|
|
1547
|
+
const agentName = agentMatch?.[1] || "unknown";
|
|
1548
|
+
const timestamp = Date.now();
|
|
1549
|
+
const sessionName = `squads-${squadName}-${agentName}-${timestamp}`;
|
|
1550
|
+
if (verbose) {
|
|
1551
|
+
writeLine(` ${colors.dim}Spawning tmux session: ${sessionName}${RESET}`);
|
|
1552
|
+
}
|
|
1553
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
1554
|
+
const claudeCmd = `claude --dangerously-skip-permissions --mcp-config '${userConfigPath}' -- '${escapedPrompt}'`;
|
|
1555
|
+
const tmux = spawn("tmux", [
|
|
1556
|
+
"new-session",
|
|
1557
|
+
"-d",
|
|
1558
|
+
// Detached
|
|
1559
|
+
"-s",
|
|
1560
|
+
sessionName,
|
|
1561
|
+
"-x",
|
|
1562
|
+
"200",
|
|
1563
|
+
// Wide terminal for better output
|
|
1564
|
+
"-y",
|
|
1565
|
+
"50",
|
|
1566
|
+
"/bin/sh",
|
|
1567
|
+
"-c",
|
|
1568
|
+
claudeCmd
|
|
1569
|
+
], {
|
|
1570
|
+
stdio: "ignore",
|
|
1571
|
+
detached: true,
|
|
1572
|
+
env: {
|
|
1573
|
+
...process.env,
|
|
1574
|
+
SQUADS_SQUAD: squadName,
|
|
1575
|
+
SQUADS_AGENT: agentName
|
|
1576
|
+
}
|
|
1570
1577
|
});
|
|
1578
|
+
tmux.unref();
|
|
1579
|
+
spawn("/bin/sh", ["-c", `sleep 2 && tmux send-keys -t '${sessionName}' Down Enter`], {
|
|
1580
|
+
stdio: "ignore",
|
|
1581
|
+
detached: true
|
|
1582
|
+
}).unref();
|
|
1583
|
+
if (verbose) {
|
|
1584
|
+
writeLine(` ${colors.dim}Attach: tmux attach -t ${sessionName}${RESET}`);
|
|
1585
|
+
}
|
|
1586
|
+
return `tmux session: ${sessionName}. Attach: tmux attach -t ${sessionName}`;
|
|
1571
1587
|
}
|
|
1572
1588
|
|
|
1573
1589
|
// src/commands/list.ts
|
|
@@ -1620,139 +1636,704 @@ async function listCommand(options) {
|
|
|
1620
1636
|
}
|
|
1621
1637
|
|
|
1622
1638
|
// src/commands/status.ts
|
|
1623
|
-
import { existsSync as
|
|
1624
|
-
import { join as
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1639
|
+
import { existsSync as existsSync8, statSync } from "fs";
|
|
1640
|
+
import { join as join8 } from "path";
|
|
1641
|
+
|
|
1642
|
+
// src/lib/sessions.ts
|
|
1643
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync5, unlinkSync, appendFileSync, createReadStream } from "fs";
|
|
1644
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
1645
|
+
import { randomBytes } from "crypto";
|
|
1646
|
+
import { createInterface } from "readline";
|
|
1647
|
+
import { execSync as execSync2 } from "child_process";
|
|
1648
|
+
var AI_TOOL_PATTERNS = [
|
|
1649
|
+
{ pattern: /^claude$/, name: "claude" },
|
|
1650
|
+
{ pattern: /^cursor$/i, name: "cursor" },
|
|
1651
|
+
{ pattern: /^aider$/, name: "aider" },
|
|
1652
|
+
{ pattern: /^gemini$/i, name: "gemini" },
|
|
1653
|
+
{ pattern: /^copilot$/i, name: "copilot" },
|
|
1654
|
+
{ pattern: /^cody$/i, name: "cody" },
|
|
1655
|
+
{ pattern: /^continue$/i, name: "continue" }
|
|
1656
|
+
];
|
|
1657
|
+
var STALE_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
1658
|
+
var HISTORY_FILE = "history.jsonl";
|
|
1659
|
+
var ACTIVE_DIR = "active";
|
|
1660
|
+
var SQUAD_DIR_MAP = {
|
|
1661
|
+
"hq": "company",
|
|
1662
|
+
"agents-squads-web": "website",
|
|
1663
|
+
"company": "company",
|
|
1664
|
+
"product": "product",
|
|
1665
|
+
"engineering": "engineering",
|
|
1666
|
+
"research": "research",
|
|
1667
|
+
"intelligence": "intelligence",
|
|
1668
|
+
"customer": "customer",
|
|
1669
|
+
"finance": "finance",
|
|
1670
|
+
"marketing": "marketing"
|
|
1671
|
+
};
|
|
1672
|
+
function findAgentsDir() {
|
|
1673
|
+
let dir = process.cwd();
|
|
1674
|
+
for (let i = 0; i < 5; i++) {
|
|
1675
|
+
const agentsPath = join6(dir, ".agents");
|
|
1676
|
+
if (existsSync6(agentsPath)) {
|
|
1677
|
+
return agentsPath;
|
|
1678
|
+
}
|
|
1679
|
+
const parent = dirname3(dir);
|
|
1680
|
+
if (parent === dir) break;
|
|
1681
|
+
dir = parent;
|
|
1631
1682
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1683
|
+
return null;
|
|
1684
|
+
}
|
|
1685
|
+
function getSessionsBaseDir() {
|
|
1686
|
+
const agentsDir = findAgentsDir();
|
|
1687
|
+
if (!agentsDir) return null;
|
|
1688
|
+
const sessionsDir = join6(agentsDir, "sessions");
|
|
1689
|
+
if (!existsSync6(sessionsDir)) {
|
|
1690
|
+
mkdirSync4(sessionsDir, { recursive: true });
|
|
1636
1691
|
}
|
|
1692
|
+
return sessionsDir;
|
|
1637
1693
|
}
|
|
1638
|
-
|
|
1639
|
-
const
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
const
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1694
|
+
function getSessionsDir() {
|
|
1695
|
+
const baseDir = getSessionsBaseDir();
|
|
1696
|
+
if (!baseDir) return null;
|
|
1697
|
+
const activeDir = join6(baseDir, ACTIVE_DIR);
|
|
1698
|
+
if (!existsSync6(activeDir)) {
|
|
1699
|
+
mkdirSync4(activeDir, { recursive: true });
|
|
1700
|
+
}
|
|
1701
|
+
return activeDir;
|
|
1702
|
+
}
|
|
1703
|
+
function getHistoryFilePath() {
|
|
1704
|
+
const baseDir = getSessionsBaseDir();
|
|
1705
|
+
if (!baseDir) return null;
|
|
1706
|
+
return join6(baseDir, HISTORY_FILE);
|
|
1707
|
+
}
|
|
1708
|
+
function appendEvent(event) {
|
|
1709
|
+
const historyPath = getHistoryFilePath();
|
|
1710
|
+
if (!historyPath) return;
|
|
1711
|
+
try {
|
|
1712
|
+
const line = JSON.stringify(event) + "\n";
|
|
1713
|
+
appendFileSync(historyPath, line);
|
|
1714
|
+
} catch {
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
function detectSquad(cwd = process.cwd()) {
|
|
1718
|
+
const match = cwd.match(/agents-squads\/([^/]+)/);
|
|
1719
|
+
if (match) {
|
|
1720
|
+
const dir = match[1];
|
|
1721
|
+
return SQUAD_DIR_MAP[dir] || dir;
|
|
1722
|
+
}
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
function detectAIProcesses() {
|
|
1726
|
+
const processes = [];
|
|
1727
|
+
try {
|
|
1728
|
+
const psOutput = execSync2("ps -eo pid,tty,comm 2>/dev/null", {
|
|
1729
|
+
encoding: "utf-8",
|
|
1730
|
+
timeout: 5e3
|
|
1731
|
+
}).trim();
|
|
1732
|
+
if (!psOutput) return [];
|
|
1733
|
+
const lines = psOutput.split("\n").filter((line) => line.trim());
|
|
1734
|
+
for (const line of lines) {
|
|
1735
|
+
const parts = line.trim().split(/\s+/);
|
|
1736
|
+
if (parts.length < 3) continue;
|
|
1737
|
+
const pid = parseInt(parts[0], 10);
|
|
1738
|
+
const tty = parts[1];
|
|
1739
|
+
const comm = parts.slice(2).join(" ");
|
|
1740
|
+
if (isNaN(pid)) continue;
|
|
1741
|
+
let toolName = null;
|
|
1742
|
+
for (const { pattern, name } of AI_TOOL_PATTERNS) {
|
|
1743
|
+
if (pattern.test(comm)) {
|
|
1744
|
+
toolName = name;
|
|
1745
|
+
break;
|
|
1686
1746
|
}
|
|
1687
1747
|
}
|
|
1748
|
+
if (!toolName) continue;
|
|
1749
|
+
let cwd = "";
|
|
1750
|
+
try {
|
|
1751
|
+
const lsofOutput = execSync2(`lsof -p ${pid} 2>/dev/null | grep cwd | awk '{print $NF}'`, {
|
|
1752
|
+
encoding: "utf-8",
|
|
1753
|
+
timeout: 3e3
|
|
1754
|
+
}).trim();
|
|
1755
|
+
cwd = lsofOutput || "";
|
|
1756
|
+
} catch {
|
|
1757
|
+
cwd = "";
|
|
1758
|
+
}
|
|
1759
|
+
const squad = detectSquad(cwd);
|
|
1760
|
+
processes.push({
|
|
1761
|
+
pid,
|
|
1762
|
+
tty,
|
|
1763
|
+
cwd,
|
|
1764
|
+
squad,
|
|
1765
|
+
tool: toolName
|
|
1766
|
+
});
|
|
1688
1767
|
}
|
|
1689
|
-
|
|
1690
|
-
writeLine(row);
|
|
1768
|
+
} catch {
|
|
1691
1769
|
}
|
|
1692
|
-
|
|
1693
|
-
writeLine();
|
|
1694
|
-
writeLine(` ${colors.dim}$${RESET} squads status ${colors.cyan}<squad>${RESET} ${colors.dim}Squad details${RESET}`);
|
|
1695
|
-
writeLine(` ${colors.dim}$${RESET} squads dash ${colors.dim}Full dashboard${RESET}`);
|
|
1696
|
-
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}<squad>${RESET} ${colors.dim}Execute a squad${RESET}`);
|
|
1697
|
-
writeLine();
|
|
1770
|
+
return processes;
|
|
1698
1771
|
}
|
|
1699
|
-
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1772
|
+
function getLiveSessionSummary() {
|
|
1773
|
+
const processes = detectAIProcesses();
|
|
1774
|
+
const bySquad = {};
|
|
1775
|
+
const byTool = {};
|
|
1776
|
+
for (const proc of processes) {
|
|
1777
|
+
const squad = proc.squad || "unknown";
|
|
1778
|
+
bySquad[squad] = (bySquad[squad] || 0) + 1;
|
|
1779
|
+
byTool[proc.tool] = (byTool[proc.tool] || 0) + 1;
|
|
1704
1780
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1781
|
+
return {
|
|
1782
|
+
totalSessions: processes.length,
|
|
1783
|
+
bySquad,
|
|
1784
|
+
squadCount: Object.keys(bySquad).length,
|
|
1785
|
+
byTool
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function generateSessionId() {
|
|
1789
|
+
return randomBytes(8).toString("hex");
|
|
1790
|
+
}
|
|
1791
|
+
var currentSessionId = null;
|
|
1792
|
+
function getSessionId() {
|
|
1793
|
+
if (currentSessionId) return currentSessionId;
|
|
1794
|
+
const sessionsDir = getSessionsDir();
|
|
1795
|
+
if (sessionsDir) {
|
|
1796
|
+
const pid = process.pid;
|
|
1797
|
+
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
1798
|
+
for (const file of files) {
|
|
1799
|
+
try {
|
|
1800
|
+
const content = readFileSync5(join6(sessionsDir, file), "utf-8");
|
|
1801
|
+
const session2 = JSON.parse(content);
|
|
1802
|
+
if (session2.pid === pid) {
|
|
1803
|
+
currentSessionId = session2.sessionId;
|
|
1804
|
+
return currentSessionId;
|
|
1805
|
+
}
|
|
1806
|
+
} catch {
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1711
1809
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1810
|
+
currentSessionId = generateSessionId();
|
|
1811
|
+
return currentSessionId;
|
|
1812
|
+
}
|
|
1813
|
+
function startSession(squad) {
|
|
1814
|
+
const sessionsDir = getSessionsDir();
|
|
1815
|
+
if (!sessionsDir) return null;
|
|
1816
|
+
const sessionId = getSessionId();
|
|
1817
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1818
|
+
const cwd = process.cwd();
|
|
1819
|
+
const detectedSquad = squad || detectSquad(cwd);
|
|
1820
|
+
const session2 = {
|
|
1821
|
+
sessionId,
|
|
1822
|
+
squad: detectedSquad,
|
|
1823
|
+
startedAt: now,
|
|
1824
|
+
lastHeartbeat: now,
|
|
1825
|
+
cwd,
|
|
1826
|
+
pid: process.pid
|
|
1827
|
+
};
|
|
1828
|
+
const sessionPath = join6(sessionsDir, `${sessionId}.json`);
|
|
1829
|
+
writeFileSync5(sessionPath, JSON.stringify(session2, null, 2));
|
|
1830
|
+
appendEvent({
|
|
1831
|
+
type: "start",
|
|
1832
|
+
sessionId,
|
|
1833
|
+
squad: detectedSquad,
|
|
1834
|
+
ts: now,
|
|
1835
|
+
cwd,
|
|
1836
|
+
pid: process.pid
|
|
1837
|
+
});
|
|
1838
|
+
return session2;
|
|
1839
|
+
}
|
|
1840
|
+
function updateHeartbeat() {
|
|
1841
|
+
const sessionsDir = getSessionsDir();
|
|
1842
|
+
if (!sessionsDir) return false;
|
|
1843
|
+
const sessionId = getSessionId();
|
|
1844
|
+
const sessionPath = join6(sessionsDir, `${sessionId}.json`);
|
|
1845
|
+
if (!existsSync6(sessionPath)) {
|
|
1846
|
+
startSession();
|
|
1847
|
+
return true;
|
|
1723
1848
|
}
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1849
|
+
try {
|
|
1850
|
+
const content = readFileSync5(sessionPath, "utf-8");
|
|
1851
|
+
const session2 = JSON.parse(content);
|
|
1852
|
+
session2.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
|
|
1853
|
+
writeFileSync5(sessionPath, JSON.stringify(session2, null, 2));
|
|
1854
|
+
return true;
|
|
1855
|
+
} catch {
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
function stopSession() {
|
|
1860
|
+
const sessionsDir = getSessionsDir();
|
|
1861
|
+
if (!sessionsDir) return false;
|
|
1862
|
+
const sessionId = getSessionId();
|
|
1863
|
+
const sessionPath = join6(sessionsDir, `${sessionId}.json`);
|
|
1864
|
+
if (existsSync6(sessionPath)) {
|
|
1865
|
+
let squad = null;
|
|
1866
|
+
let durationMs;
|
|
1867
|
+
try {
|
|
1868
|
+
const content = readFileSync5(sessionPath, "utf-8");
|
|
1869
|
+
const session2 = JSON.parse(content);
|
|
1870
|
+
squad = session2.squad;
|
|
1871
|
+
durationMs = Date.now() - new Date(session2.startedAt).getTime();
|
|
1872
|
+
} catch {
|
|
1730
1873
|
}
|
|
1874
|
+
unlinkSync(sessionPath);
|
|
1875
|
+
currentSessionId = null;
|
|
1876
|
+
appendEvent({
|
|
1877
|
+
type: "stop",
|
|
1878
|
+
sessionId,
|
|
1879
|
+
squad,
|
|
1880
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1881
|
+
durationMs
|
|
1882
|
+
});
|
|
1883
|
+
return true;
|
|
1731
1884
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
function getActiveSessions() {
|
|
1888
|
+
const sessionsDir = getSessionsDir();
|
|
1889
|
+
if (!sessionsDir) return [];
|
|
1890
|
+
const now = Date.now();
|
|
1891
|
+
const sessions2 = [];
|
|
1892
|
+
try {
|
|
1893
|
+
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
1894
|
+
for (const file of files) {
|
|
1895
|
+
try {
|
|
1896
|
+
const filePath = join6(sessionsDir, file);
|
|
1897
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
1898
|
+
const session2 = JSON.parse(content);
|
|
1899
|
+
const lastHeartbeat = new Date(session2.lastHeartbeat).getTime();
|
|
1900
|
+
if (now - lastHeartbeat < STALE_THRESHOLD_MS) {
|
|
1901
|
+
sessions2.push(session2);
|
|
1751
1902
|
}
|
|
1903
|
+
} catch {
|
|
1752
1904
|
}
|
|
1753
1905
|
}
|
|
1906
|
+
} catch {
|
|
1754
1907
|
}
|
|
1755
|
-
|
|
1908
|
+
return sessions2;
|
|
1909
|
+
}
|
|
1910
|
+
function getSessionSummary() {
|
|
1911
|
+
const sessions2 = getActiveSessions();
|
|
1912
|
+
const bySquad = {};
|
|
1913
|
+
for (const session2 of sessions2) {
|
|
1914
|
+
const squad = session2.squad || "unknown";
|
|
1915
|
+
bySquad[squad] = (bySquad[squad] || 0) + 1;
|
|
1916
|
+
}
|
|
1917
|
+
return {
|
|
1918
|
+
totalSessions: sessions2.length,
|
|
1919
|
+
bySquad,
|
|
1920
|
+
squadCount: Object.keys(bySquad).length
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
function cleanupStaleSessions() {
|
|
1924
|
+
const sessionsDir = getSessionsDir();
|
|
1925
|
+
if (!sessionsDir) return 0;
|
|
1926
|
+
const now = Date.now();
|
|
1927
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1928
|
+
let cleaned = 0;
|
|
1929
|
+
try {
|
|
1930
|
+
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
1931
|
+
for (const file of files) {
|
|
1932
|
+
try {
|
|
1933
|
+
const filePath = join6(sessionsDir, file);
|
|
1934
|
+
const content = readFileSync5(filePath, "utf-8");
|
|
1935
|
+
const session2 = JSON.parse(content);
|
|
1936
|
+
const lastHeartbeat = new Date(session2.lastHeartbeat).getTime();
|
|
1937
|
+
if (now - lastHeartbeat >= STALE_THRESHOLD_MS) {
|
|
1938
|
+
const durationMs = now - new Date(session2.startedAt).getTime();
|
|
1939
|
+
unlinkSync(filePath);
|
|
1940
|
+
cleaned++;
|
|
1941
|
+
appendEvent({
|
|
1942
|
+
type: "stale_cleanup",
|
|
1943
|
+
sessionId: session2.sessionId,
|
|
1944
|
+
squad: session2.squad,
|
|
1945
|
+
ts: nowIso,
|
|
1946
|
+
durationMs
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
} catch {
|
|
1950
|
+
try {
|
|
1951
|
+
unlinkSync(join6(sessionsDir, file));
|
|
1952
|
+
cleaned++;
|
|
1953
|
+
} catch {
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
} catch {
|
|
1958
|
+
}
|
|
1959
|
+
return cleaned;
|
|
1960
|
+
}
|
|
1961
|
+
async function readSessionHistory(options = {}) {
|
|
1962
|
+
const historyPath = getHistoryFilePath();
|
|
1963
|
+
if (!historyPath || !existsSync6(historyPath)) return [];
|
|
1964
|
+
const events = [];
|
|
1965
|
+
const sinceMs = options.since?.getTime() || 0;
|
|
1966
|
+
const untilMs = options.until?.getTime() || Date.now();
|
|
1967
|
+
return new Promise((resolve) => {
|
|
1968
|
+
const rl = createInterface({
|
|
1969
|
+
input: createReadStream(historyPath),
|
|
1970
|
+
crlfDelay: Infinity
|
|
1971
|
+
});
|
|
1972
|
+
rl.on("line", (line) => {
|
|
1973
|
+
if (!line.trim()) return;
|
|
1974
|
+
try {
|
|
1975
|
+
const event = JSON.parse(line);
|
|
1976
|
+
const eventMs = new Date(event.ts).getTime();
|
|
1977
|
+
if (eventMs < sinceMs || eventMs > untilMs) return;
|
|
1978
|
+
if (options.squad && event.squad !== options.squad) return;
|
|
1979
|
+
if (options.type && event.type !== options.type) return;
|
|
1980
|
+
events.push(event);
|
|
1981
|
+
} catch {
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1984
|
+
rl.on("close", () => {
|
|
1985
|
+
if (options.limit && events.length > options.limit) {
|
|
1986
|
+
resolve(events.slice(-options.limit));
|
|
1987
|
+
} else {
|
|
1988
|
+
resolve(events);
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
rl.on("error", () => {
|
|
1992
|
+
resolve([]);
|
|
1993
|
+
});
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
async function getSessionHistoryStats(options = {}) {
|
|
1997
|
+
const events = await readSessionHistory(options);
|
|
1998
|
+
const stats = {
|
|
1999
|
+
totalSessions: 0,
|
|
2000
|
+
totalDurationMs: 0,
|
|
2001
|
+
avgDurationMs: 0,
|
|
2002
|
+
bySquad: {},
|
|
2003
|
+
byDate: {},
|
|
2004
|
+
peakConcurrent: 0
|
|
2005
|
+
};
|
|
2006
|
+
const activeSessions = /* @__PURE__ */ new Map();
|
|
2007
|
+
let currentConcurrent = 0;
|
|
2008
|
+
for (const event of events) {
|
|
2009
|
+
const squad = event.squad || "unknown";
|
|
2010
|
+
const date = event.ts.split("T")[0];
|
|
2011
|
+
if (event.type === "start") {
|
|
2012
|
+
stats.totalSessions++;
|
|
2013
|
+
stats.byDate[date] = (stats.byDate[date] || 0) + 1;
|
|
2014
|
+
if (!stats.bySquad[squad]) {
|
|
2015
|
+
stats.bySquad[squad] = { count: 0, durationMs: 0 };
|
|
2016
|
+
}
|
|
2017
|
+
stats.bySquad[squad].count++;
|
|
2018
|
+
activeSessions.set(event.sessionId, {
|
|
2019
|
+
squad: event.squad,
|
|
2020
|
+
startTs: new Date(event.ts).getTime()
|
|
2021
|
+
});
|
|
2022
|
+
currentConcurrent++;
|
|
2023
|
+
stats.peakConcurrent = Math.max(stats.peakConcurrent, currentConcurrent);
|
|
2024
|
+
}
|
|
2025
|
+
if (event.type === "stop" || event.type === "stale_cleanup") {
|
|
2026
|
+
const duration = event.durationMs || 0;
|
|
2027
|
+
stats.totalDurationMs += duration;
|
|
2028
|
+
if (stats.bySquad[squad]) {
|
|
2029
|
+
stats.bySquad[squad].durationMs += duration;
|
|
2030
|
+
}
|
|
2031
|
+
if (activeSessions.has(event.sessionId)) {
|
|
2032
|
+
activeSessions.delete(event.sessionId);
|
|
2033
|
+
currentConcurrent = Math.max(0, currentConcurrent - 1);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
const completedSessions = events.filter((e) => e.type === "stop" || e.type === "stale_cleanup").length;
|
|
2038
|
+
if (completedSessions > 0) {
|
|
2039
|
+
stats.avgDurationMs = Math.round(stats.totalDurationMs / completedSessions);
|
|
2040
|
+
}
|
|
2041
|
+
return stats;
|
|
2042
|
+
}
|
|
2043
|
+
async function getRecentSessions(limit = 20) {
|
|
2044
|
+
const events = await readSessionHistory({ limit: limit * 3 });
|
|
2045
|
+
const sessionEvents = /* @__PURE__ */ new Map();
|
|
2046
|
+
for (const event of events) {
|
|
2047
|
+
if (!sessionEvents.has(event.sessionId)) {
|
|
2048
|
+
sessionEvents.set(event.sessionId, []);
|
|
2049
|
+
}
|
|
2050
|
+
sessionEvents.get(event.sessionId).push(event);
|
|
2051
|
+
}
|
|
2052
|
+
const startEvents = events.filter((e) => e.type === "start").slice(-limit);
|
|
2053
|
+
return startEvents.reverse();
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/lib/update.ts
|
|
2057
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
2058
|
+
import { join as join7, dirname as dirname4 } from "path";
|
|
2059
|
+
import { homedir as homedir2 } from "os";
|
|
2060
|
+
import { execSync as execSync3 } from "child_process";
|
|
2061
|
+
import { fileURLToPath } from "url";
|
|
2062
|
+
function getPackageVersion() {
|
|
2063
|
+
try {
|
|
2064
|
+
const __filename2 = fileURLToPath(import.meta.url);
|
|
2065
|
+
const __dirname2 = dirname4(__filename2);
|
|
2066
|
+
const possiblePaths = [
|
|
2067
|
+
join7(__dirname2, "..", "..", "package.json"),
|
|
2068
|
+
// From dist/lib/
|
|
2069
|
+
join7(__dirname2, "..", "package.json"),
|
|
2070
|
+
// From dist/
|
|
2071
|
+
join7(__dirname2, "package.json")
|
|
2072
|
+
// Same dir
|
|
2073
|
+
];
|
|
2074
|
+
for (const pkgPath of possiblePaths) {
|
|
2075
|
+
if (existsSync7(pkgPath)) {
|
|
2076
|
+
const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
2077
|
+
return pkg.version || "0.0.0";
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
} catch {
|
|
2081
|
+
}
|
|
2082
|
+
return "0.0.0";
|
|
2083
|
+
}
|
|
2084
|
+
var CURRENT_VERSION = getPackageVersion();
|
|
2085
|
+
var CACHE_DIR = join7(homedir2(), ".squads");
|
|
2086
|
+
var CACHE_FILE = join7(CACHE_DIR, "update-check.json");
|
|
2087
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2088
|
+
function isNewerVersion(v1, v2) {
|
|
2089
|
+
const parts1 = v1.replace(/^v/, "").split(".").map(Number);
|
|
2090
|
+
const parts2 = v2.replace(/^v/, "").split(".").map(Number);
|
|
2091
|
+
for (let i = 0; i < 3; i++) {
|
|
2092
|
+
const p1 = parts1[i] || 0;
|
|
2093
|
+
const p2 = parts2[i] || 0;
|
|
2094
|
+
if (p2 > p1) return true;
|
|
2095
|
+
if (p2 < p1) return false;
|
|
2096
|
+
}
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2099
|
+
function readCache() {
|
|
2100
|
+
try {
|
|
2101
|
+
if (!existsSync7(CACHE_FILE)) return null;
|
|
2102
|
+
const data = JSON.parse(readFileSync6(CACHE_FILE, "utf-8"));
|
|
2103
|
+
return data;
|
|
2104
|
+
} catch {
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
function writeCache(latestVersion) {
|
|
2109
|
+
try {
|
|
2110
|
+
if (!existsSync7(CACHE_DIR)) {
|
|
2111
|
+
mkdirSync5(CACHE_DIR, { recursive: true });
|
|
2112
|
+
}
|
|
2113
|
+
const cache = {
|
|
2114
|
+
latestVersion,
|
|
2115
|
+
checkedAt: Date.now()
|
|
2116
|
+
};
|
|
2117
|
+
writeFileSync6(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
2118
|
+
} catch {
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
function fetchLatestVersion() {
|
|
2122
|
+
try {
|
|
2123
|
+
const result = execSync3("npm view squads-cli version 2>/dev/null", {
|
|
2124
|
+
encoding: "utf-8",
|
|
2125
|
+
timeout: 5e3
|
|
2126
|
+
}).trim();
|
|
2127
|
+
return result || null;
|
|
2128
|
+
} catch {
|
|
2129
|
+
return null;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
function checkForUpdate() {
|
|
2133
|
+
const result = {
|
|
2134
|
+
currentVersion: CURRENT_VERSION,
|
|
2135
|
+
latestVersion: CURRENT_VERSION,
|
|
2136
|
+
updateAvailable: false
|
|
2137
|
+
};
|
|
2138
|
+
const cache = readCache();
|
|
2139
|
+
const now = Date.now();
|
|
2140
|
+
if (cache && now - cache.checkedAt < CACHE_TTL_MS) {
|
|
2141
|
+
result.latestVersion = cache.latestVersion;
|
|
2142
|
+
result.updateAvailable = isNewerVersion(CURRENT_VERSION, cache.latestVersion);
|
|
2143
|
+
return result;
|
|
2144
|
+
}
|
|
2145
|
+
const latestVersion = fetchLatestVersion();
|
|
2146
|
+
if (latestVersion) {
|
|
2147
|
+
writeCache(latestVersion);
|
|
2148
|
+
result.latestVersion = latestVersion;
|
|
2149
|
+
result.updateAvailable = isNewerVersion(CURRENT_VERSION, latestVersion);
|
|
2150
|
+
} else if (cache) {
|
|
2151
|
+
result.latestVersion = cache.latestVersion;
|
|
2152
|
+
result.updateAvailable = isNewerVersion(CURRENT_VERSION, cache.latestVersion);
|
|
2153
|
+
}
|
|
2154
|
+
return result;
|
|
2155
|
+
}
|
|
2156
|
+
function performUpdate() {
|
|
2157
|
+
try {
|
|
2158
|
+
execSync3("npm update -g squads-cli", {
|
|
2159
|
+
encoding: "utf-8",
|
|
2160
|
+
stdio: "inherit",
|
|
2161
|
+
timeout: 12e4
|
|
2162
|
+
// 2 minutes
|
|
2163
|
+
});
|
|
2164
|
+
try {
|
|
2165
|
+
unlinkSync2(CACHE_FILE);
|
|
2166
|
+
} catch {
|
|
2167
|
+
}
|
|
2168
|
+
return { success: true };
|
|
2169
|
+
} catch (err) {
|
|
2170
|
+
return {
|
|
2171
|
+
success: false,
|
|
2172
|
+
error: err instanceof Error ? err.message : "Unknown error"
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
function refreshVersionCache() {
|
|
2177
|
+
const latestVersion = fetchLatestVersion();
|
|
2178
|
+
if (latestVersion) {
|
|
2179
|
+
writeCache(latestVersion);
|
|
2180
|
+
return {
|
|
2181
|
+
currentVersion: CURRENT_VERSION,
|
|
2182
|
+
latestVersion,
|
|
2183
|
+
updateAvailable: isNewerVersion(CURRENT_VERSION, latestVersion)
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
return checkForUpdate();
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// src/commands/status.ts
|
|
2190
|
+
async function statusCommand(squadName, options = {}) {
|
|
2191
|
+
const squadsDir = findSquadsDir();
|
|
2192
|
+
if (!squadsDir) {
|
|
2193
|
+
writeLine(`${colors.red}No .agents/squads directory found${RESET}`);
|
|
2194
|
+
writeLine(`${colors.dim}Run \`squads init\` to create one.${RESET}`);
|
|
2195
|
+
process.exit(1);
|
|
2196
|
+
}
|
|
2197
|
+
if (squadName) {
|
|
2198
|
+
await showSquadStatus(squadName, squadsDir, options);
|
|
2199
|
+
} else {
|
|
2200
|
+
await showOverallStatus(squadsDir, options);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
async function showOverallStatus(squadsDir, _options) {
|
|
2204
|
+
const squads = listSquads(squadsDir);
|
|
2205
|
+
const memoryDir = findMemoryDir();
|
|
2206
|
+
cleanupStaleSessions();
|
|
2207
|
+
const sessionSummary = getLiveSessionSummary();
|
|
2208
|
+
writeLine();
|
|
2209
|
+
writeLine(` ${gradient("squads")} ${colors.dim}status${RESET}`);
|
|
2210
|
+
const updateInfo = checkForUpdate();
|
|
2211
|
+
if (updateInfo.updateAvailable) {
|
|
2212
|
+
writeLine(` ${colors.cyan}\u2B06${RESET} Update available: ${colors.dim}${updateInfo.currentVersion}${RESET} \u2192 ${colors.green}${updateInfo.latestVersion}${RESET} ${colors.dim}(run \`squads update\`)${RESET}`);
|
|
2213
|
+
}
|
|
2214
|
+
if (sessionSummary.totalSessions > 0) {
|
|
2215
|
+
const sessionText = sessionSummary.totalSessions === 1 ? "session" : "sessions";
|
|
2216
|
+
const squadText = sessionSummary.squadCount === 1 ? "squad" : "squads";
|
|
2217
|
+
let toolInfo = "";
|
|
2218
|
+
if (sessionSummary.byTool && Object.keys(sessionSummary.byTool).length > 0) {
|
|
2219
|
+
const toolParts = Object.entries(sessionSummary.byTool).sort((a, b) => b[1] - a[1]).map(([tool, count]) => `${colors.dim}${tool}${RESET} ${colors.cyan}${count}${RESET}`);
|
|
2220
|
+
toolInfo = ` ${colors.dim}(${RESET}${toolParts.join(` ${colors.dim}\xB7${RESET} `)}${colors.dim})${RESET}`;
|
|
2221
|
+
}
|
|
2222
|
+
writeLine(` ${colors.green}${icons.active}${RESET} ${colors.white}${sessionSummary.totalSessions}${RESET} active ${sessionText} ${colors.dim}across${RESET} ${colors.cyan}${sessionSummary.squadCount}${RESET} ${squadText}${toolInfo}`);
|
|
2223
|
+
}
|
|
2224
|
+
writeLine();
|
|
2225
|
+
const totalSquads = squads.length;
|
|
2226
|
+
const activeCount = squads.length;
|
|
2227
|
+
writeLine(` ${colors.cyan}${activeCount}${RESET}/${totalSquads} squads ${colors.dim}\u2502${RESET} ${colors.dim}memory: ${memoryDir ? "enabled" : "none"}${RESET}`);
|
|
2228
|
+
writeLine();
|
|
2229
|
+
const w = { name: 16, agents: 8, memory: 14, activity: 12 };
|
|
2230
|
+
const tableWidth = w.name + w.agents + w.memory + w.activity + 6;
|
|
2231
|
+
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
2232
|
+
const header = ` ${colors.purple}${box.vertical}${RESET} ${bold}${padEnd("SQUAD", w.name)}${RESET}${bold}${padEnd("AGENTS", w.agents)}${RESET}${bold}${padEnd("MEMORY", w.memory)}${RESET}${bold}ACTIVITY${RESET} ${colors.purple}${box.vertical}${RESET}`;
|
|
2233
|
+
writeLine(header);
|
|
2234
|
+
writeLine(` ${colors.purple}${box.teeRight}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.teeLeft}${RESET}`);
|
|
2235
|
+
for (const squadName of squads) {
|
|
2236
|
+
const agents = listAgents(squadsDir, squadName);
|
|
2237
|
+
let memoryStatus = `${colors.dim}none${RESET}`;
|
|
2238
|
+
let lastActivity = `${colors.dim}\u2014${RESET}`;
|
|
2239
|
+
let activityColor = colors.dim;
|
|
2240
|
+
if (memoryDir) {
|
|
2241
|
+
const squadMemoryPath = join8(memoryDir, squadName);
|
|
2242
|
+
if (existsSync8(squadMemoryPath)) {
|
|
2243
|
+
const states = getSquadState(squadName);
|
|
2244
|
+
memoryStatus = `${colors.green}${states.length} ${states.length === 1 ? "entry" : "entries"}${RESET}`;
|
|
2245
|
+
let mostRecent = 0;
|
|
2246
|
+
for (const state of states) {
|
|
2247
|
+
const stat = statSync(state.path);
|
|
2248
|
+
if (stat.mtimeMs > mostRecent) {
|
|
2249
|
+
mostRecent = stat.mtimeMs;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
if (mostRecent > 0) {
|
|
2253
|
+
const daysAgo = Math.floor((Date.now() - mostRecent) / (1e3 * 60 * 60 * 24));
|
|
2254
|
+
if (daysAgo === 0) {
|
|
2255
|
+
lastActivity = "today";
|
|
2256
|
+
activityColor = colors.green;
|
|
2257
|
+
} else if (daysAgo === 1) {
|
|
2258
|
+
lastActivity = "yesterday";
|
|
2259
|
+
activityColor = colors.green;
|
|
2260
|
+
} else if (daysAgo < 7) {
|
|
2261
|
+
lastActivity = `${daysAgo}d ago`;
|
|
2262
|
+
activityColor = colors.yellow;
|
|
2263
|
+
} else {
|
|
2264
|
+
lastActivity = `${daysAgo}d ago`;
|
|
2265
|
+
activityColor = colors.dim;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
const row = ` ${colors.purple}${box.vertical}${RESET} ${colors.cyan}${padEnd(squadName, w.name)}${RESET}${padEnd(String(agents.length), w.agents)}${padEnd(memoryStatus, w.memory)}${padEnd(`${activityColor}${lastActivity}${RESET}`, w.activity)}${colors.purple}${box.vertical}${RESET}`;
|
|
2271
|
+
writeLine(row);
|
|
2272
|
+
}
|
|
2273
|
+
writeLine(` ${colors.purple}${box.bottomLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.bottomRight}${RESET}`);
|
|
2274
|
+
writeLine();
|
|
2275
|
+
writeLine(` ${colors.dim}$${RESET} squads status ${colors.cyan}<squad>${RESET} ${colors.dim}Squad details${RESET}`);
|
|
2276
|
+
writeLine(` ${colors.dim}$${RESET} squads dash ${colors.dim}Full dashboard${RESET}`);
|
|
2277
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}<squad>${RESET} ${colors.dim}Execute a squad${RESET}`);
|
|
2278
|
+
writeLine();
|
|
2279
|
+
}
|
|
2280
|
+
async function showSquadStatus(squadName, squadsDir, options) {
|
|
2281
|
+
const squad = loadSquad(squadName);
|
|
2282
|
+
if (!squad) {
|
|
2283
|
+
writeLine(`${colors.red}Squad "${squadName}" not found.${RESET}`);
|
|
2284
|
+
process.exit(1);
|
|
2285
|
+
}
|
|
2286
|
+
writeLine();
|
|
2287
|
+
writeLine(` ${gradient("squads")} ${colors.dim}status${RESET} ${colors.cyan}${squad.name}${RESET}`);
|
|
2288
|
+
writeLine();
|
|
2289
|
+
if (squad.mission) {
|
|
2290
|
+
writeLine(` ${colors.dim}${squad.mission}${RESET}`);
|
|
2291
|
+
writeLine();
|
|
2292
|
+
}
|
|
2293
|
+
const agents = listAgents(squadsDir, squadName);
|
|
2294
|
+
const w = { name: 24, role: 36 };
|
|
2295
|
+
const tableWidth = w.name + w.role + 4;
|
|
2296
|
+
writeLine(` ${bold}Agents${RESET} ${colors.dim}(${agents.length})${RESET}`);
|
|
2297
|
+
writeLine();
|
|
2298
|
+
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
2299
|
+
for (const agent of agents) {
|
|
2300
|
+
const status = agent.status?.toLowerCase() === "active" ? icons.active : icons.pending;
|
|
2301
|
+
const role = options.verbose && agent.role ? `${colors.dim}${agent.role.substring(0, w.role - 2)}${RESET}` : "";
|
|
2302
|
+
const row = ` ${colors.purple}${box.vertical}${RESET} ${status} ${padEnd(agent.name, w.name - 2)}${padEnd(role, w.role)}${colors.purple}${box.vertical}${RESET}`;
|
|
2303
|
+
writeLine(row);
|
|
2304
|
+
}
|
|
2305
|
+
writeLine(` ${colors.purple}${box.bottomLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.bottomRight}${RESET}`);
|
|
2306
|
+
if (squad.pipelines.length > 0) {
|
|
2307
|
+
writeLine();
|
|
2308
|
+
writeLine(` ${bold}Pipelines${RESET}`);
|
|
2309
|
+
for (const pipeline of squad.pipelines) {
|
|
2310
|
+
writeLine(` ${colors.dim}${pipeline.agents.join(" \u2192 ")}${RESET}`);
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
const memoryDir = findMemoryDir();
|
|
2314
|
+
if (memoryDir) {
|
|
2315
|
+
const states = getSquadState(squadName);
|
|
2316
|
+
if (states.length > 0) {
|
|
2317
|
+
writeLine();
|
|
2318
|
+
writeLine(` ${bold}Memory${RESET} ${colors.dim}(${states.length} ${states.length === 1 ? "entry" : "entries"})${RESET}`);
|
|
2319
|
+
writeLine();
|
|
2320
|
+
for (const state of states) {
|
|
2321
|
+
const updated = state.content.match(/Updated:\s*(\S+)/)?.[1] || "unknown";
|
|
2322
|
+
writeLine(` ${icons.progress} ${colors.white}${state.agent}${RESET}`);
|
|
2323
|
+
writeLine(` ${colors.dim}\u2514 updated: ${updated}${RESET}`);
|
|
2324
|
+
if (options.verbose) {
|
|
2325
|
+
const signalsMatch = state.content.match(/## Active Signals([\s\S]*?)(?=##|$)/);
|
|
2326
|
+
if (signalsMatch) {
|
|
2327
|
+
const signalLines = signalsMatch[1].split("\n").filter((l) => l.match(/^\d+\./)).slice(0, 3);
|
|
2328
|
+
for (const sig of signalLines) {
|
|
2329
|
+
writeLine(` ${colors.dim} ${sig.trim()}${RESET}`);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
writeLine();
|
|
1756
2337
|
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squadName}${RESET} ${colors.dim}Run the squad${RESET}`);
|
|
1757
2338
|
writeLine(` ${colors.dim}$${RESET} squads memory show ${colors.cyan}${squadName}${RESET} ${colors.dim}View full memory${RESET}`);
|
|
1758
2339
|
writeLine(` ${colors.dim}$${RESET} squads status ${colors.cyan}${squadName}${RESET} -v ${colors.dim}Verbose status${RESET}`);
|
|
@@ -1997,9 +2578,9 @@ async function memorySearchCommand(query, options = {}) {
|
|
|
1997
2578
|
}
|
|
1998
2579
|
|
|
1999
2580
|
// src/commands/sync.ts
|
|
2000
|
-
import { execSync as
|
|
2001
|
-
import { existsSync as
|
|
2002
|
-
import { join as
|
|
2581
|
+
import { execSync as execSync4 } from "child_process";
|
|
2582
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, readdirSync as readdirSync4 } from "fs";
|
|
2583
|
+
import { join as join9 } from "path";
|
|
2003
2584
|
var PATH_TO_SQUAD = {
|
|
2004
2585
|
"squads-cli": "product",
|
|
2005
2586
|
"agents-squads-web": "website",
|
|
@@ -2027,21 +2608,21 @@ var MESSAGE_TO_SQUAD = {
|
|
|
2027
2608
|
"infra": "engineering"
|
|
2028
2609
|
};
|
|
2029
2610
|
function getLastSyncTime(memoryDir) {
|
|
2030
|
-
const syncFile =
|
|
2031
|
-
if (
|
|
2032
|
-
return
|
|
2611
|
+
const syncFile = join9(memoryDir, ".last-sync");
|
|
2612
|
+
if (existsSync9(syncFile)) {
|
|
2613
|
+
return readFileSync7(syncFile, "utf-8").trim();
|
|
2033
2614
|
}
|
|
2034
2615
|
return null;
|
|
2035
2616
|
}
|
|
2036
2617
|
function updateLastSyncTime(memoryDir) {
|
|
2037
|
-
const syncFile =
|
|
2038
|
-
|
|
2618
|
+
const syncFile = join9(memoryDir, ".last-sync");
|
|
2619
|
+
writeFileSync7(syncFile, (/* @__PURE__ */ new Date()).toISOString());
|
|
2039
2620
|
}
|
|
2040
2621
|
function getRecentCommits(since) {
|
|
2041
2622
|
const commits = [];
|
|
2042
2623
|
try {
|
|
2043
2624
|
const sinceArg = since ? `--since="${since}"` : "-n 20";
|
|
2044
|
-
const logOutput =
|
|
2625
|
+
const logOutput = execSync4(
|
|
2045
2626
|
`git log ${sinceArg} --format="%H|%aI|%s" --name-only`,
|
|
2046
2627
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
2047
2628
|
).trim();
|
|
@@ -2107,22 +2688,22 @@ ${messages}
|
|
|
2107
2688
|
`;
|
|
2108
2689
|
}
|
|
2109
2690
|
function appendToSquadMemory(memoryDir, squad, summary) {
|
|
2110
|
-
const squadMemoryDir =
|
|
2111
|
-
if (!
|
|
2112
|
-
|
|
2691
|
+
const squadMemoryDir = join9(memoryDir, squad);
|
|
2692
|
+
if (!existsSync9(squadMemoryDir)) {
|
|
2693
|
+
mkdirSync6(squadMemoryDir, { recursive: true });
|
|
2113
2694
|
}
|
|
2114
2695
|
let agentDir;
|
|
2115
|
-
const existingDirs =
|
|
2696
|
+
const existingDirs = existsSync9(squadMemoryDir) ? readdirSync4(squadMemoryDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name) : [];
|
|
2116
2697
|
if (existingDirs.length > 0) {
|
|
2117
|
-
agentDir =
|
|
2698
|
+
agentDir = join9(squadMemoryDir, existingDirs[0]);
|
|
2118
2699
|
} else {
|
|
2119
|
-
agentDir =
|
|
2120
|
-
|
|
2700
|
+
agentDir = join9(squadMemoryDir, `${squad}-lead`);
|
|
2701
|
+
mkdirSync6(agentDir, { recursive: true });
|
|
2121
2702
|
}
|
|
2122
|
-
const statePath =
|
|
2703
|
+
const statePath = join9(agentDir, "state.md");
|
|
2123
2704
|
let content = "";
|
|
2124
|
-
if (
|
|
2125
|
-
content =
|
|
2705
|
+
if (existsSync9(statePath)) {
|
|
2706
|
+
content = readFileSync7(statePath, "utf-8");
|
|
2126
2707
|
} else {
|
|
2127
2708
|
content = `# ${squad} Squad - State
|
|
2128
2709
|
|
|
@@ -2134,9 +2715,53 @@ Updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
|
2134
2715
|
`Updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`
|
|
2135
2716
|
);
|
|
2136
2717
|
content += summary;
|
|
2137
|
-
|
|
2718
|
+
writeFileSync7(statePath, content);
|
|
2138
2719
|
return true;
|
|
2139
2720
|
}
|
|
2721
|
+
function gitPullMemory() {
|
|
2722
|
+
try {
|
|
2723
|
+
execSync4("git fetch origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
2724
|
+
const status = execSync4("git status -sb", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
2725
|
+
const behindMatch = status.match(/behind (\d+)/);
|
|
2726
|
+
const aheadMatch = status.match(/ahead (\d+)/);
|
|
2727
|
+
const behind = behindMatch ? parseInt(behindMatch[1]) : 0;
|
|
2728
|
+
const ahead = aheadMatch ? parseInt(aheadMatch[1]) : 0;
|
|
2729
|
+
if (behind === 0) {
|
|
2730
|
+
return { success: true, output: "Already up to date", behind: 0, ahead };
|
|
2731
|
+
}
|
|
2732
|
+
const output = execSync4("git pull --rebase origin main", {
|
|
2733
|
+
encoding: "utf-8",
|
|
2734
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2735
|
+
});
|
|
2736
|
+
return { success: true, output: output.trim(), behind, ahead };
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
const err = error;
|
|
2739
|
+
return { success: false, output: err.message || "Pull failed", behind: 0, ahead: 0 };
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
function gitPushMemory() {
|
|
2743
|
+
try {
|
|
2744
|
+
const status = execSync4("git status --porcelain .agents/memory/", {
|
|
2745
|
+
encoding: "utf-8",
|
|
2746
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2747
|
+
}).trim();
|
|
2748
|
+
if (status) {
|
|
2749
|
+
execSync4("git add .agents/memory/", { stdio: ["pipe", "pipe", "pipe"] });
|
|
2750
|
+
execSync4('git commit -m "chore: sync squad memory"', {
|
|
2751
|
+
encoding: "utf-8",
|
|
2752
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
const output = execSync4("git push origin main", {
|
|
2756
|
+
encoding: "utf-8",
|
|
2757
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2758
|
+
});
|
|
2759
|
+
return { success: true, output: output.trim() || "Pushed successfully" };
|
|
2760
|
+
} catch (error) {
|
|
2761
|
+
const err = error;
|
|
2762
|
+
return { success: false, output: err.message || "Push failed" };
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2140
2765
|
async function syncCommand(options = {}) {
|
|
2141
2766
|
const memoryDir = findMemoryDir();
|
|
2142
2767
|
const squadsDir = findSquadsDir();
|
|
@@ -2148,6 +2773,25 @@ async function syncCommand(options = {}) {
|
|
|
2148
2773
|
writeLine();
|
|
2149
2774
|
writeLine(` ${gradient("squads")} ${colors.dim}memory sync${RESET}`);
|
|
2150
2775
|
writeLine();
|
|
2776
|
+
const doPull = options.pull !== false;
|
|
2777
|
+
const doPush = options.push === true;
|
|
2778
|
+
if (doPull) {
|
|
2779
|
+
writeLine(` ${icons.progress} Pulling from remote...`);
|
|
2780
|
+
const pullResult = gitPullMemory();
|
|
2781
|
+
if (pullResult.success) {
|
|
2782
|
+
if (pullResult.behind > 0) {
|
|
2783
|
+
writeLine(` ${icons.success} Pulled ${colors.cyan}${pullResult.behind}${RESET} commits from remote`);
|
|
2784
|
+
} else {
|
|
2785
|
+
writeLine(` ${icons.success} ${colors.dim}Already up to date${RESET}`);
|
|
2786
|
+
}
|
|
2787
|
+
if (pullResult.ahead > 0) {
|
|
2788
|
+
writeLine(` ${colors.dim} ${pullResult.ahead} local commits to push${RESET}`);
|
|
2789
|
+
}
|
|
2790
|
+
} else {
|
|
2791
|
+
writeLine(` ${icons.error} ${colors.red}Pull failed: ${pullResult.output}${RESET}`);
|
|
2792
|
+
}
|
|
2793
|
+
writeLine();
|
|
2794
|
+
}
|
|
2151
2795
|
const lastSync = getLastSyncTime(memoryDir);
|
|
2152
2796
|
if (lastSync) {
|
|
2153
2797
|
writeLine(` ${colors.dim}Last sync: ${lastSync.split("T")[0]}${RESET}`);
|
|
@@ -2189,8 +2833,21 @@ async function syncCommand(options = {}) {
|
|
|
2189
2833
|
writeLine(` ${colors.green}${updated}${RESET} squad memories updated`);
|
|
2190
2834
|
writeLine();
|
|
2191
2835
|
updateLastSyncTime(memoryDir);
|
|
2836
|
+
if (doPush) {
|
|
2837
|
+
writeLine(` ${icons.progress} Pushing to remote...`);
|
|
2838
|
+
const pushResult = gitPushMemory();
|
|
2839
|
+
if (pushResult.success) {
|
|
2840
|
+
writeLine(` ${icons.success} ${colors.green}Pushed memory updates to remote${RESET}`);
|
|
2841
|
+
} else {
|
|
2842
|
+
writeLine(` ${icons.error} ${colors.red}Push failed: ${pushResult.output}${RESET}`);
|
|
2843
|
+
}
|
|
2844
|
+
writeLine();
|
|
2845
|
+
}
|
|
2192
2846
|
writeLine(` ${colors.dim}$${RESET} squads memory show ${colors.cyan}<squad>${RESET} ${colors.dim}View updated memory${RESET}`);
|
|
2193
2847
|
writeLine(` ${colors.dim}$${RESET} squads status ${colors.dim}See all squads${RESET}`);
|
|
2848
|
+
if (!doPush && updated > 0) {
|
|
2849
|
+
writeLine(` ${colors.dim}$${RESET} squads memory sync --push ${colors.dim}Push changes to remote${RESET}`);
|
|
2850
|
+
}
|
|
2194
2851
|
writeLine();
|
|
2195
2852
|
}
|
|
2196
2853
|
|
|
@@ -2318,28 +2975,28 @@ async function goalProgressCommand(squadName, goalIndex, progress2) {
|
|
|
2318
2975
|
}
|
|
2319
2976
|
|
|
2320
2977
|
// src/commands/feedback.ts
|
|
2321
|
-
import { readFileSync as
|
|
2322
|
-
import { join as
|
|
2978
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5 } from "fs";
|
|
2979
|
+
import { join as join10, dirname as dirname6 } from "path";
|
|
2323
2980
|
function getFeedbackPath(squadName) {
|
|
2324
2981
|
const memoryDir = findMemoryDir();
|
|
2325
2982
|
if (!memoryDir) return null;
|
|
2326
2983
|
const squad = loadSquad(squadName);
|
|
2327
2984
|
const agentName = squad?.agents[0]?.name || `${squadName}-lead`;
|
|
2328
|
-
return
|
|
2985
|
+
return join10(memoryDir, squadName, agentName, "feedback.md");
|
|
2329
2986
|
}
|
|
2330
2987
|
function getOutputPath(squadName) {
|
|
2331
2988
|
const memoryDir = findMemoryDir();
|
|
2332
2989
|
if (!memoryDir) return null;
|
|
2333
2990
|
const squad = loadSquad(squadName);
|
|
2334
2991
|
const agentName = squad?.agents[0]?.name || `${squadName}-lead`;
|
|
2335
|
-
return
|
|
2992
|
+
return join10(memoryDir, squadName, agentName, "output.md");
|
|
2336
2993
|
}
|
|
2337
2994
|
function getLastExecution(squadName) {
|
|
2338
2995
|
const outputPath = getOutputPath(squadName);
|
|
2339
|
-
if (!outputPath || !
|
|
2996
|
+
if (!outputPath || !existsSync10(outputPath)) {
|
|
2340
2997
|
return null;
|
|
2341
2998
|
}
|
|
2342
|
-
const content =
|
|
2999
|
+
const content = readFileSync8(outputPath, "utf-8");
|
|
2343
3000
|
const lines = content.split("\n");
|
|
2344
3001
|
let date = "unknown";
|
|
2345
3002
|
let summary = lines.slice(0, 5).join("\n");
|
|
@@ -2390,9 +3047,9 @@ async function feedbackAddCommand(squadName, rating, feedback2, options) {
|
|
|
2390
3047
|
return;
|
|
2391
3048
|
}
|
|
2392
3049
|
const lastExec = getLastExecution(squadName);
|
|
2393
|
-
const dir =
|
|
2394
|
-
if (!
|
|
2395
|
-
|
|
3050
|
+
const dir = dirname6(feedbackPath);
|
|
3051
|
+
if (!existsSync10(dir)) {
|
|
3052
|
+
mkdirSync7(dir, { recursive: true });
|
|
2396
3053
|
}
|
|
2397
3054
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2398
3055
|
let entry = `
|
|
@@ -2420,15 +3077,15 @@ _Date: ${date}_
|
|
|
2420
3077
|
}
|
|
2421
3078
|
}
|
|
2422
3079
|
let existing = "";
|
|
2423
|
-
if (
|
|
2424
|
-
existing =
|
|
3080
|
+
if (existsSync10(feedbackPath)) {
|
|
3081
|
+
existing = readFileSync8(feedbackPath, "utf-8");
|
|
2425
3082
|
} else {
|
|
2426
3083
|
existing = `# ${squadName} - Feedback Log
|
|
2427
3084
|
|
|
2428
3085
|
> Execution feedback and learnings
|
|
2429
3086
|
`;
|
|
2430
3087
|
}
|
|
2431
|
-
|
|
3088
|
+
writeFileSync8(feedbackPath, existing + entry);
|
|
2432
3089
|
const stars = `${colors.yellow}${"\u2605".repeat(ratingNum)}${"\u2606".repeat(5 - ratingNum)}${RESET}`;
|
|
2433
3090
|
writeLine();
|
|
2434
3091
|
writeLine(` ${icons.success} Feedback recorded for ${colors.cyan}${squadName}${RESET}`);
|
|
@@ -2441,11 +3098,11 @@ _Date: ${date}_
|
|
|
2441
3098
|
}
|
|
2442
3099
|
async function feedbackShowCommand(squadName, options) {
|
|
2443
3100
|
const feedbackPath = getFeedbackPath(squadName);
|
|
2444
|
-
if (!feedbackPath || !
|
|
3101
|
+
if (!feedbackPath || !existsSync10(feedbackPath)) {
|
|
2445
3102
|
writeLine(` ${colors.yellow}No feedback recorded for ${squadName}${RESET}`);
|
|
2446
3103
|
return;
|
|
2447
3104
|
}
|
|
2448
|
-
const content =
|
|
3105
|
+
const content = readFileSync8(feedbackPath, "utf-8");
|
|
2449
3106
|
const entries = parseFeedbackHistory(content);
|
|
2450
3107
|
const limit = options.limit ? parseInt(options.limit) : 5;
|
|
2451
3108
|
const recent = entries.slice(-limit).reverse();
|
|
@@ -2480,7 +3137,7 @@ async function feedbackStatsCommand() {
|
|
|
2480
3137
|
writeLine();
|
|
2481
3138
|
writeLine(` ${gradient("squads")} ${colors.dim}feedback stats${RESET}`);
|
|
2482
3139
|
writeLine();
|
|
2483
|
-
const squads =
|
|
3140
|
+
const squads = readdirSync5(memoryDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2484
3141
|
const w = { squad: 18, avg: 12, count: 8, trend: 6 };
|
|
2485
3142
|
const tableWidth = w.squad + w.avg + w.count + w.trend + 4;
|
|
2486
3143
|
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
@@ -2489,10 +3146,10 @@ async function feedbackStatsCommand() {
|
|
|
2489
3146
|
writeLine(` ${colors.purple}${box.teeRight}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.teeLeft}${RESET}`);
|
|
2490
3147
|
for (const squad of squads) {
|
|
2491
3148
|
const feedbackPath = getFeedbackPath(squad);
|
|
2492
|
-
if (!feedbackPath || !
|
|
3149
|
+
if (!feedbackPath || !existsSync10(feedbackPath)) {
|
|
2493
3150
|
continue;
|
|
2494
3151
|
}
|
|
2495
|
-
const content =
|
|
3152
|
+
const content = readFileSync8(feedbackPath, "utf-8");
|
|
2496
3153
|
const entries = parseFeedbackHistory(content);
|
|
2497
3154
|
if (entries.length === 0) continue;
|
|
2498
3155
|
const avgRating = entries.reduce((sum, e) => sum + e.rating, 0) / entries.length;
|
|
@@ -2515,9 +3172,9 @@ async function feedbackStatsCommand() {
|
|
|
2515
3172
|
}
|
|
2516
3173
|
|
|
2517
3174
|
// src/commands/dashboard.ts
|
|
2518
|
-
import { readdirSync as
|
|
2519
|
-
import { join as
|
|
2520
|
-
import { homedir as
|
|
3175
|
+
import { readdirSync as readdirSync6, existsSync as existsSync11, statSync as statSync2 } from "fs";
|
|
3176
|
+
import { join as join11 } from "path";
|
|
3177
|
+
import { homedir as homedir3 } from "os";
|
|
2521
3178
|
|
|
2522
3179
|
// src/lib/costs.ts
|
|
2523
3180
|
var MODEL_PRICING = {
|
|
@@ -2528,16 +3185,29 @@ var MODEL_PRICING = {
|
|
|
2528
3185
|
"claude-3-5-haiku-20241022": { input: 0.8, output: 4 },
|
|
2529
3186
|
default: { input: 3, output: 15 }
|
|
2530
3187
|
};
|
|
2531
|
-
var DEFAULT_DAILY_BUDGET =
|
|
3188
|
+
var DEFAULT_DAILY_BUDGET = 200;
|
|
2532
3189
|
var DEFAULT_DAILY_CALL_LIMIT = 1e3;
|
|
2533
3190
|
var BRIDGE_URL = process.env.SQUADS_BRIDGE_URL || "http://localhost:8088";
|
|
3191
|
+
var FETCH_TIMEOUT_MS = 2e3;
|
|
3192
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
3193
|
+
const controller = new AbortController();
|
|
3194
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
3195
|
+
try {
|
|
3196
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
|
3197
|
+
clearTimeout(timeoutId);
|
|
3198
|
+
return response;
|
|
3199
|
+
} catch (error) {
|
|
3200
|
+
clearTimeout(timeoutId);
|
|
3201
|
+
throw error;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
2534
3204
|
function calcCost(model, inputTokens, outputTokens) {
|
|
2535
3205
|
const pricing = MODEL_PRICING[model] || MODEL_PRICING.default;
|
|
2536
3206
|
return inputTokens / 1e6 * pricing.input + outputTokens / 1e6 * pricing.output;
|
|
2537
3207
|
}
|
|
2538
3208
|
async function fetchFromBridge(period = "day") {
|
|
2539
3209
|
try {
|
|
2540
|
-
const response = await
|
|
3210
|
+
const response = await fetchWithTimeout(`${BRIDGE_URL}/api/cost/summary?period=${period}`, {
|
|
2541
3211
|
headers: { "Content-Type": "application/json" }
|
|
2542
3212
|
});
|
|
2543
3213
|
if (!response.ok) {
|
|
@@ -2589,7 +3259,7 @@ async function fetchFromLangfuse(limit = 100) {
|
|
|
2589
3259
|
try {
|
|
2590
3260
|
const auth = Buffer.from(`${publicKey}:${secretKey}`).toString("base64");
|
|
2591
3261
|
const url = `${host}/api/public/observations?limit=${limit}`;
|
|
2592
|
-
const response = await
|
|
3262
|
+
const response = await fetchWithTimeout(url, {
|
|
2593
3263
|
headers: {
|
|
2594
3264
|
Authorization: `Basic ${auth}`,
|
|
2595
3265
|
"Content-Type": "application/json"
|
|
@@ -2684,41 +3354,78 @@ function formatCostBar(usedPercent, width = 20) {
|
|
|
2684
3354
|
const empty = width - filled;
|
|
2685
3355
|
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
2686
3356
|
}
|
|
2687
|
-
async function
|
|
3357
|
+
async function fetchBridgeStats() {
|
|
2688
3358
|
try {
|
|
2689
|
-
const
|
|
3359
|
+
const statsResponse = await fetchWithTimeout(`${BRIDGE_URL}/stats`, {
|
|
2690
3360
|
headers: { "Content-Type": "application/json" }
|
|
2691
3361
|
});
|
|
2692
|
-
if (!
|
|
2693
|
-
return
|
|
2694
|
-
}
|
|
2695
|
-
const data = await response.json();
|
|
2696
|
-
const rateLimits = data.rate_limits || {};
|
|
2697
|
-
const limits = {};
|
|
2698
|
-
for (const [key, value] of Object.entries(rateLimits)) {
|
|
2699
|
-
limits[key] = {
|
|
2700
|
-
model: value.model || key,
|
|
2701
|
-
requestsLimit: value.requests_limit || 0,
|
|
2702
|
-
requestsRemaining: value.requests_remaining || 0,
|
|
2703
|
-
requestsReset: value.requests_reset,
|
|
2704
|
-
tokensLimit: value.tokens_limit || 0,
|
|
2705
|
-
tokensRemaining: value.tokens_remaining || 0,
|
|
2706
|
-
tokensReset: value.tokens_reset,
|
|
2707
|
-
inputTokensLimit: value.input_tokens_limit,
|
|
2708
|
-
inputTokensRemaining: value.input_tokens_remaining,
|
|
2709
|
-
outputTokensLimit: value.output_tokens_limit,
|
|
2710
|
-
outputTokensRemaining: value.output_tokens_remaining,
|
|
2711
|
-
capturedAt: value.captured_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
2712
|
-
};
|
|
3362
|
+
if (!statsResponse.ok) {
|
|
3363
|
+
return null;
|
|
2713
3364
|
}
|
|
2714
|
-
|
|
3365
|
+
const stats = await statsResponse.json();
|
|
3366
|
+
const healthResponse = await fetchWithTimeout(`${BRIDGE_URL}/health`, {
|
|
3367
|
+
headers: { "Content-Type": "application/json" }
|
|
3368
|
+
});
|
|
3369
|
+
const health = healthResponse.ok ? await healthResponse.json() : {};
|
|
3370
|
+
const [costResponse, weekResponse] = await Promise.all([
|
|
3371
|
+
fetchWithTimeout(`${BRIDGE_URL}/api/cost/summary?period=day`, {
|
|
3372
|
+
headers: { "Content-Type": "application/json" }
|
|
3373
|
+
}),
|
|
3374
|
+
fetchWithTimeout(`${BRIDGE_URL}/api/cost/summary?period=week`, {
|
|
3375
|
+
headers: { "Content-Type": "application/json" }
|
|
3376
|
+
})
|
|
3377
|
+
]);
|
|
3378
|
+
const costData = costResponse.ok ? await costResponse.json() : {};
|
|
3379
|
+
const weekData = weekResponse.ok ? await weekResponse.json() : {};
|
|
3380
|
+
return {
|
|
3381
|
+
status: stats.status || "unknown",
|
|
3382
|
+
source: stats.source || "none",
|
|
3383
|
+
today: {
|
|
3384
|
+
generations: stats.today?.generations || 0,
|
|
3385
|
+
inputTokens: stats.today?.input_tokens || 0,
|
|
3386
|
+
outputTokens: stats.today?.output_tokens || 0,
|
|
3387
|
+
costUsd: stats.today?.cost_usd || 0
|
|
3388
|
+
},
|
|
3389
|
+
week: weekData.totals ? {
|
|
3390
|
+
generations: weekData.totals.generations || 0,
|
|
3391
|
+
inputTokens: weekData.totals.input_tokens || 0,
|
|
3392
|
+
outputTokens: weekData.totals.output_tokens || 0,
|
|
3393
|
+
costUsd: weekData.totals.cost_usd || 0,
|
|
3394
|
+
byModel: (weekData.by_model || []).map((m) => ({
|
|
3395
|
+
model: m.model || "unknown",
|
|
3396
|
+
generations: m.generations || 0,
|
|
3397
|
+
costUsd: m.cost_usd || 0
|
|
3398
|
+
}))
|
|
3399
|
+
} : void 0,
|
|
3400
|
+
budget: {
|
|
3401
|
+
daily: stats.budget?.daily || DEFAULT_DAILY_BUDGET,
|
|
3402
|
+
used: stats.budget?.used || 0,
|
|
3403
|
+
remaining: stats.budget?.remaining || DEFAULT_DAILY_BUDGET,
|
|
3404
|
+
usedPct: stats.budget?.used_pct || 0
|
|
3405
|
+
},
|
|
3406
|
+
bySquad: (stats.by_squad || []).map((s) => ({
|
|
3407
|
+
squad: s.squad || "unknown",
|
|
3408
|
+
costUsd: s.cost_usd || 0,
|
|
3409
|
+
generations: s.generations || 0
|
|
3410
|
+
})),
|
|
3411
|
+
byModel: (costData.by_model || []).map((m) => ({
|
|
3412
|
+
model: m.model || "unknown",
|
|
3413
|
+
generations: m.generations || 0,
|
|
3414
|
+
costUsd: m.cost_usd || 0
|
|
3415
|
+
})),
|
|
3416
|
+
health: {
|
|
3417
|
+
postgres: health.postgres || "unknown",
|
|
3418
|
+
redis: health.redis || "unknown",
|
|
3419
|
+
langfuse: health.langfuse || "unknown"
|
|
3420
|
+
}
|
|
3421
|
+
};
|
|
2715
3422
|
} catch {
|
|
2716
|
-
return
|
|
3423
|
+
return null;
|
|
2717
3424
|
}
|
|
2718
3425
|
}
|
|
2719
3426
|
async function fetchInsights(period = "week") {
|
|
2720
3427
|
try {
|
|
2721
|
-
const response = await
|
|
3428
|
+
const response = await fetchWithTimeout(`${BRIDGE_URL}/api/insights?period=${period}`, {
|
|
2722
3429
|
headers: { "Content-Type": "application/json" }
|
|
2723
3430
|
});
|
|
2724
3431
|
if (!response.ok) {
|
|
@@ -2793,7 +3500,8 @@ function getPool() {
|
|
|
2793
3500
|
connectionString: DATABASE_URL,
|
|
2794
3501
|
max: 5,
|
|
2795
3502
|
idleTimeoutMillis: 3e4,
|
|
2796
|
-
connectionTimeoutMillis:
|
|
3503
|
+
connectionTimeoutMillis: 1500
|
|
3504
|
+
// Fast timeout for CLI responsiveness
|
|
2797
3505
|
});
|
|
2798
3506
|
pool.on("error", (err) => {
|
|
2799
3507
|
console.error("Unexpected database pool error:", err);
|
|
@@ -2895,21 +3603,27 @@ async function getDashboardHistory(limit = 30) {
|
|
|
2895
3603
|
return [];
|
|
2896
3604
|
}
|
|
2897
3605
|
}
|
|
3606
|
+
async function closeDatabase() {
|
|
3607
|
+
if (pool) {
|
|
3608
|
+
await pool.end();
|
|
3609
|
+
pool = null;
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
2898
3612
|
|
|
2899
3613
|
// src/commands/dashboard.ts
|
|
2900
3614
|
function getLastActivityDate(squadName) {
|
|
2901
3615
|
const memoryDir = findMemoryDir();
|
|
2902
3616
|
if (!memoryDir) return "unknown";
|
|
2903
|
-
const squadMemory =
|
|
2904
|
-
if (!
|
|
3617
|
+
const squadMemory = join11(memoryDir, squadName);
|
|
3618
|
+
if (!existsSync11(squadMemory)) return "\u2014";
|
|
2905
3619
|
let latestTime = 0;
|
|
2906
3620
|
try {
|
|
2907
|
-
const agents =
|
|
3621
|
+
const agents = readdirSync6(squadMemory, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
2908
3622
|
for (const agent of agents) {
|
|
2909
|
-
const agentPath =
|
|
2910
|
-
const files =
|
|
3623
|
+
const agentPath = join11(squadMemory, agent.name);
|
|
3624
|
+
const files = readdirSync6(agentPath).filter((f) => f.endsWith(".md"));
|
|
2911
3625
|
for (const file of files) {
|
|
2912
|
-
const filePath =
|
|
3626
|
+
const filePath = join11(agentPath, file);
|
|
2913
3627
|
const stats = statSync2(filePath);
|
|
2914
3628
|
if (stats.mtimeMs > latestTime) {
|
|
2915
3629
|
latestTime = stats.mtimeMs;
|
|
@@ -2937,54 +3651,119 @@ async function dashboardCommand(options = {}) {
|
|
|
2937
3651
|
await renderCeoReport(squadsDir);
|
|
2938
3652
|
return;
|
|
2939
3653
|
}
|
|
3654
|
+
const baseDir = findAgentsSquadsDir();
|
|
2940
3655
|
const squadNames = listSquads(squadsDir);
|
|
3656
|
+
const skipGitHub = options.fast !== false;
|
|
3657
|
+
const timeout = (promise, ms, fallback) => Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve(fallback), ms))]);
|
|
3658
|
+
const [gitStats, ghStats, costs, bridgeStats, activity, dbAvailable, history, insights] = await Promise.all([
|
|
3659
|
+
// Git stats (local, ~1s)
|
|
3660
|
+
Promise.resolve(baseDir ? getMultiRepoGitStats(baseDir, 30) : null),
|
|
3661
|
+
// GitHub stats (network, ~20-30s) - skip by default for fast mode
|
|
3662
|
+
skipGitHub ? Promise.resolve(null) : Promise.resolve(baseDir ? getGitHubStatsOptimized(baseDir, 30) : null),
|
|
3663
|
+
// Langfuse costs (network, 2s timeout)
|
|
3664
|
+
timeout(fetchCostSummary(100), 2e3, null),
|
|
3665
|
+
// Bridge stats (local network, 2s timeout)
|
|
3666
|
+
timeout(fetchBridgeStats(), 2e3, null),
|
|
3667
|
+
// Activity sparkline (local, <1s)
|
|
3668
|
+
Promise.resolve(baseDir ? getActivitySparkline(baseDir, 14) : []),
|
|
3669
|
+
// Database availability check (1.5s timeout)
|
|
3670
|
+
timeout(isDatabaseAvailable(), 1500, false),
|
|
3671
|
+
// Dashboard history (1.5s timeout)
|
|
3672
|
+
timeout(getDashboardHistory(14).catch(() => []), 1500, []),
|
|
3673
|
+
// Insights (2s timeout)
|
|
3674
|
+
timeout(fetchInsights("week").catch(() => null), 2e3, null)
|
|
3675
|
+
]);
|
|
3676
|
+
const cache = { gitStats, ghStats, costs, bridgeStats, activity, dbAvailable, history, insights };
|
|
2941
3677
|
const squadData = [];
|
|
2942
|
-
const baseDir = findAgentsSquadsDir();
|
|
2943
|
-
const ghStats = baseDir ? getGitHubStats(baseDir, 30) : null;
|
|
2944
3678
|
for (const name of squadNames) {
|
|
2945
3679
|
const squad = loadSquad(name);
|
|
2946
3680
|
if (!squad) continue;
|
|
2947
3681
|
const lastActivity = getLastActivityDate(name);
|
|
2948
3682
|
const github = ghStats?.bySquad.get(name) || null;
|
|
2949
3683
|
let status = "active";
|
|
2950
|
-
const
|
|
2951
|
-
if (
|
|
3684
|
+
const activeGoals = squad.goals.filter((g) => !g.completed);
|
|
3685
|
+
if (activeGoals.length === 0) {
|
|
2952
3686
|
status = "needs-goal";
|
|
2953
3687
|
} else if (lastActivity.includes("w") || lastActivity === "\u2014") {
|
|
2954
3688
|
status = "stale";
|
|
2955
3689
|
}
|
|
2956
|
-
const
|
|
2957
|
-
const
|
|
3690
|
+
const totalGoals = squad.goals.length;
|
|
3691
|
+
const completedGoals = squad.goals.filter((g) => g.completed).length;
|
|
2958
3692
|
const hasProgress = squad.goals.filter((g) => g.progress).length;
|
|
2959
|
-
const goalProgress =
|
|
3693
|
+
const goalProgress = totalGoals > 0 ? Math.round((completedGoals + hasProgress * 0.3) / totalGoals * 100) : 0;
|
|
3694
|
+
const repoSquadMap = {
|
|
3695
|
+
website: ["agents-squads-web"],
|
|
3696
|
+
product: ["squads-cli"],
|
|
3697
|
+
engineering: ["hq", "squads-cli"],
|
|
3698
|
+
research: ["research"],
|
|
3699
|
+
intelligence: ["intelligence"],
|
|
3700
|
+
customer: ["customer"],
|
|
3701
|
+
finance: ["finance"],
|
|
3702
|
+
company: ["company", "hq"],
|
|
3703
|
+
marketing: ["marketing", "agents-squads-web"],
|
|
3704
|
+
cli: ["squads-cli"]
|
|
3705
|
+
};
|
|
3706
|
+
let squadCommits = 0;
|
|
3707
|
+
if (gitStats) {
|
|
3708
|
+
for (const [repo, commits] of gitStats.commitsByRepo) {
|
|
3709
|
+
if (repoSquadMap[name]?.includes(repo)) {
|
|
3710
|
+
squadCommits += commits;
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
const githubStats = github || {
|
|
3715
|
+
prsOpened: 0,
|
|
3716
|
+
prsMerged: 0,
|
|
3717
|
+
issuesClosed: 0,
|
|
3718
|
+
issuesOpen: 0,
|
|
3719
|
+
commits: 0,
|
|
3720
|
+
recentIssues: [],
|
|
3721
|
+
recentPRs: []
|
|
3722
|
+
};
|
|
3723
|
+
githubStats.commits = squadCommits;
|
|
2960
3724
|
squadData.push({
|
|
2961
3725
|
name,
|
|
2962
3726
|
mission: squad.mission,
|
|
2963
3727
|
goals: squad.goals,
|
|
2964
3728
|
lastActivity,
|
|
2965
3729
|
status,
|
|
2966
|
-
github,
|
|
3730
|
+
github: githubStats,
|
|
2967
3731
|
goalProgress
|
|
2968
3732
|
});
|
|
2969
3733
|
}
|
|
2970
|
-
const totalGoals = squadData.reduce((sum, s) => sum + s.goals.length, 0);
|
|
2971
|
-
const activeGoals = squadData.reduce((sum, s) => sum + s.goals.filter((g) => !g.completed).length, 0);
|
|
2972
|
-
const completedGoals = totalGoals - activeGoals;
|
|
2973
|
-
const completionRate = totalGoals > 0 ? Math.round(completedGoals / totalGoals * 100) : 0;
|
|
2974
3734
|
const activeSquads = squadData.filter((s) => s.status === "active").length;
|
|
2975
3735
|
const totalPRs = ghStats ? ghStats.prsMerged : 0;
|
|
2976
3736
|
const totalIssuesClosed = ghStats ? ghStats.issuesClosed : 0;
|
|
2977
3737
|
const totalIssuesOpen = ghStats ? ghStats.issuesOpen : 0;
|
|
3738
|
+
cleanupStaleSessions();
|
|
3739
|
+
const sessionSummary = getLiveSessionSummary();
|
|
2978
3740
|
writeLine();
|
|
2979
3741
|
writeLine(` ${gradient("squads")} ${colors.dim}dashboard${RESET}`);
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
3742
|
+
const updateInfo = checkForUpdate();
|
|
3743
|
+
if (updateInfo.updateAvailable) {
|
|
3744
|
+
writeLine(` ${colors.cyan}\u2B06${RESET} Update available: ${colors.dim}${updateInfo.currentVersion}${RESET} \u2192 ${colors.green}${updateInfo.latestVersion}${RESET} ${colors.dim}(run \`squads update\`)${RESET}`);
|
|
3745
|
+
}
|
|
3746
|
+
if (sessionSummary.totalSessions > 0) {
|
|
3747
|
+
const sessionText = sessionSummary.totalSessions === 1 ? "session" : "sessions";
|
|
3748
|
+
const squadText = sessionSummary.squadCount === 1 ? "squad" : "squads";
|
|
3749
|
+
let toolInfo = "";
|
|
3750
|
+
if (sessionSummary.byTool && Object.keys(sessionSummary.byTool).length > 0) {
|
|
3751
|
+
const toolParts = Object.entries(sessionSummary.byTool).sort((a, b) => b[1] - a[1]).map(([tool, count]) => `${colors.dim}${tool}${RESET} ${colors.cyan}${count}${RESET}`);
|
|
3752
|
+
toolInfo = ` ${colors.dim}(${RESET}${toolParts.join(` ${colors.dim}\xB7${RESET} `)}${colors.dim})${RESET}`;
|
|
3753
|
+
}
|
|
3754
|
+
writeLine(` ${colors.green}${icons.active}${RESET} ${colors.white}${sessionSummary.totalSessions}${RESET} active ${sessionText} ${colors.dim}across${RESET} ${colors.cyan}${sessionSummary.squadCount}${RESET} ${squadText}${toolInfo}`);
|
|
3755
|
+
}
|
|
3756
|
+
writeLine();
|
|
3757
|
+
const statsParts = [`${colors.cyan}${activeSquads}${RESET}/${squadData.length} squads`];
|
|
3758
|
+
if (ghStats) {
|
|
3759
|
+
statsParts.push(`${colors.green}${totalPRs}${RESET} PRs merged`);
|
|
3760
|
+
statsParts.push(`${colors.purple}${totalIssuesClosed}${RESET} closed`);
|
|
3761
|
+
statsParts.push(`${colors.yellow}${totalIssuesOpen}${RESET} open`);
|
|
3762
|
+
} else {
|
|
3763
|
+
statsParts.push(`${colors.cyan}${gitStats?.totalCommits || 0}${RESET} commits`);
|
|
3764
|
+
statsParts.push(`${colors.dim}use -f for PRs/issues${RESET}`);
|
|
3765
|
+
}
|
|
3766
|
+
writeLine(` ${statsParts.join(` ${colors.dim}\u2502${RESET} `)}`);
|
|
2988
3767
|
writeLine();
|
|
2989
3768
|
const overallProgress = squadData.length > 0 ? Math.round(squadData.reduce((sum, s) => sum + s.goalProgress, 0) / squadData.length) : 0;
|
|
2990
3769
|
writeLine(` ${progressBar(overallProgress, 32)} ${colors.dim}${overallProgress}% goal progress${RESET}`);
|
|
@@ -2992,8 +3771,7 @@ async function dashboardCommand(options = {}) {
|
|
|
2992
3771
|
const w = { name: 13, commits: 7, prs: 4, issues: 6, goals: 6, bar: 10 };
|
|
2993
3772
|
const tableWidth = w.name + w.commits + w.prs + w.issues + w.goals + w.bar + 12;
|
|
2994
3773
|
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
2995
|
-
|
|
2996
|
-
writeLine(header);
|
|
3774
|
+
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}`);
|
|
2997
3775
|
writeLine(` ${colors.purple}${box.teeRight}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.teeLeft}${RESET}`);
|
|
2998
3776
|
const sortedSquads = [...squadData].sort((a, b) => {
|
|
2999
3777
|
const aActivity = (a.github?.commits || 0) + (a.github?.prsMerged || 0) * 5;
|
|
@@ -3011,16 +3789,15 @@ async function dashboardCommand(options = {}) {
|
|
|
3011
3789
|
const commitColor = commits > 10 ? colors.green : commits > 0 ? colors.cyan : colors.dim;
|
|
3012
3790
|
const prColor = prs > 0 ? colors.green : colors.dim;
|
|
3013
3791
|
const issueColor = issuesClosed > 0 ? colors.green : colors.dim;
|
|
3014
|
-
|
|
3015
|
-
const row = ` ${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(issuesDisplay, w.issues)}${RESET}${padEnd(`${activeCount}/${totalCount}`, w.goals)}${progressBar(squad.goalProgress, 8)} ${colors.purple}${box.vertical}${RESET}`;
|
|
3016
|
-
writeLine(row);
|
|
3792
|
+
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(`${activeCount}/${totalCount}`, w.goals)}${progressBar(squad.goalProgress, 8)} ${colors.purple}${box.vertical}${RESET}`);
|
|
3017
3793
|
}
|
|
3018
3794
|
writeLine(` ${colors.purple}${box.bottomLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.bottomRight}${RESET}`);
|
|
3019
3795
|
writeLine();
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3796
|
+
renderGitPerformanceCached(cache);
|
|
3797
|
+
renderTokenEconomicsCached(cache);
|
|
3798
|
+
renderInfrastructureCached(cache);
|
|
3799
|
+
renderHistoricalTrendsCached(cache);
|
|
3800
|
+
renderInsightsCached(cache);
|
|
3024
3801
|
const allActiveGoals = squadData.flatMap(
|
|
3025
3802
|
(s) => s.goals.filter((g) => !g.completed).map((g) => ({ squad: s.name, goal: g }))
|
|
3026
3803
|
);
|
|
@@ -3031,12 +3808,9 @@ async function dashboardCommand(options = {}) {
|
|
|
3031
3808
|
for (const { squad, goal: goal2 } of allActiveGoals.slice(0, maxGoals)) {
|
|
3032
3809
|
const hasProgress = goal2.progress && goal2.progress.length > 0;
|
|
3033
3810
|
const icon = hasProgress ? icons.progress : icons.empty;
|
|
3034
|
-
|
|
3035
|
-
const goalText = truncate(goal2.description, 48);
|
|
3036
|
-
writeLine(` ${icon} ${squadLabel} ${goalText}`);
|
|
3811
|
+
writeLine(` ${icon} ${colors.dim}${squad}${RESET} ${truncate(goal2.description, 48)}`);
|
|
3037
3812
|
if (hasProgress) {
|
|
3038
|
-
|
|
3039
|
-
writeLine(` ${colors.dim}\u2514${RESET} ${colors.green}${progressText}${RESET}`);
|
|
3813
|
+
writeLine(` ${colors.dim}\u2514${RESET} ${colors.green}${truncate(goal2.progress, 52)}${RESET}`);
|
|
3040
3814
|
}
|
|
3041
3815
|
}
|
|
3042
3816
|
if (allActiveGoals.length > maxGoals) {
|
|
@@ -3047,82 +3821,41 @@ async function dashboardCommand(options = {}) {
|
|
|
3047
3821
|
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}<squad>${RESET} ${colors.dim}Execute a squad${RESET}`);
|
|
3048
3822
|
writeLine(` ${colors.dim}$${RESET} squads goal set ${colors.dim}Add a goal${RESET}`);
|
|
3049
3823
|
writeLine();
|
|
3050
|
-
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
const dbAvailable = await isDatabaseAvailable();
|
|
3054
|
-
if (!dbAvailable) return;
|
|
3055
|
-
const gitStats = baseDir ? getMultiRepoGitStats(baseDir, 30) : null;
|
|
3056
|
-
const costs = await fetchCostSummary(100);
|
|
3057
|
-
const squadsData = squadData.map((s) => ({
|
|
3058
|
-
name: s.name,
|
|
3059
|
-
commits: s.github?.commits || 0,
|
|
3060
|
-
prsOpened: s.github?.prsOpened || 0,
|
|
3061
|
-
prsMerged: s.github?.prsMerged || 0,
|
|
3062
|
-
issuesClosed: s.github?.issuesClosed || 0,
|
|
3063
|
-
issuesOpen: s.github?.issuesOpen || 0,
|
|
3064
|
-
goalsActive: s.goals.filter((g) => !g.completed).length,
|
|
3065
|
-
goalsTotal: s.goals.length,
|
|
3066
|
-
progress: s.goalProgress
|
|
3067
|
-
}));
|
|
3068
|
-
const authorsData = gitStats ? Array.from(gitStats.commitsByAuthor.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([name, commits]) => ({ name, commits })) : [];
|
|
3069
|
-
const reposData = gitStats ? Array.from(gitStats.commitsByRepo.entries()).sort((a, b) => b[1] - a[1]).map(([name, commits]) => ({ name, commits })) : [];
|
|
3070
|
-
const totalInputTokens = costs?.bySquad.reduce((sum, s) => sum + s.inputTokens, 0) || 0;
|
|
3071
|
-
const totalOutputTokens = costs?.bySquad.reduce((sum, s) => sum + s.outputTokens, 0) || 0;
|
|
3072
|
-
const overallProgress = squadData.length > 0 ? Math.round(squadData.reduce((sum, s) => sum + s.goalProgress, 0) / squadData.length) : 0;
|
|
3073
|
-
const snapshot = {
|
|
3074
|
-
totalSquads: squadData.length,
|
|
3075
|
-
totalCommits: gitStats?.totalCommits || 0,
|
|
3076
|
-
totalPrsMerged: ghStats?.prsMerged || 0,
|
|
3077
|
-
totalIssuesClosed: ghStats?.issuesClosed || 0,
|
|
3078
|
-
totalIssuesOpen: ghStats?.issuesOpen || 0,
|
|
3079
|
-
goalProgressPct: overallProgress,
|
|
3080
|
-
costUsd: costs?.totalCost || 0,
|
|
3081
|
-
dailyBudgetUsd: costs?.dailyBudget || 50,
|
|
3082
|
-
inputTokens: totalInputTokens,
|
|
3083
|
-
outputTokens: totalOutputTokens,
|
|
3084
|
-
commits30d: gitStats?.totalCommits || 0,
|
|
3085
|
-
avgCommitsPerDay: gitStats?.avgCommitsPerDay || 0,
|
|
3086
|
-
activeDays: gitStats?.activeDays || 0,
|
|
3087
|
-
peakCommits: gitStats?.peakDay?.count || 0,
|
|
3088
|
-
peakDate: gitStats?.peakDay?.date || null,
|
|
3089
|
-
squadsData,
|
|
3090
|
-
authorsData,
|
|
3091
|
-
reposData
|
|
3092
|
-
};
|
|
3093
|
-
await saveDashboardSnapshot(snapshot);
|
|
3824
|
+
saveSnapshotCached(squadData, cache, baseDir).catch(() => {
|
|
3825
|
+
});
|
|
3826
|
+
await closeDatabase();
|
|
3094
3827
|
}
|
|
3095
3828
|
function findAgentsSquadsDir() {
|
|
3096
3829
|
const candidates = [
|
|
3097
|
-
|
|
3098
|
-
|
|
3830
|
+
join11(process.cwd(), ".."),
|
|
3831
|
+
join11(homedir3(), "agents-squads")
|
|
3099
3832
|
];
|
|
3100
3833
|
for (const dir of candidates) {
|
|
3101
|
-
if (
|
|
3834
|
+
if (existsSync11(join11(dir, "hq"))) {
|
|
3102
3835
|
return dir;
|
|
3103
3836
|
}
|
|
3104
3837
|
}
|
|
3105
3838
|
return null;
|
|
3106
3839
|
}
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
if (
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
}
|
|
3114
|
-
|
|
3115
|
-
const activity = getActivitySparkline(baseDir, 14);
|
|
3116
|
-
if (stats.totalCommits === 0) {
|
|
3840
|
+
function formatK(n) {
|
|
3841
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
|
3842
|
+
if (n >= 1e3) return (n / 1e3).toFixed(0) + "k";
|
|
3843
|
+
return String(n);
|
|
3844
|
+
}
|
|
3845
|
+
function renderGitPerformanceCached(cache) {
|
|
3846
|
+
const { gitStats: stats, activity } = cache;
|
|
3847
|
+
if (!stats || stats.totalCommits === 0) {
|
|
3117
3848
|
writeLine(` ${bold}Git Activity${RESET} ${colors.dim}(no commits in 30d)${RESET}`);
|
|
3118
3849
|
writeLine();
|
|
3119
3850
|
return;
|
|
3120
3851
|
}
|
|
3121
3852
|
writeLine(` ${bold}Git Activity${RESET} ${colors.dim}(30d)${RESET}`);
|
|
3122
3853
|
writeLine();
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3854
|
+
if (activity.length > 0) {
|
|
3855
|
+
const spark = sparkline(activity);
|
|
3856
|
+
writeLine(` ${colors.dim}Last 14d:${RESET} ${spark}`);
|
|
3857
|
+
writeLine();
|
|
3858
|
+
}
|
|
3126
3859
|
const metrics = [
|
|
3127
3860
|
`${colors.cyan}${stats.totalCommits}${RESET} commits`,
|
|
3128
3861
|
`${colors.green}${stats.avgCommitsPerDay}${RESET}/day`,
|
|
@@ -3149,8 +3882,8 @@ async function renderGitPerformance() {
|
|
|
3149
3882
|
writeLine();
|
|
3150
3883
|
}
|
|
3151
3884
|
}
|
|
3152
|
-
|
|
3153
|
-
const costs =
|
|
3885
|
+
function renderTokenEconomicsCached(cache) {
|
|
3886
|
+
const costs = cache.costs;
|
|
3154
3887
|
if (!costs) {
|
|
3155
3888
|
writeLine(` ${bold}Token Economics${RESET} ${colors.dim}(no data)${RESET}`);
|
|
3156
3889
|
writeLine(` ${colors.dim}Set LANGFUSE_PUBLIC_KEY & LANGFUSE_SECRET_KEY for cost tracking${RESET}`);
|
|
@@ -3165,81 +3898,13 @@ async function renderTokenEconomics(squadNames) {
|
|
|
3165
3898
|
writeLine(` ${colors.green}$${costs.totalCost.toFixed(2)}${RESET} used ${colors.dim}\u2502${RESET} ${colors.cyan}$${costs.idleBudget.toFixed(2)}${RESET} idle`);
|
|
3166
3899
|
writeLine();
|
|
3167
3900
|
const tier = parseInt(process.env.ANTHROPIC_TIER || "4", 10);
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
const
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
3: { opus: { itpm: 8e5, otpm: 16e4 }, sonnet: { itpm: 8e5, otpm: 16e4 }, haiku: { itpm: 1e6, otpm: 2e5 } },
|
|
3174
|
-
4: { opus: { itpm: 2e6, otpm: 4e5 }, sonnet: { itpm: 2e6, otpm: 4e5 }, haiku: { itpm: 4e6, otpm: 8e5 } }
|
|
3175
|
-
};
|
|
3176
|
-
const modelShortNames = {
|
|
3177
|
-
"claude-opus-4-5-20251101": "opus-4.5",
|
|
3178
|
-
"claude-sonnet-4-20250514": "sonnet-4",
|
|
3179
|
-
"claude-haiku-4-5-20251001": "haiku-4.5",
|
|
3180
|
-
"claude-3-5-sonnet-20241022": "sonnet-3.5",
|
|
3181
|
-
"claude-3-5-haiku-20241022": "haiku-3.5"
|
|
3182
|
-
};
|
|
3183
|
-
const modelToFamily = {
|
|
3184
|
-
"claude-opus-4-5-20251101": "opus",
|
|
3185
|
-
"claude-sonnet-4-20250514": "sonnet",
|
|
3186
|
-
"claude-haiku-4-5-20251001": "haiku",
|
|
3187
|
-
"claude-3-5-sonnet-20241022": "sonnet",
|
|
3188
|
-
"claude-3-5-haiku-20241022": "haiku"
|
|
3189
|
-
};
|
|
3190
|
-
const modelStats = {};
|
|
3191
|
-
for (const squad of costs.bySquad) {
|
|
3192
|
-
for (const [model, count] of Object.entries(squad.models)) {
|
|
3193
|
-
if (!modelStats[model]) {
|
|
3194
|
-
modelStats[model] = { calls: 0, input: 0, output: 0, cached: 0 };
|
|
3195
|
-
}
|
|
3196
|
-
modelStats[model].calls += count;
|
|
3197
|
-
}
|
|
3198
|
-
const totalCalls2 = Object.values(squad.models).reduce((a, b) => a + b, 0);
|
|
3199
|
-
for (const [model, count] of Object.entries(squad.models)) {
|
|
3200
|
-
const ratio = totalCalls2 > 0 ? count / totalCalls2 : 0;
|
|
3201
|
-
modelStats[model].input += Math.round(squad.inputTokens * ratio);
|
|
3202
|
-
modelStats[model].output += Math.round(squad.outputTokens * ratio);
|
|
3203
|
-
}
|
|
3204
|
-
}
|
|
3205
|
-
const totalInput = costs.bySquad.reduce((sum, s) => sum + s.inputTokens, 0);
|
|
3206
|
-
const totalOutput = costs.bySquad.reduce((sum, s) => sum + s.outputTokens, 0);
|
|
3207
|
-
const totalCalls = costs.bySquad.reduce((sum, s) => sum + s.calls, 0);
|
|
3208
|
-
const hourlyRate = costs.totalCost;
|
|
3901
|
+
writeLine(` ${colors.dim}Rate Limits (Tier ${tier})${RESET}`);
|
|
3902
|
+
writeLine();
|
|
3903
|
+
const now = /* @__PURE__ */ new Date();
|
|
3904
|
+
const hoursElapsed = Math.max(now.getHours() + now.getMinutes() / 60, 1);
|
|
3905
|
+
const hourlyRate = costs.totalCost / hoursElapsed;
|
|
3209
3906
|
const dailyProjection = hourlyRate * 24;
|
|
3210
3907
|
const monthlyProjection = dailyProjection * 30;
|
|
3211
|
-
const rateLimits = await fetchRateLimits();
|
|
3212
|
-
const hasRealLimits = rateLimits.source === "proxy" && Object.keys(rateLimits.limits).length > 0;
|
|
3213
|
-
if (hasRealLimits) {
|
|
3214
|
-
writeLine(` ${colors.dim}Rate Limits${RESET} ${colors.green}(live)${RESET}`);
|
|
3215
|
-
for (const [family, limits] of Object.entries(rateLimits.limits)) {
|
|
3216
|
-
const name = family === "opus" ? "opus" : family === "sonnet" ? "sonnet" : family === "haiku" ? "haiku" : family;
|
|
3217
|
-
const reqUsed = limits.requestsLimit - limits.requestsRemaining;
|
|
3218
|
-
const reqPct = limits.requestsLimit > 0 ? reqUsed / limits.requestsLimit * 100 : 0;
|
|
3219
|
-
const reqColor = reqPct > 80 ? colors.red : reqPct > 50 ? colors.yellow : colors.green;
|
|
3220
|
-
const tokUsed = limits.tokensLimit - limits.tokensRemaining;
|
|
3221
|
-
const tokPct = limits.tokensLimit > 0 ? tokUsed / limits.tokensLimit * 100 : 0;
|
|
3222
|
-
const tokColor = tokPct > 80 ? colors.red : tokPct > 50 ? colors.yellow : colors.green;
|
|
3223
|
-
writeLine(` ${colors.cyan}${padEnd(name, 8)}${RESET} ${reqColor}${String(reqUsed).padStart(4)}${RESET}${colors.dim}/${limits.requestsLimit}req${RESET} ${tokColor}${formatK(tokUsed)}${RESET}${colors.dim}/${formatK(limits.tokensLimit)}tok${RESET}`);
|
|
3224
|
-
}
|
|
3225
|
-
} else {
|
|
3226
|
-
writeLine(` ${colors.dim}Rate Limits (Tier ${tier})${RESET}`);
|
|
3227
|
-
const sortedModels = Object.entries(modelStats).sort((a, b) => b[1].calls - a[1].calls);
|
|
3228
|
-
for (const [model, stats] of sortedModels.slice(0, 3)) {
|
|
3229
|
-
const name = modelShortNames[model] || model.split("-").slice(1, 3).join("-");
|
|
3230
|
-
const family = modelToFamily[model] || "sonnet";
|
|
3231
|
-
const limits = tokenLimits[tier]?.[family] || { itpm: 1e6, otpm: 2e5 };
|
|
3232
|
-
const rpmPct = stats.calls / rpmLimit * 100;
|
|
3233
|
-
const rpmColor = rpmPct > 80 ? colors.red : rpmPct > 50 ? colors.yellow : colors.green;
|
|
3234
|
-
writeLine(` ${colors.cyan}${padEnd(name, 11)}${RESET} ${rpmColor}${String(stats.calls).padStart(4)}${RESET}${colors.dim}rpm${RESET} ${colors.dim}${formatK(stats.input)}${RESET}${colors.dim}/${formatK(limits.itpm)}i${RESET} ${colors.dim}${formatK(stats.output)}${RESET}${colors.dim}/${formatK(limits.otpm)}o${RESET}`);
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
writeLine();
|
|
3238
|
-
if (costs.totalCachedTokens > 0 || costs.cacheHitRate > 0) {
|
|
3239
|
-
const cacheColor = costs.cacheHitRate > 50 ? colors.green : costs.cacheHitRate > 20 ? colors.yellow : colors.red;
|
|
3240
|
-
writeLine(` ${colors.dim}Cache:${RESET} ${cacheColor}${costs.cacheHitRate.toFixed(1)}%${RESET} hit rate ${colors.dim}(${formatK(costs.totalCachedTokens)} cached / ${formatK(costs.totalInputTokens + costs.totalCachedTokens)} total)${RESET}`);
|
|
3241
|
-
writeLine();
|
|
3242
|
-
}
|
|
3243
3908
|
writeLine(` ${colors.dim}Projections${RESET}`);
|
|
3244
3909
|
const projColor = dailyProjection > costs.dailyBudget ? colors.red : colors.green;
|
|
3245
3910
|
writeLine(` ${colors.dim}Daily:${RESET} ${projColor}~$${dailyProjection.toFixed(2)}${RESET}${colors.dim}/${costs.dailyBudget}${RESET} ${colors.dim}Monthly:${RESET} ${colors.cyan}~$${monthlyProjection.toFixed(0)}${RESET}`);
|
|
@@ -3251,100 +3916,83 @@ async function renderTokenEconomics(squadNames) {
|
|
|
3251
3916
|
}
|
|
3252
3917
|
writeLine();
|
|
3253
3918
|
}
|
|
3254
|
-
function
|
|
3255
|
-
|
|
3256
|
-
if (
|
|
3257
|
-
|
|
3258
|
-
}
|
|
3259
|
-
|
|
3260
|
-
const dbAvailable = await isDatabaseAvailable();
|
|
3261
|
-
if (!dbAvailable) return;
|
|
3262
|
-
const history = await getDashboardHistory(14);
|
|
3263
|
-
if (history.length < 2) return;
|
|
3264
|
-
writeLine(` ${bold}Usage Trends${RESET} ${colors.dim}(${history.length}d history)${RESET}`);
|
|
3265
|
-
writeLine();
|
|
3266
|
-
const dailyCosts = history.map((h) => h.costUsd).reverse();
|
|
3267
|
-
const costSparkStr = sparkline(dailyCosts);
|
|
3268
|
-
const totalSpend = dailyCosts.reduce((sum, c) => sum + c, 0);
|
|
3269
|
-
const avgDaily = totalSpend / dailyCosts.length;
|
|
3270
|
-
writeLine(` ${colors.dim}Cost:${RESET} ${costSparkStr} ${colors.green}$${totalSpend.toFixed(2)}${RESET} total ${colors.dim}($${avgDaily.toFixed(2)}/day avg)${RESET}`);
|
|
3271
|
-
const inputTokens = history.map((h) => h.inputTokens).reverse();
|
|
3272
|
-
const totalInput = inputTokens.reduce((sum, t) => sum + t, 0);
|
|
3273
|
-
const tokenSparkStr = sparkline(inputTokens);
|
|
3274
|
-
writeLine(` ${colors.dim}Tokens:${RESET} ${tokenSparkStr} ${colors.cyan}${formatK(totalInput)}${RESET} input ${colors.dim}(${formatK(Math.round(totalInput / inputTokens.length))}/day)${RESET}`);
|
|
3275
|
-
const goalProgress = history.map((h) => h.goalProgressPct).reverse();
|
|
3276
|
-
const latestProgress = goalProgress[goalProgress.length - 1] || 0;
|
|
3277
|
-
const earliestProgress = goalProgress[0] || 0;
|
|
3278
|
-
const progressDelta = latestProgress - earliestProgress;
|
|
3279
|
-
const progressColor = progressDelta > 0 ? colors.green : progressDelta < 0 ? colors.red : colors.dim;
|
|
3280
|
-
const progressSign = progressDelta > 0 ? "+" : "";
|
|
3281
|
-
writeLine(` ${colors.dim}Goals:${RESET} ${sparkline(goalProgress)} ${colors.purple}${latestProgress}%${RESET} ${progressColor}${progressSign}${progressDelta.toFixed(0)}%${RESET}${colors.dim} vs start${RESET}`);
|
|
3282
|
-
writeLine();
|
|
3283
|
-
}
|
|
3284
|
-
async function renderInsights() {
|
|
3285
|
-
const insights = await fetchInsights("week");
|
|
3286
|
-
if (insights.source === "none" || insights.taskMetrics.length === 0) {
|
|
3919
|
+
function renderInfrastructureCached(cache) {
|
|
3920
|
+
const stats = cache.bridgeStats;
|
|
3921
|
+
if (!stats) {
|
|
3922
|
+
writeLine(` ${bold}Infrastructure${RESET} ${colors.dim}(bridge offline)${RESET}`);
|
|
3923
|
+
writeLine(` ${colors.dim}Start with: cd docker && docker-compose up -d${RESET}`);
|
|
3924
|
+
writeLine();
|
|
3287
3925
|
return;
|
|
3288
3926
|
}
|
|
3289
|
-
writeLine(` ${bold}
|
|
3927
|
+
writeLine(` ${bold}Infrastructure${RESET} ${colors.dim}(${stats.source})${RESET}`);
|
|
3290
3928
|
writeLine();
|
|
3291
|
-
const
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
{
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
const retryRate = totals.tasks > 0 ? (totals.withRetries / totals.tasks * 100).toFixed(0) : "0";
|
|
3307
|
-
const retryColor = parseInt(retryRate) > 30 ? colors.red : parseInt(retryRate) > 15 ? colors.yellow : colors.green;
|
|
3308
|
-
writeLine(` ${colors.dim}Retries:${RESET} ${retryColor}${totals.retries}${RESET}${colors.dim} total${RESET} ${retryColor}${retryRate}%${RESET}${colors.dim} of tasks needed retry${RESET}`);
|
|
3929
|
+
const pgStatus = stats.health.postgres === "connected" ? `${colors.green}\u25CF${RESET}` : `${colors.red}\u25CF${RESET}`;
|
|
3930
|
+
const redisStatus = stats.health.redis === "connected" ? `${colors.green}\u25CF${RESET}` : stats.health.redis === "disabled" ? `${colors.dim}\u25CB${RESET}` : `${colors.red}\u25CF${RESET}`;
|
|
3931
|
+
const otelWorking = stats.health.postgres === "connected" && stats.today.generations > 0;
|
|
3932
|
+
const otelStatus = otelWorking ? `${colors.green}\u25CF${RESET}` : `${colors.dim}\u25CB${RESET}`;
|
|
3933
|
+
writeLine(` ${pgStatus} postgres ${redisStatus} redis ${otelStatus} otel`);
|
|
3934
|
+
writeLine();
|
|
3935
|
+
if (stats.today.generations > 0 || stats.today.costUsd > 0) {
|
|
3936
|
+
const costColor = stats.budget.usedPct > 80 ? colors.red : stats.budget.usedPct > 50 ? colors.yellow : colors.green;
|
|
3937
|
+
writeLine(` ${colors.dim}Today:${RESET} ${colors.cyan}${stats.today.generations}${RESET}${colors.dim} calls${RESET} ${costColor}$${stats.today.costUsd.toFixed(2)}${RESET}${colors.dim}/$${stats.budget.daily}${RESET} ${colors.dim}${formatK(stats.today.inputTokens)}+${formatK(stats.today.outputTokens)} tokens${RESET}`);
|
|
3938
|
+
if (stats.byModel && stats.byModel.length > 0) {
|
|
3939
|
+
const modelLine = stats.byModel.map((m) => {
|
|
3940
|
+
const shortName = m.model.includes("opus") ? "opus" : m.model.includes("sonnet") ? "sonnet" : m.model.includes("haiku") ? "haiku" : m.model.slice(0, 10);
|
|
3941
|
+
return `${colors.dim}${shortName}${RESET} ${colors.cyan}${m.generations}${RESET}`;
|
|
3942
|
+
}).join(" ");
|
|
3943
|
+
writeLine(` ${colors.dim}Models:${RESET} ${modelLine}`);
|
|
3309
3944
|
}
|
|
3310
3945
|
}
|
|
3311
|
-
|
|
3312
|
-
(
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
}),
|
|
3318
|
-
{ feedback: 0, qualitySum: 0, helpfulSum: 0, fixSum: 0 }
|
|
3319
|
-
);
|
|
3320
|
-
if (qualityTotals.feedback > 0) {
|
|
3321
|
-
const avgQuality = qualityTotals.qualitySum / qualityTotals.feedback;
|
|
3322
|
-
const helpfulPct = qualityTotals.helpfulSum / qualityTotals.feedback * 100;
|
|
3323
|
-
const fixPct = qualityTotals.fixSum / qualityTotals.feedback * 100;
|
|
3324
|
-
const qualityColor = avgQuality >= 4 ? colors.green : avgQuality >= 3 ? colors.yellow : colors.red;
|
|
3325
|
-
const stars = "\u2605".repeat(Math.round(avgQuality)) + "\u2606".repeat(5 - Math.round(avgQuality));
|
|
3326
|
-
writeLine(` ${colors.dim}Quality:${RESET} ${qualityColor}${stars}${RESET} ${colors.dim}(${avgQuality.toFixed(1)}/5)${RESET} ${colors.green}${helpfulPct.toFixed(0)}%${RESET}${colors.dim} helpful${RESET} ${fixPct > 20 ? colors.red : colors.dim}${fixPct.toFixed(0)}% needed fixes${RESET}`);
|
|
3327
|
-
}
|
|
3328
|
-
const contextMetrics = insights.taskMetrics.filter((t) => t.avgContextPct > 0);
|
|
3329
|
-
if (contextMetrics.length > 0) {
|
|
3330
|
-
const avgContext = contextMetrics.reduce((sum, t) => sum + t.avgContextPct, 0) / contextMetrics.length;
|
|
3331
|
-
const maxContext = Math.max(...contextMetrics.map((t) => t.maxContextTokens));
|
|
3332
|
-
const contextColor = avgContext < 40 ? colors.green : avgContext < 70 ? colors.yellow : colors.red;
|
|
3333
|
-
const contextStatus = avgContext < 40 ? "lean" : avgContext < 70 ? "moderate" : "heavy";
|
|
3334
|
-
writeLine(` ${colors.dim}Context:${RESET} ${contextColor}${avgContext.toFixed(0)}%${RESET}${colors.dim} avg utilization (${contextStatus})${RESET} ${colors.dim}peak ${formatK(maxContext)} tokens${RESET}`);
|
|
3335
|
-
}
|
|
3336
|
-
writeLine();
|
|
3337
|
-
if (insights.topTools.length > 0) {
|
|
3338
|
-
const toolLine = insights.topTools.slice(0, 5).map((t) => {
|
|
3339
|
-
const successColor = t.successRate >= 95 ? colors.green : t.successRate >= 80 ? colors.yellow : colors.red;
|
|
3340
|
-
return `${colors.dim}${t.toolName.replace("mcp__", "").slice(0, 12)}${RESET} ${successColor}${t.successRate.toFixed(0)}%${RESET}`;
|
|
3341
|
-
}).join(" ");
|
|
3342
|
-
writeLine(` ${colors.dim}Tools:${RESET} ${toolLine}`);
|
|
3343
|
-
if (insights.toolFailureRate > 5) {
|
|
3344
|
-
writeLine(` ${colors.yellow}\u26A0${RESET} ${colors.yellow}${insights.toolFailureRate.toFixed(1)}% tool failure rate${RESET}`);
|
|
3345
|
-
}
|
|
3346
|
-
writeLine();
|
|
3946
|
+
if (stats.week && stats.week.generations > 0) {
|
|
3947
|
+
const weekModelLine = stats.week.byModel?.map((m) => {
|
|
3948
|
+
const shortName = m.model.includes("opus") ? "opus" : m.model.includes("sonnet") ? "sonnet" : m.model.includes("haiku") ? "haiku" : m.model.slice(0, 10);
|
|
3949
|
+
return `${colors.dim}${shortName}${RESET} ${colors.purple}$${m.costUsd.toFixed(0)}${RESET}`;
|
|
3950
|
+
}).join(" ") || "";
|
|
3951
|
+
writeLine(` ${colors.dim}Week:${RESET} ${colors.cyan}${stats.week.generations}${RESET}${colors.dim} calls${RESET} ${colors.purple}$${stats.week.costUsd.toFixed(2)}${RESET} ${weekModelLine}`);
|
|
3347
3952
|
}
|
|
3953
|
+
writeLine();
|
|
3954
|
+
}
|
|
3955
|
+
async function saveSnapshotCached(squadData, cache, baseDir) {
|
|
3956
|
+
if (!cache.dbAvailable) return;
|
|
3957
|
+
const { gitStats, ghStats, costs } = cache;
|
|
3958
|
+
const squadsData = squadData.map((s) => ({
|
|
3959
|
+
name: s.name,
|
|
3960
|
+
commits: s.github?.commits || 0,
|
|
3961
|
+
prsOpened: s.github?.prsOpened || 0,
|
|
3962
|
+
prsMerged: s.github?.prsMerged || 0,
|
|
3963
|
+
issuesClosed: s.github?.issuesClosed || 0,
|
|
3964
|
+
issuesOpen: s.github?.issuesOpen || 0,
|
|
3965
|
+
goalsActive: s.goals.filter((g) => !g.completed).length,
|
|
3966
|
+
goalsTotal: s.goals.length,
|
|
3967
|
+
progress: s.goalProgress
|
|
3968
|
+
}));
|
|
3969
|
+
const authorsData = gitStats ? Array.from(gitStats.commitsByAuthor.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([name, commits]) => ({ name, commits })) : [];
|
|
3970
|
+
const reposData = gitStats ? Array.from(gitStats.commitsByRepo.entries()).sort((a, b) => b[1] - a[1]).map(([name, commits]) => ({ name, commits })) : [];
|
|
3971
|
+
const totalInputTokens = costs?.bySquad.reduce((sum, s) => sum + s.inputTokens, 0) || 0;
|
|
3972
|
+
const totalOutputTokens = costs?.bySquad.reduce((sum, s) => sum + s.outputTokens, 0) || 0;
|
|
3973
|
+
const overallProgress = squadData.length > 0 ? Math.round(squadData.reduce((sum, s) => sum + s.goalProgress, 0) / squadData.length) : 0;
|
|
3974
|
+
const snapshot = {
|
|
3975
|
+
totalSquads: squadData.length,
|
|
3976
|
+
totalCommits: gitStats?.totalCommits || 0,
|
|
3977
|
+
totalPrsMerged: ghStats?.prsMerged || 0,
|
|
3978
|
+
totalIssuesClosed: ghStats?.issuesClosed || 0,
|
|
3979
|
+
totalIssuesOpen: ghStats?.issuesOpen || 0,
|
|
3980
|
+
goalProgressPct: overallProgress,
|
|
3981
|
+
costUsd: costs?.totalCost || 0,
|
|
3982
|
+
dailyBudgetUsd: costs?.dailyBudget || 50,
|
|
3983
|
+
inputTokens: totalInputTokens,
|
|
3984
|
+
outputTokens: totalOutputTokens,
|
|
3985
|
+
commits30d: gitStats?.totalCommits || 0,
|
|
3986
|
+
avgCommitsPerDay: gitStats?.avgCommitsPerDay || 0,
|
|
3987
|
+
activeDays: gitStats?.activeDays || 0,
|
|
3988
|
+
peakCommits: gitStats?.peakDay?.count || 0,
|
|
3989
|
+
peakDate: gitStats?.peakDay?.date || null,
|
|
3990
|
+
squadsData,
|
|
3991
|
+
authorsData,
|
|
3992
|
+
reposData
|
|
3993
|
+
};
|
|
3994
|
+
const saveTimeout = new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
3995
|
+
await Promise.race([saveDashboardSnapshot(snapshot), saveTimeout]);
|
|
3348
3996
|
}
|
|
3349
3997
|
var P0_KEYWORDS = ["revenue", "first", "launch", "publish", "ship", "critical", "urgent"];
|
|
3350
3998
|
var P1_KEYWORDS = ["track", "establish", "identify", "define", "fix"];
|
|
@@ -3453,9 +4101,62 @@ async function renderCeoReport(squadsDir) {
|
|
|
3453
4101
|
writeLine(` ${colors.dim}$${RESET} squads goal list ${colors.dim}All active goals${RESET}`);
|
|
3454
4102
|
writeLine();
|
|
3455
4103
|
}
|
|
4104
|
+
function renderHistoricalTrendsCached(cache) {
|
|
4105
|
+
if (!cache.dbAvailable) return;
|
|
4106
|
+
const history = cache.history;
|
|
4107
|
+
if (history.length < 2) return;
|
|
4108
|
+
writeLine(` ${bold}Usage Trends${RESET} ${colors.dim}(${history.length}d history)${RESET}`);
|
|
4109
|
+
writeLine();
|
|
4110
|
+
const dailyCosts = history.map((h) => h.costUsd).reverse();
|
|
4111
|
+
const costSparkStr = sparkline(dailyCosts);
|
|
4112
|
+
const totalSpend = dailyCosts.reduce((sum, c) => sum + c, 0);
|
|
4113
|
+
const avgDaily = totalSpend / dailyCosts.length;
|
|
4114
|
+
writeLine(` ${colors.dim}Cost:${RESET} ${costSparkStr} ${colors.green}$${totalSpend.toFixed(2)}${RESET} total ${colors.dim}($${avgDaily.toFixed(2)}/day avg)${RESET}`);
|
|
4115
|
+
const inputTokens = history.map((h) => h.inputTokens).reverse();
|
|
4116
|
+
const totalInput = inputTokens.reduce((sum, t) => sum + t, 0);
|
|
4117
|
+
const tokenSparkStr = sparkline(inputTokens);
|
|
4118
|
+
writeLine(` ${colors.dim}Tokens:${RESET} ${tokenSparkStr} ${colors.cyan}${formatK(totalInput)}${RESET} input ${colors.dim}(${formatK(Math.round(totalInput / inputTokens.length))}/day)${RESET}`);
|
|
4119
|
+
const goalProgress = history.map((h) => h.goalProgressPct).reverse();
|
|
4120
|
+
const latestProgress = goalProgress[goalProgress.length - 1] || 0;
|
|
4121
|
+
const earliestProgress = goalProgress[0] || 0;
|
|
4122
|
+
const progressDelta = latestProgress - earliestProgress;
|
|
4123
|
+
const progressColor = progressDelta > 0 ? colors.green : progressDelta < 0 ? colors.red : colors.dim;
|
|
4124
|
+
const progressSign = progressDelta > 0 ? "+" : "";
|
|
4125
|
+
writeLine(` ${colors.dim}Goals:${RESET} ${sparkline(goalProgress)} ${colors.purple}${latestProgress}%${RESET} ${progressColor}${progressSign}${progressDelta.toFixed(0)}%${RESET}${colors.dim} vs start${RESET}`);
|
|
4126
|
+
writeLine();
|
|
4127
|
+
}
|
|
4128
|
+
function renderInsightsCached(cache) {
|
|
4129
|
+
const insights = cache.insights;
|
|
4130
|
+
if (!insights || insights.source === "none" || insights.taskMetrics.length === 0) {
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
writeLine(` ${bold}Agent Insights${RESET} ${colors.dim}(${insights.days}d)${RESET}`);
|
|
4134
|
+
writeLine();
|
|
4135
|
+
const totals = insights.taskMetrics.reduce(
|
|
4136
|
+
(acc, t) => ({
|
|
4137
|
+
tasks: acc.tasks + t.tasksTotal,
|
|
4138
|
+
completed: acc.completed + t.tasksCompleted,
|
|
4139
|
+
failed: acc.failed + t.tasksFailed,
|
|
4140
|
+
retries: acc.retries + t.totalRetries,
|
|
4141
|
+
withRetries: acc.withRetries + t.tasksWithRetries
|
|
4142
|
+
}),
|
|
4143
|
+
{ tasks: 0, completed: 0, failed: 0, retries: 0, withRetries: 0 }
|
|
4144
|
+
);
|
|
4145
|
+
if (totals.tasks > 0) {
|
|
4146
|
+
const successRate = totals.tasks > 0 ? (totals.completed / totals.tasks * 100).toFixed(0) : "0";
|
|
4147
|
+
const successColor = parseInt(successRate) >= 80 ? colors.green : parseInt(successRate) >= 60 ? colors.yellow : colors.red;
|
|
4148
|
+
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}`);
|
|
4149
|
+
if (totals.retries > 0) {
|
|
4150
|
+
const retryRate = totals.tasks > 0 ? (totals.withRetries / totals.tasks * 100).toFixed(0) : "0";
|
|
4151
|
+
const retryColor = parseInt(retryRate) > 30 ? colors.red : parseInt(retryRate) > 15 ? colors.yellow : colors.green;
|
|
4152
|
+
writeLine(` ${colors.dim}Retries:${RESET} ${retryColor}${totals.retries}${RESET}${colors.dim} total${RESET} ${retryColor}${retryRate}%${RESET}${colors.dim} of tasks needed retry${RESET}`);
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
writeLine();
|
|
4156
|
+
}
|
|
3456
4157
|
|
|
3457
4158
|
// src/commands/issues.ts
|
|
3458
|
-
import { execSync as
|
|
4159
|
+
import { execSync as execSync5 } from "child_process";
|
|
3459
4160
|
var DEFAULT_ORG = "agents-squads";
|
|
3460
4161
|
var DEFAULT_REPOS = ["hq", "agents-squads-web", "squads-cli"];
|
|
3461
4162
|
async function issuesCommand(options = {}) {
|
|
@@ -3465,7 +4166,7 @@ async function issuesCommand(options = {}) {
|
|
|
3465
4166
|
writeLine(` ${gradient("squads")} ${colors.dim}issues${RESET}`);
|
|
3466
4167
|
writeLine();
|
|
3467
4168
|
try {
|
|
3468
|
-
|
|
4169
|
+
execSync5("gh --version", { stdio: "pipe" });
|
|
3469
4170
|
} catch {
|
|
3470
4171
|
writeLine(` ${colors.red}GitHub CLI (gh) not found${RESET}`);
|
|
3471
4172
|
writeLine(` ${colors.dim}Install: brew install gh${RESET}`);
|
|
@@ -3476,7 +4177,7 @@ async function issuesCommand(options = {}) {
|
|
|
3476
4177
|
let totalOpen = 0;
|
|
3477
4178
|
for (const repo of repos) {
|
|
3478
4179
|
try {
|
|
3479
|
-
const result =
|
|
4180
|
+
const result = execSync5(
|
|
3480
4181
|
`gh issue list -R ${org}/${repo} --state open --json number,title,state,labels,createdAt --limit 50`,
|
|
3481
4182
|
{ stdio: "pipe", encoding: "utf-8" }
|
|
3482
4183
|
);
|
|
@@ -3527,7 +4228,7 @@ async function issuesCommand(options = {}) {
|
|
|
3527
4228
|
}
|
|
3528
4229
|
|
|
3529
4230
|
// src/commands/solve-issues.ts
|
|
3530
|
-
import { execSync as
|
|
4231
|
+
import { execSync as execSync6, spawn as spawn2 } from "child_process";
|
|
3531
4232
|
import ora3 from "ora";
|
|
3532
4233
|
var DEFAULT_ORG2 = "agents-squads";
|
|
3533
4234
|
var DEFAULT_REPOS2 = ["hq", "agents-squads-web", "squads-cli", "agents-squads"];
|
|
@@ -3537,7 +4238,7 @@ async function solveIssuesCommand(options = {}) {
|
|
|
3537
4238
|
writeLine(` ${gradient("squads")} ${colors.dim}solve-issues${RESET}`);
|
|
3538
4239
|
writeLine();
|
|
3539
4240
|
try {
|
|
3540
|
-
|
|
4241
|
+
execSync6("gh --version", { stdio: "pipe" });
|
|
3541
4242
|
} catch {
|
|
3542
4243
|
writeLine(` ${colors.red}GitHub CLI (gh) not found${RESET}`);
|
|
3543
4244
|
writeLine(` ${colors.dim}Install: brew install gh${RESET}`);
|
|
@@ -3547,7 +4248,7 @@ async function solveIssuesCommand(options = {}) {
|
|
|
3547
4248
|
if (options.issue) {
|
|
3548
4249
|
const repo = options.repo || "hq";
|
|
3549
4250
|
try {
|
|
3550
|
-
const result =
|
|
4251
|
+
const result = execSync6(
|
|
3551
4252
|
`gh issue view ${options.issue} -R ${DEFAULT_ORG2}/${repo} --json number,title,labels,body`,
|
|
3552
4253
|
{ stdio: "pipe", encoding: "utf-8" }
|
|
3553
4254
|
);
|
|
@@ -3560,7 +4261,7 @@ async function solveIssuesCommand(options = {}) {
|
|
|
3560
4261
|
} else {
|
|
3561
4262
|
for (const repo of repos) {
|
|
3562
4263
|
try {
|
|
3563
|
-
const result =
|
|
4264
|
+
const result = execSync6(
|
|
3564
4265
|
`gh issue list -R ${DEFAULT_ORG2}/${repo} --label "ready-to-fix" --state open --json number,title,labels --limit 20`,
|
|
3565
4266
|
{ stdio: "pipe", encoding: "utf-8" }
|
|
3566
4267
|
);
|
|
@@ -3614,7 +4315,7 @@ function showSolveInstructions(issues) {
|
|
|
3614
4315
|
async function solveWithClaude(issues) {
|
|
3615
4316
|
const spinner = ora3("Starting issue solver...").start();
|
|
3616
4317
|
try {
|
|
3617
|
-
|
|
4318
|
+
execSync6("which claude", { stdio: "pipe" });
|
|
3618
4319
|
} catch {
|
|
3619
4320
|
spinner.fail("Claude CLI not found");
|
|
3620
4321
|
writeLine(` ${colors.dim}Install: npm install -g @anthropic-ai/claude-code${RESET}`);
|
|
@@ -3700,9 +4401,9 @@ function executeClaudePrompt(prompt) {
|
|
|
3700
4401
|
}
|
|
3701
4402
|
|
|
3702
4403
|
// src/commands/open-issues.ts
|
|
3703
|
-
import { execSync as
|
|
3704
|
-
import { readdirSync as
|
|
3705
|
-
import { join as
|
|
4404
|
+
import { execSync as execSync7, spawn as spawn3 } from "child_process";
|
|
4405
|
+
import { readdirSync as readdirSync7 } from "fs";
|
|
4406
|
+
import { join as join12 } from "path";
|
|
3706
4407
|
import ora4 from "ora";
|
|
3707
4408
|
var ISSUE_FINDER_PATTERNS = [
|
|
3708
4409
|
"*-eval.md",
|
|
@@ -3757,10 +4458,10 @@ async function openIssuesCommand(options = {}) {
|
|
|
3757
4458
|
}
|
|
3758
4459
|
function findEvalAgents(squadsDir, filterSquad) {
|
|
3759
4460
|
const agents = [];
|
|
3760
|
-
const squads =
|
|
4461
|
+
const squads = readdirSync7(squadsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).filter((d) => !filterSquad || d.name === filterSquad).map((d) => d.name);
|
|
3761
4462
|
for (const squad of squads) {
|
|
3762
|
-
const squadPath =
|
|
3763
|
-
const files =
|
|
4463
|
+
const squadPath = join12(squadsDir, squad);
|
|
4464
|
+
const files = readdirSync7(squadPath).filter((f) => f.endsWith(".md"));
|
|
3764
4465
|
for (const file of files) {
|
|
3765
4466
|
const isEval = ISSUE_FINDER_PATTERNS.some((pattern) => {
|
|
3766
4467
|
const regex = new RegExp("^" + pattern.replace("*", ".*") + "$");
|
|
@@ -3770,7 +4471,7 @@ function findEvalAgents(squadsDir, filterSquad) {
|
|
|
3770
4471
|
agents.push({
|
|
3771
4472
|
name: file,
|
|
3772
4473
|
squad,
|
|
3773
|
-
path:
|
|
4474
|
+
path: join12(squadPath, file)
|
|
3774
4475
|
});
|
|
3775
4476
|
}
|
|
3776
4477
|
}
|
|
@@ -3796,7 +4497,7 @@ function showRunInstructions(agents) {
|
|
|
3796
4497
|
async function runEvaluators(agents) {
|
|
3797
4498
|
const spinner = ora4("Starting evaluators...").start();
|
|
3798
4499
|
try {
|
|
3799
|
-
|
|
4500
|
+
execSync7("which claude", { stdio: "pipe" });
|
|
3800
4501
|
} catch {
|
|
3801
4502
|
spinner.fail("Claude CLI not found");
|
|
3802
4503
|
writeLine(` ${colors.dim}Install: npm install -g @anthropic-ai/claude-code${RESET}`);
|
|
@@ -3880,9 +4581,9 @@ import open from "open";
|
|
|
3880
4581
|
|
|
3881
4582
|
// src/lib/auth.ts
|
|
3882
4583
|
import { createClient } from "@supabase/supabase-js";
|
|
3883
|
-
import { existsSync as
|
|
3884
|
-
import { join as
|
|
3885
|
-
import { homedir as
|
|
4584
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync9, mkdirSync as mkdirSync8 } from "fs";
|
|
4585
|
+
import { join as join13 } from "path";
|
|
4586
|
+
import { homedir as homedir4 } from "os";
|
|
3886
4587
|
import http from "http";
|
|
3887
4588
|
var PERSONAL_DOMAINS = [
|
|
3888
4589
|
"gmail.com",
|
|
@@ -3910,8 +4611,8 @@ var PERSONAL_DOMAINS = [
|
|
|
3910
4611
|
"tutanota.com",
|
|
3911
4612
|
"hey.com"
|
|
3912
4613
|
];
|
|
3913
|
-
var AUTH_DIR =
|
|
3914
|
-
var AUTH_PATH =
|
|
4614
|
+
var AUTH_DIR = join13(homedir4(), ".squads-cli");
|
|
4615
|
+
var AUTH_PATH = join13(AUTH_DIR, "auth.json");
|
|
3915
4616
|
function isPersonalEmail(email) {
|
|
3916
4617
|
const domain = email.split("@")[1]?.toLowerCase();
|
|
3917
4618
|
return PERSONAL_DOMAINS.includes(domain);
|
|
@@ -3919,23 +4620,23 @@ function isPersonalEmail(email) {
|
|
|
3919
4620
|
function getEmailDomain(email) {
|
|
3920
4621
|
return email.split("@")[1]?.toLowerCase() || "";
|
|
3921
4622
|
}
|
|
3922
|
-
function saveSession(
|
|
3923
|
-
if (!
|
|
3924
|
-
|
|
4623
|
+
function saveSession(session2) {
|
|
4624
|
+
if (!existsSync13(AUTH_DIR)) {
|
|
4625
|
+
mkdirSync8(AUTH_DIR, { recursive: true });
|
|
3925
4626
|
}
|
|
3926
|
-
|
|
4627
|
+
writeFileSync9(AUTH_PATH, JSON.stringify(session2, null, 2));
|
|
3927
4628
|
}
|
|
3928
4629
|
function loadSession() {
|
|
3929
|
-
if (!
|
|
4630
|
+
if (!existsSync13(AUTH_PATH)) return null;
|
|
3930
4631
|
try {
|
|
3931
|
-
return JSON.parse(
|
|
4632
|
+
return JSON.parse(readFileSync10(AUTH_PATH, "utf-8"));
|
|
3932
4633
|
} catch {
|
|
3933
4634
|
return null;
|
|
3934
4635
|
}
|
|
3935
4636
|
}
|
|
3936
4637
|
function clearSession() {
|
|
3937
|
-
if (
|
|
3938
|
-
|
|
4638
|
+
if (existsSync13(AUTH_PATH)) {
|
|
4639
|
+
writeFileSync9(AUTH_PATH, "");
|
|
3939
4640
|
}
|
|
3940
4641
|
}
|
|
3941
4642
|
function startAuthCallbackServer(port = 54321) {
|
|
@@ -4032,7 +4733,7 @@ ${chalk2.dim("Want to stay updated?")}
|
|
|
4032
4733
|
await track("cli.login.personal_email", { domain: getEmailDomain(email) });
|
|
4033
4734
|
return;
|
|
4034
4735
|
}
|
|
4035
|
-
const
|
|
4736
|
+
const session2 = {
|
|
4036
4737
|
email,
|
|
4037
4738
|
domain: getEmailDomain(email),
|
|
4038
4739
|
status: "pending",
|
|
@@ -4040,9 +4741,9 @@ ${chalk2.dim("Want to stay updated?")}
|
|
|
4040
4741
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4041
4742
|
accessToken: token
|
|
4042
4743
|
};
|
|
4043
|
-
saveSession(
|
|
4744
|
+
saveSession(session2);
|
|
4044
4745
|
spinner.succeed(`Logged in as ${chalk2.cyan(email)}`);
|
|
4045
|
-
await track("cli.login.success", { domain:
|
|
4746
|
+
await track("cli.login.success", { domain: session2.domain });
|
|
4046
4747
|
console.log(`
|
|
4047
4748
|
${chalk2.green("\u2713 Thanks for signing up!")}
|
|
4048
4749
|
|
|
@@ -4064,18 +4765,18 @@ ${chalk2.dim("Questions? Email us at")} ${chalk2.cyan("hello@agents-squads.com")
|
|
|
4064
4765
|
}
|
|
4065
4766
|
}
|
|
4066
4767
|
async function logoutCommand() {
|
|
4067
|
-
const
|
|
4068
|
-
if (!
|
|
4768
|
+
const session2 = loadSession();
|
|
4769
|
+
if (!session2) {
|
|
4069
4770
|
console.log(chalk2.yellow("Not logged in."));
|
|
4070
4771
|
return;
|
|
4071
4772
|
}
|
|
4072
4773
|
clearSession();
|
|
4073
|
-
console.log(chalk2.green(`\u2713 Logged out from ${
|
|
4774
|
+
console.log(chalk2.green(`\u2713 Logged out from ${session2.email}`));
|
|
4074
4775
|
await track("cli.logout");
|
|
4075
4776
|
}
|
|
4076
4777
|
async function whoamiCommand() {
|
|
4077
|
-
const
|
|
4078
|
-
if (!
|
|
4778
|
+
const session2 = loadSession();
|
|
4779
|
+
if (!session2) {
|
|
4079
4780
|
console.log(chalk2.yellow("Not logged in."));
|
|
4080
4781
|
console.log(chalk2.dim("Run: squads login"));
|
|
4081
4782
|
return;
|
|
@@ -4083,34 +4784,100 @@ async function whoamiCommand() {
|
|
|
4083
4784
|
console.log(`
|
|
4084
4785
|
${chalk2.bold("Current Session")}
|
|
4085
4786
|
${chalk2.dim("\u2500".repeat(30))}
|
|
4086
|
-
Email: ${chalk2.cyan(
|
|
4087
|
-
Domain: ${
|
|
4088
|
-
Status: ${
|
|
4089
|
-
Since: ${new Date(
|
|
4787
|
+
Email: ${chalk2.cyan(session2.email)}
|
|
4788
|
+
Domain: ${session2.domain}
|
|
4789
|
+
Status: ${session2.status === "active" ? chalk2.green("Active") : chalk2.yellow("Pending")}
|
|
4790
|
+
Since: ${new Date(session2.createdAt).toLocaleDateString()}
|
|
4090
4791
|
`);
|
|
4091
4792
|
}
|
|
4092
4793
|
|
|
4794
|
+
// src/commands/update.ts
|
|
4795
|
+
import { createInterface as createInterface2 } from "readline";
|
|
4796
|
+
async function confirm(message) {
|
|
4797
|
+
const rl = createInterface2({
|
|
4798
|
+
input: process.stdin,
|
|
4799
|
+
output: process.stdout
|
|
4800
|
+
});
|
|
4801
|
+
return new Promise((resolve) => {
|
|
4802
|
+
rl.question(` ${message} [y/N]: `, (answer) => {
|
|
4803
|
+
rl.close();
|
|
4804
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
4805
|
+
});
|
|
4806
|
+
});
|
|
4807
|
+
}
|
|
4808
|
+
async function updateCommand(options = {}) {
|
|
4809
|
+
writeLine();
|
|
4810
|
+
writeLine(` ${gradient("squads")} ${colors.dim}update${RESET}`);
|
|
4811
|
+
writeLine();
|
|
4812
|
+
if (options.check) {
|
|
4813
|
+
writeLine(` ${colors.dim}Checking for updates...${RESET}`);
|
|
4814
|
+
const info2 = refreshVersionCache();
|
|
4815
|
+
writeLine();
|
|
4816
|
+
if (info2.updateAvailable) {
|
|
4817
|
+
writeLine(` ${colors.cyan}\u2B06${RESET} Update available: ${colors.dim}${info2.currentVersion}${RESET} \u2192 ${colors.green}${info2.latestVersion}${RESET}`);
|
|
4818
|
+
writeLine();
|
|
4819
|
+
writeLine(` ${colors.dim}Run \`squads update\` to install${RESET}`);
|
|
4820
|
+
} else {
|
|
4821
|
+
writeLine(` ${colors.green}${icons.success}${RESET} Already on latest version ${colors.cyan}${info2.currentVersion}${RESET}`);
|
|
4822
|
+
}
|
|
4823
|
+
writeLine();
|
|
4824
|
+
return;
|
|
4825
|
+
}
|
|
4826
|
+
writeLine(` ${colors.dim}Checking npm registry...${RESET}`);
|
|
4827
|
+
const info = refreshVersionCache();
|
|
4828
|
+
if (!info.updateAvailable) {
|
|
4829
|
+
writeLine();
|
|
4830
|
+
writeLine(` ${colors.green}${icons.success}${RESET} Already on latest version ${colors.cyan}${info.currentVersion}${RESET}`);
|
|
4831
|
+
writeLine();
|
|
4832
|
+
return;
|
|
4833
|
+
}
|
|
4834
|
+
writeLine();
|
|
4835
|
+
writeLine(` ${colors.cyan}\u2B06${RESET} Update available: ${colors.dim}${info.currentVersion}${RESET} \u2192 ${colors.green}${info.latestVersion}${RESET}`);
|
|
4836
|
+
writeLine();
|
|
4837
|
+
const shouldUpdate = options.yes || await confirm("Update now?");
|
|
4838
|
+
if (!shouldUpdate) {
|
|
4839
|
+
writeLine();
|
|
4840
|
+
writeLine(` ${colors.dim}Update skipped${RESET}`);
|
|
4841
|
+
writeLine();
|
|
4842
|
+
return;
|
|
4843
|
+
}
|
|
4844
|
+
writeLine();
|
|
4845
|
+
writeLine(` ${colors.dim}Installing update...${RESET}`);
|
|
4846
|
+
writeLine();
|
|
4847
|
+
const result = performUpdate();
|
|
4848
|
+
writeLine();
|
|
4849
|
+
if (result.success) {
|
|
4850
|
+
writeLine(` ${colors.green}${icons.success}${RESET} Updated to ${colors.green}${info.latestVersion}${RESET}`);
|
|
4851
|
+
writeLine(` ${colors.dim}Restart your terminal to use the new version${RESET}`);
|
|
4852
|
+
} else {
|
|
4853
|
+
writeLine(` ${colors.red}${icons.error}${RESET} Update failed: ${result.error}`);
|
|
4854
|
+
writeLine(` ${colors.dim}Try manually: npm update -g squads-cli${RESET}`);
|
|
4855
|
+
process.exitCode = 1;
|
|
4856
|
+
}
|
|
4857
|
+
writeLine();
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4093
4860
|
// src/commands/progress.ts
|
|
4094
|
-
import { execSync as
|
|
4095
|
-
import { existsSync as
|
|
4096
|
-
import { join as
|
|
4861
|
+
import { execSync as execSync8 } from "child_process";
|
|
4862
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync10, mkdirSync as mkdirSync9 } from "fs";
|
|
4863
|
+
import { join as join14 } from "path";
|
|
4097
4864
|
function getTasksFilePath() {
|
|
4098
4865
|
const memoryDir = findMemoryDir();
|
|
4099
4866
|
if (!memoryDir) {
|
|
4100
4867
|
const cwd = process.cwd();
|
|
4101
|
-
const agentsDir =
|
|
4102
|
-
if (!
|
|
4103
|
-
|
|
4868
|
+
const agentsDir = join14(cwd, ".agents");
|
|
4869
|
+
if (!existsSync14(agentsDir)) {
|
|
4870
|
+
mkdirSync9(agentsDir, { recursive: true });
|
|
4104
4871
|
}
|
|
4105
|
-
return
|
|
4872
|
+
return join14(agentsDir, "tasks.json");
|
|
4106
4873
|
}
|
|
4107
|
-
return
|
|
4874
|
+
return join14(memoryDir, "..", "tasks.json");
|
|
4108
4875
|
}
|
|
4109
4876
|
function loadTasks() {
|
|
4110
4877
|
const tasksPath = getTasksFilePath();
|
|
4111
|
-
if (
|
|
4878
|
+
if (existsSync14(tasksPath)) {
|
|
4112
4879
|
try {
|
|
4113
|
-
return JSON.parse(
|
|
4880
|
+
return JSON.parse(readFileSync11(tasksPath, "utf-8"));
|
|
4114
4881
|
} catch {
|
|
4115
4882
|
return { tasks: [], lastUpdated: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4116
4883
|
}
|
|
@@ -4120,7 +4887,7 @@ function loadTasks() {
|
|
|
4120
4887
|
function saveTasks(data) {
|
|
4121
4888
|
const tasksPath = getTasksFilePath();
|
|
4122
4889
|
data.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
4123
|
-
|
|
4890
|
+
writeFileSync10(tasksPath, JSON.stringify(data, null, 2));
|
|
4124
4891
|
}
|
|
4125
4892
|
function getRecentActivity() {
|
|
4126
4893
|
const activity = [];
|
|
@@ -4136,7 +4903,7 @@ function getRecentActivity() {
|
|
|
4136
4903
|
marketing: ["marketing", "content", "social"]
|
|
4137
4904
|
};
|
|
4138
4905
|
try {
|
|
4139
|
-
const logOutput =
|
|
4906
|
+
const logOutput = execSync8(
|
|
4140
4907
|
'git log --since="24 hours ago" --format="%h|%aI|%s" 2>/dev/null',
|
|
4141
4908
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
4142
4909
|
).trim();
|
|
@@ -4270,7 +5037,7 @@ function getElapsedTime(startTime) {
|
|
|
4270
5037
|
}
|
|
4271
5038
|
|
|
4272
5039
|
// src/commands/results.ts
|
|
4273
|
-
import { execSync as
|
|
5040
|
+
import { execSync as execSync9 } from "child_process";
|
|
4274
5041
|
function getGitStats(days = 7) {
|
|
4275
5042
|
const stats = /* @__PURE__ */ new Map();
|
|
4276
5043
|
const squadKeywords = {
|
|
@@ -4285,7 +5052,7 @@ function getGitStats(days = 7) {
|
|
|
4285
5052
|
marketing: ["marketing"]
|
|
4286
5053
|
};
|
|
4287
5054
|
try {
|
|
4288
|
-
const logOutput =
|
|
5055
|
+
const logOutput = execSync9(
|
|
4289
5056
|
`git log --since="${days} days ago" --format="%s" --name-only 2>/dev/null`,
|
|
4290
5057
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
4291
5058
|
).trim();
|
|
@@ -4324,7 +5091,7 @@ function getGitHubStats2(days = 7) {
|
|
|
4324
5091
|
const prsMerged = /* @__PURE__ */ new Map();
|
|
4325
5092
|
const issuesClosed = /* @__PURE__ */ new Map();
|
|
4326
5093
|
try {
|
|
4327
|
-
const prsOutput =
|
|
5094
|
+
const prsOutput = execSync9(
|
|
4328
5095
|
`gh pr list --state all --json title,createdAt,mergedAt --limit 50 2>/dev/null`,
|
|
4329
5096
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
4330
5097
|
);
|
|
@@ -4339,7 +5106,7 @@ function getGitHubStats2(days = 7) {
|
|
|
4339
5106
|
prsMerged.set(squad, (prsMerged.get(squad) || 0) + 1);
|
|
4340
5107
|
}
|
|
4341
5108
|
}
|
|
4342
|
-
const issuesOutput =
|
|
5109
|
+
const issuesOutput = execSync9(
|
|
4343
5110
|
`gh issue list --state closed --json title,closedAt --limit 50 2>/dev/null`,
|
|
4344
5111
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
4345
5112
|
);
|
|
@@ -4471,19 +5238,19 @@ async function resultsCommand(options = {}) {
|
|
|
4471
5238
|
}
|
|
4472
5239
|
|
|
4473
5240
|
// src/commands/workers.ts
|
|
4474
|
-
import { execSync as
|
|
4475
|
-
import { existsSync as
|
|
4476
|
-
import { join as
|
|
5241
|
+
import { execSync as execSync10 } from "child_process";
|
|
5242
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
|
|
5243
|
+
import { join as join15 } from "path";
|
|
4477
5244
|
function getTasksFilePath2() {
|
|
4478
5245
|
const memoryDir = findMemoryDir();
|
|
4479
5246
|
if (!memoryDir) return null;
|
|
4480
|
-
return
|
|
5247
|
+
return join15(memoryDir, "..", "tasks.json");
|
|
4481
5248
|
}
|
|
4482
5249
|
function loadActiveTasks() {
|
|
4483
5250
|
const tasksPath = getTasksFilePath2();
|
|
4484
|
-
if (!tasksPath || !
|
|
5251
|
+
if (!tasksPath || !existsSync15(tasksPath)) return [];
|
|
4485
5252
|
try {
|
|
4486
|
-
const data = JSON.parse(
|
|
5253
|
+
const data = JSON.parse(readFileSync12(tasksPath, "utf-8"));
|
|
4487
5254
|
return data.tasks?.filter((t) => t.status === "active") || [];
|
|
4488
5255
|
} catch {
|
|
4489
5256
|
return [];
|
|
@@ -4492,7 +5259,7 @@ function loadActiveTasks() {
|
|
|
4492
5259
|
function getRunningProcesses() {
|
|
4493
5260
|
const processes = [];
|
|
4494
5261
|
try {
|
|
4495
|
-
const psOutput =
|
|
5262
|
+
const psOutput = execSync10(
|
|
4496
5263
|
'ps aux | grep -E "claude|squads|astro|node.*agent" | grep -v grep',
|
|
4497
5264
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
4498
5265
|
).trim();
|
|
@@ -4543,7 +5310,7 @@ async function workersCommand(options = {}) {
|
|
|
4543
5310
|
writeLine();
|
|
4544
5311
|
if (options.kill) {
|
|
4545
5312
|
try {
|
|
4546
|
-
|
|
5313
|
+
execSync10(`kill ${options.kill}`, { stdio: "pipe" });
|
|
4547
5314
|
writeLine(` ${icons.success} Killed process ${colors.cyan}${options.kill}${RESET}`);
|
|
4548
5315
|
writeLine();
|
|
4549
5316
|
return;
|
|
@@ -4629,26 +5396,689 @@ function getElapsedTime2(startTime) {
|
|
|
4629
5396
|
return "<1m";
|
|
4630
5397
|
}
|
|
4631
5398
|
|
|
5399
|
+
// src/commands/sessions.ts
|
|
5400
|
+
async function sessionsCommand(options = {}) {
|
|
5401
|
+
cleanupStaleSessions();
|
|
5402
|
+
const sessions2 = getActiveSessions();
|
|
5403
|
+
const summary = getSessionSummary();
|
|
5404
|
+
if (options.json) {
|
|
5405
|
+
console.log(JSON.stringify({ sessions: sessions2, summary }, null, 2));
|
|
5406
|
+
return;
|
|
5407
|
+
}
|
|
5408
|
+
writeLine();
|
|
5409
|
+
writeLine(` ${gradient("squads")} ${colors.dim}sessions${RESET}`);
|
|
5410
|
+
writeLine();
|
|
5411
|
+
if (sessions2.length === 0) {
|
|
5412
|
+
writeLine(` ${colors.dim}No active sessions${RESET}`);
|
|
5413
|
+
writeLine();
|
|
5414
|
+
writeLine(` ${colors.dim}Sessions are tracked automatically when Claude Code runs.${RESET}`);
|
|
5415
|
+
writeLine(` ${colors.dim}Each session updates its heartbeat via squads CLI commands.${RESET}`);
|
|
5416
|
+
writeLine();
|
|
5417
|
+
return;
|
|
5418
|
+
}
|
|
5419
|
+
const squadText = summary.squadCount === 1 ? "squad" : "squads";
|
|
5420
|
+
const sessionText = summary.totalSessions === 1 ? "session" : "sessions";
|
|
5421
|
+
writeLine(` ${colors.green}${summary.totalSessions}${RESET} active ${sessionText} ${colors.dim}across${RESET} ${colors.cyan}${summary.squadCount}${RESET} ${squadText}`);
|
|
5422
|
+
writeLine();
|
|
5423
|
+
const bySquad = {};
|
|
5424
|
+
for (const session2 of sessions2) {
|
|
5425
|
+
const squad = session2.squad || "unknown";
|
|
5426
|
+
if (!bySquad[squad]) bySquad[squad] = [];
|
|
5427
|
+
bySquad[squad].push(session2);
|
|
5428
|
+
}
|
|
5429
|
+
const w = { squad: 16, sessions: 10, activity: 14 };
|
|
5430
|
+
const tableWidth = w.squad + w.sessions + w.activity + 4;
|
|
5431
|
+
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
5432
|
+
const header = ` ${colors.purple}${box.vertical}${RESET} ${bold}${padEnd("SQUAD", w.squad)}${RESET}${bold}${padEnd("SESSIONS", w.sessions)}${RESET}${bold}LAST ACTIVITY${RESET} ${colors.purple}${box.vertical}${RESET}`;
|
|
5433
|
+
writeLine(header);
|
|
5434
|
+
writeLine(` ${colors.purple}${box.teeRight}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.teeLeft}${RESET}`);
|
|
5435
|
+
for (const [squad, squadSessions] of Object.entries(bySquad).sort()) {
|
|
5436
|
+
let mostRecent = 0;
|
|
5437
|
+
for (const session2 of squadSessions) {
|
|
5438
|
+
const ts = new Date(session2.lastHeartbeat).getTime();
|
|
5439
|
+
if (ts > mostRecent) mostRecent = ts;
|
|
5440
|
+
}
|
|
5441
|
+
const lastActivity = formatTimeAgo(mostRecent);
|
|
5442
|
+
const activityColor = getActivityColor(mostRecent);
|
|
5443
|
+
const row = ` ${colors.purple}${box.vertical}${RESET} ${colors.cyan}${padEnd(squad, w.squad)}${RESET}${padEnd(String(squadSessions.length), w.sessions)}${padEnd(`${activityColor}${lastActivity}${RESET}`, w.activity)}${colors.purple}${box.vertical}${RESET}`;
|
|
5444
|
+
writeLine(row);
|
|
5445
|
+
}
|
|
5446
|
+
writeLine(` ${colors.purple}${box.bottomLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.bottomRight}${RESET}`);
|
|
5447
|
+
if (options.verbose) {
|
|
5448
|
+
writeLine();
|
|
5449
|
+
writeLine(` ${bold}Session Details${RESET}`);
|
|
5450
|
+
writeLine();
|
|
5451
|
+
for (const session2 of sessions2) {
|
|
5452
|
+
const squad = session2.squad || "unknown";
|
|
5453
|
+
const ago = formatTimeAgo(new Date(session2.lastHeartbeat).getTime());
|
|
5454
|
+
writeLine(` ${icons.active} ${colors.white}${session2.sessionId}${RESET}`);
|
|
5455
|
+
writeLine(` ${colors.dim}squad: ${squad} | pid: ${session2.pid} | heartbeat: ${ago}${RESET}`);
|
|
5456
|
+
writeLine(` ${colors.dim}cwd: ${session2.cwd}${RESET}`);
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
writeLine();
|
|
5460
|
+
writeLine(` ${colors.dim}$${RESET} squads sessions -v ${colors.dim}Show session details${RESET}`);
|
|
5461
|
+
writeLine();
|
|
5462
|
+
}
|
|
5463
|
+
function formatTimeAgo(timestamp) {
|
|
5464
|
+
const now = Date.now();
|
|
5465
|
+
const diff = now - timestamp;
|
|
5466
|
+
const seconds = Math.floor(diff / 1e3);
|
|
5467
|
+
const minutes = Math.floor(seconds / 60);
|
|
5468
|
+
if (minutes >= 1) {
|
|
5469
|
+
return `${minutes}m ago`;
|
|
5470
|
+
}
|
|
5471
|
+
return `${seconds}s ago`;
|
|
5472
|
+
}
|
|
5473
|
+
function getActivityColor(timestamp) {
|
|
5474
|
+
const now = Date.now();
|
|
5475
|
+
const diff = now - timestamp;
|
|
5476
|
+
const minutes = Math.floor(diff / (1e3 * 60));
|
|
5477
|
+
if (minutes < 1) return colors.green;
|
|
5478
|
+
if (minutes < 3) return colors.yellow;
|
|
5479
|
+
return colors.dim;
|
|
5480
|
+
}
|
|
5481
|
+
async function sessionsHistoryCommand(options = {}) {
|
|
5482
|
+
const days = options.days || 7;
|
|
5483
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
|
|
5484
|
+
const stats = await getSessionHistoryStats({
|
|
5485
|
+
since,
|
|
5486
|
+
squad: options.squad
|
|
5487
|
+
});
|
|
5488
|
+
const recentSessions = await getRecentSessions(10);
|
|
5489
|
+
if (options.json) {
|
|
5490
|
+
console.log(JSON.stringify({ stats, recentSessions }, null, 2));
|
|
5491
|
+
return;
|
|
5492
|
+
}
|
|
5493
|
+
writeLine();
|
|
5494
|
+
writeLine(` ${gradient("squads")} ${colors.dim}sessions history${RESET} ${colors.dim}(${days}d)${RESET}`);
|
|
5495
|
+
writeLine();
|
|
5496
|
+
if (stats.totalSessions === 0) {
|
|
5497
|
+
writeLine(` ${colors.dim}No session history found${RESET}`);
|
|
5498
|
+
writeLine();
|
|
5499
|
+
writeLine(` ${colors.dim}Session events are logged to .agents/sessions/history.jsonl${RESET}`);
|
|
5500
|
+
writeLine();
|
|
5501
|
+
return;
|
|
5502
|
+
}
|
|
5503
|
+
const avgMinutes = Math.round(stats.avgDurationMs / 6e4);
|
|
5504
|
+
const totalHours = Math.round(stats.totalDurationMs / 36e5 * 10) / 10;
|
|
5505
|
+
writeLine(` ${bold}Summary${RESET}`);
|
|
5506
|
+
writeLine(` ${colors.cyan}${stats.totalSessions}${RESET} sessions ${colors.dim}\u2502${RESET} ${colors.green}${totalHours}h${RESET} total ${colors.dim}\u2502${RESET} ${colors.yellow}${avgMinutes}m${RESET} avg ${colors.dim}\u2502${RESET} ${colors.purple}${stats.peakConcurrent}${RESET} peak`);
|
|
5507
|
+
writeLine();
|
|
5508
|
+
const squads = Object.entries(stats.bySquad).sort((a, b) => b[1].count - a[1].count);
|
|
5509
|
+
if (squads.length > 0) {
|
|
5510
|
+
const w = { squad: 16, sessions: 10, duration: 12 };
|
|
5511
|
+
const tableWidth = w.squad + w.sessions + w.duration + 4;
|
|
5512
|
+
writeLine(` ${bold}By Squad${RESET}`);
|
|
5513
|
+
writeLine();
|
|
5514
|
+
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
5515
|
+
const header = ` ${colors.purple}${box.vertical}${RESET} ${bold}${padEnd("SQUAD", w.squad)}${RESET}${bold}${padEnd("SESSIONS", w.sessions)}${RESET}${bold}DURATION${RESET} ${colors.purple}${box.vertical}${RESET}`;
|
|
5516
|
+
writeLine(header);
|
|
5517
|
+
writeLine(` ${colors.purple}${box.teeRight}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.teeLeft}${RESET}`);
|
|
5518
|
+
for (const [squad, data] of squads) {
|
|
5519
|
+
const hours = Math.round(data.durationMs / 36e5 * 10) / 10;
|
|
5520
|
+
const row = ` ${colors.purple}${box.vertical}${RESET} ${colors.cyan}${padEnd(squad, w.squad)}${RESET}${padEnd(String(data.count), w.sessions)}${padEnd(`${hours}h`, w.duration)}${colors.purple}${box.vertical}${RESET}`;
|
|
5521
|
+
writeLine(row);
|
|
5522
|
+
}
|
|
5523
|
+
writeLine(` ${colors.purple}${box.bottomLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.bottomRight}${RESET}`);
|
|
5524
|
+
}
|
|
5525
|
+
if (recentSessions.length > 0) {
|
|
5526
|
+
writeLine();
|
|
5527
|
+
writeLine(` ${bold}Recent Sessions${RESET}`);
|
|
5528
|
+
writeLine();
|
|
5529
|
+
for (const event of recentSessions.slice(0, 5)) {
|
|
5530
|
+
const squad = event.squad || "unknown";
|
|
5531
|
+
const date = new Date(event.ts);
|
|
5532
|
+
const timeStr = date.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" });
|
|
5533
|
+
const dateStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
5534
|
+
writeLine(` ${colors.dim}${dateStr} ${timeStr}${RESET} ${colors.cyan}${squad}${RESET} ${colors.dim}${event.sessionId.slice(0, 8)}${RESET}`);
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
5537
|
+
const dates = Object.entries(stats.byDate).sort((a, b) => b[0].localeCompare(a[0])).slice(0, 7);
|
|
5538
|
+
if (dates.length > 1) {
|
|
5539
|
+
writeLine();
|
|
5540
|
+
writeLine(` ${bold}Daily Activity${RESET}`);
|
|
5541
|
+
writeLine();
|
|
5542
|
+
for (const [date, count] of dates) {
|
|
5543
|
+
const bar = "\u2588".repeat(Math.min(count, 20));
|
|
5544
|
+
writeLine(` ${colors.dim}${date}${RESET} ${colors.green}${bar}${RESET} ${count}`);
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5547
|
+
writeLine();
|
|
5548
|
+
writeLine(` ${colors.dim}$${RESET} squads sessions history --days 30 ${colors.dim}Longer history${RESET}`);
|
|
5549
|
+
writeLine(` ${colors.dim}$${RESET} squads sessions history -s website ${colors.dim}Filter by squad${RESET}`);
|
|
5550
|
+
writeLine();
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5553
|
+
// src/commands/session.ts
|
|
5554
|
+
async function sessionStartCommand(options = {}) {
|
|
5555
|
+
cleanupStaleSessions();
|
|
5556
|
+
const session2 = startSession(options.squad);
|
|
5557
|
+
if (!options.quiet) {
|
|
5558
|
+
if (session2) {
|
|
5559
|
+
writeLine(`${icons.active} Session started: ${colors.cyan}${session2.sessionId}${RESET}`);
|
|
5560
|
+
if (session2.squad) {
|
|
5561
|
+
writeLine(` ${colors.dim}Squad: ${session2.squad}${RESET}`);
|
|
5562
|
+
}
|
|
5563
|
+
} else {
|
|
5564
|
+
writeLine(`${colors.yellow}Could not start session (no .agents directory)${RESET}`);
|
|
5565
|
+
}
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5568
|
+
async function sessionStopCommand(options = {}) {
|
|
5569
|
+
const stopped = stopSession();
|
|
5570
|
+
if (!options.quiet) {
|
|
5571
|
+
if (stopped) {
|
|
5572
|
+
writeLine(`${icons.progress} Session stopped`);
|
|
5573
|
+
} else {
|
|
5574
|
+
writeLine(`${colors.dim}No active session to stop${RESET}`);
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
async function sessionHeartbeatCommand(options = {}) {
|
|
5579
|
+
const updated = updateHeartbeat();
|
|
5580
|
+
if (!options.quiet) {
|
|
5581
|
+
if (updated) {
|
|
5582
|
+
writeLine(`${icons.active} Heartbeat updated`);
|
|
5583
|
+
} else {
|
|
5584
|
+
writeLine(`${colors.dim}No session to update${RESET}`);
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
}
|
|
5588
|
+
async function detectSquadCommand() {
|
|
5589
|
+
const squad = detectSquad();
|
|
5590
|
+
if (squad) {
|
|
5591
|
+
process.stdout.write(squad);
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
|
|
5595
|
+
// src/commands/stack.ts
|
|
5596
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
|
|
5597
|
+
import { join as join16 } from "path";
|
|
5598
|
+
import { homedir as homedir5 } from "os";
|
|
5599
|
+
import { execSync as execSync11, spawn as spawn4 } from "child_process";
|
|
5600
|
+
var DEFAULT_CONFIG = {
|
|
5601
|
+
SQUADS_DATABASE_URL: "postgresql://squads:squads@localhost:5433/squads",
|
|
5602
|
+
SQUADS_BRIDGE_URL: "http://localhost:8088",
|
|
5603
|
+
LANGFUSE_HOST: "http://localhost:3100",
|
|
5604
|
+
LANGFUSE_PUBLIC_KEY: "",
|
|
5605
|
+
LANGFUSE_SECRET_KEY: "",
|
|
5606
|
+
REDIS_URL: "redis://localhost:6379"
|
|
5607
|
+
};
|
|
5608
|
+
var CONFIG_PATH2 = join16(homedir5(), ".squadsrc");
|
|
5609
|
+
function loadStackConfig() {
|
|
5610
|
+
if (!existsSync16(CONFIG_PATH2)) {
|
|
5611
|
+
return null;
|
|
5612
|
+
}
|
|
5613
|
+
try {
|
|
5614
|
+
const content = readFileSync13(CONFIG_PATH2, "utf-8");
|
|
5615
|
+
const config2 = {};
|
|
5616
|
+
for (const line of content.split("\n")) {
|
|
5617
|
+
const trimmed = line.trim();
|
|
5618
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
5619
|
+
const match = trimmed.match(/^export\s+(\w+)=["']?([^"'\n]*)["']?$/);
|
|
5620
|
+
if (match) {
|
|
5621
|
+
const [, key, value] = match;
|
|
5622
|
+
if (key in DEFAULT_CONFIG) {
|
|
5623
|
+
config2[key] = value;
|
|
5624
|
+
}
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
return config2;
|
|
5628
|
+
} catch {
|
|
5629
|
+
return null;
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
function saveStackConfig(config2) {
|
|
5633
|
+
const lines = [
|
|
5634
|
+
"# Squads CLI Local Stack Configuration",
|
|
5635
|
+
"# Generated by: squads stack init",
|
|
5636
|
+
`# Date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
5637
|
+
"",
|
|
5638
|
+
"# Database",
|
|
5639
|
+
`export SQUADS_DATABASE_URL="${config2.SQUADS_DATABASE_URL}"`,
|
|
5640
|
+
"",
|
|
5641
|
+
"# Bridge API",
|
|
5642
|
+
`export SQUADS_BRIDGE_URL="${config2.SQUADS_BRIDGE_URL}"`,
|
|
5643
|
+
"",
|
|
5644
|
+
"# Langfuse",
|
|
5645
|
+
`export LANGFUSE_HOST="${config2.LANGFUSE_HOST}"`,
|
|
5646
|
+
`export LANGFUSE_PUBLIC_KEY="${config2.LANGFUSE_PUBLIC_KEY}"`,
|
|
5647
|
+
`export LANGFUSE_SECRET_KEY="${config2.LANGFUSE_SECRET_KEY}"`,
|
|
5648
|
+
"",
|
|
5649
|
+
"# Redis",
|
|
5650
|
+
`export REDIS_URL="${config2.REDIS_URL}"`,
|
|
5651
|
+
"",
|
|
5652
|
+
"# To activate: source ~/.squadsrc",
|
|
5653
|
+
""
|
|
5654
|
+
];
|
|
5655
|
+
writeFileSync11(CONFIG_PATH2, lines.join("\n"));
|
|
5656
|
+
}
|
|
5657
|
+
function applyStackConfig() {
|
|
5658
|
+
const config2 = loadStackConfig();
|
|
5659
|
+
if (!config2) return;
|
|
5660
|
+
for (const [key, value] of Object.entries(config2)) {
|
|
5661
|
+
if (value && !process.env[key]) {
|
|
5662
|
+
process.env[key] = value;
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
function isDockerRunning() {
|
|
5667
|
+
try {
|
|
5668
|
+
execSync11("docker info", { stdio: "ignore" });
|
|
5669
|
+
return true;
|
|
5670
|
+
} catch {
|
|
5671
|
+
return false;
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
function getContainerStatus(name) {
|
|
5675
|
+
try {
|
|
5676
|
+
const runningOutput = execSync11(
|
|
5677
|
+
`docker inspect ${name} --format '{{.State.Running}}'`,
|
|
5678
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
5679
|
+
).trim();
|
|
5680
|
+
const running = runningOutput === "true";
|
|
5681
|
+
if (!running) {
|
|
5682
|
+
return { name, running: false, healthy: false };
|
|
5683
|
+
}
|
|
5684
|
+
let port;
|
|
5685
|
+
try {
|
|
5686
|
+
const portOutput = execSync11(
|
|
5687
|
+
`docker inspect ${name} --format '{{range .NetworkSettings.Ports}}{{range .}}{{.HostPort}}{{end}}{{end}}'`,
|
|
5688
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
5689
|
+
).trim();
|
|
5690
|
+
port = portOutput || void 0;
|
|
5691
|
+
} catch {
|
|
5692
|
+
}
|
|
5693
|
+
let healthy = true;
|
|
5694
|
+
try {
|
|
5695
|
+
const healthOutput = execSync11(
|
|
5696
|
+
`docker inspect ${name} --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}'`,
|
|
5697
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
5698
|
+
).trim();
|
|
5699
|
+
if (healthOutput === "healthy" || healthOutput === "none") {
|
|
5700
|
+
healthy = true;
|
|
5701
|
+
} else if (healthOutput === "starting") {
|
|
5702
|
+
healthy = false;
|
|
5703
|
+
} else {
|
|
5704
|
+
healthy = false;
|
|
5705
|
+
}
|
|
5706
|
+
} catch {
|
|
5707
|
+
healthy = true;
|
|
5708
|
+
}
|
|
5709
|
+
return { name, running, healthy, port };
|
|
5710
|
+
} catch {
|
|
5711
|
+
return { name, running: false, healthy: false };
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
async function checkService(url, timeout = 2e3) {
|
|
5715
|
+
try {
|
|
5716
|
+
const controller = new AbortController();
|
|
5717
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
5718
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
5719
|
+
clearTimeout(timeoutId);
|
|
5720
|
+
return response.ok;
|
|
5721
|
+
} catch {
|
|
5722
|
+
return false;
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
function getLangfuseKeysFromDockerEnv() {
|
|
5726
|
+
const envPaths2 = [
|
|
5727
|
+
join16(process.cwd(), "docker", ".env"),
|
|
5728
|
+
join16(process.cwd(), "..", "squads-cli", "docker", ".env"),
|
|
5729
|
+
join16(homedir5(), "agents-squads", "squads-cli", "docker", ".env")
|
|
5730
|
+
];
|
|
5731
|
+
for (const envPath of envPaths2) {
|
|
5732
|
+
if (existsSync16(envPath)) {
|
|
5733
|
+
const content = readFileSync13(envPath, "utf-8");
|
|
5734
|
+
const publicMatch = content.match(/LANGFUSE_PUBLIC_KEY=(\S+)/);
|
|
5735
|
+
const secretMatch = content.match(/LANGFUSE_SECRET_KEY=(\S+)/);
|
|
5736
|
+
if (publicMatch && secretMatch) {
|
|
5737
|
+
return {
|
|
5738
|
+
publicKey: publicMatch[1],
|
|
5739
|
+
secretKey: secretMatch[1]
|
|
5740
|
+
};
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
return null;
|
|
5745
|
+
}
|
|
5746
|
+
function findDockerComposeDir() {
|
|
5747
|
+
const candidates = [
|
|
5748
|
+
join16(process.cwd(), "docker"),
|
|
5749
|
+
join16(process.cwd(), "..", "squads-cli", "docker"),
|
|
5750
|
+
join16(homedir5(), "agents-squads", "squads-cli", "docker")
|
|
5751
|
+
];
|
|
5752
|
+
for (const dir of candidates) {
|
|
5753
|
+
if (existsSync16(join16(dir, "docker-compose.yml"))) {
|
|
5754
|
+
return dir;
|
|
5755
|
+
}
|
|
5756
|
+
}
|
|
5757
|
+
return null;
|
|
5758
|
+
}
|
|
5759
|
+
async function stackInitCommand() {
|
|
5760
|
+
writeLine();
|
|
5761
|
+
writeLine(` ${gradient("squads")} ${colors.dim}stack init${RESET}`);
|
|
5762
|
+
writeLine();
|
|
5763
|
+
if (!isDockerRunning()) {
|
|
5764
|
+
writeLine(` ${colors.red}${icons.error}${RESET} Docker is not running`);
|
|
5765
|
+
writeLine(` ${colors.dim}Start Docker Desktop and try again${RESET}`);
|
|
5766
|
+
writeLine();
|
|
5767
|
+
return;
|
|
5768
|
+
}
|
|
5769
|
+
writeLine(` ${colors.green}${icons.success}${RESET} Docker is running`);
|
|
5770
|
+
const containers = [
|
|
5771
|
+
"squads-postgres",
|
|
5772
|
+
"squads-redis",
|
|
5773
|
+
"squads-bridge",
|
|
5774
|
+
"squads-langfuse",
|
|
5775
|
+
"squads-otel-collector"
|
|
5776
|
+
];
|
|
5777
|
+
const statuses = containers.map(getContainerStatus);
|
|
5778
|
+
const allRunning = statuses.every((s) => s.running);
|
|
5779
|
+
writeLine();
|
|
5780
|
+
writeLine(` ${bold}Containers${RESET}`);
|
|
5781
|
+
for (const status of statuses) {
|
|
5782
|
+
const icon = status.running ? status.healthy ? `${colors.green}\u25CF${RESET}` : `${colors.yellow}\u25CF${RESET}` : `${colors.red}\u25CB${RESET}`;
|
|
5783
|
+
const portInfo = status.port ? `${colors.dim}:${status.port}${RESET}` : "";
|
|
5784
|
+
writeLine(` ${icon} ${status.name}${portInfo}`);
|
|
5785
|
+
}
|
|
5786
|
+
if (!allRunning) {
|
|
5787
|
+
writeLine();
|
|
5788
|
+
writeLine(` ${colors.yellow}${icons.warning}${RESET} Some containers not running`);
|
|
5789
|
+
writeLine(` ${colors.dim}Run: squads stack up${RESET}`);
|
|
5790
|
+
writeLine();
|
|
5791
|
+
return;
|
|
5792
|
+
}
|
|
5793
|
+
const langfuseKeys = getLangfuseKeysFromDockerEnv();
|
|
5794
|
+
const config2 = {
|
|
5795
|
+
...DEFAULT_CONFIG,
|
|
5796
|
+
LANGFUSE_PUBLIC_KEY: langfuseKeys?.publicKey || "",
|
|
5797
|
+
LANGFUSE_SECRET_KEY: langfuseKeys?.secretKey || ""
|
|
5798
|
+
};
|
|
5799
|
+
writeLine();
|
|
5800
|
+
writeLine(` ${bold}Testing connections${RESET}`);
|
|
5801
|
+
const bridgeOk = await checkService(`${config2.SQUADS_BRIDGE_URL}/health`);
|
|
5802
|
+
const langfuseOk = await checkService(`${config2.LANGFUSE_HOST}/api/public/health`);
|
|
5803
|
+
writeLine(
|
|
5804
|
+
` ${bridgeOk ? colors.green + icons.success : colors.red + icons.error}${RESET} Bridge ${colors.dim}${config2.SQUADS_BRIDGE_URL}${RESET}`
|
|
5805
|
+
);
|
|
5806
|
+
writeLine(
|
|
5807
|
+
` ${langfuseOk ? colors.green + icons.success : colors.red + icons.error}${RESET} Langfuse ${colors.dim}${config2.LANGFUSE_HOST}${RESET}`
|
|
5808
|
+
);
|
|
5809
|
+
saveStackConfig(config2);
|
|
5810
|
+
writeLine();
|
|
5811
|
+
writeLine(` ${colors.green}${icons.success}${RESET} Config saved to ${colors.cyan}~/.squadsrc${RESET}`);
|
|
5812
|
+
writeLine();
|
|
5813
|
+
writeLine(` ${colors.dim}To activate in current shell:${RESET}`);
|
|
5814
|
+
writeLine(` ${colors.cyan}source ~/.squadsrc${RESET}`);
|
|
5815
|
+
writeLine();
|
|
5816
|
+
writeLine(` ${colors.dim}Or add to your ~/.zshrc:${RESET}`);
|
|
5817
|
+
writeLine(` ${colors.cyan}[ -f ~/.squadsrc ] && source ~/.squadsrc${RESET}`);
|
|
5818
|
+
writeLine();
|
|
5819
|
+
}
|
|
5820
|
+
async function stackStatusCommand() {
|
|
5821
|
+
writeLine();
|
|
5822
|
+
writeLine(` ${gradient("squads")} ${colors.dim}stack status${RESET}`);
|
|
5823
|
+
writeLine();
|
|
5824
|
+
if (!isDockerRunning()) {
|
|
5825
|
+
writeLine(` ${colors.red}${icons.error}${RESET} Docker is not running`);
|
|
5826
|
+
writeLine();
|
|
5827
|
+
return;
|
|
5828
|
+
}
|
|
5829
|
+
const containers = [
|
|
5830
|
+
{ name: "squads-postgres", service: "Database" },
|
|
5831
|
+
{ name: "squads-redis", service: "Cache" },
|
|
5832
|
+
{ name: "squads-bridge", service: "Bridge API" },
|
|
5833
|
+
{ name: "squads-langfuse", service: "Langfuse" },
|
|
5834
|
+
{ name: "squads-otel-collector", service: "Telemetry" }
|
|
5835
|
+
];
|
|
5836
|
+
const w = { service: 12, container: 22, status: 12 };
|
|
5837
|
+
const tableWidth = w.service + w.container + w.status + 6;
|
|
5838
|
+
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
5839
|
+
writeLine(
|
|
5840
|
+
` ${colors.purple}${box.vertical}${RESET} ${bold}${padEnd("SERVICE", w.service)}${RESET}${bold}${padEnd("CONTAINER", w.container)}${RESET}${bold}STATUS${RESET} ${colors.purple}${box.vertical}${RESET}`
|
|
5841
|
+
);
|
|
5842
|
+
writeLine(` ${colors.purple}${box.teeRight}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.teeLeft}${RESET}`);
|
|
5843
|
+
for (const { name, service } of containers) {
|
|
5844
|
+
const status = getContainerStatus(name);
|
|
5845
|
+
const statusIcon = status.running ? status.healthy ? `${colors.green}\u25CF healthy${RESET}` : `${colors.yellow}\u25CF starting${RESET}` : `${colors.red}\u25CB stopped${RESET}`;
|
|
5846
|
+
writeLine(
|
|
5847
|
+
` ${colors.purple}${box.vertical}${RESET} ${colors.cyan}${padEnd(service, w.service)}${RESET}${colors.dim}${padEnd(name, w.container)}${RESET}${statusIcon} ${colors.purple}${box.vertical}${RESET}`
|
|
5848
|
+
);
|
|
5849
|
+
}
|
|
5850
|
+
writeLine(` ${colors.purple}${box.bottomLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.bottomRight}${RESET}`);
|
|
5851
|
+
writeLine();
|
|
5852
|
+
const config2 = loadStackConfig();
|
|
5853
|
+
if (config2) {
|
|
5854
|
+
writeLine(` ${colors.green}${icons.success}${RESET} Config loaded from ${colors.cyan}~/.squadsrc${RESET}`);
|
|
5855
|
+
const envVars = ["SQUADS_DATABASE_URL", "SQUADS_BRIDGE_URL", "LANGFUSE_HOST"];
|
|
5856
|
+
const missing = envVars.filter((v) => !process.env[v]);
|
|
5857
|
+
if (missing.length > 0) {
|
|
5858
|
+
writeLine(` ${colors.yellow}${icons.warning}${RESET} Env vars not in shell: ${colors.dim}${missing.join(", ")}${RESET}`);
|
|
5859
|
+
writeLine(` ${colors.dim}Run: source ~/.squadsrc${RESET}`);
|
|
5860
|
+
} else {
|
|
5861
|
+
writeLine(` ${colors.green}${icons.success}${RESET} All env vars loaded`);
|
|
5862
|
+
}
|
|
5863
|
+
} else {
|
|
5864
|
+
writeLine(` ${colors.yellow}${icons.warning}${RESET} No config found`);
|
|
5865
|
+
writeLine(` ${colors.dim}Run: squads stack init${RESET}`);
|
|
5866
|
+
}
|
|
5867
|
+
writeLine();
|
|
5868
|
+
}
|
|
5869
|
+
function stackEnvCommand() {
|
|
5870
|
+
const config2 = loadStackConfig();
|
|
5871
|
+
if (!config2) {
|
|
5872
|
+
console.log(`# Squads Local Stack - Default Config`);
|
|
5873
|
+
console.log(`# Run 'squads stack init' to auto-detect and save`);
|
|
5874
|
+
console.log();
|
|
5875
|
+
for (const [key, value] of Object.entries(DEFAULT_CONFIG)) {
|
|
5876
|
+
console.log(`export ${key}="${value}"`);
|
|
5877
|
+
}
|
|
5878
|
+
return;
|
|
5879
|
+
}
|
|
5880
|
+
console.log(`# Squads Local Stack Config`);
|
|
5881
|
+
console.log(`# From: ~/.squadsrc`);
|
|
5882
|
+
console.log();
|
|
5883
|
+
for (const [key, value] of Object.entries(config2)) {
|
|
5884
|
+
console.log(`export ${key}="${value}"`);
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
async function stackUpCommand() {
|
|
5888
|
+
writeLine();
|
|
5889
|
+
writeLine(` ${gradient("squads")} ${colors.dim}stack up${RESET}`);
|
|
5890
|
+
writeLine();
|
|
5891
|
+
const composeDir = findDockerComposeDir();
|
|
5892
|
+
if (!composeDir) {
|
|
5893
|
+
writeLine(` ${colors.red}${icons.error}${RESET} docker-compose.yml not found`);
|
|
5894
|
+
writeLine();
|
|
5895
|
+
writeLine(` ${colors.dim}Expected locations:${RESET}`);
|
|
5896
|
+
writeLine(` ${colors.dim} ./docker/docker-compose.yml${RESET}`);
|
|
5897
|
+
writeLine(` ${colors.dim} ~/agents-squads/squads-cli/docker/docker-compose.yml${RESET}`);
|
|
5898
|
+
writeLine();
|
|
5899
|
+
return;
|
|
5900
|
+
}
|
|
5901
|
+
writeLine(` ${colors.cyan}${icons.progress}${RESET} Starting containers from ${colors.dim}${composeDir}${RESET}`);
|
|
5902
|
+
writeLine();
|
|
5903
|
+
try {
|
|
5904
|
+
const child = spawn4("docker-compose", ["up", "-d"], {
|
|
5905
|
+
cwd: composeDir,
|
|
5906
|
+
stdio: "inherit"
|
|
5907
|
+
});
|
|
5908
|
+
await new Promise((resolve, reject) => {
|
|
5909
|
+
child.on("close", (code) => {
|
|
5910
|
+
if (code === 0) resolve();
|
|
5911
|
+
else reject(new Error(`docker-compose exited with code ${code}`));
|
|
5912
|
+
});
|
|
5913
|
+
child.on("error", reject);
|
|
5914
|
+
});
|
|
5915
|
+
writeLine();
|
|
5916
|
+
writeLine(` ${colors.green}${icons.success}${RESET} Stack started`);
|
|
5917
|
+
writeLine(` ${colors.dim}Run: squads stack init${RESET} to configure CLI`);
|
|
5918
|
+
writeLine();
|
|
5919
|
+
} catch (error) {
|
|
5920
|
+
writeLine(` ${colors.red}${icons.error}${RESET} Failed to start stack`);
|
|
5921
|
+
writeLine(` ${colors.dim}${error}${RESET}`);
|
|
5922
|
+
writeLine();
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
async function stackHealthCommand(verbose = false) {
|
|
5926
|
+
writeLine();
|
|
5927
|
+
writeLine(` ${gradient("squads")} ${colors.dim}stack health${RESET}`);
|
|
5928
|
+
writeLine();
|
|
5929
|
+
if (!isDockerRunning()) {
|
|
5930
|
+
writeLine(` ${colors.red}${icons.error}${RESET} Docker is not running`);
|
|
5931
|
+
writeLine();
|
|
5932
|
+
process.exitCode = 1;
|
|
5933
|
+
return;
|
|
5934
|
+
}
|
|
5935
|
+
const containers = [
|
|
5936
|
+
{ name: "squads-postgres", service: "postgres", healthUrl: "", critical: true },
|
|
5937
|
+
{ name: "squads-redis", service: "redis", healthUrl: "", critical: true },
|
|
5938
|
+
{ name: "squads-neo4j", service: "neo4j", healthUrl: "http://localhost:7474", critical: false },
|
|
5939
|
+
{ name: "squads-bridge", service: "bridge", healthUrl: "http://localhost:8088/health", critical: true },
|
|
5940
|
+
{ name: "squads-otel-collector", service: "otel", healthUrl: "", critical: false },
|
|
5941
|
+
{ name: "squads-langfuse", service: "langfuse", healthUrl: "http://localhost:3100/api/public/health", critical: false },
|
|
5942
|
+
{ name: "squads-mem0", service: "mem0", healthUrl: "http://localhost:8000/health", critical: false },
|
|
5943
|
+
{ name: "squads-engram-mcp", service: "engram", healthUrl: "http://localhost:8080/", critical: false }
|
|
5944
|
+
];
|
|
5945
|
+
let hasFailures = false;
|
|
5946
|
+
const results = [];
|
|
5947
|
+
for (const container of containers) {
|
|
5948
|
+
const status = getContainerStatus(container.name);
|
|
5949
|
+
let ok = status.running && status.healthy;
|
|
5950
|
+
let statusText = "";
|
|
5951
|
+
if (!status.running) {
|
|
5952
|
+
statusText = "stopped";
|
|
5953
|
+
ok = false;
|
|
5954
|
+
} else if (!status.healthy) {
|
|
5955
|
+
statusText = "unhealthy";
|
|
5956
|
+
ok = false;
|
|
5957
|
+
} else if (container.healthUrl) {
|
|
5958
|
+
const httpOk = await checkService(container.healthUrl);
|
|
5959
|
+
if (!httpOk) {
|
|
5960
|
+
statusText = "not responding";
|
|
5961
|
+
ok = false;
|
|
5962
|
+
} else {
|
|
5963
|
+
statusText = "healthy";
|
|
5964
|
+
}
|
|
5965
|
+
} else {
|
|
5966
|
+
statusText = "healthy";
|
|
5967
|
+
}
|
|
5968
|
+
let logs;
|
|
5969
|
+
if (!ok && verbose) {
|
|
5970
|
+
try {
|
|
5971
|
+
logs = execSync11(`docker logs ${container.name} --tail 10 2>&1`, {
|
|
5972
|
+
encoding: "utf-8",
|
|
5973
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5974
|
+
});
|
|
5975
|
+
} catch {
|
|
5976
|
+
logs = "(no logs available)";
|
|
5977
|
+
}
|
|
5978
|
+
}
|
|
5979
|
+
if (!ok && container.critical) {
|
|
5980
|
+
hasFailures = true;
|
|
5981
|
+
}
|
|
5982
|
+
results.push({ name: container.name, service: container.service, ok, status: statusText, logs });
|
|
5983
|
+
}
|
|
5984
|
+
for (const r of results) {
|
|
5985
|
+
const icon = r.ok ? `${colors.green}\u2713${RESET}` : `${colors.red}\u2717${RESET}`;
|
|
5986
|
+
const statusColor = r.ok ? colors.green : colors.red;
|
|
5987
|
+
writeLine(` ${icon} ${padEnd(r.service, 10)} ${statusColor}${r.status}${RESET}`);
|
|
5988
|
+
if (r.logs && !r.ok) {
|
|
5989
|
+
writeLine(` ${colors.dim}${"-".repeat(40)}${RESET}`);
|
|
5990
|
+
for (const line of r.logs.split("\n").slice(0, 5)) {
|
|
5991
|
+
writeLine(` ${colors.dim}${line.substring(0, 70)}${RESET}`);
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
writeLine();
|
|
5996
|
+
const healthy = results.filter((r) => r.ok).length;
|
|
5997
|
+
const total = results.length;
|
|
5998
|
+
if (hasFailures) {
|
|
5999
|
+
writeLine(` ${colors.red}${icons.error}${RESET} ${healthy}/${total} services healthy - critical failures`);
|
|
6000
|
+
writeLine(` ${colors.dim}Run with -v for logs: squads stack health -v${RESET}`);
|
|
6001
|
+
process.exitCode = 1;
|
|
6002
|
+
} else if (healthy < total) {
|
|
6003
|
+
writeLine(` ${colors.yellow}${icons.warning}${RESET} ${healthy}/${total} services healthy - non-critical issues`);
|
|
6004
|
+
} else {
|
|
6005
|
+
writeLine(` ${colors.green}${icons.success}${RESET} ${healthy}/${total} services healthy`);
|
|
6006
|
+
}
|
|
6007
|
+
writeLine();
|
|
6008
|
+
}
|
|
6009
|
+
function stackLogsCommand(service, tail = 50) {
|
|
6010
|
+
const containerMap = {
|
|
6011
|
+
postgres: "squads-postgres",
|
|
6012
|
+
redis: "squads-redis",
|
|
6013
|
+
neo4j: "squads-neo4j",
|
|
6014
|
+
bridge: "squads-bridge",
|
|
6015
|
+
otel: "squads-otel-collector",
|
|
6016
|
+
langfuse: "squads-langfuse",
|
|
6017
|
+
mem0: "squads-mem0",
|
|
6018
|
+
engram: "squads-engram-mcp"
|
|
6019
|
+
};
|
|
6020
|
+
const container = containerMap[service] || `squads-${service}`;
|
|
6021
|
+
try {
|
|
6022
|
+
execSync11(`docker logs ${container} --tail ${tail}`, { stdio: "inherit" });
|
|
6023
|
+
} catch {
|
|
6024
|
+
writeLine(` ${colors.red}${icons.error}${RESET} Container ${container} not found`);
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
6027
|
+
async function stackDownCommand() {
|
|
6028
|
+
writeLine();
|
|
6029
|
+
writeLine(` ${gradient("squads")} ${colors.dim}stack down${RESET}`);
|
|
6030
|
+
writeLine();
|
|
6031
|
+
const composeDir = findDockerComposeDir();
|
|
6032
|
+
if (!composeDir) {
|
|
6033
|
+
writeLine(` ${colors.red}${icons.error}${RESET} docker-compose.yml not found`);
|
|
6034
|
+
writeLine();
|
|
6035
|
+
return;
|
|
6036
|
+
}
|
|
6037
|
+
writeLine(` ${colors.cyan}${icons.progress}${RESET} Stopping containers...`);
|
|
6038
|
+
writeLine();
|
|
6039
|
+
try {
|
|
6040
|
+
const child = spawn4("docker-compose", ["down"], {
|
|
6041
|
+
cwd: composeDir,
|
|
6042
|
+
stdio: "inherit"
|
|
6043
|
+
});
|
|
6044
|
+
await new Promise((resolve, reject) => {
|
|
6045
|
+
child.on("close", (code) => {
|
|
6046
|
+
if (code === 0) resolve();
|
|
6047
|
+
else reject(new Error(`docker-compose exited with code ${code}`));
|
|
6048
|
+
});
|
|
6049
|
+
child.on("error", reject);
|
|
6050
|
+
});
|
|
6051
|
+
writeLine();
|
|
6052
|
+
writeLine(` ${colors.green}${icons.success}${RESET} Stack stopped`);
|
|
6053
|
+
writeLine();
|
|
6054
|
+
} catch (error) {
|
|
6055
|
+
writeLine(` ${colors.red}${icons.error}${RESET} Failed to stop stack`);
|
|
6056
|
+
writeLine(` ${colors.dim}${error}${RESET}`);
|
|
6057
|
+
writeLine();
|
|
6058
|
+
}
|
|
6059
|
+
}
|
|
6060
|
+
|
|
4632
6061
|
// src/cli.ts
|
|
4633
6062
|
var envPaths = [
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
6063
|
+
join17(process.cwd(), ".env"),
|
|
6064
|
+
join17(process.cwd(), "..", "hq", ".env"),
|
|
6065
|
+
join17(homedir6(), "agents-squads", "hq", ".env")
|
|
4637
6066
|
];
|
|
4638
6067
|
for (const envPath of envPaths) {
|
|
4639
|
-
if (
|
|
6068
|
+
if (existsSync17(envPath)) {
|
|
4640
6069
|
config({ path: envPath, quiet: true });
|
|
4641
6070
|
break;
|
|
4642
6071
|
}
|
|
4643
6072
|
}
|
|
6073
|
+
applyStackConfig();
|
|
4644
6074
|
registerExitHandler();
|
|
4645
6075
|
var program = new Command();
|
|
4646
6076
|
program.name("squads").description("A CLI for humans and agents").version(version);
|
|
4647
6077
|
program.command("init").description("Initialize a new squad project").option("-t, --template <template>", "Project template", "default").action(initCommand);
|
|
4648
|
-
program.command("run <target>").description("Run a squad or agent").option("-v, --verbose", "Verbose output").option("-d, --dry-run", "Show what would be run without executing").option("-e, --execute", "Execute agent via Claude CLI (requires claude installed)").option("-a, --agent <agent>", "Run specific agent within squad").action(runCommand);
|
|
6078
|
+
program.command("run <target>").description("Run a squad or agent").option("-v, --verbose", "Verbose output").option("-d, --dry-run", "Show what would be run without executing").option("-e, --execute", "Execute agent via Claude CLI (requires claude installed)").option("-a, --agent <agent>", "Run specific agent within squad").option("-t, --timeout <minutes>", "Execution timeout in minutes (default: 30)", "30").action((target, options) => runCommand(target, { ...options, timeout: parseInt(options.timeout, 10) }));
|
|
4649
6079
|
program.command("list").description("List agents and squads").option("-s, --squads", "List squads only").option("-a, --agents", "List agents only").action(listCommand);
|
|
4650
6080
|
program.command("status [squad]").description("Show squad status and state").option("-v, --verbose", "Show detailed status").action(statusCommand);
|
|
4651
|
-
program.command("dashboard").alias("dash").description("Show comprehensive goals and metrics dashboard").option("-v, --verbose", "Show additional details").option("-c, --ceo", "Executive summary with priorities and blockers").action(dashboardCommand);
|
|
6081
|
+
program.command("dashboard").alias("dash").description("Show comprehensive goals and metrics dashboard").option("-v, --verbose", "Show additional details").option("-c, --ceo", "Executive summary with priorities and blockers").option("-f, --full", "Include GitHub PR/issue stats (slower, ~30s)").action((options) => dashboardCommand({ ...options, fast: !options.full }));
|
|
4652
6082
|
program.command("issues").description("Show GitHub issues across repos").option("-o, --org <org>", "GitHub organization", "agents-squads").option("-r, --repos <repos>", "Comma-separated repo names").action(issuesCommand);
|
|
4653
6083
|
program.command("solve-issues").description("Solve ready-to-fix issues by creating PRs").option("-r, --repo <repo>", "Target repo (hq, agents-squads-web)").option("-i, --issue <number>", "Specific issue number", parseInt).option("-d, --dry-run", "Show what would be solved").option("-e, --execute", "Execute with Claude CLI").action(solveIssuesCommand);
|
|
4654
6084
|
program.command("open-issues").description("Run evaluators/critics to find and create issues").option("-s, --squad <squad>", "Target squad (website, engineering, etc.)").option("-a, --agent <agent>", "Specific evaluator agent").option("-d, --dry-run", "Show what would run").option("-e, --execute", "Execute with Claude CLI").action(openIssuesCommand);
|
|
@@ -4662,7 +6092,7 @@ memory.command("query <query>").description("Search across all squad memory").op
|
|
|
4662
6092
|
memory.command("show <squad>").description("Show memory for a squad").action(memoryShowCommand);
|
|
4663
6093
|
memory.command("update <squad> <content>").description("Add to squad memory").option("-a, --agent <agent>", "Specific agent (default: squad-lead)").option("-t, --type <type>", "Memory type: state, learnings, feedback", "learnings").action(memoryUpdateCommand);
|
|
4664
6094
|
memory.command("list").description("List all memory entries").action(memoryListCommand);
|
|
4665
|
-
memory.command("sync").description("Sync memory from
|
|
6095
|
+
memory.command("sync").description("Sync memory from git: pull remote changes, process commits, optionally push").option("-v, --verbose", "Show detailed commit info").option("-p, --push", "Push local memory changes to remote after sync").option("--no-pull", "Skip pulling from remote").action((options) => syncCommand({ verbose: options.verbose, push: options.push, pull: options.pull }));
|
|
4666
6096
|
memory.command("search <query>").description("Search conversations stored in postgres (via squads-bridge)").option("-l, --limit <limit>", "Number of results", "10").option("-r, --role <role>", "Filter by role: user, assistant, thinking").option("-i, --importance <importance>", "Filter by importance: low, normal, high").action((query, opts) => memorySearchCommand(query, {
|
|
4667
6097
|
limit: parseInt(opts.limit, 10),
|
|
4668
6098
|
role: opts.role,
|
|
@@ -4677,9 +6107,29 @@ var feedback = program.command("feedback").description("Record and view executio
|
|
|
4677
6107
|
feedback.command("add <squad> <rating> <feedback>").description("Add feedback for last execution (rating 1-5)").option("-l, --learning <learnings...>", "Learnings to extract").action(feedbackAddCommand);
|
|
4678
6108
|
feedback.command("show <squad>").description("Show feedback history").option("-n, --limit <n>", "Number of entries to show", "5").action(feedbackShowCommand);
|
|
4679
6109
|
feedback.command("stats").description("Show feedback summary across all squads").action(feedbackStatsCommand);
|
|
6110
|
+
var sessions = program.command("sessions").description("Show active Claude Code sessions across squads").option("-v, --verbose", "Show session details").option("-j, --json", "Output as JSON").action(sessionsCommand);
|
|
6111
|
+
sessions.command("history").description("Show session history and statistics").option("-d, --days <days>", "Days of history to show", "7").option("-s, --squad <squad>", "Filter by squad").option("-j, --json", "Output as JSON").action((options) => sessionsHistoryCommand({
|
|
6112
|
+
days: parseInt(options.days, 10),
|
|
6113
|
+
squad: options.squad,
|
|
6114
|
+
json: options.json
|
|
6115
|
+
}));
|
|
6116
|
+
var session = program.command("session").description("Manage current session lifecycle");
|
|
6117
|
+
session.command("start").description("Register a new session").option("-s, --squad <squad>", "Override squad detection").option("-q, --quiet", "Suppress output").action((options) => sessionStartCommand({ squad: options.squad, quiet: options.quiet }));
|
|
6118
|
+
session.command("stop").description("End current session").option("-q, --quiet", "Suppress output").action((options) => sessionStopCommand({ quiet: options.quiet }));
|
|
6119
|
+
session.command("heartbeat").description("Update session heartbeat").option("-q, --quiet", "Suppress output").action((options) => sessionHeartbeatCommand({ quiet: options.quiet }));
|
|
6120
|
+
program.command("detect-squad").description("Detect current squad based on cwd (for use in hooks)").action(detectSquadCommand);
|
|
6121
|
+
var stack = program.command("stack").description("Manage local Docker stack (postgres, redis, langfuse, bridge)");
|
|
6122
|
+
stack.command("init").description("Auto-detect Docker containers and configure CLI connection").action(stackInitCommand);
|
|
6123
|
+
stack.command("status").description("Show container health and connection status").action(stackStatusCommand);
|
|
6124
|
+
stack.command("env").description("Print environment variables for shell export").action(stackEnvCommand);
|
|
6125
|
+
stack.command("up").description("Start Docker containers via docker-compose").action(stackUpCommand);
|
|
6126
|
+
stack.command("down").description("Stop Docker containers").action(stackDownCommand);
|
|
6127
|
+
stack.command("health").description("Comprehensive health check with diagnostics").option("-v, --verbose", "Show logs for unhealthy services").action((options) => stackHealthCommand(options.verbose));
|
|
6128
|
+
stack.command("logs <service>").description("Show logs for a service (postgres, redis, neo4j, bridge, langfuse, mem0, engram)").option("-n, --tail <lines>", "Number of lines to show", "50").action((service, options) => stackLogsCommand(service, parseInt(options.tail, 10)));
|
|
4680
6129
|
program.command("login").description("Log in to Squads (Pro & Enterprise)").action(loginCommand);
|
|
4681
6130
|
program.command("logout").description("Log out from Squads").action(logoutCommand);
|
|
4682
6131
|
program.command("whoami").description("Show current logged in user").action(whoamiCommand);
|
|
6132
|
+
program.command("update").description("Check for and install updates").option("-y, --yes", "Auto-confirm update without prompting").option("-c, --check", "Check for updates without installing").action((options) => updateCommand(options));
|
|
4683
6133
|
await program.parseAsync();
|
|
4684
6134
|
if (!process.argv.slice(2).length) {
|
|
4685
6135
|
console.log(`
|