@gonzih/cc-agent 0.15.11 → 0.15.17

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/dist/index.js CHANGED
@@ -22,6 +22,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
22
22
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
23
  import { JobManager, normalizeRepoUrl } from "./agent.js";
24
24
  import { listDrivers, getDriverStatus } from "./drivers/index.js";
25
+ import { runSwarm, getSwarmStatus, SWARM_MAX_AGENTS_HARD_CAP } from "./swarm.js";
25
26
  import { MetaAgentManager } from "./meta-agent.js";
26
27
  import { buildEvaluatorTask } from "./evaluator.js";
27
28
  import { loadProfiles, upsertProfile, deleteProfile, getProfile, interpolate } from "./profiles.js";
@@ -687,6 +688,95 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
687
688
  description: "List all available agent drivers and their status (binary found / API key configured). Use this to check which drivers are ready to use before calling spawn_agent with agent_driver.",
688
689
  inputSchema: { type: "object", properties: {} },
689
690
  },
691
+ {
692
+ name: "export_jobs",
693
+ description: "Export all job records as JSONL or JSON for statistical analysis. Each record includes id, status, repo_url, task (truncated to 500 chars), started_at, finished_at, exit_code, output_lines count, score, and duration_seconds. Use this to pull job traces, compute success rates, and study failure modes.",
694
+ inputSchema: {
695
+ type: "object",
696
+ properties: {
697
+ days: { type: "number", description: "How many days back to export (default: 7)" },
698
+ format: { type: "string", description: "Output format: 'jsonl' (one record per line) or 'json' (array). Default: 'jsonl'" },
699
+ status: { type: "string", description: "Filter by status: 'done' | 'failed' | 'cancelled' | 'running' (optional)" },
700
+ },
701
+ },
702
+ },
703
+ {
704
+ name: "get_cost_report",
705
+ description: "Longitudinal cost breakdown for research budget tracking. Returns grouped cost summary with total USD spent, job count, avg cost per job, and avg score. Useful for tracking spending by repo, day, or outcome.",
706
+ inputSchema: {
707
+ type: "object",
708
+ properties: {
709
+ days: { type: "number", description: "How many days back to include (default: 30)" },
710
+ group_by: { type: "string", description: "Group by 'repo' | 'day' | 'status' (default: 'repo')" },
711
+ },
712
+ },
713
+ },
714
+ {
715
+ name: "search_jobs",
716
+ description: "Find jobs by content of task prompt. Returns matching jobs with a task snippet showing match context. Useful for finding all jobs that involved a specific tool, repo, or task type.",
717
+ inputSchema: {
718
+ type: "object",
719
+ properties: {
720
+ query: { type: "string", description: "Search term to look for in task prompts (case-insensitive)" },
721
+ days: { type: "number", description: "How many days back to search (default: 30)" },
722
+ status: { type: "string", description: "Filter by status: 'done' | 'failed' | 'cancelled' | 'running' (optional)" },
723
+ },
724
+ required: ["query"],
725
+ },
726
+ },
727
+ {
728
+ name: "swarm_task",
729
+ description: "Auto-decompose a high-level goal into N parallel sub-tasks, fan out agents across all of them, then run a synthesis agent that produces one unified deliverable. Returns immediately with swarm_id and sub_job_ids. Use get_swarm_status to poll progress.",
730
+ inputSchema: {
731
+ type: "object",
732
+ properties: {
733
+ repo_url: {
734
+ type: "string",
735
+ description: "GitHub repo URL for all sub-agents and the synthesis agent",
736
+ },
737
+ goal: {
738
+ type: "string",
739
+ description: "High-level goal to decompose and execute across parallel agents",
740
+ },
741
+ max_agents: {
742
+ type: "number",
743
+ description: `Maximum number of parallel sub-agents (default 10, hard cap ${SWARM_MAX_AGENTS_HARD_CAP})`,
744
+ },
745
+ synthesis_output: {
746
+ type: "string",
747
+ description: "Path in the repo where the synthesis agent writes its deliverable (default: swarm-synthesis.md)",
748
+ },
749
+ synthesis_prompt: {
750
+ type: "string",
751
+ description: "Custom instruction for the synthesis agent. Defaults to: review all outputs and write a unified deliverable.",
752
+ },
753
+ max_budget_per_agent: {
754
+ type: "number",
755
+ description: "Max USD budget per agent (sub-agents and synthesis). Default 5.",
756
+ },
757
+ agent_model: {
758
+ type: "string",
759
+ description: "Model override for all spawned agents (optional)",
760
+ },
761
+ agent_driver: {
762
+ type: "string",
763
+ description: "Driver for all spawned agents (optional, default: claude)",
764
+ },
765
+ },
766
+ required: ["repo_url", "goal"],
767
+ },
768
+ },
769
+ {
770
+ name: "get_swarm_status",
771
+ description: "Poll the status of a swarm created by swarm_task. Returns goal, status (running_subs | synthesizing | done | failed), sub_job counts, and synthesis_job_id once spawned.",
772
+ inputSchema: {
773
+ type: "object",
774
+ properties: {
775
+ swarm_id: { type: "string", description: "Swarm ID returned by swarm_task" },
776
+ },
777
+ required: ["swarm_id"],
778
+ },
779
+ },
690
780
  ],
