squads-cli 0.4.5 → 0.4.7
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 +38 -0
- package/dist/{chunk-G63RBKDH.js → chunk-EGOVJOZJ.js} +2 -1
- package/dist/chunk-EGOVJOZJ.js.map +1 -0
- package/dist/cli.js +1873 -83
- package/dist/cli.js.map +1 -1
- package/dist/{sessions-JCQ34BEU.js → sessions-SEITSWEV.js} +2 -2
- package/docker/init-db.sql +79 -0
- package/docker/squads-bridge/squads_bridge.py +179 -16
- package/package.json +4 -1
- package/dist/chunk-G63RBKDH.js.map +0 -1
- /package/dist/{sessions-JCQ34BEU.js.map → sessions-SEITSWEV.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -28,15 +28,15 @@ import {
|
|
|
28
28
|
truncate,
|
|
29
29
|
updateHeartbeat,
|
|
30
30
|
writeLine
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-EGOVJOZJ.js";
|
|
32
32
|
import {
|
|
33
33
|
__require
|
|
34
34
|
} from "./chunk-7OCVIDC7.js";
|
|
35
35
|
|
|
36
36
|
// src/cli.ts
|
|
37
37
|
import { config } from "dotenv";
|
|
38
|
-
import { existsSync as
|
|
39
|
-
import { join as
|
|
38
|
+
import { existsSync as existsSync19 } from "fs";
|
|
39
|
+
import { join as join21 } from "path";
|
|
40
40
|
import { homedir as homedir5 } from "os";
|
|
41
41
|
import { Command } from "commander";
|
|
42
42
|
import chalk4 from "chalk";
|
|
@@ -286,8 +286,10 @@ function getMultiRepoGitStats(basePath, days = 30) {
|
|
|
286
286
|
activeDays: 0,
|
|
287
287
|
avgCommitsPerDay: 0,
|
|
288
288
|
peakDay: null,
|
|
289
|
-
repos: []
|
|
289
|
+
repos: [],
|
|
290
|
+
recentCommits: []
|
|
290
291
|
};
|
|
292
|
+
const allCommits = [];
|
|
291
293
|
for (const repo of SQUAD_REPOS) {
|
|
292
294
|
const repoPath = join(basePath, repo);
|
|
293
295
|
if (!existsSync(repoPath) || !existsSync(join(repoPath, ".git"))) {
|
|
@@ -303,7 +305,9 @@ function getMultiRepoGitStats(basePath, days = 30) {
|
|
|
303
305
|
const authors = /* @__PURE__ */ new Set();
|
|
304
306
|
let lastCommit = "";
|
|
305
307
|
for (const line of commits) {
|
|
306
|
-
const
|
|
308
|
+
const parts = line.split("|");
|
|
309
|
+
const [hash, author, date, ...messageParts] = parts;
|
|
310
|
+
const message = messageParts.join("|");
|
|
307
311
|
if (!hash) continue;
|
|
308
312
|
stats.totalCommits++;
|
|
309
313
|
authors.add(author);
|
|
@@ -314,6 +318,7 @@ function getMultiRepoGitStats(basePath, days = 30) {
|
|
|
314
318
|
stats.commitsByAuthor.set(author, authorCount + 1);
|
|
315
319
|
const repoCount = stats.commitsByRepo.get(repo) || 0;
|
|
316
320
|
stats.commitsByRepo.set(repo, repoCount + 1);
|
|
321
|
+
allCommits.push({ hash, author, date, message, repo });
|
|
317
322
|
}
|
|
318
323
|
stats.repos.push({
|
|
319
324
|
name: repo,
|
|
@@ -338,6 +343,7 @@ function getMultiRepoGitStats(basePath, days = 30) {
|
|
|
338
343
|
if (peakDate) {
|
|
339
344
|
stats.peakDay = { date: peakDate, count: peakCount };
|
|
340
345
|
}
|
|
346
|
+
stats.recentCommits = allCommits.sort((a, b) => b.date.localeCompare(a.date) || b.hash.localeCompare(a.hash)).slice(0, 5);
|
|
341
347
|
return stats;
|
|
342
348
|
}
|
|
343
349
|
function getActivitySparkline(basePath, days = 7) {
|
|
@@ -487,6 +493,9 @@ var Events = {
|
|
|
487
493
|
CLI_DASHBOARD: "cli.dashboard",
|
|
488
494
|
CLI_WORKERS: "cli.workers",
|
|
489
495
|
CLI_TONIGHT: "cli.tonight",
|
|
496
|
+
CLI_CONTEXT: "cli.context",
|
|
497
|
+
CLI_COST: "cli.cost",
|
|
498
|
+
CLI_EXEC: "cli.exec",
|
|
490
499
|
// Goals
|
|
491
500
|
CLI_GOAL_SET: "cli.goal.set",
|
|
492
501
|
CLI_GOAL_LIST: "cli.goal.list",
|
|
@@ -544,6 +553,29 @@ async function confirm(question, defaultYes = true) {
|
|
|
544
553
|
});
|
|
545
554
|
});
|
|
546
555
|
}
|
|
556
|
+
async function promptEmail() {
|
|
557
|
+
const rl = createInterface({
|
|
558
|
+
input: process.stdin,
|
|
559
|
+
output: process.stdout
|
|
560
|
+
});
|
|
561
|
+
return new Promise((resolve) => {
|
|
562
|
+
rl.question(` ${chalk.dim("\u{1F4EC} Get product updates? (optional, Enter to skip):")} `, (answer) => {
|
|
563
|
+
rl.close();
|
|
564
|
+
const email = answer.trim();
|
|
565
|
+
if (!email) {
|
|
566
|
+
resolve(null);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
570
|
+
if (emailRegex.test(email)) {
|
|
571
|
+
resolve(email);
|
|
572
|
+
} else {
|
|
573
|
+
console.log(chalk.dim(" Invalid email, skipping..."));
|
|
574
|
+
resolve(null);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
}
|
|
547
579
|
function commandExists(cmd) {
|
|
548
580
|
try {
|
|
549
581
|
execSync2(`which ${cmd}`, { stdio: "ignore" });
|
|
@@ -1101,6 +1133,15 @@ squads goal list # View goals
|
|
|
1101
1133
|
console.log(chalk.dim(" \u2022 .claude/settings.json - Claude Code hooks"));
|
|
1102
1134
|
console.log(chalk.dim(" \u2022 CLAUDE.md - Agent instructions"));
|
|
1103
1135
|
console.log();
|
|
1136
|
+
const email = await promptEmail();
|
|
1137
|
+
if (email) {
|
|
1138
|
+
await track(Events.CLI_INIT, {
|
|
1139
|
+
event: "email_capture",
|
|
1140
|
+
email
|
|
1141
|
+
});
|
|
1142
|
+
console.log(chalk.dim(` \u2713 We'll send updates to ${email}`));
|
|
1143
|
+
console.log();
|
|
1144
|
+
}
|
|
1104
1145
|
console.log(chalk.bold(" \u{1F449} Try it now:"));
|
|
1105
1146
|
console.log();
|
|
1106
1147
|
console.log(` ${chalk.yellow.bold("squads status")}`);
|
|
@@ -1121,6 +1162,7 @@ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync
|
|
|
1121
1162
|
// src/lib/squad-parser.ts
|
|
1122
1163
|
import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1123
1164
|
import { join as join3, basename } from "path";
|
|
1165
|
+
import matter from "gray-matter";
|
|
1124
1166
|
function findSquadsDir() {
|
|
1125
1167
|
let dir = process.cwd();
|
|
1126
1168
|
for (let i = 0; i < 5; i++) {
|
|
@@ -1183,17 +1225,24 @@ function listAgents(squadsDir, squadName) {
|
|
|
1183
1225
|
return agents;
|
|
1184
1226
|
}
|
|
1185
1227
|
function parseSquadFile(filePath) {
|
|
1186
|
-
const
|
|
1187
|
-
const
|
|
1228
|
+
const rawContent = readFileSync2(filePath, "utf-8");
|
|
1229
|
+
const { data: frontmatter, content: bodyContent } = matter(rawContent);
|
|
1230
|
+
const fm = frontmatter;
|
|
1231
|
+
const lines = bodyContent.split("\n");
|
|
1188
1232
|
const squad = {
|
|
1189
|
-
name: basename(filePath).replace(".md", ""),
|
|
1190
|
-
mission: "",
|
|
1233
|
+
name: fm.name || basename(filePath).replace(".md", ""),
|
|
1234
|
+
mission: fm.mission || "",
|
|
1191
1235
|
agents: [],
|
|
1192
1236
|
pipelines: [],
|
|
1193
1237
|
triggers: { scheduled: [], event: [], manual: [] },
|
|
1194
1238
|
dependencies: [],
|
|
1195
1239
|
outputPath: "",
|
|
1196
|
-
goals: []
|
|
1240
|
+
goals: [],
|
|
1241
|
+
// Apply frontmatter fields
|
|
1242
|
+
effort: fm.effort,
|
|
1243
|
+
context: fm.context,
|
|
1244
|
+
repo: fm.repo,
|
|
1245
|
+
stack: fm.stack
|
|
1197
1246
|
};
|
|
1198
1247
|
let currentSection = "";
|
|
1199
1248
|
let inTable = false;
|
|
@@ -1213,6 +1262,10 @@ function parseSquadFile(filePath) {
|
|
|
1213
1262
|
squad.mission = line.trim();
|
|
1214
1263
|
}
|
|
1215
1264
|
}
|
|
1265
|
+
const effortMatch = line.match(/^effort:\s*(high|medium|low)/i);
|
|
1266
|
+
if (effortMatch && !squad.effort) {
|
|
1267
|
+
squad.effort = effortMatch[1].toLowerCase();
|
|
1268
|
+
}
|
|
1216
1269
|
if (currentSection.includes("agent") || currentSection.includes("orchestrator") || currentSection.includes("evaluator") || currentSection.includes("builder") || currentSection.includes("priority")) {
|
|
1217
1270
|
if (line.includes("|") && line.includes("Agent")) {
|
|
1218
1271
|
inTable = true;
|
|
@@ -1225,12 +1278,16 @@ function parseSquadFile(filePath) {
|
|
|
1225
1278
|
const roleIdx = tableHeaders.findIndex((h) => h === "role");
|
|
1226
1279
|
const triggerIdx = tableHeaders.findIndex((h) => h === "trigger");
|
|
1227
1280
|
const statusIdx = tableHeaders.findIndex((h) => h === "status");
|
|
1281
|
+
const effortIdx = tableHeaders.findIndex((h) => h === "effort");
|
|
1228
1282
|
if (agentIdx >= 0 && cells[agentIdx]) {
|
|
1283
|
+
const effortValue = effortIdx >= 0 ? cells[effortIdx]?.toLowerCase() : void 0;
|
|
1284
|
+
const effort = ["high", "medium", "low"].includes(effortValue || "") ? effortValue : void 0;
|
|
1229
1285
|
squad.agents.push({
|
|
1230
1286
|
name: cells[agentIdx],
|
|
1231
1287
|
role: roleIdx >= 0 ? cells[roleIdx] : "",
|
|
1232
1288
|
trigger: triggerIdx >= 0 ? cells[triggerIdx] : "manual",
|
|
1233
|
-
status: statusIdx >= 0 ? cells[statusIdx] : "active"
|
|
1289
|
+
status: statusIdx >= 0 ? cells[statusIdx] : "active",
|
|
1290
|
+
effort
|
|
1234
1291
|
});
|
|
1235
1292
|
}
|
|
1236
1293
|
}
|
|
@@ -1377,7 +1434,280 @@ function updateGoalInSquad(squadName, goalIndex, updates) {
|
|
|
1377
1434
|
return false;
|
|
1378
1435
|
}
|
|
1379
1436
|
|
|
1437
|
+
// src/lib/permissions.ts
|
|
1438
|
+
import { minimatch } from "minimatch";
|
|
1439
|
+
function getDefaultContext(squad, agent) {
|
|
1440
|
+
return {
|
|
1441
|
+
squad,
|
|
1442
|
+
agent,
|
|
1443
|
+
permissions: {
|
|
1444
|
+
mode: "warn",
|
|
1445
|
+
bash: ["*"],
|
|
1446
|
+
// All commands allowed by default
|
|
1447
|
+
write: ["**"],
|
|
1448
|
+
// All paths writable by default
|
|
1449
|
+
read: ["**"],
|
|
1450
|
+
// All paths readable by default
|
|
1451
|
+
mcp: {
|
|
1452
|
+
allow: ["*"],
|
|
1453
|
+
// All MCP servers allowed
|
|
1454
|
+
deny: []
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
function validateBashCommand(command, allowedCommands) {
|
|
1460
|
+
const baseCommand = command.trim().split(/\s+/)[0];
|
|
1461
|
+
if (allowedCommands.includes("*")) {
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
const isAllowed = allowedCommands.some((allowed) => {
|
|
1465
|
+
if (allowed === baseCommand) return true;
|
|
1466
|
+
if (allowed.includes("*")) {
|
|
1467
|
+
return minimatch(baseCommand, allowed);
|
|
1468
|
+
}
|
|
1469
|
+
return false;
|
|
1470
|
+
});
|
|
1471
|
+
if (!isAllowed) {
|
|
1472
|
+
return {
|
|
1473
|
+
type: "bash",
|
|
1474
|
+
requested: baseCommand,
|
|
1475
|
+
reason: `Bash command '${baseCommand}' not in allowlist: [${allowedCommands.join(", ")}]`,
|
|
1476
|
+
severity: "error"
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
function validateFilePath(path3, allowedGlobs, operation) {
|
|
1482
|
+
if (allowedGlobs.includes("**") || allowedGlobs.includes("*")) {
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
const isAllowed = allowedGlobs.some((glob) => minimatch(path3, glob));
|
|
1486
|
+
if (!isAllowed) {
|
|
1487
|
+
return {
|
|
1488
|
+
type: operation,
|
|
1489
|
+
requested: path3,
|
|
1490
|
+
reason: `${operation === "write" ? "Write" : "Read"} to '${path3}' not allowed. Permitted paths: [${allowedGlobs.join(", ")}]`,
|
|
1491
|
+
severity: "error"
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
function validateMcpServer(server, allow, deny) {
|
|
1497
|
+
const isDenied = deny.some((pattern) => {
|
|
1498
|
+
if (pattern === server) return true;
|
|
1499
|
+
if (pattern.includes("*")) return minimatch(server, pattern);
|
|
1500
|
+
return false;
|
|
1501
|
+
});
|
|
1502
|
+
if (isDenied) {
|
|
1503
|
+
return {
|
|
1504
|
+
type: "mcp",
|
|
1505
|
+
requested: server,
|
|
1506
|
+
reason: `MCP server '${server}' is explicitly denied`,
|
|
1507
|
+
severity: "error"
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
if (allow.includes("*")) {
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
const isAllowed = allow.some((pattern) => {
|
|
1514
|
+
if (pattern === server) return true;
|
|
1515
|
+
if (pattern.includes("*")) return minimatch(server, pattern);
|
|
1516
|
+
return false;
|
|
1517
|
+
});
|
|
1518
|
+
if (!isAllowed) {
|
|
1519
|
+
return {
|
|
1520
|
+
type: "mcp",
|
|
1521
|
+
requested: server,
|
|
1522
|
+
reason: `MCP server '${server}' not in allowlist: [${allow.join(", ")}]`,
|
|
1523
|
+
severity: "error"
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
function validateExecution(context2, request) {
|
|
1529
|
+
const violations = [];
|
|
1530
|
+
if (request.bashCommands) {
|
|
1531
|
+
for (const cmd of request.bashCommands) {
|
|
1532
|
+
const violation = validateBashCommand(cmd, context2.permissions.bash);
|
|
1533
|
+
if (violation) {
|
|
1534
|
+
violations.push(violation);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
if (request.writePaths) {
|
|
1539
|
+
for (const path3 of request.writePaths) {
|
|
1540
|
+
const violation = validateFilePath(path3, context2.permissions.write, "write");
|
|
1541
|
+
if (violation) {
|
|
1542
|
+
violations.push(violation);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (request.readPaths) {
|
|
1547
|
+
for (const path3 of request.readPaths) {
|
|
1548
|
+
const violation = validateFilePath(path3, context2.permissions.read, "read");
|
|
1549
|
+
if (violation) {
|
|
1550
|
+
violations.push(violation);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (request.mcpServers) {
|
|
1555
|
+
for (const server of request.mcpServers) {
|
|
1556
|
+
const violation = validateMcpServer(
|
|
1557
|
+
server,
|
|
1558
|
+
context2.permissions.mcp.allow,
|
|
1559
|
+
context2.permissions.mcp.deny
|
|
1560
|
+
);
|
|
1561
|
+
if (violation) {
|
|
1562
|
+
violations.push(violation);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
const hasErrors = violations.some((v) => v.severity === "error");
|
|
1567
|
+
const allowed = context2.permissions.mode !== "strict" || !hasErrors;
|
|
1568
|
+
return {
|
|
1569
|
+
allowed,
|
|
1570
|
+
violations,
|
|
1571
|
+
mode: context2.permissions.mode
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
function formatViolations(result) {
|
|
1575
|
+
const lines = [];
|
|
1576
|
+
if (result.violations.length === 0) {
|
|
1577
|
+
return lines;
|
|
1578
|
+
}
|
|
1579
|
+
const modeLabel = {
|
|
1580
|
+
warn: "\u26A0\uFE0F PERMISSION WARNING",
|
|
1581
|
+
strict: "\u{1F6AB} PERMISSION DENIED",
|
|
1582
|
+
audit: "\u{1F4DD} PERMISSION AUDIT"
|
|
1583
|
+
}[result.mode];
|
|
1584
|
+
lines.push(modeLabel);
|
|
1585
|
+
lines.push("");
|
|
1586
|
+
for (const v of result.violations) {
|
|
1587
|
+
const icon = v.severity === "error" ? "\u2717" : "\u26A0";
|
|
1588
|
+
lines.push(` ${icon} [${v.type}] ${v.reason}`);
|
|
1589
|
+
}
|
|
1590
|
+
if (result.mode === "warn") {
|
|
1591
|
+
lines.push("");
|
|
1592
|
+
lines.push(" Continuing with warnings (mode: warn)");
|
|
1593
|
+
} else if (result.mode === "audit") {
|
|
1594
|
+
lines.push("");
|
|
1595
|
+
lines.push(" Logged for audit, continuing (mode: audit)");
|
|
1596
|
+
} else if (!result.allowed) {
|
|
1597
|
+
lines.push("");
|
|
1598
|
+
lines.push(" Execution blocked (mode: strict)");
|
|
1599
|
+
}
|
|
1600
|
+
return lines;
|
|
1601
|
+
}
|
|
1602
|
+
function parsePermissionsYaml(content) {
|
|
1603
|
+
const yamlMatch = content.match(/```ya?ml\n([\s\S]*?)```/);
|
|
1604
|
+
if (!yamlMatch) return null;
|
|
1605
|
+
const yamlContent = yamlMatch[1];
|
|
1606
|
+
const permissions = {};
|
|
1607
|
+
const modeMatch = yamlContent.match(/^\s*mode:\s*(warn|strict|audit)/m);
|
|
1608
|
+
if (modeMatch) {
|
|
1609
|
+
permissions.mode = modeMatch[1];
|
|
1610
|
+
}
|
|
1611
|
+
const bashMatch = yamlContent.match(/^\s*bash:\s*\[(.*?)\]/m);
|
|
1612
|
+
if (bashMatch) {
|
|
1613
|
+
permissions.bash = bashMatch[1].split(",").map((s) => s.trim());
|
|
1614
|
+
}
|
|
1615
|
+
const writeMatch = yamlContent.match(/^\s*write:\s*\[(.*?)\]/m);
|
|
1616
|
+
if (writeMatch) {
|
|
1617
|
+
permissions.write = writeMatch[1].split(",").map((s) => s.trim());
|
|
1618
|
+
}
|
|
1619
|
+
const readMatch = yamlContent.match(/^\s*read:\s*\[(.*?)\]/m);
|
|
1620
|
+
if (readMatch) {
|
|
1621
|
+
permissions.read = readMatch[1].split(",").map((s) => s.trim());
|
|
1622
|
+
}
|
|
1623
|
+
const mcpAllowMatch = yamlContent.match(/^\s*allow:\s*\[(.*?)\]/m);
|
|
1624
|
+
const mcpDenyMatch = yamlContent.match(/^\s*deny:\s*\[(.*?)\]/m);
|
|
1625
|
+
if (mcpAllowMatch || mcpDenyMatch) {
|
|
1626
|
+
permissions.mcp = {
|
|
1627
|
+
allow: mcpAllowMatch ? mcpAllowMatch[1].split(",").map((s) => s.trim()) : ["*"],
|
|
1628
|
+
deny: mcpDenyMatch ? mcpDenyMatch[1].split(",").map((s) => s.trim()) : []
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
return Object.keys(permissions).length > 0 ? permissions : null;
|
|
1632
|
+
}
|
|
1633
|
+
function buildContextFromSquad(squadName, squadContent, agentName) {
|
|
1634
|
+
const context2 = getDefaultContext(squadName, agentName);
|
|
1635
|
+
const parsed = parsePermissionsYaml(squadContent);
|
|
1636
|
+
if (parsed) {
|
|
1637
|
+
if (parsed.mode) context2.permissions.mode = parsed.mode;
|
|
1638
|
+
if (parsed.bash) context2.permissions.bash = parsed.bash;
|
|
1639
|
+
if (parsed.write) context2.permissions.write = parsed.write;
|
|
1640
|
+
if (parsed.read) context2.permissions.read = parsed.read;
|
|
1641
|
+
if (parsed.mcp) context2.permissions.mcp = parsed.mcp;
|
|
1642
|
+
}
|
|
1643
|
+
return context2;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1380
1646
|
// src/commands/run.ts
|
|
1647
|
+
async function registerContextWithBridge(ctx) {
|
|
1648
|
+
const bridgeUrl = process.env.SQUADS_BRIDGE_URL || "http://localhost:8088";
|
|
1649
|
+
try {
|
|
1650
|
+
const response = await fetch(`${bridgeUrl}/api/context/register`, {
|
|
1651
|
+
method: "POST",
|
|
1652
|
+
headers: { "Content-Type": "application/json" },
|
|
1653
|
+
body: JSON.stringify({
|
|
1654
|
+
execution_id: ctx.executionId,
|
|
1655
|
+
squad: ctx.squad,
|
|
1656
|
+
agent: ctx.agent,
|
|
1657
|
+
task_type: ctx.taskType,
|
|
1658
|
+
trigger: ctx.trigger
|
|
1659
|
+
})
|
|
1660
|
+
});
|
|
1661
|
+
if (!response.ok) {
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
return true;
|
|
1665
|
+
} catch {
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
function generateExecutionId() {
|
|
1670
|
+
const timestamp = Date.now().toString(36);
|
|
1671
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
1672
|
+
return `exec_${timestamp}_${random}`;
|
|
1673
|
+
}
|
|
1674
|
+
function selectMcpConfig(squadName) {
|
|
1675
|
+
const home = process.env.HOME || "";
|
|
1676
|
+
const configsDir = join4(home, ".claude", "mcp-configs");
|
|
1677
|
+
const squadConfigs = {
|
|
1678
|
+
website: "website.json",
|
|
1679
|
+
// chrome-devtools, nano-banana
|
|
1680
|
+
research: "research.json",
|
|
1681
|
+
// x-mcp
|
|
1682
|
+
intelligence: "research.json",
|
|
1683
|
+
// x-mcp
|
|
1684
|
+
analytics: "data.json",
|
|
1685
|
+
// supabase, grafana, ga4-admin
|
|
1686
|
+
engineering: "data.json"
|
|
1687
|
+
// supabase, grafana
|
|
1688
|
+
};
|
|
1689
|
+
const configFile = squadConfigs[squadName.toLowerCase()];
|
|
1690
|
+
if (configFile) {
|
|
1691
|
+
const configPath = join4(configsDir, configFile);
|
|
1692
|
+
if (existsSync4(configPath)) {
|
|
1693
|
+
return configPath;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return join4(home, ".claude.json");
|
|
1697
|
+
}
|
|
1698
|
+
function detectTaskType(agentName) {
|
|
1699
|
+
const name = agentName.toLowerCase();
|
|
1700
|
+
if (name.includes("eval") || name.includes("critic") || name.includes("review") || name.includes("test")) {
|
|
1701
|
+
return "evaluation";
|
|
1702
|
+
}
|
|
1703
|
+
if (name.includes("lead") || name.includes("orchestrator")) {
|
|
1704
|
+
return "lead";
|
|
1705
|
+
}
|
|
1706
|
+
if (name.includes("research") || name.includes("analyst") || name.includes("intel")) {
|
|
1707
|
+
return "research";
|
|
1708
|
+
}
|
|
1709
|
+
return "execution";
|
|
1710
|
+
}
|
|
1381
1711
|
function ensureProjectTrusted(projectPath) {
|
|
1382
1712
|
const configPath = join4(process.env.HOME || "", ".claude.json");
|
|
1383
1713
|
if (!existsSync4(configPath)) {
|
|
@@ -1419,34 +1749,75 @@ function logExecution(record) {
|
|
|
1419
1749
|
}
|
|
1420
1750
|
let content = "";
|
|
1421
1751
|
if (existsSync4(logPath)) {
|
|
1422
|
-
content = readFileSync3(logPath, "utf-8");
|
|
1752
|
+
content = readFileSync3(logPath, "utf-8").trimEnd();
|
|
1423
1753
|
} else {
|
|
1424
|
-
content = `# ${record.squadName}/${record.agentName} - Execution Log
|
|
1425
|
-
|
|
1426
|
-
`;
|
|
1754
|
+
content = `# ${record.squadName}/${record.agentName} - Execution Log`;
|
|
1427
1755
|
}
|
|
1428
1756
|
const entry = `
|
|
1757
|
+
|
|
1429
1758
|
---
|
|
1759
|
+
<!-- exec:${record.executionId} -->
|
|
1430
1760
|
**${record.startTime}** | Status: ${record.status}
|
|
1431
|
-
|
|
1432
|
-
|
|
1761
|
+
- ID: \`${record.executionId}\`
|
|
1762
|
+
- Trigger: ${record.trigger || "manual"}
|
|
1763
|
+
- Task Type: ${record.taskType || "execution"}
|
|
1433
1764
|
`;
|
|
1434
1765
|
writeFileSync3(logPath, content + entry);
|
|
1435
1766
|
}
|
|
1436
|
-
function updateExecutionStatus(squadName, agentName, status,
|
|
1767
|
+
function updateExecutionStatus(squadName, agentName, executionId, status, details) {
|
|
1437
1768
|
const logPath = getExecutionLogPath(squadName, agentName);
|
|
1438
1769
|
if (!logPath || !existsSync4(logPath)) return;
|
|
1439
1770
|
let content = readFileSync3(logPath, "utf-8");
|
|
1440
1771
|
const endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
);
|
|
1772
|
+
const execMarker = `<!-- exec:${executionId} -->`;
|
|
1773
|
+
const markerIndex = content.indexOf(execMarker);
|
|
1774
|
+
if (markerIndex === -1) return;
|
|
1775
|
+
const nextEntryIndex = content.indexOf("\n---\n", markerIndex + 1);
|
|
1776
|
+
const entryEnd = nextEntryIndex === -1 ? content.length : nextEntryIndex;
|
|
1777
|
+
const entryStart = content.lastIndexOf("\n---\n", markerIndex);
|
|
1778
|
+
const currentEntry = content.slice(entryStart, entryEnd);
|
|
1779
|
+
const durationStr = details?.durationMs ? `${(details.durationMs / 1e3).toFixed(1)}s` : "unknown";
|
|
1780
|
+
let updatedEntry = currentEntry.replace(/Status: running/, `Status: ${status}`) + `- Completed: ${endTime}
|
|
1781
|
+
- Duration: ${durationStr}`;
|
|
1782
|
+
if (details?.outcome) {
|
|
1783
|
+
updatedEntry += `
|
|
1784
|
+
- Outcome: ${details.outcome}`;
|
|
1785
|
+
}
|
|
1786
|
+
if (details?.error) {
|
|
1787
|
+
updatedEntry += `
|
|
1788
|
+
- Error: ${details.error}`;
|
|
1789
|
+
}
|
|
1790
|
+
content = content.slice(0, entryStart) + updatedEntry + content.slice(entryEnd);
|
|
1448
1791
|
writeFileSync3(logPath, content);
|
|
1449
1792
|
}
|
|
1793
|
+
function extractMcpServersFromDefinition(definition) {
|
|
1794
|
+
const servers = /* @__PURE__ */ new Set();
|
|
1795
|
+
const knownServers = [
|
|
1796
|
+
"chrome-devtools",
|
|
1797
|
+
"firecrawl",
|
|
1798
|
+
"supabase",
|
|
1799
|
+
"grafana",
|
|
1800
|
+
"context7",
|
|
1801
|
+
"huggingface",
|
|
1802
|
+
"nano-banana"
|
|
1803
|
+
];
|
|
1804
|
+
for (const server of knownServers) {
|
|
1805
|
+
if (definition.toLowerCase().includes(server)) {
|
|
1806
|
+
servers.add(server);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
const mcpMatch = definition.match(/mcp:\s*\n((?:\s*-\s*\S+\s*\n?)+)/i);
|
|
1810
|
+
if (mcpMatch) {
|
|
1811
|
+
const lines = mcpMatch[1].split("\n");
|
|
1812
|
+
for (const line of lines) {
|
|
1813
|
+
const serverMatch = line.match(/^\s*-\s*(\S+)/);
|
|
1814
|
+
if (serverMatch) {
|
|
1815
|
+
servers.add(serverMatch[1]);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
return Array.from(servers);
|
|
1820
|
+
}
|
|
1450
1821
|
async function runCommand(target, options) {
|
|
1451
1822
|
const squadsDir = findSquadsDir();
|
|
1452
1823
|
if (!squadsDir) {
|
|
@@ -1476,6 +1847,9 @@ async function runCommand(target, options) {
|
|
|
1476
1847
|
}
|
|
1477
1848
|
async function runSquad(squad, squadsDir, options) {
|
|
1478
1849
|
if (!squad) return;
|
|
1850
|
+
if (!options.effort && squad.effort) {
|
|
1851
|
+
options.effort = squad.effort;
|
|
1852
|
+
}
|
|
1479
1853
|
const startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
1480
1854
|
writeLine();
|
|
1481
1855
|
writeLine(` ${gradient("squads")} ${colors.dim}run${RESET} ${colors.cyan}${squad.name}${RESET}`);
|
|
@@ -1665,7 +2039,7 @@ Begin by assessing pending work, then delegate to agents via Task tool.`;
|
|
|
1665
2039
|
writeLine(` ${gradient("Launching")} lead session${options.foreground ? " (foreground)" : ""}...`);
|
|
1666
2040
|
writeLine();
|
|
1667
2041
|
try {
|
|
1668
|
-
const result = await executeWithClaude(prompt2, options.verbose, timeoutMins, options.foreground, options.useApi);
|
|
2042
|
+
const result = await executeWithClaude(prompt2, options.verbose, timeoutMins, options.foreground, options.useApi, options.effort, options.skills, options.trigger || "manual");
|
|
1669
2043
|
if (options.foreground) {
|
|
1670
2044
|
writeLine();
|
|
1671
2045
|
writeLine(` ${icons.success} Lead session completed`);
|
|
@@ -1686,7 +2060,10 @@ Begin by assessing pending work, then delegate to agents via Task tool.`;
|
|
|
1686
2060
|
}
|
|
1687
2061
|
async function runAgent(agentName, agentPath, squadName, options) {
|
|
1688
2062
|
const spinner = ora2(`Running agent: ${agentName}`).start();
|
|
1689
|
-
const
|
|
2063
|
+
const startMs = Date.now();
|
|
2064
|
+
const startTime = new Date(startMs).toISOString();
|
|
2065
|
+
const executionId = generateExecutionId();
|
|
2066
|
+
const taskType = detectTaskType(agentName);
|
|
1690
2067
|
const definition = loadAgentDefinition(agentPath);
|
|
1691
2068
|
if (options.dryRun) {
|
|
1692
2069
|
spinner.info(`[DRY RUN] Would run ${agentName}`);
|
|
@@ -1696,11 +2073,40 @@ async function runAgent(agentName, agentPath, squadName, options) {
|
|
|
1696
2073
|
}
|
|
1697
2074
|
return;
|
|
1698
2075
|
}
|
|
2076
|
+
const squadsDir = findSquadsDir();
|
|
2077
|
+
if (squadsDir) {
|
|
2078
|
+
const squadFilePath = join4(squadsDir, squadName, "SQUAD.md");
|
|
2079
|
+
if (existsSync4(squadFilePath)) {
|
|
2080
|
+
const squadContent = readFileSync3(squadFilePath, "utf-8");
|
|
2081
|
+
const permContext = buildContextFromSquad(squadName, squadContent, agentName);
|
|
2082
|
+
const mcpServers = extractMcpServersFromDefinition(definition);
|
|
2083
|
+
const execRequest = {
|
|
2084
|
+
mcpServers
|
|
2085
|
+
};
|
|
2086
|
+
const permResult = validateExecution(permContext, execRequest);
|
|
2087
|
+
if (permResult.violations.length > 0) {
|
|
2088
|
+
spinner.stop();
|
|
2089
|
+
const violationLines = formatViolations(permResult);
|
|
2090
|
+
for (const line of violationLines) {
|
|
2091
|
+
writeLine(` ${line}`);
|
|
2092
|
+
}
|
|
2093
|
+
writeLine();
|
|
2094
|
+
if (!permResult.allowed) {
|
|
2095
|
+
writeLine(` ${colors.red}Execution blocked due to permission violations.${RESET}`);
|
|
2096
|
+
writeLine(` ${colors.dim}Configure permissions in ${squadFilePath}${RESET}`);
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
1699
2102
|
logExecution({
|
|
1700
2103
|
squadName,
|
|
1701
2104
|
agentName,
|
|
2105
|
+
executionId,
|
|
1702
2106
|
startTime,
|
|
1703
|
-
status: "running"
|
|
2107
|
+
status: "running",
|
|
2108
|
+
trigger: options.trigger || "manual",
|
|
2109
|
+
taskType
|
|
1704
2110
|
});
|
|
1705
2111
|
const timeoutMins = options.timeout || 30;
|
|
1706
2112
|
const prompt2 = `Execute the ${agentName} agent from squad ${squadName}.
|
|
@@ -1732,7 +2138,7 @@ CRITICAL: When you have completed your tasks OR reached the time limit:
|
|
|
1732
2138
|
if (options.execute && claudeAvailable) {
|
|
1733
2139
|
spinner.text = options.foreground ? `Running ${agentName} in foreground...` : `Launching ${agentName} as background task...`;
|
|
1734
2140
|
try {
|
|
1735
|
-
const result = await executeWithClaude(prompt2, options.verbose, options.timeout || 30, options.foreground, options.useApi);
|
|
2141
|
+
const result = await executeWithClaude(prompt2, options.verbose, options.timeout || 30, options.foreground, options.useApi, options.effort, options.skills, options.trigger || "manual");
|
|
1736
2142
|
if (options.foreground) {
|
|
1737
2143
|
spinner.succeed(`Agent ${agentName} completed`);
|
|
1738
2144
|
} else {
|
|
@@ -1744,7 +2150,10 @@ CRITICAL: When you have completed your tasks OR reached the time limit:
|
|
|
1744
2150
|
}
|
|
1745
2151
|
} catch (error) {
|
|
1746
2152
|
spinner.fail(`Agent ${agentName} failed to launch`);
|
|
1747
|
-
updateExecutionStatus(squadName, agentName, "failed",
|
|
2153
|
+
updateExecutionStatus(squadName, agentName, executionId, "failed", {
|
|
2154
|
+
error: String(error),
|
|
2155
|
+
durationMs: Date.now() - startMs
|
|
2156
|
+
});
|
|
1748
2157
|
writeLine(` ${colors.red}${String(error)}${RESET}`);
|
|
1749
2158
|
}
|
|
1750
2159
|
} else {
|
|
@@ -1770,28 +2179,45 @@ async function checkClaudeCliAvailable() {
|
|
|
1770
2179
|
check.on("error", () => resolve(false));
|
|
1771
2180
|
});
|
|
1772
2181
|
}
|
|
1773
|
-
async function executeWithClaude(prompt2, verbose, _timeoutMinutes = 30, foreground, useApi) {
|
|
1774
|
-
const userConfigPath = join4(process.env.HOME || "", ".claude.json");
|
|
2182
|
+
async function executeWithClaude(prompt2, verbose, _timeoutMinutes = 30, foreground, useApi, effort, skills, trigger = "manual") {
|
|
1775
2183
|
const projectRoot = getProjectRoot();
|
|
1776
2184
|
ensureProjectTrusted(projectRoot);
|
|
1777
2185
|
const squadMatch = prompt2.match(/squad (\w+)/);
|
|
1778
2186
|
const agentMatch = prompt2.match(/(\w+) agent/);
|
|
1779
2187
|
const squadName = process.env.SQUADS_SQUAD || squadMatch?.[1] || "unknown";
|
|
1780
2188
|
const agentName = process.env.SQUADS_AGENT || agentMatch?.[1] || "unknown";
|
|
2189
|
+
const mcpConfigPath = selectMcpConfig(squadName);
|
|
2190
|
+
const execContext = {
|
|
2191
|
+
squad: squadName,
|
|
2192
|
+
agent: agentName,
|
|
2193
|
+
taskType: detectTaskType(agentName),
|
|
2194
|
+
trigger,
|
|
2195
|
+
executionId: generateExecutionId()
|
|
2196
|
+
};
|
|
1781
2197
|
const { ANTHROPIC_API_KEY: _apiKey, ...envWithoutApiKey } = process.env;
|
|
1782
2198
|
const spawnEnv = useApi ? process.env : envWithoutApiKey;
|
|
1783
2199
|
const escapedPrompt = prompt2.replace(/'/g, "'\\''");
|
|
2200
|
+
await registerContextWithBridge(execContext);
|
|
1784
2201
|
if (foreground) {
|
|
1785
2202
|
if (verbose) {
|
|
1786
2203
|
writeLine(` ${colors.dim}Project: ${projectRoot}${RESET}`);
|
|
1787
2204
|
writeLine(` ${colors.dim}Mode: foreground${RESET}`);
|
|
1788
2205
|
writeLine(` ${colors.dim}Auth: ${useApi ? "API credits" : "subscription"}${RESET}`);
|
|
2206
|
+
writeLine(` ${colors.dim}Execution: ${execContext.executionId}${RESET}`);
|
|
2207
|
+
writeLine(` ${colors.dim}Task type: ${execContext.taskType}${RESET}`);
|
|
2208
|
+
writeLine(` ${colors.dim}Trigger: ${execContext.trigger}${RESET}`);
|
|
2209
|
+
if (effort) {
|
|
2210
|
+
writeLine(` ${colors.dim}Effort: ${effort}${RESET}`);
|
|
2211
|
+
}
|
|
2212
|
+
if (skills && skills.length > 0) {
|
|
2213
|
+
writeLine(` ${colors.dim}Skills: ${skills.join(", ")}${RESET}`);
|
|
2214
|
+
}
|
|
1789
2215
|
}
|
|
1790
2216
|
return new Promise((resolve, reject) => {
|
|
1791
2217
|
const claude = spawn2("claude", [
|
|
1792
2218
|
"--dangerously-skip-permissions",
|
|
1793
2219
|
"--mcp-config",
|
|
1794
|
-
|
|
2220
|
+
mcpConfigPath,
|
|
1795
2221
|
"--",
|
|
1796
2222
|
prompt2
|
|
1797
2223
|
], {
|
|
@@ -1799,8 +2225,17 @@ async function executeWithClaude(prompt2, verbose, _timeoutMinutes = 30, foregro
|
|
|
1799
2225
|
cwd: projectRoot,
|
|
1800
2226
|
env: {
|
|
1801
2227
|
...spawnEnv,
|
|
1802
|
-
|
|
1803
|
-
|
|
2228
|
+
// Telemetry context for per-agent cost tracking
|
|
2229
|
+
SQUADS_SQUAD: execContext.squad,
|
|
2230
|
+
SQUADS_AGENT: execContext.agent,
|
|
2231
|
+
SQUADS_TASK_TYPE: execContext.taskType,
|
|
2232
|
+
SQUADS_TRIGGER: execContext.trigger,
|
|
2233
|
+
SQUADS_EXECUTION_ID: execContext.executionId,
|
|
2234
|
+
// OTel resource attributes for telemetry pipeline
|
|
2235
|
+
OTEL_RESOURCE_ATTRIBUTES: `squads.squad=${execContext.squad},squads.agent=${execContext.agent},squads.task_type=${execContext.taskType},squads.trigger=${execContext.trigger},squads.execution_id=${execContext.executionId}`,
|
|
2236
|
+
// Claude-specific options
|
|
2237
|
+
...effort && { CLAUDE_EFFORT: effort },
|
|
2238
|
+
...skills && skills.length > 0 && { CLAUDE_SKILLS: skills.join(",") }
|
|
1804
2239
|
}
|
|
1805
2240
|
});
|
|
1806
2241
|
claude.on("close", (code) => {
|
|
@@ -1819,9 +2254,19 @@ async function executeWithClaude(prompt2, verbose, _timeoutMinutes = 30, foregro
|
|
|
1819
2254
|
if (verbose) {
|
|
1820
2255
|
writeLine(` ${colors.dim}Project: ${projectRoot}${RESET}`);
|
|
1821
2256
|
writeLine(` ${colors.dim}Session: ${sessionName}${RESET}`);
|
|
2257
|
+
writeLine(` ${colors.dim}MCP config: ${mcpConfigPath}${RESET}`);
|
|
1822
2258
|
writeLine(` ${colors.dim}Auth: ${useApi ? "API credits" : "subscription"}${RESET}`);
|
|
2259
|
+
writeLine(` ${colors.dim}Execution: ${execContext.executionId}${RESET}`);
|
|
2260
|
+
writeLine(` ${colors.dim}Task type: ${execContext.taskType}${RESET}`);
|
|
2261
|
+
writeLine(` ${colors.dim}Trigger: ${execContext.trigger}${RESET}`);
|
|
2262
|
+
if (effort) {
|
|
2263
|
+
writeLine(` ${colors.dim}Effort: ${effort}${RESET}`);
|
|
2264
|
+
}
|
|
2265
|
+
if (skills && skills.length > 0) {
|
|
2266
|
+
writeLine(` ${colors.dim}Skills: ${skills.join(", ")}${RESET}`);
|
|
2267
|
+
}
|
|
1823
2268
|
}
|
|
1824
|
-
const claudeCmd = `cd '${projectRoot}' && claude --dangerously-skip-permissions --mcp-config '${
|
|
2269
|
+
const claudeCmd = `cd '${projectRoot}' && claude --dangerously-skip-permissions --mcp-config '${mcpConfigPath}' -- '${escapedPrompt}'; tmux kill-session -t ${sessionName} 2>/dev/null`;
|
|
1825
2270
|
const tmux = spawn2("tmux", [
|
|
1826
2271
|
"new-session",
|
|
1827
2272
|
"-d",
|
|
@@ -1841,8 +2286,17 @@ async function executeWithClaude(prompt2, verbose, _timeoutMinutes = 30, foregro
|
|
|
1841
2286
|
detached: true,
|
|
1842
2287
|
env: {
|
|
1843
2288
|
...spawnEnv,
|
|
1844
|
-
|
|
1845
|
-
|
|
2289
|
+
// Telemetry context for per-agent cost tracking
|
|
2290
|
+
SQUADS_SQUAD: execContext.squad,
|
|
2291
|
+
SQUADS_AGENT: execContext.agent,
|
|
2292
|
+
SQUADS_TASK_TYPE: execContext.taskType,
|
|
2293
|
+
SQUADS_TRIGGER: execContext.trigger,
|
|
2294
|
+
SQUADS_EXECUTION_ID: execContext.executionId,
|
|
2295
|
+
// OTel resource attributes for telemetry pipeline
|
|
2296
|
+
OTEL_RESOURCE_ATTRIBUTES: `squads.squad=${execContext.squad},squads.agent=${execContext.agent},squads.task_type=${execContext.taskType},squads.trigger=${execContext.trigger},squads.execution_id=${execContext.executionId}`,
|
|
2297
|
+
// Claude-specific options
|
|
2298
|
+
...effort && { CLAUDE_EFFORT: effort },
|
|
2299
|
+
...skills && skills.length > 0 && { CLAUDE_SKILLS: skills.join(",") }
|
|
1846
2300
|
}
|
|
1847
2301
|
});
|
|
1848
2302
|
tmux.unref();
|
|
@@ -4673,9 +5127,9 @@ function getPool() {
|
|
|
4673
5127
|
async function isDatabaseAvailable() {
|
|
4674
5128
|
try {
|
|
4675
5129
|
const pool2 = getPool();
|
|
4676
|
-
const
|
|
4677
|
-
await
|
|
4678
|
-
|
|
5130
|
+
const client2 = await pool2.connect();
|
|
5131
|
+
await client2.query("SELECT 1");
|
|
5132
|
+
client2.release();
|
|
4679
5133
|
return true;
|
|
4680
5134
|
} catch (err) {
|
|
4681
5135
|
if (process.env.DEBUG) {
|
|
@@ -4687,8 +5141,8 @@ async function isDatabaseAvailable() {
|
|
|
4687
5141
|
async function saveDashboardSnapshot(snapshot) {
|
|
4688
5142
|
try {
|
|
4689
5143
|
const pool2 = getPool();
|
|
4690
|
-
const
|
|
4691
|
-
const result = await
|
|
5144
|
+
const client2 = await pool2.connect();
|
|
5145
|
+
const result = await client2.query(`
|
|
4692
5146
|
INSERT INTO squads.dashboard_snapshots (
|
|
4693
5147
|
total_squads, total_commits, total_prs_merged, total_issues_closed, total_issues_open,
|
|
4694
5148
|
goal_progress_pct, cost_usd, daily_budget_usd, input_tokens, output_tokens,
|
|
@@ -4717,7 +5171,7 @@ async function saveDashboardSnapshot(snapshot) {
|
|
|
4717
5171
|
JSON.stringify(snapshot.authorsData),
|
|
4718
5172
|
JSON.stringify(snapshot.reposData)
|
|
4719
5173
|
]);
|
|
4720
|
-
|
|
5174
|
+
client2.release();
|
|
4721
5175
|
return result.rows[0]?.id ?? null;
|
|
4722
5176
|
} catch (err) {
|
|
4723
5177
|
if (process.env.DEBUG) {
|
|
@@ -4728,8 +5182,8 @@ async function saveDashboardSnapshot(snapshot) {
|
|
|
4728
5182
|
}
|
|
4729
5183
|
async function getDashboardHistory(limit = 30) {
|
|
4730
5184
|
try {
|
|
4731
|
-
const
|
|
4732
|
-
const result = await
|
|
5185
|
+
const client2 = await getPool().connect();
|
|
5186
|
+
const result = await client2.query(`
|
|
4733
5187
|
SELECT
|
|
4734
5188
|
total_squads, total_commits, total_prs_merged, total_issues_closed, total_issues_open,
|
|
4735
5189
|
goal_progress_pct, cost_usd, daily_budget_usd, input_tokens, output_tokens,
|
|
@@ -4739,7 +5193,7 @@ async function getDashboardHistory(limit = 30) {
|
|
|
4739
5193
|
ORDER BY captured_at DESC
|
|
4740
5194
|
LIMIT $1
|
|
4741
5195
|
`, [limit]);
|
|
4742
|
-
|
|
5196
|
+
client2.release();
|
|
4743
5197
|
return result.rows.map((row) => ({
|
|
4744
5198
|
totalSquads: row.total_squads,
|
|
4745
5199
|
totalCommits: row.total_commits,
|
|
@@ -4969,16 +5423,50 @@ async function dashboardCommand(options = {}) {
|
|
|
4969
5423
|
renderAcquisitionCached(cache);
|
|
4970
5424
|
renderHistoricalTrendsCached(cache);
|
|
4971
5425
|
renderInsightsCached(cache);
|
|
5426
|
+
if (gitStats && gitStats.recentCommits && gitStats.recentCommits.length > 0) {
|
|
5427
|
+
writeLine(` ${bold}Working On${RESET}`);
|
|
5428
|
+
writeLine();
|
|
5429
|
+
for (const commit of gitStats.recentCommits.slice(0, 3)) {
|
|
5430
|
+
const shortHash = commit.hash.slice(0, 7);
|
|
5431
|
+
const shortMsg = truncate(commit.message, 45);
|
|
5432
|
+
writeLine(` ${colors.dim}${shortHash}${RESET} ${shortMsg} ${colors.dim}(${commit.repo})${RESET}`);
|
|
5433
|
+
}
|
|
5434
|
+
writeLine();
|
|
5435
|
+
}
|
|
4972
5436
|
const allActiveGoals = squadData.flatMap(
|
|
4973
5437
|
(s) => s.goals.filter((g) => !g.completed).map((g) => ({ squad: s.name, goal: g }))
|
|
4974
5438
|
);
|
|
4975
5439
|
if (allActiveGoals.length > 0) {
|
|
4976
|
-
|
|
5440
|
+
const activeSquads2 = new Set(gitStats?.recentCommits?.map((c) => {
|
|
5441
|
+
const repoSquadMap = {
|
|
5442
|
+
"agents-squads-web": "website",
|
|
5443
|
+
"squads-cli": "cli",
|
|
5444
|
+
"hq": "engineering",
|
|
5445
|
+
"company": "company",
|
|
5446
|
+
"product": "product",
|
|
5447
|
+
"research": "research",
|
|
5448
|
+
"intelligence": "intelligence",
|
|
5449
|
+
"customer": "customer",
|
|
5450
|
+
"finance": "finance",
|
|
5451
|
+
"marketing": "marketing"
|
|
5452
|
+
};
|
|
5453
|
+
return repoSquadMap[c.repo] || c.repo;
|
|
5454
|
+
}) || []);
|
|
5455
|
+
const sortedGoals = [...allActiveGoals].sort((a, b) => {
|
|
5456
|
+
const aActive = activeSquads2.has(a.squad) ? 1 : 0;
|
|
5457
|
+
const bActive = activeSquads2.has(b.squad) ? 1 : 0;
|
|
5458
|
+
if (aActive !== bActive) return bActive - aActive;
|
|
5459
|
+
const aHasProgress = a.goal.progress ? 1 : 0;
|
|
5460
|
+
const bHasProgress = b.goal.progress ? 1 : 0;
|
|
5461
|
+
return bHasProgress - aHasProgress;
|
|
5462
|
+
});
|
|
5463
|
+
writeLine(` ${bold}Goals${RESET} ${colors.dim}(${allActiveGoals.length} active)${RESET}`);
|
|
4977
5464
|
writeLine();
|
|
4978
|
-
const maxGoals =
|
|
4979
|
-
for (const { squad, goal: goal2 } of
|
|
5465
|
+
const maxGoals = 3;
|
|
5466
|
+
for (const { squad, goal: goal2 } of sortedGoals.slice(0, maxGoals)) {
|
|
4980
5467
|
const hasProgress = goal2.progress && goal2.progress.length > 0;
|
|
4981
|
-
const
|
|
5468
|
+
const isActive = activeSquads2.has(squad);
|
|
5469
|
+
const icon = isActive ? icons.active : hasProgress ? icons.progress : icons.empty;
|
|
4982
5470
|
writeLine(` ${icon} ${colors.dim}${squad}${RESET} ${truncate(goal2.description, 48)}`);
|
|
4983
5471
|
if (hasProgress) {
|
|
4984
5472
|
writeLine(` ${colors.dim}\u2514${RESET} ${colors.green}${truncate(goal2.progress, 52)}${RESET}`);
|
|
@@ -6579,12 +7067,12 @@ function formatDuration(ms) {
|
|
|
6579
7067
|
}
|
|
6580
7068
|
function groupByDate(executions) {
|
|
6581
7069
|
const groups = /* @__PURE__ */ new Map();
|
|
6582
|
-
for (const
|
|
6583
|
-
const dateKey =
|
|
7070
|
+
for (const exec3 of executions) {
|
|
7071
|
+
const dateKey = exec3.startedAt.toISOString().split("T")[0];
|
|
6584
7072
|
if (!groups.has(dateKey)) {
|
|
6585
7073
|
groups.set(dateKey, []);
|
|
6586
7074
|
}
|
|
6587
|
-
groups.get(dateKey).push(
|
|
7075
|
+
groups.get(dateKey).push(exec3);
|
|
6588
7076
|
}
|
|
6589
7077
|
return groups;
|
|
6590
7078
|
}
|
|
@@ -6615,13 +7103,13 @@ async function historyCommand(options = {}) {
|
|
|
6615
7103
|
]);
|
|
6616
7104
|
const seenIds = /* @__PURE__ */ new Set();
|
|
6617
7105
|
const allExecutions = [];
|
|
6618
|
-
for (const
|
|
6619
|
-
seenIds.add(
|
|
6620
|
-
allExecutions.push(
|
|
7106
|
+
for (const exec3 of bridgeExecs) {
|
|
7107
|
+
seenIds.add(exec3.id);
|
|
7108
|
+
allExecutions.push(exec3);
|
|
6621
7109
|
}
|
|
6622
|
-
for (const
|
|
6623
|
-
if (!seenIds.has(
|
|
6624
|
-
allExecutions.push(
|
|
7110
|
+
for (const exec3 of localExecs) {
|
|
7111
|
+
if (!seenIds.has(exec3.id)) {
|
|
7112
|
+
allExecutions.push(exec3);
|
|
6625
7113
|
}
|
|
6626
7114
|
}
|
|
6627
7115
|
allExecutions.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
|
|
@@ -6645,14 +7133,14 @@ async function historyCommand(options = {}) {
|
|
|
6645
7133
|
writeLine(` ${colors.purple}\u250C${"\u2500".repeat(60)}\u2510${RESET}`);
|
|
6646
7134
|
writeLine(` ${colors.purple}\u2502${RESET} ${padEnd("TIME", 7)}${padEnd("SQUAD", 13)}${padEnd("AGENT", 16)}${padEnd("DURATION", 10)}${padEnd("STATUS", 8)}${colors.purple}\u2502${RESET}`);
|
|
6647
7135
|
writeLine(` ${colors.purple}\u251C${"\u2500".repeat(60)}\u2524${RESET}`);
|
|
6648
|
-
for (const
|
|
6649
|
-
const time =
|
|
6650
|
-
const squadName = truncate(
|
|
6651
|
-
const agentName = truncate(
|
|
6652
|
-
const duration = formatDuration(
|
|
7136
|
+
for (const exec3 of execs) {
|
|
7137
|
+
const time = exec3.startedAt.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
7138
|
+
const squadName = truncate(exec3.squad, 11);
|
|
7139
|
+
const agentName = truncate(exec3.agent, 14);
|
|
7140
|
+
const duration = formatDuration(exec3.durationMs);
|
|
6653
7141
|
let statusIcon;
|
|
6654
7142
|
let statusColor;
|
|
6655
|
-
switch (
|
|
7143
|
+
switch (exec3.status) {
|
|
6656
7144
|
case "success":
|
|
6657
7145
|
statusIcon = icons.success;
|
|
6658
7146
|
statusColor = colors.green;
|
|
@@ -6670,14 +7158,14 @@ async function historyCommand(options = {}) {
|
|
|
6670
7158
|
statusColor = colors.dim;
|
|
6671
7159
|
}
|
|
6672
7160
|
writeLine(` ${colors.purple}\u2502${RESET} ${colors.dim}${time}${RESET} ${colors.cyan}${padEnd(squadName, 12)}${RESET}${padEnd(agentName, 16)}${padEnd(duration, 10)}${statusColor}${statusIcon}${RESET} ${colors.purple}\u2502${RESET}`);
|
|
6673
|
-
if (verbose && (
|
|
6674
|
-
const costStr =
|
|
6675
|
-
const tokenStr =
|
|
7161
|
+
if (verbose && (exec3.cost || exec3.tokens)) {
|
|
7162
|
+
const costStr = exec3.cost ? `$${exec3.cost.toFixed(2)}` : "";
|
|
7163
|
+
const tokenStr = exec3.tokens ? `${exec3.tokens.toLocaleString()} tokens` : "";
|
|
6676
7164
|
const details = [costStr, tokenStr].filter(Boolean).join(" \u2502 ");
|
|
6677
7165
|
writeLine(` ${colors.purple}\u2502${RESET} ${colors.dim}\u2514 ${details}${RESET}${" ".repeat(Math.max(0, 45 - details.length))}${colors.purple}\u2502${RESET}`);
|
|
6678
7166
|
}
|
|
6679
|
-
if (
|
|
6680
|
-
writeLine(` ${colors.purple}\u2502${RESET} ${colors.red}\u2514 ${truncate(
|
|
7167
|
+
if (exec3.error) {
|
|
7168
|
+
writeLine(` ${colors.purple}\u2502${RESET} ${colors.red}\u2514 ${truncate(exec3.error, 45)}${RESET}${" ".repeat(Math.max(0, 45 - exec3.error.length))}${colors.purple}\u2502${RESET}`);
|
|
6681
7169
|
}
|
|
6682
7170
|
}
|
|
6683
7171
|
writeLine(` ${colors.purple}\u2514${"\u2500".repeat(60)}\u2518${RESET}`);
|
|
@@ -7927,15 +8415,1305 @@ function registerTriggerCommand(program2) {
|
|
|
7927
8415
|
});
|
|
7928
8416
|
}
|
|
7929
8417
|
|
|
7930
|
-
// src/commands/
|
|
8418
|
+
// src/commands/skill.ts
|
|
7931
8419
|
import ora6 from "ora";
|
|
8420
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10, readFileSync as readFileSync14 } from "fs";
|
|
8421
|
+
import { join as join18, basename as basename2, dirname as dirname5 } from "path";
|
|
8422
|
+
|
|
8423
|
+
// src/lib/anthropic.ts
|
|
8424
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
8425
|
+
import { readFileSync as readFileSync13, readdirSync as readdirSync7 } from "fs";
|
|
8426
|
+
import { join as join17 } from "path";
|
|
8427
|
+
var client = null;
|
|
8428
|
+
function getClient() {
|
|
8429
|
+
if (!client) {
|
|
8430
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
8431
|
+
if (!apiKey) {
|
|
8432
|
+
throw new Error("ANTHROPIC_API_KEY environment variable is required for skills management");
|
|
8433
|
+
}
|
|
8434
|
+
client = new Anthropic({ apiKey });
|
|
8435
|
+
}
|
|
8436
|
+
return client;
|
|
8437
|
+
}
|
|
8438
|
+
async function listSkills() {
|
|
8439
|
+
getClient();
|
|
8440
|
+
try {
|
|
8441
|
+
console.warn("Skills API: Using placeholder implementation. Actual API may differ.");
|
|
8442
|
+
return [];
|
|
8443
|
+
} catch (error) {
|
|
8444
|
+
if (error instanceof Error && error.message.includes("404")) {
|
|
8445
|
+
return [];
|
|
8446
|
+
}
|
|
8447
|
+
throw error;
|
|
8448
|
+
}
|
|
8449
|
+
}
|
|
8450
|
+
function loadSkillFiles(skillPath) {
|
|
8451
|
+
const files = [];
|
|
8452
|
+
function walkDir(dir, prefix = "") {
|
|
8453
|
+
const entries = readdirSync7(dir, { withFileTypes: true });
|
|
8454
|
+
for (const entry of entries) {
|
|
8455
|
+
const fullPath = join17(dir, entry.name);
|
|
8456
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
8457
|
+
if (entry.isDirectory()) {
|
|
8458
|
+
walkDir(fullPath, relativePath);
|
|
8459
|
+
} else if (entry.isFile()) {
|
|
8460
|
+
const content = readFileSync13(fullPath, "utf-8");
|
|
8461
|
+
files.push({
|
|
8462
|
+
name: relativePath,
|
|
8463
|
+
content
|
|
8464
|
+
});
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
8467
|
+
}
|
|
8468
|
+
walkDir(skillPath);
|
|
8469
|
+
return files;
|
|
8470
|
+
}
|
|
8471
|
+
function extractSkillTitle(files) {
|
|
8472
|
+
const skillMd = files.find((f) => f.name === "SKILL.md" || f.name.endsWith("/SKILL.md"));
|
|
8473
|
+
if (!skillMd) {
|
|
8474
|
+
throw new Error("SKILL.md not found in skill directory");
|
|
8475
|
+
}
|
|
8476
|
+
const content = skillMd.content;
|
|
8477
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
8478
|
+
if (frontmatterMatch) {
|
|
8479
|
+
const nameMatch = frontmatterMatch[1].match(/^name:\s*(.+)$/m);
|
|
8480
|
+
if (nameMatch) {
|
|
8481
|
+
return nameMatch[1].trim();
|
|
8482
|
+
}
|
|
8483
|
+
}
|
|
8484
|
+
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
8485
|
+
if (headingMatch) {
|
|
8486
|
+
return headingMatch[1].trim();
|
|
8487
|
+
}
|
|
8488
|
+
throw new Error("Could not extract skill title from SKILL.md");
|
|
8489
|
+
}
|
|
8490
|
+
function extractSkillDescription(files) {
|
|
8491
|
+
const skillMd = files.find((f) => f.name === "SKILL.md" || f.name.endsWith("/SKILL.md"));
|
|
8492
|
+
if (!skillMd) return void 0;
|
|
8493
|
+
const content = skillMd.content;
|
|
8494
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
8495
|
+
if (frontmatterMatch) {
|
|
8496
|
+
const descMatch = frontmatterMatch[1].match(/^description:\s*(.+)$/m);
|
|
8497
|
+
if (descMatch) {
|
|
8498
|
+
return descMatch[1].trim();
|
|
8499
|
+
}
|
|
8500
|
+
}
|
|
8501
|
+
const paragraphMatch = content.match(/^#.+\n+(.+)/m);
|
|
8502
|
+
if (paragraphMatch) {
|
|
8503
|
+
return paragraphMatch[1].trim();
|
|
8504
|
+
}
|
|
8505
|
+
return void 0;
|
|
8506
|
+
}
|
|
8507
|
+
async function uploadSkill(skillPath) {
|
|
8508
|
+
getClient();
|
|
8509
|
+
const files = loadSkillFiles(skillPath);
|
|
8510
|
+
if (files.length === 0) {
|
|
8511
|
+
throw new Error(`No files found in skill directory: ${skillPath}`);
|
|
8512
|
+
}
|
|
8513
|
+
const totalSize = files.reduce((sum, f) => sum + Buffer.byteLength(f.content, "utf-8"), 0);
|
|
8514
|
+
const maxSize = 8 * 1024 * 1024;
|
|
8515
|
+
if (totalSize > maxSize) {
|
|
8516
|
+
throw new Error(`Skill size ${(totalSize / 1024 / 1024).toFixed(2)}MB exceeds 8MB limit`);
|
|
8517
|
+
}
|
|
8518
|
+
const displayTitle = extractSkillTitle(files);
|
|
8519
|
+
const description = extractSkillDescription(files);
|
|
8520
|
+
try {
|
|
8521
|
+
console.log(`Uploading skill: ${displayTitle}`);
|
|
8522
|
+
console.log(`Files: ${files.map((f) => f.name).join(", ")}`);
|
|
8523
|
+
console.log(`Total size: ${(totalSize / 1024).toFixed(2)}KB`);
|
|
8524
|
+
const skill = {
|
|
8525
|
+
id: `skill_${Date.now()}`,
|
|
8526
|
+
display_title: displayTitle,
|
|
8527
|
+
description,
|
|
8528
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8529
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8530
|
+
files
|
|
8531
|
+
};
|
|
8532
|
+
console.warn("Skills API: Using placeholder implementation. Skill not actually uploaded.");
|
|
8533
|
+
return skill;
|
|
8534
|
+
} catch (error) {
|
|
8535
|
+
throw new Error(`Failed to upload skill: ${error instanceof Error ? error.message : String(error)}`);
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
8538
|
+
async function deleteSkill(skillId) {
|
|
8539
|
+
getClient();
|
|
8540
|
+
try {
|
|
8541
|
+
console.log(`Deleting skill: ${skillId}`);
|
|
8542
|
+
console.warn("Skills API: Using placeholder implementation. Skill not actually deleted.");
|
|
8543
|
+
} catch (error) {
|
|
8544
|
+
throw new Error(`Failed to delete skill: ${error instanceof Error ? error.message : String(error)}`);
|
|
8545
|
+
}
|
|
8546
|
+
}
|
|
8547
|
+
async function getSkill(skillId) {
|
|
8548
|
+
getClient();
|
|
8549
|
+
try {
|
|
8550
|
+
console.log(`Getting skill: ${skillId}`);
|
|
8551
|
+
console.warn("Skills API: Using placeholder implementation.");
|
|
8552
|
+
return null;
|
|
8553
|
+
} catch (error) {
|
|
8554
|
+
if (error instanceof Error && error.message.includes("404")) {
|
|
8555
|
+
return null;
|
|
8556
|
+
}
|
|
8557
|
+
throw error;
|
|
8558
|
+
}
|
|
8559
|
+
}
|
|
8560
|
+
function isApiKeyConfigured() {
|
|
8561
|
+
return !!process.env.ANTHROPIC_API_KEY;
|
|
8562
|
+
}
|
|
8563
|
+
|
|
8564
|
+
// src/commands/skill.ts
|
|
8565
|
+
function registerSkillCommand(program2) {
|
|
8566
|
+
const skill = program2.command("skill").description("Manage Agent Skills (upload, list, delete)");
|
|
8567
|
+
skill.command("list").description("List uploaded skills from Anthropic API").action(skillListCommand);
|
|
8568
|
+
skill.command("upload <path>").description("Upload a skill directory to Anthropic API").action(skillUploadCommand);
|
|
8569
|
+
skill.command("delete <skillId>").description("Delete a skill from Anthropic API").option("-f, --force", "Skip confirmation prompt").action(skillDeleteCommand);
|
|
8570
|
+
skill.command("show <skillId>").description("Show details for a skill").action(skillShowCommand);
|
|
8571
|
+
skill.command("convert <agent>").description("Convert an agent.md file to SKILL.md format").option("-o, --output <path>", "Output directory (default: .agents/skills/<agent-name>)").action(skillConvertCommand);
|
|
8572
|
+
}
|
|
8573
|
+
async function skillListCommand() {
|
|
8574
|
+
writeLine();
|
|
8575
|
+
writeLine(` ${gradient("Skills")} ${colors.dim}list${RESET}`);
|
|
8576
|
+
writeLine();
|
|
8577
|
+
if (!isApiKeyConfigured()) {
|
|
8578
|
+
writeLine(` ${icons.error} ${colors.red}ANTHROPIC_API_KEY not configured${RESET}`);
|
|
8579
|
+
writeLine(` ${colors.dim}Set the environment variable to use skills management.${RESET}`);
|
|
8580
|
+
writeLine();
|
|
8581
|
+
return;
|
|
8582
|
+
}
|
|
8583
|
+
const spinner = ora6("Fetching skills...").start();
|
|
8584
|
+
try {
|
|
8585
|
+
const skills = await listSkills();
|
|
8586
|
+
if (skills.length === 0) {
|
|
8587
|
+
spinner.info("No skills uploaded yet");
|
|
8588
|
+
writeLine();
|
|
8589
|
+
writeLine(` ${colors.dim}Upload a skill:${RESET}`);
|
|
8590
|
+
writeLine(` ${colors.cyan}squads skill upload <path>${RESET}`);
|
|
8591
|
+
writeLine();
|
|
8592
|
+
writeLine(` ${colors.dim}Convert an agent to skill format:${RESET}`);
|
|
8593
|
+
writeLine(` ${colors.cyan}squads skill convert <squad>/<agent>${RESET}`);
|
|
8594
|
+
writeLine();
|
|
8595
|
+
return;
|
|
8596
|
+
}
|
|
8597
|
+
spinner.succeed(`Found ${skills.length} skill(s)`);
|
|
8598
|
+
writeLine();
|
|
8599
|
+
writeLine(` ${colors.dim}ID${RESET} ${colors.dim}Title${RESET} ${colors.dim}Updated${RESET}`);
|
|
8600
|
+
writeLine(` ${colors.dim}${"\u2500".repeat(60)}${RESET}`);
|
|
8601
|
+
for (const skill of skills) {
|
|
8602
|
+
const id = skill.id.slice(0, 20).padEnd(22);
|
|
8603
|
+
const title = (skill.display_title || "Untitled").slice(0, 20).padEnd(22);
|
|
8604
|
+
const updated = new Date(skill.updated_at).toLocaleDateString();
|
|
8605
|
+
writeLine(` ${colors.cyan}${id}${RESET}${title}${colors.dim}${updated}${RESET}`);
|
|
8606
|
+
}
|
|
8607
|
+
writeLine();
|
|
8608
|
+
} catch (error) {
|
|
8609
|
+
spinner.fail("Failed to list skills");
|
|
8610
|
+
writeLine(` ${colors.red}${error instanceof Error ? error.message : String(error)}${RESET}`);
|
|
8611
|
+
writeLine();
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
async function skillUploadCommand(skillPath) {
|
|
8615
|
+
writeLine();
|
|
8616
|
+
writeLine(` ${gradient("Skills")} ${colors.dim}upload${RESET}`);
|
|
8617
|
+
writeLine();
|
|
8618
|
+
if (!isApiKeyConfigured()) {
|
|
8619
|
+
writeLine(` ${icons.error} ${colors.red}ANTHROPIC_API_KEY not configured${RESET}`);
|
|
8620
|
+
writeLine(` ${colors.dim}Set the environment variable to use skills management.${RESET}`);
|
|
8621
|
+
writeLine();
|
|
8622
|
+
return;
|
|
8623
|
+
}
|
|
8624
|
+
const fullPath = skillPath.startsWith("/") ? skillPath : join18(process.cwd(), skillPath);
|
|
8625
|
+
if (!existsSync17(fullPath)) {
|
|
8626
|
+
writeLine(` ${icons.error} ${colors.red}Directory not found: ${skillPath}${RESET}`);
|
|
8627
|
+
writeLine();
|
|
8628
|
+
return;
|
|
8629
|
+
}
|
|
8630
|
+
const skillMdPath = join18(fullPath, "SKILL.md");
|
|
8631
|
+
if (!existsSync17(skillMdPath)) {
|
|
8632
|
+
writeLine(` ${icons.error} ${colors.red}SKILL.md not found in ${skillPath}${RESET}`);
|
|
8633
|
+
writeLine();
|
|
8634
|
+
writeLine(` ${colors.dim}Create a SKILL.md file or use:${RESET}`);
|
|
8635
|
+
writeLine(` ${colors.cyan}squads skill convert <squad>/<agent>${RESET}`);
|
|
8636
|
+
writeLine();
|
|
8637
|
+
return;
|
|
8638
|
+
}
|
|
8639
|
+
const spinner = ora6("Uploading skill...").start();
|
|
8640
|
+
try {
|
|
8641
|
+
const skill = await uploadSkill(fullPath);
|
|
8642
|
+
spinner.succeed(`Skill uploaded: ${skill.display_title}`);
|
|
8643
|
+
writeLine();
|
|
8644
|
+
writeLine(` ${colors.dim}ID:${RESET} ${colors.cyan}${skill.id}${RESET}`);
|
|
8645
|
+
if (skill.description) {
|
|
8646
|
+
writeLine(` ${colors.dim}Description:${RESET} ${skill.description}`);
|
|
8647
|
+
}
|
|
8648
|
+
writeLine();
|
|
8649
|
+
writeLine(` ${colors.dim}Use in run command:${RESET}`);
|
|
8650
|
+
writeLine(` ${colors.cyan}squads run <squad> --skills ${skill.id}${RESET}`);
|
|
8651
|
+
writeLine();
|
|
8652
|
+
} catch (error) {
|
|
8653
|
+
spinner.fail("Failed to upload skill");
|
|
8654
|
+
writeLine(` ${colors.red}${error instanceof Error ? error.message : String(error)}${RESET}`);
|
|
8655
|
+
writeLine();
|
|
8656
|
+
}
|
|
8657
|
+
}
|
|
8658
|
+
async function skillDeleteCommand(skillId, options) {
|
|
8659
|
+
writeLine();
|
|
8660
|
+
writeLine(` ${gradient("Skills")} ${colors.dim}delete${RESET}`);
|
|
8661
|
+
writeLine();
|
|
8662
|
+
if (!isApiKeyConfigured()) {
|
|
8663
|
+
writeLine(` ${icons.error} ${colors.red}ANTHROPIC_API_KEY not configured${RESET}`);
|
|
8664
|
+
writeLine();
|
|
8665
|
+
return;
|
|
8666
|
+
}
|
|
8667
|
+
if (!options.force) {
|
|
8668
|
+
writeLine(` ${colors.yellow}Warning: This will permanently delete the skill.${RESET}`);
|
|
8669
|
+
writeLine(` ${colors.dim}Use --force to skip this confirmation.${RESET}`);
|
|
8670
|
+
writeLine();
|
|
8671
|
+
return;
|
|
8672
|
+
}
|
|
8673
|
+
const spinner = ora6("Deleting skill...").start();
|
|
8674
|
+
try {
|
|
8675
|
+
await deleteSkill(skillId);
|
|
8676
|
+
spinner.succeed(`Skill deleted: ${skillId}`);
|
|
8677
|
+
writeLine();
|
|
8678
|
+
} catch (error) {
|
|
8679
|
+
spinner.fail("Failed to delete skill");
|
|
8680
|
+
writeLine(` ${colors.red}${error instanceof Error ? error.message : String(error)}${RESET}`);
|
|
8681
|
+
writeLine();
|
|
8682
|
+
}
|
|
8683
|
+
}
|
|
8684
|
+
async function skillShowCommand(skillId) {
|
|
8685
|
+
writeLine();
|
|
8686
|
+
writeLine(` ${gradient("Skills")} ${colors.dim}show${RESET}`);
|
|
8687
|
+
writeLine();
|
|
8688
|
+
if (!isApiKeyConfigured()) {
|
|
8689
|
+
writeLine(` ${icons.error} ${colors.red}ANTHROPIC_API_KEY not configured${RESET}`);
|
|
8690
|
+
writeLine();
|
|
8691
|
+
return;
|
|
8692
|
+
}
|
|
8693
|
+
const spinner = ora6("Fetching skill...").start();
|
|
8694
|
+
try {
|
|
8695
|
+
const skill = await getSkill(skillId);
|
|
8696
|
+
if (!skill) {
|
|
8697
|
+
spinner.fail(`Skill not found: ${skillId}`);
|
|
8698
|
+
writeLine();
|
|
8699
|
+
return;
|
|
8700
|
+
}
|
|
8701
|
+
spinner.succeed(`Found skill: ${skill.display_title}`);
|
|
8702
|
+
writeLine();
|
|
8703
|
+
writeLine(` ${colors.dim}ID:${RESET} ${colors.cyan}${skill.id}${RESET}`);
|
|
8704
|
+
writeLine(` ${colors.dim}Title:${RESET} ${skill.display_title}`);
|
|
8705
|
+
if (skill.description) {
|
|
8706
|
+
writeLine(` ${colors.dim}Description:${RESET} ${skill.description}`);
|
|
8707
|
+
}
|
|
8708
|
+
writeLine(` ${colors.dim}Created:${RESET} ${new Date(skill.created_at).toLocaleString()}`);
|
|
8709
|
+
writeLine(` ${colors.dim}Updated:${RESET} ${new Date(skill.updated_at).toLocaleString()}`);
|
|
8710
|
+
if (skill.files && skill.files.length > 0) {
|
|
8711
|
+
writeLine();
|
|
8712
|
+
writeLine(` ${colors.dim}Files:${RESET}`);
|
|
8713
|
+
for (const file of skill.files) {
|
|
8714
|
+
const size = Buffer.byteLength(file.content, "utf-8");
|
|
8715
|
+
writeLine(` ${colors.cyan}${file.name}${RESET} ${colors.dim}(${formatBytes(size)})${RESET}`);
|
|
8716
|
+
}
|
|
8717
|
+
}
|
|
8718
|
+
writeLine();
|
|
8719
|
+
} catch (error) {
|
|
8720
|
+
spinner.fail("Failed to fetch skill");
|
|
8721
|
+
writeLine(` ${colors.red}${error instanceof Error ? error.message : String(error)}${RESET}`);
|
|
8722
|
+
writeLine();
|
|
8723
|
+
}
|
|
8724
|
+
}
|
|
8725
|
+
async function skillConvertCommand(agentPath, options) {
|
|
8726
|
+
writeLine();
|
|
8727
|
+
writeLine(` ${gradient("Skills")} ${colors.dim}convert${RESET}`);
|
|
8728
|
+
writeLine();
|
|
8729
|
+
const squadsDir = findSquadsDir();
|
|
8730
|
+
if (!squadsDir) {
|
|
8731
|
+
writeLine(` ${icons.error} ${colors.red}No .agents/squads directory found${RESET}`);
|
|
8732
|
+
writeLine();
|
|
8733
|
+
return;
|
|
8734
|
+
}
|
|
8735
|
+
let agentFilePath;
|
|
8736
|
+
let squadName;
|
|
8737
|
+
let agentName;
|
|
8738
|
+
if (agentPath.includes("/")) {
|
|
8739
|
+
const [squad, agent] = agentPath.split("/");
|
|
8740
|
+
squadName = squad;
|
|
8741
|
+
agentName = agent.replace(".md", "");
|
|
8742
|
+
agentFilePath = join18(squadsDir, squad, `${agentName}.md`);
|
|
8743
|
+
} else {
|
|
8744
|
+
agentName = agentPath.replace(".md", "");
|
|
8745
|
+
const foundPath = findAgentFile(squadsDir, agentName);
|
|
8746
|
+
if (!foundPath) {
|
|
8747
|
+
writeLine(` ${icons.error} ${colors.red}Agent not found: ${agentPath}${RESET}`);
|
|
8748
|
+
writeLine(` ${colors.dim}Use format: squad/agent (e.g., cli/code-eval)${RESET}`);
|
|
8749
|
+
writeLine();
|
|
8750
|
+
return;
|
|
8751
|
+
}
|
|
8752
|
+
agentFilePath = foundPath;
|
|
8753
|
+
squadName = basename2(dirname5(agentFilePath));
|
|
8754
|
+
}
|
|
8755
|
+
if (!existsSync17(agentFilePath)) {
|
|
8756
|
+
writeLine(` ${icons.error} ${colors.red}Agent file not found: ${agentFilePath}${RESET}`);
|
|
8757
|
+
writeLine();
|
|
8758
|
+
return;
|
|
8759
|
+
}
|
|
8760
|
+
const agentContent = readFileSync14(agentFilePath, "utf-8");
|
|
8761
|
+
const skillName = `${squadName}-${agentName}`;
|
|
8762
|
+
const outputDir = options.output || join18(dirname5(squadsDir), "skills", skillName);
|
|
8763
|
+
if (!existsSync17(outputDir)) {
|
|
8764
|
+
mkdirSync9(outputDir, { recursive: true });
|
|
8765
|
+
}
|
|
8766
|
+
const skillMd = convertAgentToSkill(agentContent, squadName, agentName);
|
|
8767
|
+
const skillMdPath = join18(outputDir, "SKILL.md");
|
|
8768
|
+
writeFileSync10(skillMdPath, skillMd);
|
|
8769
|
+
writeLine(` ${icons.success} ${colors.green}Converted:${RESET} ${agentPath}`);
|
|
8770
|
+
writeLine();
|
|
8771
|
+
writeLine(` ${colors.dim}Output:${RESET} ${outputDir}`);
|
|
8772
|
+
writeLine(` ${colors.dim}Files:${RESET}`);
|
|
8773
|
+
writeLine(` ${colors.cyan}SKILL.md${RESET}`);
|
|
8774
|
+
writeLine();
|
|
8775
|
+
writeLine(` ${colors.dim}Next steps:${RESET}`);
|
|
8776
|
+
writeLine(` 1. Review and edit ${colors.cyan}SKILL.md${RESET}`);
|
|
8777
|
+
writeLine(` 2. Add scripts or examples if needed`);
|
|
8778
|
+
writeLine(` 3. Upload: ${colors.cyan}squads skill upload ${outputDir}${RESET}`);
|
|
8779
|
+
writeLine();
|
|
8780
|
+
}
|
|
8781
|
+
function findAgentFile(squadsDir, agentName) {
|
|
8782
|
+
const { readdirSync: readdirSync9 } = __require("fs");
|
|
8783
|
+
const squads = readdirSync9(squadsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_")).map((d) => d.name);
|
|
8784
|
+
for (const squad of squads) {
|
|
8785
|
+
const agentPath = join18(squadsDir, squad, `${agentName}.md`);
|
|
8786
|
+
if (existsSync17(agentPath)) {
|
|
8787
|
+
return agentPath;
|
|
8788
|
+
}
|
|
8789
|
+
}
|
|
8790
|
+
return null;
|
|
8791
|
+
}
|
|
8792
|
+
function convertAgentToSkill(agentContent, squadName, agentName) {
|
|
8793
|
+
const existingMeta = {};
|
|
8794
|
+
let bodyContent = agentContent;
|
|
8795
|
+
const frontmatterMatch = agentContent.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)/);
|
|
8796
|
+
if (frontmatterMatch) {
|
|
8797
|
+
const frontmatter = frontmatterMatch[1];
|
|
8798
|
+
bodyContent = frontmatterMatch[2];
|
|
8799
|
+
for (const line of frontmatter.split("\n")) {
|
|
8800
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
8801
|
+
if (match) {
|
|
8802
|
+
existingMeta[match[1]] = match[2].trim();
|
|
8803
|
+
}
|
|
8804
|
+
}
|
|
8805
|
+
}
|
|
8806
|
+
const title = existingMeta.name || bodyContent.match(/^#\s+(?:Agent:\s*)?(.+)$/m)?.[1] || `${squadName}-${agentName}`;
|
|
8807
|
+
const roleMatch = bodyContent.match(/^(?:role|description):\s*(.+)$/mi) || bodyContent.match(/^##?\s*(?:Role|Purpose|Description)\s*\n+(.+)/mi);
|
|
8808
|
+
const description = existingMeta.description || roleMatch?.[1] || `Agent from ${squadName} squad. Use when working on ${agentName}-related tasks.`;
|
|
8809
|
+
const skillMd = `---
|
|
8810
|
+
name: ${squadName}-${agentName}
|
|
8811
|
+
description: ${description}
|
|
8812
|
+
---
|
|
8813
|
+
|
|
8814
|
+
# ${title}
|
|
8815
|
+
|
|
8816
|
+
${bodyContent.trim()}
|
|
8817
|
+
`;
|
|
8818
|
+
return skillMd;
|
|
8819
|
+
}
|
|
8820
|
+
function formatBytes(bytes) {
|
|
8821
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
8822
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
8823
|
+
return `${(bytes / 1024 / 1024).toFixed(2)}MB`;
|
|
8824
|
+
}
|
|
8825
|
+
|
|
8826
|
+
// src/commands/permissions.ts
|
|
8827
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
8828
|
+
import { join as join19 } from "path";
|
|
8829
|
+
function registerPermissionsCommand(program2) {
|
|
8830
|
+
const permissions = program2.command("permissions").alias("perms").description("Manage and validate squad permissions");
|
|
8831
|
+
permissions.command("show <squad>").description("Show permission context for a squad").action(permissionsShowCommand);
|
|
8832
|
+
permissions.command("check <squad>").description("Validate permissions before execution").option("-a, --agent <agent>", "Specify agent for context").option("--mcp <servers...>", "MCP servers to validate").option("--bash <commands...>", "Bash commands to validate").option("--write <paths...>", "Write paths to validate").option("--read <paths...>", "Read paths to validate").action(permissionsCheckCommand);
|
|
8833
|
+
}
|
|
8834
|
+
async function permissionsShowCommand(squadName) {
|
|
8835
|
+
writeLine();
|
|
8836
|
+
writeLine(` ${gradient("Permissions")} ${colors.dim}show${RESET} ${colors.cyan}${squadName}${RESET}`);
|
|
8837
|
+
writeLine();
|
|
8838
|
+
const squadsDir = findSquadsDir();
|
|
8839
|
+
if (!squadsDir) {
|
|
8840
|
+
writeLine(` ${icons.error} ${colors.red}No .agents/squads directory found${RESET}`);
|
|
8841
|
+
writeLine();
|
|
8842
|
+
return;
|
|
8843
|
+
}
|
|
8844
|
+
const squad = loadSquad(squadName);
|
|
8845
|
+
if (!squad) {
|
|
8846
|
+
writeLine(` ${icons.error} ${colors.red}Squad "${squadName}" not found${RESET}`);
|
|
8847
|
+
writeLine();
|
|
8848
|
+
return;
|
|
8849
|
+
}
|
|
8850
|
+
const squadFilePath = join19(squadsDir, squadName, "SQUAD.md");
|
|
8851
|
+
const squadContent = readFileSync15(squadFilePath, "utf-8");
|
|
8852
|
+
const context2 = buildContextFromSquad(squadName, squadContent);
|
|
8853
|
+
const defaults = getDefaultContext(squadName);
|
|
8854
|
+
const isDefault = JSON.stringify(context2.permissions) === JSON.stringify(defaults.permissions);
|
|
8855
|
+
writeLine(` ${bold}Squad:${RESET} ${squadName}`);
|
|
8856
|
+
if (squad.mission) {
|
|
8857
|
+
writeLine(` ${colors.dim}${squad.mission}${RESET}`);
|
|
8858
|
+
}
|
|
8859
|
+
writeLine();
|
|
8860
|
+
if (isDefault) {
|
|
8861
|
+
writeLine(` ${icons.warning} ${colors.yellow}Using default (permissive) permissions${RESET}`);
|
|
8862
|
+
writeLine(` ${colors.dim}Add a permissions block to SQUAD.md to restrict access.${RESET}`);
|
|
8863
|
+
writeLine();
|
|
8864
|
+
}
|
|
8865
|
+
writeLine(` ${bold}Mode:${RESET} ${formatMode(context2.permissions.mode)}`);
|
|
8866
|
+
writeLine();
|
|
8867
|
+
writeLine(` ${bold}Bash Commands:${RESET}`);
|
|
8868
|
+
if (context2.permissions.bash.includes("*")) {
|
|
8869
|
+
writeLine(` ${colors.dim}All commands allowed (*)${RESET}`);
|
|
8870
|
+
} else {
|
|
8871
|
+
for (const cmd of context2.permissions.bash) {
|
|
8872
|
+
writeLine(` ${icons.active} ${cmd}`);
|
|
8873
|
+
}
|
|
8874
|
+
}
|
|
8875
|
+
writeLine();
|
|
8876
|
+
writeLine(` ${bold}Write Paths:${RESET}`);
|
|
8877
|
+
if (context2.permissions.write.includes("**") || context2.permissions.write.includes("*")) {
|
|
8878
|
+
writeLine(` ${colors.dim}All paths writable (**)${RESET}`);
|
|
8879
|
+
} else {
|
|
8880
|
+
for (const path3 of context2.permissions.write) {
|
|
8881
|
+
writeLine(` ${icons.active} ${path3}`);
|
|
8882
|
+
}
|
|
8883
|
+
}
|
|
8884
|
+
writeLine();
|
|
8885
|
+
writeLine(` ${bold}Read Paths:${RESET}`);
|
|
8886
|
+
if (context2.permissions.read.includes("**") || context2.permissions.read.includes("*")) {
|
|
8887
|
+
writeLine(` ${colors.dim}All paths readable (**)${RESET}`);
|
|
8888
|
+
} else {
|
|
8889
|
+
for (const path3 of context2.permissions.read) {
|
|
8890
|
+
writeLine(` ${icons.active} ${path3}`);
|
|
8891
|
+
}
|
|
8892
|
+
}
|
|
8893
|
+
writeLine();
|
|
8894
|
+
writeLine(` ${bold}MCP Servers:${RESET}`);
|
|
8895
|
+
if (context2.permissions.mcp.allow.includes("*")) {
|
|
8896
|
+
writeLine(` ${colors.dim}All servers allowed (*)${RESET}`);
|
|
8897
|
+
} else {
|
|
8898
|
+
writeLine(` ${colors.green}Allow:${RESET}`);
|
|
8899
|
+
for (const server of context2.permissions.mcp.allow) {
|
|
8900
|
+
writeLine(` ${icons.success} ${server}`);
|
|
8901
|
+
}
|
|
8902
|
+
}
|
|
8903
|
+
if (context2.permissions.mcp.deny.length > 0) {
|
|
8904
|
+
writeLine(` ${colors.red}Deny:${RESET}`);
|
|
8905
|
+
for (const server of context2.permissions.mcp.deny) {
|
|
8906
|
+
writeLine(` ${icons.error} ${server}`);
|
|
8907
|
+
}
|
|
8908
|
+
}
|
|
8909
|
+
writeLine();
|
|
8910
|
+
if (isDefault) {
|
|
8911
|
+
writeLine(` ${bold}Add permissions to SQUAD.md:${RESET}`);
|
|
8912
|
+
writeLine();
|
|
8913
|
+
writeLine(` ${colors.dim}\`\`\`yaml${RESET}`);
|
|
8914
|
+
writeLine(` ${colors.dim}permissions:${RESET}`);
|
|
8915
|
+
writeLine(` ${colors.dim} mode: warn # or strict, audit${RESET}`);
|
|
8916
|
+
writeLine(` ${colors.dim} bash: [npm, git, gh]${RESET}`);
|
|
8917
|
+
writeLine(` ${colors.dim} write: [agents-squads-web/**]${RESET}`);
|
|
8918
|
+
writeLine(` ${colors.dim} read: [hq/.agents/**]${RESET}`);
|
|
8919
|
+
writeLine(` ${colors.dim} mcp:${RESET}`);
|
|
8920
|
+
writeLine(` ${colors.dim} allow: [chrome-devtools]${RESET}`);
|
|
8921
|
+
writeLine(` ${colors.dim} deny: [supabase]${RESET}`);
|
|
8922
|
+
writeLine(` ${colors.dim}\`\`\`${RESET}`);
|
|
8923
|
+
writeLine();
|
|
8924
|
+
}
|
|
8925
|
+
}
|
|
8926
|
+
async function permissionsCheckCommand(squadName, options) {
|
|
8927
|
+
writeLine();
|
|
8928
|
+
writeLine(` ${gradient("Permissions")} ${colors.dim}check${RESET} ${colors.cyan}${squadName}${RESET}`);
|
|
8929
|
+
if (options.agent) {
|
|
8930
|
+
writeLine(` ${colors.dim}Agent: ${options.agent}${RESET}`);
|
|
8931
|
+
}
|
|
8932
|
+
writeLine();
|
|
8933
|
+
const squadsDir = findSquadsDir();
|
|
8934
|
+
if (!squadsDir) {
|
|
8935
|
+
writeLine(` ${icons.error} ${colors.red}No .agents/squads directory found${RESET}`);
|
|
8936
|
+
writeLine();
|
|
8937
|
+
return;
|
|
8938
|
+
}
|
|
8939
|
+
const squad = loadSquad(squadName);
|
|
8940
|
+
if (!squad) {
|
|
8941
|
+
writeLine(` ${icons.error} ${colors.red}Squad "${squadName}" not found${RESET}`);
|
|
8942
|
+
writeLine();
|
|
8943
|
+
return;
|
|
8944
|
+
}
|
|
8945
|
+
const squadFilePath = join19(squadsDir, squadName, "SQUAD.md");
|
|
8946
|
+
const squadContent = readFileSync15(squadFilePath, "utf-8");
|
|
8947
|
+
const context2 = buildContextFromSquad(squadName, squadContent, options.agent);
|
|
8948
|
+
const request = {
|
|
8949
|
+
mcpServers: options.mcp,
|
|
8950
|
+
bashCommands: options.bash,
|
|
8951
|
+
writePaths: options.write,
|
|
8952
|
+
readPaths: options.read
|
|
8953
|
+
};
|
|
8954
|
+
if (!options.mcp && !options.bash && !options.write && !options.read) {
|
|
8955
|
+
writeLine(` ${colors.yellow}No resources specified to check.${RESET}`);
|
|
8956
|
+
writeLine();
|
|
8957
|
+
writeLine(` ${colors.dim}Examples:${RESET}`);
|
|
8958
|
+
writeLine(` ${colors.cyan}squads perms check ${squadName} --mcp chrome-devtools firecrawl${RESET}`);
|
|
8959
|
+
writeLine(` ${colors.cyan}squads perms check ${squadName} --bash npm git docker${RESET}`);
|
|
8960
|
+
writeLine(` ${colors.cyan}squads perms check ${squadName} --write src/** --read config/**${RESET}`);
|
|
8961
|
+
writeLine();
|
|
8962
|
+
return;
|
|
8963
|
+
}
|
|
8964
|
+
const result = validateExecution(context2, request);
|
|
8965
|
+
if (result.violations.length === 0) {
|
|
8966
|
+
writeLine(` ${icons.success} ${colors.green}All permissions valid${RESET}`);
|
|
8967
|
+
writeLine();
|
|
8968
|
+
if (options.mcp) {
|
|
8969
|
+
writeLine(` ${colors.dim}MCP:${RESET} ${options.mcp.join(", ")}`);
|
|
8970
|
+
}
|
|
8971
|
+
if (options.bash) {
|
|
8972
|
+
writeLine(` ${colors.dim}Bash:${RESET} ${options.bash.join(", ")}`);
|
|
8973
|
+
}
|
|
8974
|
+
if (options.write) {
|
|
8975
|
+
writeLine(` ${colors.dim}Write:${RESET} ${options.write.join(", ")}`);
|
|
8976
|
+
}
|
|
8977
|
+
if (options.read) {
|
|
8978
|
+
writeLine(` ${colors.dim}Read:${RESET} ${options.read.join(", ")}`);
|
|
8979
|
+
}
|
|
8980
|
+
writeLine();
|
|
8981
|
+
} else {
|
|
8982
|
+
const violationLines = formatViolations(result);
|
|
8983
|
+
for (const line of violationLines) {
|
|
8984
|
+
writeLine(` ${line}`);
|
|
8985
|
+
}
|
|
8986
|
+
writeLine();
|
|
8987
|
+
if (!result.allowed) {
|
|
8988
|
+
process.exit(1);
|
|
8989
|
+
}
|
|
8990
|
+
}
|
|
8991
|
+
}
|
|
8992
|
+
function formatMode(mode) {
|
|
8993
|
+
switch (mode) {
|
|
8994
|
+
case "strict":
|
|
8995
|
+
return `${colors.red}strict${RESET} ${colors.dim}(blocks on violation)${RESET}`;
|
|
8996
|
+
case "warn":
|
|
8997
|
+
return `${colors.yellow}warn${RESET} ${colors.dim}(logs and continues)${RESET}`;
|
|
8998
|
+
case "audit":
|
|
8999
|
+
return `${colors.cyan}audit${RESET} ${colors.dim}(logs to trail)${RESET}`;
|
|
9000
|
+
default:
|
|
9001
|
+
return mode;
|
|
9002
|
+
}
|
|
9003
|
+
}
|
|
9004
|
+
|
|
9005
|
+
// src/commands/context.ts
|
|
9006
|
+
async function contextShowCommand(squadName, options = {}) {
|
|
9007
|
+
await track(Events.CLI_CONTEXT, { squad: squadName });
|
|
9008
|
+
const squadsDir = findSquadsDir();
|
|
9009
|
+
if (!squadsDir) {
|
|
9010
|
+
writeLine(`${colors.red}No .agents/squads directory found${RESET}`);
|
|
9011
|
+
writeLine(`${colors.dim}Run \`squads init\` to create one.${RESET}`);
|
|
9012
|
+
process.exit(1);
|
|
9013
|
+
}
|
|
9014
|
+
const squad = loadSquad(squadName);
|
|
9015
|
+
if (!squad) {
|
|
9016
|
+
writeLine(`${colors.red}Squad "${squadName}" not found.${RESET}`);
|
|
9017
|
+
process.exit(1);
|
|
9018
|
+
}
|
|
9019
|
+
if (options.json) {
|
|
9020
|
+
console.log(JSON.stringify({
|
|
9021
|
+
name: squad.name,
|
|
9022
|
+
mission: squad.mission,
|
|
9023
|
+
repo: squad.repo,
|
|
9024
|
+
stack: squad.stack,
|
|
9025
|
+
effort: squad.effort,
|
|
9026
|
+
context: squad.context
|
|
9027
|
+
}, null, 2));
|
|
9028
|
+
return;
|
|
9029
|
+
}
|
|
9030
|
+
writeLine();
|
|
9031
|
+
writeLine(` ${gradient("squads")} ${colors.dim}context${RESET} ${colors.cyan}${squad.name}${RESET}`);
|
|
9032
|
+
const ctx = squad.context;
|
|
9033
|
+
if (!ctx) {
|
|
9034
|
+
writeLine();
|
|
9035
|
+
writeLine(` ${colors.yellow}No context defined in SQUAD.md frontmatter${RESET}`);
|
|
9036
|
+
writeLine();
|
|
9037
|
+
writeLine(` ${colors.dim}Add a frontmatter block to ${squad.name}/SQUAD.md:${RESET}`);
|
|
9038
|
+
writeLine();
|
|
9039
|
+
writeLine(` ${colors.dim}---${RESET}`);
|
|
9040
|
+
writeLine(` ${colors.dim}name: ${squad.name}${RESET}`);
|
|
9041
|
+
writeLine(` ${colors.dim}context:${RESET}`);
|
|
9042
|
+
writeLine(` ${colors.dim} mcp: [chrome-devtools]${RESET}`);
|
|
9043
|
+
writeLine(` ${colors.dim} model: { default: sonnet }${RESET}`);
|
|
9044
|
+
writeLine(` ${colors.dim} budget: { daily: 10 }${RESET}`);
|
|
9045
|
+
writeLine(` ${colors.dim}---${RESET}`);
|
|
9046
|
+
writeLine();
|
|
9047
|
+
return;
|
|
9048
|
+
}
|
|
9049
|
+
const tableWidth = 54;
|
|
9050
|
+
writeLine();
|
|
9051
|
+
writeLine(` ${colors.purple}${box.horizontal.repeat(tableWidth)}${RESET}`);
|
|
9052
|
+
if (ctx.mcp && ctx.mcp.length > 0) {
|
|
9053
|
+
writeLine(` ${bold}MCP${RESET} ${colors.cyan}${ctx.mcp.join(", ")}${RESET}`);
|
|
9054
|
+
} else {
|
|
9055
|
+
writeLine(` ${bold}MCP${RESET} ${colors.dim}none${RESET}`);
|
|
9056
|
+
}
|
|
9057
|
+
if (ctx.skills && ctx.skills.length > 0) {
|
|
9058
|
+
writeLine(` ${bold}Skills${RESET} ${colors.cyan}${ctx.skills.join(", ")}${RESET}`);
|
|
9059
|
+
}
|
|
9060
|
+
if (ctx.memory?.load && ctx.memory.load.length > 0) {
|
|
9061
|
+
writeLine(` ${bold}Memory${RESET} ${colors.cyan}${ctx.memory.load.join(", ")}${RESET}`);
|
|
9062
|
+
}
|
|
9063
|
+
if (ctx.model) {
|
|
9064
|
+
const modelParts = [];
|
|
9065
|
+
if (ctx.model.default) modelParts.push(`${colors.white}${ctx.model.default}${RESET} ${colors.dim}(default)${RESET}`);
|
|
9066
|
+
if (ctx.model.expensive) modelParts.push(`${colors.yellow}${ctx.model.expensive}${RESET} ${colors.dim}(expensive)${RESET}`);
|
|
9067
|
+
if (ctx.model.cheap) modelParts.push(`${colors.green}${ctx.model.cheap}${RESET} ${colors.dim}(cheap)${RESET}`);
|
|
9068
|
+
writeLine(` ${bold}Model${RESET} ${modelParts.join(", ")}`);
|
|
9069
|
+
}
|
|
9070
|
+
if (ctx.budget) {
|
|
9071
|
+
const budgetParts = [];
|
|
9072
|
+
if (ctx.budget.daily) budgetParts.push(`$${ctx.budget.daily}/day`);
|
|
9073
|
+
if (ctx.budget.weekly) budgetParts.push(`$${ctx.budget.weekly}/week`);
|
|
9074
|
+
if (ctx.budget.perExecution) budgetParts.push(`$${ctx.budget.perExecution}/run`);
|
|
9075
|
+
writeLine(` ${bold}Budget${RESET} ${colors.green}${budgetParts.join(", ")}${RESET}`);
|
|
9076
|
+
}
|
|
9077
|
+
if (squad.effort) {
|
|
9078
|
+
const effortColor = squad.effort === "high" ? colors.red : squad.effort === "medium" ? colors.yellow : colors.green;
|
|
9079
|
+
writeLine(` ${bold}Effort${RESET} ${effortColor}${squad.effort}${RESET}`);
|
|
9080
|
+
}
|
|
9081
|
+
if (squad.repo) {
|
|
9082
|
+
writeLine(` ${bold}Repo${RESET} ${colors.dim}${squad.repo}${RESET}`);
|
|
9083
|
+
}
|
|
9084
|
+
if (squad.stack) {
|
|
9085
|
+
writeLine(` ${bold}Stack${RESET} ${colors.dim}${squad.stack}${RESET}`);
|
|
9086
|
+
}
|
|
9087
|
+
writeLine(` ${colors.purple}${box.horizontal.repeat(tableWidth)}${RESET}`);
|
|
9088
|
+
writeLine();
|
|
9089
|
+
writeLine(` ${colors.dim}$${RESET} squads context show ${colors.cyan}${squad.name}${RESET} --json ${colors.dim}JSON output${RESET}`);
|
|
9090
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET} ${colors.dim}Run with this context${RESET}`);
|
|
9091
|
+
writeLine();
|
|
9092
|
+
}
|
|
9093
|
+
async function contextListCommand(options = {}) {
|
|
9094
|
+
await track(Events.CLI_CONTEXT, { action: "list" });
|
|
9095
|
+
const squadsDir = findSquadsDir();
|
|
9096
|
+
if (!squadsDir) {
|
|
9097
|
+
writeLine(`${colors.red}No .agents/squads directory found${RESET}`);
|
|
9098
|
+
process.exit(1);
|
|
9099
|
+
}
|
|
9100
|
+
const squads = listSquads(squadsDir);
|
|
9101
|
+
if (options.json) {
|
|
9102
|
+
const contexts = {};
|
|
9103
|
+
for (const name of squads) {
|
|
9104
|
+
const squad = loadSquad(name);
|
|
9105
|
+
if (squad) {
|
|
9106
|
+
contexts[name] = squad.context;
|
|
9107
|
+
}
|
|
9108
|
+
}
|
|
9109
|
+
console.log(JSON.stringify(contexts, null, 2));
|
|
9110
|
+
return;
|
|
9111
|
+
}
|
|
9112
|
+
writeLine();
|
|
9113
|
+
writeLine(` ${gradient("squads")} ${colors.dim}context list${RESET}`);
|
|
9114
|
+
writeLine();
|
|
9115
|
+
const w = { name: 14, mcp: 24, model: 12, budget: 12 };
|
|
9116
|
+
const tableWidth = w.name + w.mcp + w.model + w.budget + 6;
|
|
9117
|
+
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
9118
|
+
const header = ` ${colors.purple}${box.vertical}${RESET} ${bold}${padEnd("SQUAD", w.name)}${RESET}${bold}${padEnd("MCP", w.mcp)}${RESET}${bold}${padEnd("MODEL", w.model)}${RESET}${bold}BUDGET${RESET} ${colors.purple}${box.vertical}${RESET}`;
|
|
9119
|
+
writeLine(header);
|
|
9120
|
+
writeLine(` ${colors.purple}${box.teeRight}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.teeLeft}${RESET}`);
|
|
9121
|
+
for (const name of squads) {
|
|
9122
|
+
const squad = loadSquad(name);
|
|
9123
|
+
const ctx = squad?.context;
|
|
9124
|
+
const mcpStr = ctx?.mcp?.slice(0, 2).join(", ") || `${colors.dim}\u2014${RESET}`;
|
|
9125
|
+
const modelStr = ctx?.model?.default || `${colors.dim}\u2014${RESET}`;
|
|
9126
|
+
const budgetStr = ctx?.budget?.daily ? `$${ctx.budget.daily}/d` : `${colors.dim}\u2014${RESET}`;
|
|
9127
|
+
const row = ` ${colors.purple}${box.vertical}${RESET} ${colors.cyan}${padEnd(name, w.name)}${RESET}${padEnd(mcpStr, w.mcp)}${padEnd(modelStr, w.model)}${padEnd(budgetStr, w.budget)}${colors.purple}${box.vertical}${RESET}`;
|
|
9128
|
+
writeLine(row);
|
|
9129
|
+
}
|
|
9130
|
+
writeLine(` ${colors.purple}${box.bottomLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.bottomRight}${RESET}`);
|
|
9131
|
+
writeLine();
|
|
9132
|
+
}
|
|
9133
|
+
|
|
9134
|
+
// src/commands/cost.ts
|
|
9135
|
+
function getBudgetStatus(squadName, spent, dailyBudget, weeklyBudget) {
|
|
9136
|
+
let status = "no-budget";
|
|
9137
|
+
let dailyPercent = null;
|
|
9138
|
+
let weeklyPercent = null;
|
|
9139
|
+
if (dailyBudget !== null) {
|
|
9140
|
+
dailyPercent = spent / dailyBudget * 100;
|
|
9141
|
+
if (dailyPercent >= 100) {
|
|
9142
|
+
status = "over";
|
|
9143
|
+
} else if (dailyPercent >= 80) {
|
|
9144
|
+
status = "warning";
|
|
9145
|
+
} else {
|
|
9146
|
+
status = "ok";
|
|
9147
|
+
}
|
|
9148
|
+
}
|
|
9149
|
+
if (weeklyBudget !== null) {
|
|
9150
|
+
weeklyPercent = spent / weeklyBudget * 100;
|
|
9151
|
+
}
|
|
9152
|
+
return {
|
|
9153
|
+
squad: squadName,
|
|
9154
|
+
spent,
|
|
9155
|
+
dailyBudget,
|
|
9156
|
+
weeklyBudget,
|
|
9157
|
+
dailyPercent,
|
|
9158
|
+
weeklyPercent,
|
|
9159
|
+
status
|
|
9160
|
+
};
|
|
9161
|
+
}
|
|
9162
|
+
async function costCommand(options = {}) {
|
|
9163
|
+
await track(Events.CLI_COST, { squad: options.squad || "all" });
|
|
9164
|
+
const stats = await fetchBridgeStats();
|
|
9165
|
+
const plan = detectPlan();
|
|
9166
|
+
if (options.json) {
|
|
9167
|
+
const result = buildJsonOutput(stats, plan, options.squad);
|
|
9168
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9169
|
+
return;
|
|
9170
|
+
}
|
|
9171
|
+
writeLine();
|
|
9172
|
+
writeLine(` ${gradient("squads")} ${colors.dim}cost${RESET}${options.squad ? ` ${colors.cyan}${options.squad}${RESET}` : ""}`);
|
|
9173
|
+
writeLine();
|
|
9174
|
+
if (!stats) {
|
|
9175
|
+
writeLine(` ${colors.yellow}\u26A0 Bridge unavailable${RESET}`);
|
|
9176
|
+
writeLine(` ${colors.dim}Run \`squads stack up\` to start infrastructure${RESET}`);
|
|
9177
|
+
writeLine();
|
|
9178
|
+
return;
|
|
9179
|
+
}
|
|
9180
|
+
const todaySection = ` ${bold}Today${RESET}`;
|
|
9181
|
+
writeLine(todaySection);
|
|
9182
|
+
writeLine(` ${colors.cyan}$${stats.today.costUsd.toFixed(2)}${RESET} ${colors.dim}|${RESET} ${stats.today.generations.toLocaleString()} calls ${colors.dim}|${RESET} ${formatTokens(stats.today.inputTokens + stats.today.outputTokens)} tokens`);
|
|
9183
|
+
if (stats.byModel && stats.byModel.length > 0) {
|
|
9184
|
+
const modelParts = stats.byModel.filter((m) => m.costUsd > 0).sort((a, b) => b.costUsd - a.costUsd).slice(0, 4).map((m) => `${colors.dim}${shortModelName(m.model)}${RESET} $${m.costUsd.toFixed(0)}`);
|
|
9185
|
+
if (modelParts.length > 0) {
|
|
9186
|
+
writeLine(` ${colors.dim}Models:${RESET} ${modelParts.join(" \xB7 ")}`);
|
|
9187
|
+
}
|
|
9188
|
+
}
|
|
9189
|
+
writeLine();
|
|
9190
|
+
if (stats.week) {
|
|
9191
|
+
writeLine(` ${bold}Week${RESET}`);
|
|
9192
|
+
writeLine(` ${colors.purple}$${stats.week.costUsd.toFixed(2)}${RESET} ${colors.dim}|${RESET} ${stats.week.generations.toLocaleString()} calls ${colors.dim}|${RESET} ${formatTokens(stats.week.inputTokens + stats.week.outputTokens)} tokens`);
|
|
9193
|
+
writeLine();
|
|
9194
|
+
}
|
|
9195
|
+
writeLine(` ${bold}Budget${RESET} ${colors.dim}(daily)${RESET}`);
|
|
9196
|
+
const budgetBar = createBudgetBar(stats.budget.usedPct);
|
|
9197
|
+
const budgetColor = stats.budget.usedPct >= 100 ? colors.red : stats.budget.usedPct >= 80 ? colors.yellow : colors.green;
|
|
9198
|
+
writeLine(` ${budgetBar} ${budgetColor}$${stats.budget.used.toFixed(0)}${RESET}/${colors.dim}$${stats.budget.daily}${RESET} (${stats.budget.usedPct.toFixed(0)}%)`);
|
|
9199
|
+
writeLine();
|
|
9200
|
+
if (stats.bySquad && stats.bySquad.length > 0 && !options.squad) {
|
|
9201
|
+
writeLine(` ${bold}By Squad${RESET}`);
|
|
9202
|
+
writeLine();
|
|
9203
|
+
const squadsDir = findSquadsDir();
|
|
9204
|
+
const squadBudgets = /* @__PURE__ */ new Map();
|
|
9205
|
+
if (squadsDir) {
|
|
9206
|
+
for (const name of listSquads(squadsDir)) {
|
|
9207
|
+
const squad = loadSquad(name);
|
|
9208
|
+
if (squad?.context?.budget) {
|
|
9209
|
+
squadBudgets.set(name, {
|
|
9210
|
+
daily: squad.context.budget.daily || null,
|
|
9211
|
+
weekly: squad.context.budget.weekly || null
|
|
9212
|
+
});
|
|
9213
|
+
}
|
|
9214
|
+
}
|
|
9215
|
+
}
|
|
9216
|
+
const tableWidth = 52;
|
|
9217
|
+
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
9218
|
+
const w = { name: 14, spent: 10, budget: 12, status: 14 };
|
|
9219
|
+
const header = ` ${colors.purple}${box.vertical}${RESET} ${bold}${padEnd("SQUAD", w.name)}${RESET}${bold}${padEnd("SPENT", w.spent)}${RESET}${bold}${padEnd("BUDGET", w.budget)}${RESET}${bold}STATUS${RESET} ${colors.purple}${box.vertical}${RESET}`;
|
|
9220
|
+
writeLine(header);
|
|
9221
|
+
writeLine(` ${colors.purple}${box.teeRight}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.teeLeft}${RESET}`);
|
|
9222
|
+
for (const sq of stats.bySquad.sort((a, b) => b.costUsd - a.costUsd)) {
|
|
9223
|
+
const budgetInfo = squadBudgets.get(sq.squad);
|
|
9224
|
+
const dailyBudget = budgetInfo?.daily || null;
|
|
9225
|
+
const spentStr = `$${sq.costUsd.toFixed(2)}`;
|
|
9226
|
+
const budgetStr = dailyBudget ? `$${dailyBudget}/d` : `${colors.dim}\u2014${RESET}`;
|
|
9227
|
+
let statusStr;
|
|
9228
|
+
if (dailyBudget) {
|
|
9229
|
+
const pct = sq.costUsd / dailyBudget * 100;
|
|
9230
|
+
if (pct >= 100) {
|
|
9231
|
+
statusStr = `${colors.red}\u25CF OVER${RESET}`;
|
|
9232
|
+
} else if (pct >= 80) {
|
|
9233
|
+
statusStr = `${colors.yellow}\u25CF ${pct.toFixed(0)}%${RESET}`;
|
|
9234
|
+
} else {
|
|
9235
|
+
statusStr = `${colors.green}\u2713 ${pct.toFixed(0)}%${RESET}`;
|
|
9236
|
+
}
|
|
9237
|
+
} else {
|
|
9238
|
+
statusStr = `${colors.dim}no budget${RESET}`;
|
|
9239
|
+
}
|
|
9240
|
+
const row = ` ${colors.purple}${box.vertical}${RESET} ${colors.cyan}${padEnd(sq.squad, w.name)}${RESET}${padEnd(spentStr, w.spent)}${padEnd(budgetStr, w.budget)}${padEnd(statusStr, w.status)}${colors.purple}${box.vertical}${RESET}`;
|
|
9241
|
+
writeLine(row);
|
|
9242
|
+
}
|
|
9243
|
+
writeLine(` ${colors.purple}${box.bottomLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.bottomRight}${RESET}`);
|
|
9244
|
+
writeLine();
|
|
9245
|
+
}
|
|
9246
|
+
if (options.squad) {
|
|
9247
|
+
const squadStats = stats.bySquad?.find((s) => s.squad === options.squad);
|
|
9248
|
+
const squad = loadSquad(options.squad);
|
|
9249
|
+
if (squadStats) {
|
|
9250
|
+
writeLine(` ${bold}Squad: ${options.squad}${RESET}`);
|
|
9251
|
+
writeLine(` Spent today: ${colors.cyan}$${squadStats.costUsd.toFixed(2)}${RESET}`);
|
|
9252
|
+
writeLine(` Calls: ${squadStats.generations.toLocaleString()}`);
|
|
9253
|
+
if (squad?.context?.budget) {
|
|
9254
|
+
const daily = squad.context.budget.daily;
|
|
9255
|
+
const weekly = squad.context.budget.weekly;
|
|
9256
|
+
if (daily) {
|
|
9257
|
+
const pct = squadStats.costUsd / daily * 100;
|
|
9258
|
+
const statusIcon = pct >= 100 ? `${colors.red}\u25CF${RESET}` : pct >= 80 ? `${colors.yellow}\u25CF${RESET}` : `${colors.green}\u2713${RESET}`;
|
|
9259
|
+
writeLine(` Daily budget: ${statusIcon} $${squadStats.costUsd.toFixed(2)}/$${daily} (${pct.toFixed(0)}%)`);
|
|
9260
|
+
}
|
|
9261
|
+
if (weekly) {
|
|
9262
|
+
writeLine(` Weekly budget: $${weekly}/week`);
|
|
9263
|
+
}
|
|
9264
|
+
} else {
|
|
9265
|
+
writeLine(` ${colors.dim}No budget defined in SQUAD.md frontmatter${RESET}`);
|
|
9266
|
+
}
|
|
9267
|
+
} else {
|
|
9268
|
+
writeLine(` ${colors.dim}No cost data for squad "${options.squad}"${RESET}`);
|
|
9269
|
+
}
|
|
9270
|
+
writeLine();
|
|
9271
|
+
}
|
|
9272
|
+
writeLine(` ${colors.dim}Plan: ${plan.plan} (${plan.reason})${RESET}`);
|
|
9273
|
+
writeLine();
|
|
9274
|
+
}
|
|
9275
|
+
async function budgetCheckCommand(squadName, options = {}) {
|
|
9276
|
+
await track(Events.CLI_COST, { action: "budget-check", squad: squadName });
|
|
9277
|
+
const stats = await fetchBridgeStats();
|
|
9278
|
+
const squad = loadSquad(squadName);
|
|
9279
|
+
if (!stats) {
|
|
9280
|
+
if (options.json) {
|
|
9281
|
+
console.log(JSON.stringify({ error: "Bridge unavailable" }));
|
|
9282
|
+
} else {
|
|
9283
|
+
writeLine(` ${colors.yellow}\u26A0 Bridge unavailable${RESET}`);
|
|
9284
|
+
}
|
|
9285
|
+
return;
|
|
9286
|
+
}
|
|
9287
|
+
const squadStats = stats.bySquad?.find((s) => s.squad === squadName);
|
|
9288
|
+
const spent = squadStats?.costUsd || 0;
|
|
9289
|
+
const dailyBudget = squad?.context?.budget?.daily || null;
|
|
9290
|
+
const weeklyBudget = squad?.context?.budget?.weekly || null;
|
|
9291
|
+
const status = getBudgetStatus(squadName, spent, dailyBudget, weeklyBudget);
|
|
9292
|
+
if (options.json) {
|
|
9293
|
+
console.log(JSON.stringify(status, null, 2));
|
|
9294
|
+
return;
|
|
9295
|
+
}
|
|
9296
|
+
writeLine();
|
|
9297
|
+
if (status.status === "no-budget") {
|
|
9298
|
+
writeLine(` ${colors.dim}\u25CB${RESET} ${colors.cyan}${squadName}${RESET}: No budget defined`);
|
|
9299
|
+
writeLine(` ${colors.dim}Add budget to SQUAD.md frontmatter:${RESET}`);
|
|
9300
|
+
writeLine(` ${colors.dim} context:${RESET}`);
|
|
9301
|
+
writeLine(` ${colors.dim} budget: { daily: 50 }${RESET}`);
|
|
9302
|
+
} else if (status.status === "over") {
|
|
9303
|
+
writeLine(` ${colors.red}\u25CF${RESET} ${colors.cyan}${squadName}${RESET}: ${colors.red}OVER BUDGET${RESET}`);
|
|
9304
|
+
writeLine(` $${spent.toFixed(2)}/$${dailyBudget} daily (${status.dailyPercent?.toFixed(0)}%)`);
|
|
9305
|
+
} else if (status.status === "warning") {
|
|
9306
|
+
writeLine(` ${colors.yellow}\u25CF${RESET} ${colors.cyan}${squadName}${RESET}: ${colors.yellow}Approaching limit${RESET}`);
|
|
9307
|
+
writeLine(` $${spent.toFixed(2)}/$${dailyBudget} daily (${status.dailyPercent?.toFixed(0)}%)`);
|
|
9308
|
+
} else {
|
|
9309
|
+
writeLine(` ${colors.green}\u2713${RESET} ${colors.cyan}${squadName}${RESET}: OK to proceed`);
|
|
9310
|
+
writeLine(` $${spent.toFixed(2)}/$${dailyBudget} daily (${status.dailyPercent?.toFixed(0)}%)`);
|
|
9311
|
+
}
|
|
9312
|
+
writeLine();
|
|
9313
|
+
}
|
|
9314
|
+
function buildJsonOutput(stats, plan, squadFilter) {
|
|
9315
|
+
if (!stats) {
|
|
9316
|
+
return { error: "Bridge unavailable", plan };
|
|
9317
|
+
}
|
|
9318
|
+
const squadsDir = findSquadsDir();
|
|
9319
|
+
const squadBudgets = {};
|
|
9320
|
+
if (squadsDir) {
|
|
9321
|
+
for (const name of listSquads(squadsDir)) {
|
|
9322
|
+
const squad = loadSquad(name);
|
|
9323
|
+
if (squad?.context?.budget) {
|
|
9324
|
+
squadBudgets[name] = {
|
|
9325
|
+
daily: squad.context.budget.daily || null,
|
|
9326
|
+
weekly: squad.context.budget.weekly || null
|
|
9327
|
+
};
|
|
9328
|
+
}
|
|
9329
|
+
}
|
|
9330
|
+
}
|
|
9331
|
+
const bySquadWithBudget = stats.bySquad?.map((sq) => ({
|
|
9332
|
+
...sq,
|
|
9333
|
+
budget: squadBudgets[sq.squad] || null,
|
|
9334
|
+
budgetStatus: squadBudgets[sq.squad]?.daily ? getBudgetStatus(sq.squad, sq.costUsd, squadBudgets[sq.squad].daily, squadBudgets[sq.squad].weekly) : null
|
|
9335
|
+
}));
|
|
9336
|
+
if (squadFilter) {
|
|
9337
|
+
const filtered = bySquadWithBudget?.find((s) => s.squad === squadFilter);
|
|
9338
|
+
return {
|
|
9339
|
+
squad: squadFilter,
|
|
9340
|
+
...filtered,
|
|
9341
|
+
plan
|
|
9342
|
+
};
|
|
9343
|
+
}
|
|
9344
|
+
return {
|
|
9345
|
+
today: stats.today,
|
|
9346
|
+
week: stats.week,
|
|
9347
|
+
budget: stats.budget,
|
|
9348
|
+
bySquad: bySquadWithBudget,
|
|
9349
|
+
byModel: stats.byModel,
|
|
9350
|
+
plan,
|
|
9351
|
+
source: stats.source
|
|
9352
|
+
};
|
|
9353
|
+
}
|
|
9354
|
+
function shortModelName(model) {
|
|
9355
|
+
if (model.includes("opus")) return "opus";
|
|
9356
|
+
if (model.includes("sonnet")) return "sonnet";
|
|
9357
|
+
if (model.includes("haiku")) return "haiku";
|
|
9358
|
+
return model.split("-")[0];
|
|
9359
|
+
}
|
|
9360
|
+
function formatTokens(tokens) {
|
|
9361
|
+
if (tokens >= 1e6) {
|
|
9362
|
+
return `${(tokens / 1e6).toFixed(1)}M`;
|
|
9363
|
+
}
|
|
9364
|
+
if (tokens >= 1e3) {
|
|
9365
|
+
return `${(tokens / 1e3).toFixed(0)}k`;
|
|
9366
|
+
}
|
|
9367
|
+
return tokens.toString();
|
|
9368
|
+
}
|
|
9369
|
+
function createBudgetBar(percent, width = 10) {
|
|
9370
|
+
const filled = Math.min(Math.round(percent / 100 * width), width);
|
|
9371
|
+
const empty = width - filled;
|
|
9372
|
+
const filledChar = "\u2501";
|
|
9373
|
+
const emptyChar = "\u2501";
|
|
9374
|
+
const color = percent >= 100 ? colors.red : percent >= 80 ? colors.yellow : colors.green;
|
|
9375
|
+
return `${color}${filledChar.repeat(filled)}${colors.dim}${emptyChar.repeat(empty)}${RESET}`;
|
|
9376
|
+
}
|
|
9377
|
+
|
|
9378
|
+
// src/lib/executions.ts
|
|
9379
|
+
import { readFileSync as readFileSync16, existsSync as existsSync18, readdirSync as readdirSync8 } from "fs";
|
|
9380
|
+
import { join as join20 } from "path";
|
|
9381
|
+
function parseExecutionEntry(content, squad, agent) {
|
|
9382
|
+
const idMatch = content.match(/<!-- exec:(\S+) -->/);
|
|
9383
|
+
if (!idMatch) return null;
|
|
9384
|
+
const id = idMatch[1];
|
|
9385
|
+
const headerMatch = content.match(/\*\*([^*]+)\*\* \| Status: (\w+)/);
|
|
9386
|
+
if (!headerMatch) return null;
|
|
9387
|
+
const startTime = headerMatch[1].trim();
|
|
9388
|
+
const status = headerMatch[2];
|
|
9389
|
+
const triggerMatch = content.match(/- Trigger: (\w+)/);
|
|
9390
|
+
const taskTypeMatch = content.match(/- Task Type: (\w+)/);
|
|
9391
|
+
const completedMatch = content.match(/- Completed: ([^\n]+)/);
|
|
9392
|
+
const durationMatch = content.match(/- Duration: ([^\n]+)/);
|
|
9393
|
+
const outcomeMatch = content.match(/- Outcome: ([^\n]+)/);
|
|
9394
|
+
const errorMatch = content.match(/- Error: ([^\n]+)/);
|
|
9395
|
+
let durationMs;
|
|
9396
|
+
if (durationMatch) {
|
|
9397
|
+
const durationStr = durationMatch[1].trim();
|
|
9398
|
+
const secMatch = durationStr.match(/^([\d.]+)s$/);
|
|
9399
|
+
if (secMatch) {
|
|
9400
|
+
durationMs = parseFloat(secMatch[1]) * 1e3;
|
|
9401
|
+
}
|
|
9402
|
+
}
|
|
9403
|
+
return {
|
|
9404
|
+
id,
|
|
9405
|
+
squad,
|
|
9406
|
+
agent,
|
|
9407
|
+
startTime,
|
|
9408
|
+
endTime: completedMatch?.[1]?.trim(),
|
|
9409
|
+
durationMs,
|
|
9410
|
+
status,
|
|
9411
|
+
trigger: triggerMatch?.[1] || "manual",
|
|
9412
|
+
taskType: taskTypeMatch?.[1] || "execution",
|
|
9413
|
+
outcome: outcomeMatch?.[1]?.trim(),
|
|
9414
|
+
error: errorMatch?.[1]?.trim()
|
|
9415
|
+
};
|
|
9416
|
+
}
|
|
9417
|
+
function parseExecutionLog(filePath, squad, agent) {
|
|
9418
|
+
if (!existsSync18(filePath)) return [];
|
|
9419
|
+
const content = readFileSync16(filePath, "utf-8");
|
|
9420
|
+
const executions = [];
|
|
9421
|
+
const entries = content.split(/\n---\n/);
|
|
9422
|
+
for (const entry of entries) {
|
|
9423
|
+
if (!entry.includes("<!-- exec:")) continue;
|
|
9424
|
+
const execution = parseExecutionEntry(entry, squad, agent);
|
|
9425
|
+
if (execution) {
|
|
9426
|
+
executions.push(execution);
|
|
9427
|
+
}
|
|
9428
|
+
}
|
|
9429
|
+
for (const entry of entries) {
|
|
9430
|
+
if (entry.includes("<!-- exec:")) continue;
|
|
9431
|
+
const headerMatch = entry.match(/\*\*([^*]+)\*\* \| Status: (\w+)/);
|
|
9432
|
+
if (!headerMatch) continue;
|
|
9433
|
+
const startTime = headerMatch[1].trim();
|
|
9434
|
+
const status = headerMatch[2];
|
|
9435
|
+
const legacyId = `legacy_${startTime.replace(/[^a-z0-9]/gi, "")}`;
|
|
9436
|
+
if (executions.some((e) => e.startTime === startTime)) continue;
|
|
9437
|
+
executions.push({
|
|
9438
|
+
id: legacyId,
|
|
9439
|
+
squad,
|
|
9440
|
+
agent,
|
|
9441
|
+
startTime,
|
|
9442
|
+
status,
|
|
9443
|
+
trigger: "manual",
|
|
9444
|
+
taskType: "execution"
|
|
9445
|
+
});
|
|
9446
|
+
}
|
|
9447
|
+
return executions;
|
|
9448
|
+
}
|
|
9449
|
+
function listExecutions(options = {}) {
|
|
9450
|
+
const memoryDir = findMemoryDir();
|
|
9451
|
+
if (!memoryDir) return [];
|
|
9452
|
+
const executions = [];
|
|
9453
|
+
const { squad: filterSquad, agent: filterAgent, status: filterStatus, limit, since } = options;
|
|
9454
|
+
const squads = readdirSync8(memoryDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9455
|
+
for (const squad of squads) {
|
|
9456
|
+
if (filterSquad && squad !== filterSquad) continue;
|
|
9457
|
+
const squadPath = join20(memoryDir, squad);
|
|
9458
|
+
const agents = readdirSync8(squadPath, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9459
|
+
for (const agent of agents) {
|
|
9460
|
+
if (filterAgent && agent !== filterAgent) continue;
|
|
9461
|
+
const logPath = join20(squadPath, agent, "executions.md");
|
|
9462
|
+
const agentExecutions = parseExecutionLog(logPath, squad, agent);
|
|
9463
|
+
executions.push(...agentExecutions);
|
|
9464
|
+
}
|
|
9465
|
+
}
|
|
9466
|
+
let filtered = filterStatus ? executions.filter((e) => e.status === filterStatus) : executions;
|
|
9467
|
+
if (since) {
|
|
9468
|
+
const sinceMs = since.getTime();
|
|
9469
|
+
filtered = filtered.filter((e) => {
|
|
9470
|
+
const execDate = new Date(e.startTime).getTime();
|
|
9471
|
+
return !isNaN(execDate) && execDate >= sinceMs;
|
|
9472
|
+
});
|
|
9473
|
+
}
|
|
9474
|
+
filtered.sort((a, b) => {
|
|
9475
|
+
const aTime = new Date(a.startTime).getTime();
|
|
9476
|
+
const bTime = new Date(b.startTime).getTime();
|
|
9477
|
+
if (isNaN(aTime) || isNaN(bTime)) return 0;
|
|
9478
|
+
return bTime - aTime;
|
|
9479
|
+
});
|
|
9480
|
+
if (limit && limit > 0) {
|
|
9481
|
+
filtered = filtered.slice(0, limit);
|
|
9482
|
+
}
|
|
9483
|
+
return filtered;
|
|
9484
|
+
}
|
|
9485
|
+
function getExecutionStats(options = {}) {
|
|
9486
|
+
const executions = listExecutions(options);
|
|
9487
|
+
const running = executions.filter((e) => e.status === "running").length;
|
|
9488
|
+
const completed = executions.filter((e) => e.status === "completed").length;
|
|
9489
|
+
const failed = executions.filter((e) => e.status === "failed").length;
|
|
9490
|
+
const durations = executions.filter((e) => e.status === "completed" && e.durationMs).map((e) => e.durationMs);
|
|
9491
|
+
const avgDurationMs = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : null;
|
|
9492
|
+
const bySquad = {};
|
|
9493
|
+
for (const e of executions) {
|
|
9494
|
+
bySquad[e.squad] = (bySquad[e.squad] || 0) + 1;
|
|
9495
|
+
}
|
|
9496
|
+
const byAgent = {};
|
|
9497
|
+
for (const e of executions) {
|
|
9498
|
+
const key = `${e.squad}/${e.agent}`;
|
|
9499
|
+
byAgent[key] = (byAgent[key] || 0) + 1;
|
|
9500
|
+
}
|
|
9501
|
+
return {
|
|
9502
|
+
total: executions.length,
|
|
9503
|
+
running,
|
|
9504
|
+
completed,
|
|
9505
|
+
failed,
|
|
9506
|
+
avgDurationMs,
|
|
9507
|
+
bySquad,
|
|
9508
|
+
byAgent
|
|
9509
|
+
};
|
|
9510
|
+
}
|
|
9511
|
+
function formatDuration2(ms) {
|
|
9512
|
+
if (!ms) return "\u2014";
|
|
9513
|
+
if (ms < 1e3) {
|
|
9514
|
+
return `${ms}ms`;
|
|
9515
|
+
}
|
|
9516
|
+
if (ms < 6e4) {
|
|
9517
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
9518
|
+
}
|
|
9519
|
+
if (ms < 36e5) {
|
|
9520
|
+
const mins2 = Math.floor(ms / 6e4);
|
|
9521
|
+
const secs = Math.round(ms % 6e4 / 1e3);
|
|
9522
|
+
return `${mins2}m ${secs}s`;
|
|
9523
|
+
}
|
|
9524
|
+
const hours = Math.floor(ms / 36e5);
|
|
9525
|
+
const mins = Math.round(ms % 36e5 / 6e4);
|
|
9526
|
+
return `${hours}h ${mins}m`;
|
|
9527
|
+
}
|
|
9528
|
+
function formatRelativeTime(isoTime) {
|
|
9529
|
+
const date = new Date(isoTime);
|
|
9530
|
+
if (isNaN(date.getTime())) return isoTime;
|
|
9531
|
+
const now = Date.now();
|
|
9532
|
+
const diff = now - date.getTime();
|
|
9533
|
+
if (diff < 6e4) return "just now";
|
|
9534
|
+
if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
|
|
9535
|
+
if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
|
|
9536
|
+
if (diff < 6048e5) return `${Math.floor(diff / 864e5)}d ago`;
|
|
9537
|
+
return date.toLocaleDateString();
|
|
9538
|
+
}
|
|
9539
|
+
|
|
9540
|
+
// src/commands/exec.ts
|
|
9541
|
+
async function execListCommand(options = {}) {
|
|
9542
|
+
await track(Events.CLI_EXEC, { action: "list", squad: options.squad });
|
|
9543
|
+
const listOptions = {
|
|
9544
|
+
squad: options.squad,
|
|
9545
|
+
agent: options.agent,
|
|
9546
|
+
limit: options.limit || 20
|
|
9547
|
+
};
|
|
9548
|
+
if (options.status) {
|
|
9549
|
+
listOptions.status = options.status;
|
|
9550
|
+
}
|
|
9551
|
+
const executions = listExecutions(listOptions);
|
|
9552
|
+
if (options.json) {
|
|
9553
|
+
console.log(JSON.stringify(executions, null, 2));
|
|
9554
|
+
return;
|
|
9555
|
+
}
|
|
9556
|
+
writeLine();
|
|
9557
|
+
writeLine(` ${gradient("squads")} ${colors.dim}exec list${RESET}${options.squad ? ` ${colors.cyan}${options.squad}${RESET}` : ""}`);
|
|
9558
|
+
writeLine();
|
|
9559
|
+
if (executions.length === 0) {
|
|
9560
|
+
writeLine(` ${colors.dim}No executions found${RESET}`);
|
|
9561
|
+
writeLine();
|
|
9562
|
+
writeLine(` ${colors.dim}Executions are logged when running agents:${RESET}`);
|
|
9563
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}<squad>${RESET} --execute`);
|
|
9564
|
+
writeLine();
|
|
9565
|
+
return;
|
|
9566
|
+
}
|
|
9567
|
+
const w = { agent: 22, status: 12, duration: 10, time: 14, id: 18 };
|
|
9568
|
+
const tableWidth = w.agent + w.status + w.duration + w.time + w.id + 8;
|
|
9569
|
+
writeLine(` ${colors.purple}${box.topLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.topRight}${RESET}`);
|
|
9570
|
+
const header = ` ${colors.purple}${box.vertical}${RESET} ${bold}${padEnd("AGENT", w.agent)}${RESET}${bold}${padEnd("STATUS", w.status)}${RESET}${bold}${padEnd("DURATION", w.duration)}${RESET}${bold}${padEnd("TIME", w.time)}${RESET}${bold}ID${RESET} ${colors.purple}${box.vertical}${RESET}`;
|
|
9571
|
+
writeLine(header);
|
|
9572
|
+
writeLine(` ${colors.purple}${box.teeRight}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.teeLeft}${RESET}`);
|
|
9573
|
+
for (const exec3 of executions) {
|
|
9574
|
+
const agentName = `${exec3.squad}/${exec3.agent}`;
|
|
9575
|
+
const truncatedAgent = agentName.length > w.agent - 1 ? agentName.slice(0, w.agent - 4) + "..." : agentName;
|
|
9576
|
+
const statusIcon = exec3.status === "running" ? icons.running : exec3.status === "completed" ? icons.success : icons.error;
|
|
9577
|
+
const statusColor = exec3.status === "running" ? colors.yellow : exec3.status === "completed" ? colors.green : colors.red;
|
|
9578
|
+
const statusStr = `${statusColor}${statusIcon} ${exec3.status}${RESET}`;
|
|
9579
|
+
const durationStr = formatDuration2(exec3.durationMs);
|
|
9580
|
+
const timeStr = formatRelativeTime(exec3.startTime);
|
|
9581
|
+
const shortId = exec3.id.slice(0, 16);
|
|
9582
|
+
const row = ` ${colors.purple}${box.vertical}${RESET} ${colors.cyan}${padEnd(truncatedAgent, w.agent)}${RESET}${padEnd(statusStr, w.status + 10)}${padEnd(durationStr, w.duration)}${colors.dim}${padEnd(timeStr, w.time)}${RESET}${colors.dim}${shortId}${RESET} ${colors.purple}${box.vertical}${RESET}`;
|
|
9583
|
+
writeLine(row);
|
|
9584
|
+
}
|
|
9585
|
+
writeLine(` ${colors.purple}${box.bottomLeft}${colors.dim}${box.horizontal.repeat(tableWidth)}${colors.purple}${box.bottomRight}${RESET}`);
|
|
9586
|
+
writeLine();
|
|
9587
|
+
const stats = getExecutionStats(listOptions);
|
|
9588
|
+
const parts = [];
|
|
9589
|
+
if (stats.running > 0) parts.push(`${colors.yellow}${stats.running} running${RESET}`);
|
|
9590
|
+
if (stats.completed > 0) parts.push(`${colors.green}${stats.completed} completed${RESET}`);
|
|
9591
|
+
if (stats.failed > 0) parts.push(`${colors.red}${stats.failed} failed${RESET}`);
|
|
9592
|
+
if (parts.length > 0) {
|
|
9593
|
+
writeLine(` ${parts.join(` ${colors.dim}|${RESET} `)}`);
|
|
9594
|
+
if (stats.avgDurationMs) {
|
|
9595
|
+
writeLine(` ${colors.dim}Avg duration: ${formatDuration2(stats.avgDurationMs)}${RESET}`);
|
|
9596
|
+
}
|
|
9597
|
+
writeLine();
|
|
9598
|
+
}
|
|
9599
|
+
writeLine(` ${colors.dim}$${RESET} squads exec show ${colors.cyan}<id>${RESET} ${colors.dim}Execution details${RESET}`);
|
|
9600
|
+
writeLine(` ${colors.dim}$${RESET} squads exec --json ${colors.dim}JSON output${RESET}`);
|
|
9601
|
+
writeLine();
|
|
9602
|
+
}
|
|
9603
|
+
async function execShowCommand(executionId, options = {}) {
|
|
9604
|
+
await track(Events.CLI_EXEC, { action: "show", id: executionId });
|
|
9605
|
+
const executions = listExecutions();
|
|
9606
|
+
const matches = executions.filter(
|
|
9607
|
+
(e) => e.id === executionId || e.id.startsWith(executionId)
|
|
9608
|
+
);
|
|
9609
|
+
if (matches.length === 0) {
|
|
9610
|
+
writeLine();
|
|
9611
|
+
writeLine(` ${colors.red}Execution not found: ${executionId}${RESET}`);
|
|
9612
|
+
writeLine();
|
|
9613
|
+
writeLine(` ${colors.dim}List recent executions:${RESET}`);
|
|
9614
|
+
writeLine(` ${colors.dim}$${RESET} squads exec list`);
|
|
9615
|
+
writeLine();
|
|
9616
|
+
return;
|
|
9617
|
+
}
|
|
9618
|
+
if (matches.length > 1) {
|
|
9619
|
+
writeLine();
|
|
9620
|
+
writeLine(` ${colors.yellow}Multiple matches for "${executionId}":${RESET}`);
|
|
9621
|
+
for (const m of matches.slice(0, 5)) {
|
|
9622
|
+
writeLine(` ${colors.dim}${m.id}${RESET} - ${m.squad}/${m.agent}`);
|
|
9623
|
+
}
|
|
9624
|
+
writeLine();
|
|
9625
|
+
writeLine(` ${colors.dim}Provide a more specific ID${RESET}`);
|
|
9626
|
+
writeLine();
|
|
9627
|
+
return;
|
|
9628
|
+
}
|
|
9629
|
+
const exec3 = matches[0];
|
|
9630
|
+
if (options.json) {
|
|
9631
|
+
console.log(JSON.stringify(exec3, null, 2));
|
|
9632
|
+
return;
|
|
9633
|
+
}
|
|
9634
|
+
writeLine();
|
|
9635
|
+
writeLine(` ${gradient("squads")} ${colors.dim}exec show${RESET}`);
|
|
9636
|
+
writeLine();
|
|
9637
|
+
writeLine(` ${bold}${exec3.squad}/${exec3.agent}${RESET}`);
|
|
9638
|
+
writeLine(` ${colors.dim}${exec3.id}${RESET}`);
|
|
9639
|
+
writeLine();
|
|
9640
|
+
const statusIcon = exec3.status === "running" ? icons.running : exec3.status === "completed" ? icons.success : icons.error;
|
|
9641
|
+
const statusColor = exec3.status === "running" ? colors.yellow : exec3.status === "completed" ? colors.green : colors.red;
|
|
9642
|
+
writeLine(` ${bold}Status${RESET} ${statusColor}${statusIcon} ${exec3.status}${RESET}`);
|
|
9643
|
+
writeLine(` ${bold}Task Type${RESET} ${exec3.taskType}`);
|
|
9644
|
+
writeLine(` ${bold}Trigger${RESET} ${exec3.trigger}`);
|
|
9645
|
+
writeLine();
|
|
9646
|
+
writeLine(` ${bold}Started${RESET} ${exec3.startTime}`);
|
|
9647
|
+
if (exec3.endTime) {
|
|
9648
|
+
writeLine(` ${bold}Completed${RESET} ${exec3.endTime}`);
|
|
9649
|
+
}
|
|
9650
|
+
if (exec3.durationMs) {
|
|
9651
|
+
writeLine(` ${bold}Duration${RESET} ${formatDuration2(exec3.durationMs)}`);
|
|
9652
|
+
}
|
|
9653
|
+
writeLine();
|
|
9654
|
+
if (exec3.outcome) {
|
|
9655
|
+
writeLine(` ${bold}Outcome${RESET}`);
|
|
9656
|
+
writeLine(` ${colors.dim}${exec3.outcome}${RESET}`);
|
|
9657
|
+
writeLine();
|
|
9658
|
+
}
|
|
9659
|
+
if (exec3.error) {
|
|
9660
|
+
writeLine(` ${bold}Error${RESET}`);
|
|
9661
|
+
writeLine(` ${colors.red}${exec3.error}${RESET}`);
|
|
9662
|
+
writeLine();
|
|
9663
|
+
}
|
|
9664
|
+
writeLine(` ${colors.dim}$${RESET} squads memory show ${colors.cyan}${exec3.squad}${RESET} ${colors.dim}Squad memory${RESET}`);
|
|
9665
|
+
writeLine(` ${colors.dim}$${RESET} squads exec list --squad ${colors.cyan}${exec3.squad}${RESET} ${colors.dim}Squad history${RESET}`);
|
|
9666
|
+
writeLine();
|
|
9667
|
+
}
|
|
9668
|
+
async function execStatsCommand(options = {}) {
|
|
9669
|
+
await track(Events.CLI_EXEC, { action: "stats", squad: options.squad });
|
|
9670
|
+
const listOptions = {
|
|
9671
|
+
squad: options.squad
|
|
9672
|
+
};
|
|
9673
|
+
const stats = getExecutionStats(listOptions);
|
|
9674
|
+
if (options.json) {
|
|
9675
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
9676
|
+
return;
|
|
9677
|
+
}
|
|
9678
|
+
writeLine();
|
|
9679
|
+
writeLine(` ${gradient("squads")} ${colors.dim}exec stats${RESET}${options.squad ? ` ${colors.cyan}${options.squad}${RESET}` : ""}`);
|
|
9680
|
+
writeLine();
|
|
9681
|
+
writeLine(` ${bold}Total${RESET} ${stats.total}`);
|
|
9682
|
+
writeLine(` ${colors.yellow}Running${RESET} ${stats.running}`);
|
|
9683
|
+
writeLine(` ${colors.green}Completed${RESET} ${stats.completed}`);
|
|
9684
|
+
writeLine(` ${colors.red}Failed${RESET} ${stats.failed}`);
|
|
9685
|
+
writeLine();
|
|
9686
|
+
if (stats.avgDurationMs) {
|
|
9687
|
+
writeLine(` ${bold}Avg Duration${RESET} ${formatDuration2(stats.avgDurationMs)}`);
|
|
9688
|
+
writeLine();
|
|
9689
|
+
}
|
|
9690
|
+
const squadEntries = Object.entries(stats.bySquad).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
9691
|
+
if (squadEntries.length > 0) {
|
|
9692
|
+
writeLine(` ${bold}By Squad${RESET}`);
|
|
9693
|
+
for (const [squad, count] of squadEntries) {
|
|
9694
|
+
writeLine(` ${colors.cyan}${squad}${RESET}: ${count}`);
|
|
9695
|
+
}
|
|
9696
|
+
writeLine();
|
|
9697
|
+
}
|
|
9698
|
+
const agentEntries = Object.entries(stats.byAgent).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
9699
|
+
if (agentEntries.length > 0) {
|
|
9700
|
+
writeLine(` ${bold}Top Agents${RESET}`);
|
|
9701
|
+
for (const [agent, count] of agentEntries) {
|
|
9702
|
+
writeLine(` ${colors.cyan}${agent}${RESET}: ${count}`);
|
|
9703
|
+
}
|
|
9704
|
+
writeLine();
|
|
9705
|
+
}
|
|
9706
|
+
}
|
|
9707
|
+
|
|
9708
|
+
// src/commands/tonight.ts
|
|
9709
|
+
import ora7 from "ora";
|
|
7932
9710
|
import fs2 from "fs/promises";
|
|
7933
|
-
import path2, { dirname as
|
|
9711
|
+
import path2, { dirname as dirname6 } from "path";
|
|
7934
9712
|
import { execSync as execSync14, spawn as spawn7 } from "child_process";
|
|
7935
9713
|
function getProjectRoot2() {
|
|
7936
9714
|
const squadsDir = findSquadsDir();
|
|
7937
9715
|
if (squadsDir) {
|
|
7938
|
-
return
|
|
9716
|
+
return dirname6(dirname6(squadsDir));
|
|
7939
9717
|
}
|
|
7940
9718
|
return process.cwd();
|
|
7941
9719
|
}
|
|
@@ -8075,7 +9853,7 @@ async function tonightCommand(targets, options) {
|
|
|
8075
9853
|
totalCost: await getCurrentCost(),
|
|
8076
9854
|
stopped: false
|
|
8077
9855
|
};
|
|
8078
|
-
const spinner =
|
|
9856
|
+
const spinner = ora7("Launching agents...").start();
|
|
8079
9857
|
for (const target of targets) {
|
|
8080
9858
|
const sessionId = `${target.replace("/", "-")}-${Date.now()}`;
|
|
8081
9859
|
const logFile = path2.join(logDir, `${sessionId}.log`);
|
|
@@ -8263,12 +10041,12 @@ process.stderr.on("error", (err) => {
|
|
|
8263
10041
|
throw err;
|
|
8264
10042
|
});
|
|
8265
10043
|
var envPaths = [
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
10044
|
+
join21(process.cwd(), ".env"),
|
|
10045
|
+
join21(process.cwd(), "..", "hq", ".env"),
|
|
10046
|
+
join21(homedir5(), "agents-squads", "hq", ".env")
|
|
8269
10047
|
];
|
|
8270
10048
|
for (const envPath of envPaths) {
|
|
8271
|
-
if (
|
|
10049
|
+
if (existsSync19(envPath)) {
|
|
8272
10050
|
config({ path: envPath, quiet: true });
|
|
8273
10051
|
break;
|
|
8274
10052
|
}
|
|
@@ -8288,10 +10066,20 @@ program.name("squads").description("A CLI for humans and agents").version(versio
|
|
|
8288
10066
|
throw err;
|
|
8289
10067
|
});
|
|
8290
10068
|
program.command("init").description("Initialize a new squad project").option("-t, --template <template>", "Project template", "default").option("--skip-infra", "Skip infrastructure setup prompt").action(initCommand);
|
|
8291
|
-
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").option("-p, --parallel", "Run all agents in parallel (N tmux sessions)").option("-l, --lead", "Lead mode: single orchestrator using Task tool for parallelization").option("-f, --foreground", "Run in foreground (no tmux, blocks terminal)").option("--use-api", "Use API credits instead of subscription").action((target, options) => runCommand(target, { ...options, timeout: parseInt(options.timeout, 10) }));
|
|
10069
|
+
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").option("-p, --parallel", "Run all agents in parallel (N tmux sessions)").option("-l, --lead", "Lead mode: single orchestrator using Task tool for parallelization").option("-f, --foreground", "Run in foreground (no tmux, blocks terminal)").option("--use-api", "Use API credits instead of subscription").option("--effort <level>", "Effort level: high, medium, low (default: from SQUAD.md or high)").option("--skills <skills...>", "Skills to load (skill IDs or local paths)").action((target, options) => runCommand(target, { ...options, timeout: parseInt(options.timeout, 10) }));
|
|
8292
10070
|
program.command("list").description("List agents and squads").option("-s, --squads", "List squads only").option("-a, --agents", "List agents only").action(listCommand);
|
|
8293
10071
|
program.command("status [squad]").description("Show squad status and state").option("-v, --verbose", "Show detailed status").action(statusCommand);
|
|
8294
10072
|
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 }));
|
|
10073
|
+
var context = program.command("context").description("View and manage squad execution context");
|
|
10074
|
+
context.command("show <squad>").description("Show context for a squad (MCP, skills, model, budget)").option("--json", "Output as JSON").action(contextShowCommand);
|
|
10075
|
+
context.command("list").description("List context for all squads").option("--json", "Output as JSON").action(contextListCommand);
|
|
10076
|
+
program.command("cost").description("Show cost summary (today, week, by squad)").option("-s, --squad <squad>", "Filter to specific squad").option("--json", "Output as JSON").action(costCommand);
|
|
10077
|
+
program.command("budget").description("Check budget status for a squad").argument("<squad>", "Squad to check").option("--json", "Output as JSON").action(budgetCheckCommand);
|
|
10078
|
+
var exec2 = program.command("exec").description("View execution history and statistics");
|
|
10079
|
+
exec2.command("list").description("List recent executions").option("-s, --squad <squad>", "Filter by squad").option("-a, --agent <agent>", "Filter by agent").option("--status <status>", "Filter by status (running, completed, failed)").option("-n, --limit <n>", "Number of executions to show", "20").option("--json", "Output as JSON").action((options) => execListCommand({ ...options, limit: parseInt(options.limit, 10) }));
|
|
10080
|
+
exec2.command("show <id>").description("Show execution details").option("--json", "Output as JSON").action(execShowCommand);
|
|
10081
|
+
exec2.command("stats").description("Show execution statistics").option("-s, --squad <squad>", "Filter by squad").option("--json", "Output as JSON").action(execStatsCommand);
|
|
10082
|
+
exec2.action((options) => execListCommand(options));
|
|
8295
10083
|
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);
|
|
8296
10084
|
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);
|
|
8297
10085
|
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);
|
|
@@ -8345,11 +10133,11 @@ sessions.command("history").description("Show session history and statistics").o
|
|
|
8345
10133
|
json: options.json
|
|
8346
10134
|
}));
|
|
8347
10135
|
sessions.command("summary").description("Show pretty session summary (auto-detects current session or pass JSON)").option("-d, --data <json>", "JSON data for summary (overrides auto-detection)").option("-f, --file <path>", "Path to JSON file with summary data").option("-j, --json", "Output as JSON instead of pretty format").action(async (options) => {
|
|
8348
|
-
const { buildCurrentSessionSummary } = await import("./sessions-
|
|
10136
|
+
const { buildCurrentSessionSummary } = await import("./sessions-SEITSWEV.js");
|
|
8349
10137
|
let data;
|
|
8350
10138
|
if (options.file) {
|
|
8351
|
-
const { readFileSync:
|
|
8352
|
-
data = JSON.parse(
|
|
10139
|
+
const { readFileSync: readFileSync17 } = await import("fs");
|
|
10140
|
+
data = JSON.parse(readFileSync17(options.file, "utf-8"));
|
|
8353
10141
|
} else if (options.data) {
|
|
8354
10142
|
data = JSON.parse(options.data);
|
|
8355
10143
|
} else if (!process.stdin.isTTY) {
|
|
@@ -8382,6 +10170,8 @@ stack.command("down").description("Stop Docker containers").action(stackDownComm
|
|
|
8382
10170
|
stack.command("health").description("Comprehensive health check with diagnostics").option("-v, --verbose", "Show logs for unhealthy services").action((options) => stackHealthCommand(options.verbose));
|
|
8383
10171
|
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)));
|
|
8384
10172
|
registerTriggerCommand(program);
|
|
10173
|
+
registerSkillCommand(program);
|
|
10174
|
+
registerPermissionsCommand(program);
|
|
8385
10175
|
var tonight = program.command("tonight").description("Run agents autonomously overnight with safety limits");
|
|
8386
10176
|
tonight.command("run <targets...>").description("Start tonight mode with specified squads/agents").option("-c, --cost-cap <usd>", "Max USD to spend (default: 50)", "50").option("-s, --stop-at <time>", "Stop time HH:MM (default: 07:00)", "07:00").option("-r, --max-retries <n>", "Max restarts per agent (default: 3)", "3").option("-d, --dry-run", "Show what would run without executing").option("-v, --verbose", "Verbose output").action((targets, options) => tonightCommand(targets, {
|
|
8387
10177
|
costCap: parseFloat(options.costCap),
|