691
781
  }));
692
782
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
@@ -695,7 +785,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
695
785
  switch (name) {
696
786
  case "spawn_agent": {
697
787
  const repoUrl = normalizeRepoUrl(a.repo_url);
698
- logger.info("tool:spawn_agent", { repo_url: repoUrl, task: a.task?.slice(0, 80) });
788
+ logger.info("[mcp] spawn_agent", { repo_url: repoUrl, task: a.task?.slice(0, 80) });
699
789
  const owner = extractGithubOwner(repoUrl);
700
790
  const isTrusted = !owner || TRUSTED_OWNERS.includes(owner);
701
791
  const jobId = await manager.spawn({
@@ -786,7 +876,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
786
876
  };
787
877
  }
788
878
  case "get_job_status": {
789
- logger.info("tool:get_job_status", { job_id: a.job_id });
879
+ logger.info("[mcp] get_job_status", { job_id: a.job_id });
790
880
  const job = manager.getJob(a.job_id);
791
881
  if (!job) {
792
882
  return { content: [{ type: "text", text: JSON.stringify({ error: "Job not found" }) }] };
@@ -822,7 +912,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
822
912
  case "wait_for_job": {
823
913
  const waitJobId = a.job_id;
824
914
  const timeoutSeconds = typeof a.timeout_seconds === "number" ? a.timeout_seconds : 300;
825
- logger.info("tool:wait_for_job", { job_id: waitJobId, timeout_seconds: timeoutSeconds });
915
+ logger.info("[mcp] wait_for_job", { job_id: waitJobId, timeout_seconds: timeoutSeconds });
826
916
  const TERMINAL_STATUSES = new Set(["done", "failed", "cancelled", "rejected", "interrupted"]);
827
917
  const deadlineMs = Date.now() + timeoutSeconds * 1000;
828
918
  let waitRecord = await jobStore.getJob(waitJobId);
@@ -858,7 +948,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
858
948
  };
859
949
  }
860
950
  case "get_job_output": {
861
- logger.info("tool:get_job_output", { job_id: a.job_id, offset: a.offset });
951
+ logger.info("[mcp] get_job_output", { job_id: a.job_id, offset: a.offset });
862
952
  const offset = typeof a.offset === "number" ? a.offset : 0;
863
953
  const { lines, done, toolCalls } = await manager.getOutput(a.job_id, offset);
864
954
  return {
@@ -878,7 +968,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
878
968
  };
879
969
  }
880
970
  case "list_jobs": {
881
- logger.info("tool:list_jobs");
971
+ logger.info("[mcp] list_jobs");
882
972
  const minScore = typeof a.min_score === "number" ? a.min_score : undefined;
883
973
  let jobs = (await jobStore.listJobs()) ?? [];
884
974
  if (minScore !== undefined) {
@@ -896,7 +986,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
896
986
  };
897
987
  }
898
988
  case "cancel_job": {
899
- logger.info("tool:cancel_job", { job_id: a.job_id });
989
+ logger.info("[mcp] cancel_job", { job_id: a.job_id });
900
990
  const cancelled = manager.cancel(a.job_id);
901
991
  return {
902
992
  content: [
@@ -908,7 +998,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
908
998
  };
909
999
  }
910
1000
  case "send_message": {
911
- logger.info("tool:send_message", { job_id: a.job_id });
1001
+ logger.info("[mcp] send_message", { job_id: a.job_id });
912
1002
  const result = await manager.sendMessage(a.job_id, a.message);
913
1003
  return {
914
1004
  content: [
@@ -922,7 +1012,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
922
1012
  };
923
1013
  }
924
1014
  case "cost_summary": {
925
- logger.info("tool:cost_summary");
1015
+ logger.info("[mcp] cost_summary");
926
1016
  // Use jobStore (Redis/disk) to include all persisted jobs, not just in-memory ones
927
1017
  const allRecords = await jobStore.listJobs();
928
1018
  let totalCostUsd = 0;
@@ -955,12 +1045,12 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
955
1045
  };
956
1046
  }
957
1047
  case "get_version":
958
- logger.info("tool:get_version");
1048
+ logger.info("[mcp] get_version");
959
1049
  return {
960
1050
  content: [{ type: "text", text: JSON.stringify({ version: PKG_VERSION }) }],
961
1051
  };
962
1052
  case "create_profile": {
963
- logger.info("tool:create_profile", { name: a.name });
1053
+ logger.info("[mcp] create_profile", { name: a.name });
964
1054
  const profileName = a.name;
965
1055
  if (!/^[\w-]+$/.test(profileName)) {
966
1056
  return {
@@ -982,7 +1072,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
982
1072
  };
983
1073
  }
984
1074
  case "list_profiles": {
985
- logger.info("tool:list_profiles");
1075
+ logger.info("[mcp] list_profiles");
986
1076
  const profiles = (await loadProfiles()).map(({ name, repoUrl, description, defaultBudgetUsd, builtin }) => ({
987
1077
  name,
988
1078
  repoUrl,
@@ -996,7 +1086,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
996
1086
  };
997
1087
  }
998
1088
  case "delete_profile": {
999
- logger.info("tool:delete_profile", { name: a.name });
1089
+ logger.info("[mcp] delete_profile", { name: a.name });
1000
1090
  const deleted = await deleteProfile(a.name);
1001
1091
  return {
1002
1092
  content: [
@@ -1010,7 +1100,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1010
1100
  };
1011
1101
  }
1012
1102
  case "spawn_from_profile": {
1013
- logger.info("tool:spawn_from_profile", { profile_name: a.profile_name });
1103
+ logger.info("[mcp] spawn_from_profile", { profile_name: a.profile_name });
1014
1104
  const profile = await getProfile(a.profile_name);
1015
1105
  if (!profile) {
1016
1106
  return {
@@ -1038,7 +1128,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1038
1128
  };
1039
1129
  }
1040
1130
  case "create_plan": {
1041
- logger.info("tool:create_plan", { goal: a.goal?.slice(0, 80) });
1131
+ logger.info("[mcp] create_plan", { goal: a.goal?.slice(0, 80) });
1042
1132
  const goal = a.goal;
1043
1133
  const steps = a.steps;
1044
1134
  const stepIdToJobId = new Map();
@@ -1134,14 +1224,14 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1134
1224
  };
1135
1225
  }
1136
1226
  case "wake_job": {
1137
- logger.info("tool:wake_job", { job_id: a.job_id });
1227
+ logger.info("[mcp] wake_job", { job_id: a.job_id });
1138
1228
  const result = await manager.wakeJob(a.job_id);
1139
1229
  return {
1140
1230
  content: [{ type: "text", text: JSON.stringify(result) }],
1141
1231
  };
1142
1232
  }
1143
1233
  case "list_model_ratings": {
1144
- logger.info("tool:list_model_ratings");
1234
+ logger.info("[mcp] list_model_ratings");
1145
1235
  const ratingsFile = join(homedir(), ".cc-agent", "model-ratings.jsonl");
1146
1236
  let ratings = [];
1147
1237
  try {
@@ -1157,7 +1247,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1157
1247
  };
1158
1248
  }
1159
1249
  case "get_logs": {
1160
- logger.info("tool:get_logs", { lines: a.lines });
1250
+ logger.info("[mcp] get_logs", { lines: a.lines });
1161
1251
  const n = Math.min(typeof a.lines === "number" ? a.lines : 100, 500);
1162
1252
  const logFile = join(homedir(), ".cc-agent", "logs", "cc-agent.log");
1163
1253
  let lines = [];
@@ -1174,7 +1264,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1174
1264
  };
1175
1265
  }
1176
1266
  case "list_project_issues": {
1177
- logger.info("tool:list_project_issues", { repo: a.repo });
1267
+ logger.info("[mcp] list_project_issues", { repo: a.repo });
1178
1268
  const repo = a.repo;
1179
1269
  const state = a.state ?? "open";
1180
1270
  const labels = a.labels;
@@ -1196,7 +1286,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1196
1286
  }
1197
1287
  }
1198
1288
  case "work_on_issue": {
1199
- logger.info("tool:work_on_issue", { repo: a.repo, issue_number: a.issue_number });
1289
+ logger.info("[mcp] work_on_issue", { repo: a.repo, issue_number: a.issue_number });
1200
1290
  const repo = a.repo;
1201
1291
  const issueNumber = a.issue_number;
1202
1292
  const extraContext = a.extra_context;
@@ -1240,7 +1330,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1240
1330
  };
1241
1331
  }
1242
1332
  case "comment_on_issue": {
1243
- logger.info("tool:comment_on_issue", { repo: a.repo, issue_number: a.issue_number });
1333
+ logger.info("[mcp] comment_on_issue", { repo: a.repo, issue_number: a.issue_number });
1244
1334
  const repo = a.repo;
1245
1335
  const issueNumber = a.issue_number;
1246
1336
  const body = a.body;
@@ -1253,7 +1343,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1253
1343
  }
1254
1344
  }
1255
1345
  case "close_issue": {
1256
- logger.info("tool:close_issue", { repo: a.repo, issue_number: a.issue_number });
1346
+ logger.info("[mcp] close_issue", { repo: a.repo, issue_number: a.issue_number });
1257
1347
  const repo = a.repo;
1258
1348
  const issueNumber = a.issue_number;
1259
1349
  const comment = a.comment;
@@ -1269,14 +1359,14 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1269
1359
  }
1270
1360
  }
1271
1361
  case "approve_job": {
1272
- logger.info("tool:approve_job", { job_id: a.job_id });
1362
+ logger.info("[mcp] approve_job", { job_id: a.job_id });
1273
1363
  const result = await manager.approveJob(a.job_id);
1274
1364
  return {
1275
1365
  content: [{ type: "text", text: JSON.stringify(result) }],
1276
1366
  };
1277
1367
  }
1278
1368
  case "set_job_score": {
1279
- logger.info("tool:set_job_score", { job_id: a.job_id, score: a.score });
1369
+ logger.info("[mcp] set_job_score", { job_id: a.job_id, score: a.score });
1280
1370
  const result = manager.setJobScore(a.job_id, a.score, a.reason);
1281
1371
  return {
1282
1372
  content: [{ type: "text", text: JSON.stringify(result) }],
@@ -1286,7 +1376,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1286
1376
  const repoParam = a.repo;
1287
1377
  const ns = repoParam ?? a.namespace ?? getNamespace();
1288
1378
  const limit = typeof a.limit === "number" ? a.limit : 10;
1289
- logger.info("tool:get_learnings", { key: ns, limit });
1379
+ logger.info("[mcp] get_learnings", { key: ns, limit });
1290
1380
  const learnings = await learningsStore.getLearnings(ns, limit);
1291
1381
  return {
1292
1382
  content: [{
@@ -1297,7 +1387,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1297
1387
  }
1298
1388
  case "clear_learnings": {
1299
1389
  const ns = a.namespace ?? getNamespace();
1300
- logger.info("tool:clear_learnings", { namespace: ns });
1390
+ logger.info("[mcp] clear_learnings", { namespace: ns });
1301
1391
  await learningsStore.clearLearnings(ns);
1302
1392
  return {
1303
1393
  content: [{
@@ -1307,7 +1397,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1307
1397
  };
1308
1398
  }
1309
1399
  case "docker_ps": {
1310
- logger.info("tool:docker_ps");
1400
+ logger.info("[mcp] docker_ps");
1311
1401
  const containers = await listCcAgentContainers();
1312
1402
  return {
1313
1403
  content: [{
@@ -1317,7 +1407,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1317
1407
  };
1318
1408
  }
1319
1409
  case "list_token_status": {
1320
- logger.info("tool:list_token_status");
1410
+ logger.info("[mcp] list_token_status");
1321
1411
  const tokens = loadTokens();
1322
1412
  const status = await getTokenStatus();
1323
1413
  const tokenList = tokens.map((t, i) => ({
@@ -1338,12 +1428,12 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1338
1428
  };
1339
1429
  }
1340
1430
  case "list_crons": {
1341
- logger.info("tool:list_crons");
1431
+ logger.info("[mcp] list_crons");
1342
1432
  const crons = await cronEngine.listCrons();
1343
1433
  return { content: [{ type: "text", text: JSON.stringify({ crons, total: crons.length }) }] };
1344
1434
  }
1345
1435
  case "create_cron": {
1346
- logger.info("tool:create_cron", { schedule: a.schedule });
1436
+ logger.info("[mcp] create_cron", { schedule: a.schedule });
1347
1437
  const cron = await cronEngine.addCron({
1348
1438
  chatId: typeof a.chat_id === "number" ? a.chat_id : 0,
1349
1439
  intervalMs: a.interval_ms,
@@ -1355,12 +1445,12 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1355
1445
  return { content: [{ type: "text", text: JSON.stringify(cron) }] };
1356
1446
  }
1357
1447
  case "delete_cron": {
1358
- logger.info("tool:delete_cron", { cron_id: a.cron_id });
1448
+ logger.info("[mcp] delete_cron", { cron_id: a.cron_id });
1359
1449
  const deleted = await cronEngine.deleteCron(a.cron_id);
1360
1450
  return { content: [{ type: "text", text: JSON.stringify({ deleted, cron_id: a.cron_id }) }] };
1361
1451
  }
1362
1452
  case "update_cron": {
1363
- logger.info("tool:update_cron", { cron_id: a.cron_id });
1453
+ logger.info("[mcp] update_cron", { cron_id: a.cron_id });
1364
1454
  const updates = {};
1365
1455
  if (typeof a.interval_ms === "number")
1366
1456
  updates.intervalMs = a.interval_ms;
@@ -1374,7 +1464,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1374
1464
  return { content: [{ type: "text", text: JSON.stringify(updated ?? { error: "cron not found" }) }] };
1375
1465
  }
1376
1466
  case "list_notifications": {
1377
- logger.info("tool:list_notifications");
1467
+ logger.info("[mcp] list_notifications");
1378
1468
  const ns = getNamespace();
1379
1469
  const redis = getRedis();
1380
1470
  let messages = [];
@@ -1384,7 +1474,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1384
1474
  return { content: [{ type: "text", text: JSON.stringify({ messages, total: messages.length, namespace: ns }) }] };
1385
1475
  }
1386
1476
  case "list_active_repos": {
1387
- logger.info("tool:list_active_repos");
1477
+ logger.info("[mcp] list_active_repos");
1388
1478
  const redis = getRedis();
1389
1479
  if (!redis)
1390
1480
  return { content: [{ type: "text", text: "Redis unavailable" }] };
@@ -1431,7 +1521,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1431
1521
  };
1432
1522
  }
1433
1523
  case "get_pubsub_status": {
1434
- logger.info("tool:get_pubsub_status");
1524
+ logger.info("[mcp] get_pubsub_status");
1435
1525
  const redis = getRedis();
1436
1526
  if (!redis)
1437
1527
  return { content: [{ type: "text", text: "Redis unavailable" }] };
@@ -1459,7 +1549,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1459
1549
  };
1460
1550
  }
1461
1551
  case "start_meta_agent": {
1462
- logger.info("tool:start_meta_agent", { namespace: a.namespace });
1552
+ logger.info("[mcp] start_meta_agent", { namespace: a.namespace });
1463
1553
  const ns = a.namespace;
1464
1554
  const repoUrl = a.repo_url;
1465
1555
  try {
@@ -1481,7 +1571,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1481
1571
  }
1482
1572
  }
1483
1573
  case "message_meta_agent": {
1484
- logger.info("tool:message_meta_agent", { namespace: a.namespace });
1574
+ logger.info("[mcp] message_meta_agent", { namespace: a.namespace });
1485
1575
  const ns = a.namespace;
1486
1576
  const message = a.message;
1487
1577
  try {
@@ -1503,7 +1593,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1503
1593
  }
1504
1594
  }
1505
1595
  case "list_meta_agents": {
1506
- logger.info("tool:list_meta_agents");
1596
+ logger.info("[mcp] list_meta_agents");
1507
1597
  const agents = await metaAgentManager.listMetaAgents();
1508
1598
  return {
1509
1599
  content: [{
@@ -1513,7 +1603,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1513
1603
  };
1514
1604
  }
1515
1605
  case "stop_meta_agent": {
1516
- logger.info("tool:stop_meta_agent", { namespace: a.namespace });
1606
+ logger.info("[mcp] stop_meta_agent", { namespace: a.namespace });
1517
1607
  const ns = a.namespace;
1518
1608
  try {
1519
1609
  await metaAgentManager.stopMetaAgent(ns);
@@ -1534,7 +1624,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1534
1624
  }
1535
1625
  }
1536
1626
  case "list_drivers": {
1537
- logger.info("tool:list_drivers");
1627
+ logger.info("[mcp] list_drivers");
1538
1628
  const drivers = getDriverStatus();
1539
1629
  return {
1540
1630
  content: [{
@@ -1547,6 +1637,220 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1547
1637
  }],
1548
1638
  };
1549
1639
  }
1640
+ case "export_jobs": {
1641
+ logger.info("[mcp] export_jobs");
1642
+ const days = typeof a.days === "number" ? a.days : 7;
1643
+ const format = a.format === "json" ? "json" : "jsonl";
1644
+ const statusFilter = typeof a.status === "string" ? a.status : undefined;
1645
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
1646
+ const allRecords = await jobStore.listJobs();
1647
+ const filtered = allRecords.filter((r) => {
1648
+ if (r.startedAt && new Date(r.startedAt).getTime() < cutoff)
1649
+ return false;
1650
+ if (!r.startedAt)
1651
+ return false;
1652
+ if (statusFilter && r.status !== statusFilter)
1653
+ return false;
1654
+ return true;
1655
+ });
1656
+ const records = filtered.map((r) => {
1657
+ let durationSeconds = null;
1658
+ if (r.startedAt && r.finishedAt) {
1659
+ durationSeconds = Math.round((new Date(r.finishedAt).getTime() - new Date(r.startedAt).getTime()) / 1000);
1660
+ }
1661
+ return {
1662
+ id: r.id,
1663
+ status: r.status,
1664
+ repo_url: r.repoUrl,
1665
+ task: (r.task ?? "").slice(0, 500),
1666
+ started_at: r.startedAt ?? null,
1667
+ finished_at: r.finishedAt ?? null,
1668
+ exit_code: r.exitCode ?? null,
1669
+ output_lines: r.outputLineCount ?? 0,
1670
+ score: r.score ?? null,
1671
+ duration_seconds: durationSeconds,
1672
+ };
1673
+ });
1674
+ const text = format === "json"
1675
+ ? JSON.stringify(records)
1676
+ : records.map((r) => JSON.stringify(r)).join("\n");
1677
+ return {
1678
+ content: [{ type: "text", text }],
1679
+ };
1680
+ }
1681
+ case "get_cost_report": {
1682
+ logger.info("[mcp] get_cost_report");
1683
+ const days = typeof a.days === "number" ? a.days : 30;
1684
+ const groupBy = a.group_by === "day" ? "day" : a.group_by === "status" ? "status" : "repo";
1685
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
1686
+ const allRecords = await jobStore.listJobs();
1687
+ const filtered = allRecords.filter((r) => {
1688
+ if (!r.startedAt)
1689
+ return false;
1690
+ return new Date(r.startedAt).getTime() >= cutoff;
1691
+ });
1692
+ const groups = new Map();
1693
+ for (const r of filtered) {
1694
+ let key;
1695
+ if (groupBy === "day") {
1696
+ key = r.startedAt ? r.startedAt.slice(0, 10) : "unknown";
1697
+ }
1698
+ else if (groupBy === "status") {
1699
+ key = r.status;
1700
+ }
1701
+ else {
1702
+ key = r.repoUrl;
1703
+ }
1704
+ const g = groups.get(key) ?? { total_usd: 0, job_count: 0, scores: [] };
1705
+ g.total_usd += r.costUsd ?? 0;
1706
+ g.job_count += 1;
1707
+ if (r.score != null)
1708
+ g.scores.push(r.score);
1709
+ groups.set(key, g);
1710
+ }
1711
+ const summary = Array.from(groups.entries())
1712
+ .map(([key, g]) => ({
1713
+ group: key,
1714
+ total_usd: Math.round(g.total_usd * 10000) / 10000,
1715
+ job_count: g.job_count,
1716
+ avg_cost_usd: g.job_count > 0 ? Math.round((g.total_usd / g.job_count) * 10000) / 10000 : 0,
1717
+ avg_score: g.scores.length > 0 ? Math.round((g.scores.reduce((s, v) => s + v, 0) / g.scores.length) * 1000) / 1000 : null,
1718
+ }))
1719
+ .sort((a, b) => b.total_usd - a.total_usd);
1720
+ return {
1721
+ content: [{
1722
+ type: "text",
1723
+ text: JSON.stringify({ group_by: groupBy, days, total_groups: summary.length, summary }),
1724
+ }],
1725
+ };
1726
+ }
1727
+ case "search_jobs": {
1728
+ logger.info("[mcp] search_jobs", { query: a.query });
1729
+ const query = (a.query ?? "").toLowerCase();
1730
+ const days = typeof a.days === "number" ? a.days : 30;
1731
+ const statusFilter = typeof a.status === "string" ? a.status : undefined;
1732
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
1733
+ if (!query) {
1734
+ return {
1735
+ content: [{ type: "text", text: JSON.stringify({ error: "query is required" }) }],
1736
+ };
1737
+ }
1738
+ const allRecords = await jobStore.listJobs();
1739
+ const matches = allRecords
1740
+ .filter((r) => {
1741
+ if (!r.startedAt)
1742
+ return false;
1743
+ if (new Date(r.startedAt).getTime() < cutoff)
1744
+ return false;
1745
+ if (statusFilter && r.status !== statusFilter)
1746
+ return false;
1747
+ return (r.task ?? "").toLowerCase().includes(query);
1748
+ })
1749
+ .map((r) => {
1750
+ const task = r.task ?? "";
1751
+ const idx = task.toLowerCase().indexOf(query);
1752
+ const start = Math.max(0, idx - 50);
1753
+ const end = Math.min(task.length, idx + query.length + 50);
1754
+ const snippet = (start > 0 ? "…" : "") + task.slice(start, end) + (end < task.length ? "…" : "");
1755
+ return {
1756
+ id: r.id,
1757
+ status: r.status,
1758
+ repo_url: r.repoUrl,
1759
+ started_at: r.startedAt ?? null,
1760
+ score: r.score ?? null,
1761
+ task_snippet: snippet,
1762
+ };
1763
+ });
1764
+ return {
1765
+ content: [{
1766
+ type: "text",
1767
+ text: JSON.stringify({ query: a.query, days, total: matches.length, matches }),
1768
+ }],
1769
+ };
1770
+ }
1771
+ case "swarm_task": {
1772
+ const repoUrl = normalizeRepoUrl(a.repo_url);
1773
+ const goal = a.goal;
1774
+ const maxAgents = typeof a.max_agents === "number" ? a.max_agents : 10;
1775
+ if (maxAgents > SWARM_MAX_AGENTS_HARD_CAP) {
1776
+ return {
1777
+ content: [{
1778
+ type: "text",
1779
+ text: JSON.stringify({ error: `max_agents ${maxAgents} exceeds hard cap of ${SWARM_MAX_AGENTS_HARD_CAP}` }),
1780
+ }],
1781
+ };
1782
+ }
1783
+ logger.info("[mcp] swarm_task", { repo_url: repoUrl, goal: goal.slice(0, 80), max_agents: maxAgents });
1784
+ try {
1785
+ const result = await runSwarm({
1786
+ repoUrl,
1787
+ goal,
1788
+ maxAgents,
1789
+ synthesisOutput: a.synthesis_output,
1790
+ synthesisPrompt: a.synthesis_prompt,
1791
+ maxBudgetPerAgent: typeof a.max_budget_per_agent === "number" ? a.max_budget_per_agent : 5,
1792
+ agentModel: a.agent_model,
1793
+ agentDriver: a.agent_driver,
1794
+ manager,
1795
+ redis: getRedis(),
1796
+ getJobOutput: (id, offset) => jobStore.getOutput(id, offset),
1797
+ });
1798
+ return {
1799
+ content: [{
1800
+ type: "text",
1801
+ text: JSON.stringify({
1802
+ swarm_id: result.swarm_id,
1803
+ sub_job_ids: result.sub_job_ids,
1804
+ decomposed_tasks: result.decomposed_tasks,
1805
+ status: result.status,
1806
+ message: `Swarm started with ${result.sub_job_ids.length} sub-agents. Use get_swarm_status to poll progress.`,
1807
+ }),
1808
+ }],
1809
+ };
1810
+ }
1811
+ catch (err) {
1812
+ logger.error("[mcp] swarm_task failed", { err: String(err) });
1813
+ return {
1814
+ content: [{
1815
+ type: "text",
1816
+ text: JSON.stringify({ error: String(err) }),
1817
+ }],
1818
+ };
1819
+ }
1820
+ }
1821
+ case "get_swarm_status": {
1822
+ const swarmId = a.swarm_id;
1823
+ logger.info("[mcp] get_swarm_status", { swarm_id: swarmId });
1824
+ const record = await getSwarmStatus(swarmId, getRedis());
1825
+ if (!record) {
1826
+ return {
1827
+ content: [{
1828
+ type: "text",
1829
+ text: JSON.stringify({ error: `Swarm '${swarmId}' not found` }),
1830
+ }],
1831
+ };
1832
+ }
1833
+ const subJobsDone = record.sub_job_ids.length;
1834
+ const subJobsFailed = 0; // detailed counts require Redis lookup; status field covers this
1835
+ return {
1836
+ content: [{
1837
+ type: "text",
1838
+ text: JSON.stringify({
1839
+ swarm_id: record.swarm_id,
1840
+ goal: record.goal,
1841
+ status: record.status,
1842
+ sub_job_ids: record.sub_job_ids,
1843
+ sub_jobs_total: record.sub_job_ids.length,
1844
+ synthesis_job_id: record.synthesis_job_id ?? null,
1845
+ synthesis_output: record.synthesis_output,
1846
+ decomposed_tasks: record.decomposed_tasks,
1847
+ created_at: record.created_at,
1848
+ completed_at: record.completed_at ?? null,
1849
+ error: record.error ?? null,
1850
+ }),
1851
+ }],
1852
+ };
1853
+ }
1550
1854
  default:
1551
1855
  throw new Error(`Unknown tool: ${name}`);
1552
1856
  }
@@ -1569,6 +1873,18 @@ await manager.init();
1569
1873
  await seedBuiltinProfiles(profileStore);
1570
1874
  await coordinator.start();
1571
1875
  await cronEngine.start();
1876
+ // Startup summary — logged after all engines are initialized
1877
+ logger.info("[cc-agent] started", { version: PKG_VERSION, namespace });
1878
+ const _startupJobs = manager.list();
1879
+ const _jobCounts = {};
1880
+ for (const j of _startupJobs)
1881
+ _jobCounts[j.status] = (_jobCounts[j.status] ?? 0) + 1;
1882
+ logger.info("[cc-agent] startup", { jobs_total: _startupJobs.length, ..._jobCounts });
1883
+ const _startupCrons = await cronEngine.listCrons();
1884
+ logger.info("[cc-agent] startup", { crons_loaded: _startupCrons.length });
1885
+ for (const _cron of _startupCrons) {
1886
+ logger.info("[cron] registered", { id: _cron.id, schedule: _cron.schedule, intervalMs: _cron.intervalMs });
1887
+ }
1572
1888
  metaAgentManager.startPoller();
1573
1889
  const transport = new StdioServerTransport();
1574
1890
  await server.connect(transport);