@gonzih/cc-agent 0.15.11 → 0.15.16

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
@@ -687,6 +687,42 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
687
687
  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
688
  inputSchema: { type: "object", properties: {} },
689
689
  },
690
+ {
691
+ name: "export_jobs",
692
+ 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.",
693
+ inputSchema: {
694
+ type: "object",
695
+ properties: {
696
+ days: { type: "number", description: "How many days back to export (default: 7)" },
697
+ format: { type: "string", description: "Output format: 'jsonl' (one record per line) or 'json' (array). Default: 'jsonl'" },
698
+ status: { type: "string", description: "Filter by status: 'done' | 'failed' | 'cancelled' | 'running' (optional)" },
699
+ },
700
+ },
701
+ },
702
+ {
703
+ name: "get_cost_report",
704
+ 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.",
705
+ inputSchema: {
706
+ type: "object",
707
+ properties: {
708
+ days: { type: "number", description: "How many days back to include (default: 30)" },
709
+ group_by: { type: "string", description: "Group by 'repo' | 'day' | 'status' (default: 'repo')" },
710
+ },
711
+ },
712
+ },
713
+ {
714
+ name: "search_jobs",
715
+ 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.",
716
+ inputSchema: {
717
+ type: "object",
718
+ properties: {
719
+ query: { type: "string", description: "Search term to look for in task prompts (case-insensitive)" },
720
+ days: { type: "number", description: "How many days back to search (default: 30)" },
721
+ status: { type: "string", description: "Filter by status: 'done' | 'failed' | 'cancelled' | 'running' (optional)" },
722
+ },
723
+ required: ["query"],
724
+ },
725
+ },
690
726
  ],
691
727
  }));
692
728
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
@@ -695,7 +731,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
695
731
  switch (name) {
696
732
  case "spawn_agent": {
697
733
  const repoUrl = normalizeRepoUrl(a.repo_url);
698
- logger.info("tool:spawn_agent", { repo_url: repoUrl, task: a.task?.slice(0, 80) });
734
+ logger.info("[mcp] spawn_agent", { repo_url: repoUrl, task: a.task?.slice(0, 80) });
699
735
  const owner = extractGithubOwner(repoUrl);
700
736
  const isTrusted = !owner || TRUSTED_OWNERS.includes(owner);
701
737
  const jobId = await manager.spawn({
@@ -786,7 +822,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
786
822
  };
787
823
  }
788
824
  case "get_job_status": {
789
- logger.info("tool:get_job_status", { job_id: a.job_id });
825
+ logger.info("[mcp] get_job_status", { job_id: a.job_id });
790
826
  const job = manager.getJob(a.job_id);
791
827
  if (!job) {
792
828
  return { content: [{ type: "text", text: JSON.stringify({ error: "Job not found" }) }] };
@@ -822,7 +858,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
822
858
  case "wait_for_job": {
823
859
  const waitJobId = a.job_id;
824
860
  const timeoutSeconds = typeof a.timeout_seconds === "number" ? a.timeout_seconds : 300;
825
- logger.info("tool:wait_for_job", { job_id: waitJobId, timeout_seconds: timeoutSeconds });
861
+ logger.info("[mcp] wait_for_job", { job_id: waitJobId, timeout_seconds: timeoutSeconds });
826
862
  const TERMINAL_STATUSES = new Set(["done", "failed", "cancelled", "rejected", "interrupted"]);
827
863
  const deadlineMs = Date.now() + timeoutSeconds * 1000;
828
864
  let waitRecord = await jobStore.getJob(waitJobId);
@@ -858,7 +894,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
858
894
  };
859
895
  }
860
896
  case "get_job_output": {
861
- logger.info("tool:get_job_output", { job_id: a.job_id, offset: a.offset });
897
+ logger.info("[mcp] get_job_output", { job_id: a.job_id, offset: a.offset });
862
898
  const offset = typeof a.offset === "number" ? a.offset : 0;
863
899
  const { lines, done, toolCalls } = await manager.getOutput(a.job_id, offset);
864
900
  return {
@@ -878,7 +914,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
878
914
  };
879
915
  }
880
916
  case "list_jobs": {
881
- logger.info("tool:list_jobs");
917
+ logger.info("[mcp] list_jobs");
882
918
  const minScore = typeof a.min_score === "number" ? a.min_score : undefined;
883
919
  let jobs = (await jobStore.listJobs()) ?? [];
884
920
  if (minScore !== undefined) {
@@ -896,7 +932,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
896
932
  };
897
933
  }
898
934
  case "cancel_job": {
899
- logger.info("tool:cancel_job", { job_id: a.job_id });
935
+ logger.info("[mcp] cancel_job", { job_id: a.job_id });
900
936
  const cancelled = manager.cancel(a.job_id);
901
937
  return {
902
938
  content: [
@@ -908,7 +944,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
908
944
  };
909
945
  }
910
946
  case "send_message": {
911
- logger.info("tool:send_message", { job_id: a.job_id });
947
+ logger.info("[mcp] send_message", { job_id: a.job_id });
912
948
  const result = await manager.sendMessage(a.job_id, a.message);
913
949
  return {
914
950
  content: [
@@ -922,7 +958,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
922
958
  };
923
959
  }
924
960
  case "cost_summary": {
925
- logger.info("tool:cost_summary");
961
+ logger.info("[mcp] cost_summary");
926
962
  // Use jobStore (Redis/disk) to include all persisted jobs, not just in-memory ones
927
963
  const allRecords = await jobStore.listJobs();
928
964
  let totalCostUsd = 0;
@@ -955,12 +991,12 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
955
991
  };
956
992
  }
957
993
  case "get_version":
958
- logger.info("tool:get_version");
994
+ logger.info("[mcp] get_version");
959
995
  return {
960
996
  content: [{ type: "text", text: JSON.stringify({ version: PKG_VERSION }) }],
961
997
  };
962
998
  case "create_profile": {
963
- logger.info("tool:create_profile", { name: a.name });
999
+ logger.info("[mcp] create_profile", { name: a.name });
964
1000
  const profileName = a.name;
965
1001
  if (!/^[\w-]+$/.test(profileName)) {
966
1002
  return {
@@ -982,7 +1018,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
982
1018
  };
983
1019
  }
984
1020
  case "list_profiles": {
985
- logger.info("tool:list_profiles");
1021
+ logger.info("[mcp] list_profiles");
986
1022
  const profiles = (await loadProfiles()).map(({ name, repoUrl, description, defaultBudgetUsd, builtin }) => ({
987
1023
  name,
988
1024
  repoUrl,
@@ -996,7 +1032,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
996
1032
  };
997
1033
  }
998
1034
  case "delete_profile": {
999
- logger.info("tool:delete_profile", { name: a.name });
1035
+ logger.info("[mcp] delete_profile", { name: a.name });
1000
1036
  const deleted = await deleteProfile(a.name);
1001
1037
  return {
1002
1038
  content: [
@@ -1010,7 +1046,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1010
1046
  };
1011
1047
  }
1012
1048
  case "spawn_from_profile": {
1013
- logger.info("tool:spawn_from_profile", { profile_name: a.profile_name });
1049
+ logger.info("[mcp] spawn_from_profile", { profile_name: a.profile_name });
1014
1050
  const profile = await getProfile(a.profile_name);
1015
1051
  if (!profile) {
1016
1052
  return {
@@ -1038,7 +1074,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1038
1074
  };
1039
1075
  }
1040
1076
  case "create_plan": {
1041
- logger.info("tool:create_plan", { goal: a.goal?.slice(0, 80) });
1077
+ logger.info("[mcp] create_plan", { goal: a.goal?.slice(0, 80) });
1042
1078
  const goal = a.goal;
1043
1079
  const steps = a.steps;
1044
1080
  const stepIdToJobId = new Map();
@@ -1134,14 +1170,14 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1134
1170
  };
1135
1171
  }
1136
1172
  case "wake_job": {
1137
- logger.info("tool:wake_job", { job_id: a.job_id });
1173
+ logger.info("[mcp] wake_job", { job_id: a.job_id });
1138
1174
  const result = await manager.wakeJob(a.job_id);
1139
1175
  return {
1140
1176
  content: [{ type: "text", text: JSON.stringify(result) }],
1141
1177
  };
1142
1178
  }
1143
1179
  case "list_model_ratings": {
1144
- logger.info("tool:list_model_ratings");
1180
+ logger.info("[mcp] list_model_ratings");
1145
1181
  const ratingsFile = join(homedir(), ".cc-agent", "model-ratings.jsonl");
1146
1182
  let ratings = [];
1147
1183
  try {
@@ -1157,7 +1193,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1157
1193
  };
1158
1194
  }
1159
1195
  case "get_logs": {
1160
- logger.info("tool:get_logs", { lines: a.lines });
1196
+ logger.info("[mcp] get_logs", { lines: a.lines });
1161
1197
  const n = Math.min(typeof a.lines === "number" ? a.lines : 100, 500);
1162
1198
  const logFile = join(homedir(), ".cc-agent", "logs", "cc-agent.log");
1163
1199
  let lines = [];
@@ -1174,7 +1210,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1174
1210
  };
1175
1211
  }
1176
1212
  case "list_project_issues": {
1177
- logger.info("tool:list_project_issues", { repo: a.repo });
1213
+ logger.info("[mcp] list_project_issues", { repo: a.repo });
1178
1214
  const repo = a.repo;
1179
1215
  const state = a.state ?? "open";
1180
1216
  const labels = a.labels;
@@ -1196,7 +1232,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1196
1232
  }
1197
1233
  }
1198
1234
  case "work_on_issue": {
1199
- logger.info("tool:work_on_issue", { repo: a.repo, issue_number: a.issue_number });
1235
+ logger.info("[mcp] work_on_issue", { repo: a.repo, issue_number: a.issue_number });
1200
1236
  const repo = a.repo;
1201
1237
  const issueNumber = a.issue_number;
1202
1238
  const extraContext = a.extra_context;
@@ -1240,7 +1276,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1240
1276
  };
1241
1277
  }
1242
1278
  case "comment_on_issue": {
1243
- logger.info("tool:comment_on_issue", { repo: a.repo, issue_number: a.issue_number });
1279
+ logger.info("[mcp] comment_on_issue", { repo: a.repo, issue_number: a.issue_number });
1244
1280
  const repo = a.repo;
1245
1281
  const issueNumber = a.issue_number;
1246
1282
  const body = a.body;
@@ -1253,7 +1289,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1253
1289
  }
1254
1290
  }
1255
1291
  case "close_issue": {
1256
- logger.info("tool:close_issue", { repo: a.repo, issue_number: a.issue_number });
1292
+ logger.info("[mcp] close_issue", { repo: a.repo, issue_number: a.issue_number });
1257
1293
  const repo = a.repo;
1258
1294
  const issueNumber = a.issue_number;
1259
1295
  const comment = a.comment;
@@ -1269,14 +1305,14 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1269
1305
  }
1270
1306
  }
1271
1307
  case "approve_job": {
1272
- logger.info("tool:approve_job", { job_id: a.job_id });
1308
+ logger.info("[mcp] approve_job", { job_id: a.job_id });
1273
1309
  const result = await manager.approveJob(a.job_id);
1274
1310
  return {
1275
1311
  content: [{ type: "text", text: JSON.stringify(result) }],
1276
1312
  };
1277
1313
  }
1278
1314
  case "set_job_score": {
1279
- logger.info("tool:set_job_score", { job_id: a.job_id, score: a.score });
1315
+ logger.info("[mcp] set_job_score", { job_id: a.job_id, score: a.score });
1280
1316
  const result = manager.setJobScore(a.job_id, a.score, a.reason);
1281
1317
  return {
1282
1318
  content: [{ type: "text", text: JSON.stringify(result) }],
@@ -1286,7 +1322,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1286
1322
  const repoParam = a.repo;
1287
1323
  const ns = repoParam ?? a.namespace ?? getNamespace();
1288
1324
  const limit = typeof a.limit === "number" ? a.limit : 10;
1289
- logger.info("tool:get_learnings", { key: ns, limit });
1325
+ logger.info("[mcp] get_learnings", { key: ns, limit });
1290
1326
  const learnings = await learningsStore.getLearnings(ns, limit);
1291
1327
  return {
1292
1328
  content: [{
@@ -1297,7 +1333,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1297
1333
  }
1298
1334
  case "clear_learnings": {
1299
1335
  const ns = a.namespace ?? getNamespace();
1300
- logger.info("tool:clear_learnings", { namespace: ns });
1336
+ logger.info("[mcp] clear_learnings", { namespace: ns });
1301
1337
  await learningsStore.clearLearnings(ns);
1302
1338
  return {
1303
1339
  content: [{
@@ -1307,7 +1343,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1307
1343
  };
1308
1344
  }
1309
1345
  case "docker_ps": {
1310
- logger.info("tool:docker_ps");
1346
+ logger.info("[mcp] docker_ps");
1311
1347
  const containers = await listCcAgentContainers();
1312
1348
  return {
1313
1349
  content: [{
@@ -1317,7 +1353,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1317
1353
  };
1318
1354
  }
1319
1355
  case "list_token_status": {
1320
- logger.info("tool:list_token_status");
1356
+ logger.info("[mcp] list_token_status");
1321
1357
  const tokens = loadTokens();
1322
1358
  const status = await getTokenStatus();
1323
1359
  const tokenList = tokens.map((t, i) => ({
@@ -1338,12 +1374,12 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1338
1374
  };
1339
1375
  }
1340
1376
  case "list_crons": {
1341
- logger.info("tool:list_crons");
1377
+ logger.info("[mcp] list_crons");
1342
1378
  const crons = await cronEngine.listCrons();
1343
1379
  return { content: [{ type: "text", text: JSON.stringify({ crons, total: crons.length }) }] };
1344
1380
  }
1345
1381
  case "create_cron": {
1346
- logger.info("tool:create_cron", { schedule: a.schedule });
1382
+ logger.info("[mcp] create_cron", { schedule: a.schedule });
1347
1383
  const cron = await cronEngine.addCron({
1348
1384
  chatId: typeof a.chat_id === "number" ? a.chat_id : 0,
1349
1385
  intervalMs: a.interval_ms,
@@ -1355,12 +1391,12 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1355
1391
  return { content: [{ type: "text", text: JSON.stringify(cron) }] };
1356
1392
  }
1357
1393
  case "delete_cron": {
1358
- logger.info("tool:delete_cron", { cron_id: a.cron_id });
1394
+ logger.info("[mcp] delete_cron", { cron_id: a.cron_id });
1359
1395
  const deleted = await cronEngine.deleteCron(a.cron_id);
1360
1396
  return { content: [{ type: "text", text: JSON.stringify({ deleted, cron_id: a.cron_id }) }] };
1361
1397
  }
1362
1398
  case "update_cron": {
1363
- logger.info("tool:update_cron", { cron_id: a.cron_id });
1399
+ logger.info("[mcp] update_cron", { cron_id: a.cron_id });
1364
1400
  const updates = {};
1365
1401
  if (typeof a.interval_ms === "number")
1366
1402
  updates.intervalMs = a.interval_ms;
@@ -1374,7 +1410,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1374
1410
  return { content: [{ type: "text", text: JSON.stringify(updated ?? { error: "cron not found" }) }] };
1375
1411
  }
1376
1412
  case "list_notifications": {
1377
- logger.info("tool:list_notifications");
1413
+ logger.info("[mcp] list_notifications");
1378
1414
  const ns = getNamespace();
1379
1415
  const redis = getRedis();
1380
1416
  let messages = [];
@@ -1384,7 +1420,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1384
1420
  return { content: [{ type: "text", text: JSON.stringify({ messages, total: messages.length, namespace: ns }) }] };
1385
1421
  }
1386
1422
  case "list_active_repos": {
1387
- logger.info("tool:list_active_repos");
1423
+ logger.info("[mcp] list_active_repos");
1388
1424
  const redis = getRedis();
1389
1425
  if (!redis)
1390
1426
  return { content: [{ type: "text", text: "Redis unavailable" }] };
@@ -1431,7 +1467,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1431
1467
  };
1432
1468
  }
1433
1469
  case "get_pubsub_status": {
1434
- logger.info("tool:get_pubsub_status");
1470
+ logger.info("[mcp] get_pubsub_status");
1435
1471
  const redis = getRedis();
1436
1472
  if (!redis)
1437
1473
  return { content: [{ type: "text", text: "Redis unavailable" }] };
@@ -1459,7 +1495,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1459
1495
  };
1460
1496
  }
1461
1497
  case "start_meta_agent": {
1462
- logger.info("tool:start_meta_agent", { namespace: a.namespace });
1498
+ logger.info("[mcp] start_meta_agent", { namespace: a.namespace });
1463
1499
  const ns = a.namespace;
1464
1500
  const repoUrl = a.repo_url;
1465
1501
  try {
@@ -1481,7 +1517,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1481
1517
  }
1482
1518
  }
1483
1519
  case "message_meta_agent": {
1484
- logger.info("tool:message_meta_agent", { namespace: a.namespace });
1520
+ logger.info("[mcp] message_meta_agent", { namespace: a.namespace });
1485
1521
  const ns = a.namespace;
1486
1522
  const message = a.message;
1487
1523
  try {
@@ -1503,7 +1539,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1503
1539
  }
1504
1540
  }
1505
1541
  case "list_meta_agents": {
1506
- logger.info("tool:list_meta_agents");
1542
+ logger.info("[mcp] list_meta_agents");
1507
1543
  const agents = await metaAgentManager.listMetaAgents();
1508
1544
  return {
1509
1545
  content: [{
@@ -1513,7 +1549,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1513
1549
  };
1514
1550
  }
1515
1551
  case "stop_meta_agent": {
1516
- logger.info("tool:stop_meta_agent", { namespace: a.namespace });
1552
+ logger.info("[mcp] stop_meta_agent", { namespace: a.namespace });
1517
1553
  const ns = a.namespace;
1518
1554
  try {
1519
1555
  await metaAgentManager.stopMetaAgent(ns);
@@ -1534,7 +1570,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1534
1570
  }
1535
1571
  }
1536
1572
  case "list_drivers": {
1537
- logger.info("tool:list_drivers");
1573
+ logger.info("[mcp] list_drivers");
1538
1574
  const drivers = getDriverStatus();
1539
1575
  return {
1540
1576
  content: [{
@@ -1547,6 +1583,137 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1547
1583
  }],
1548
1584
  };
1549
1585
  }
1586
+ case "export_jobs": {
1587
+ logger.info("[mcp] export_jobs");
1588
+ const days = typeof a.days === "number" ? a.days : 7;
1589
+ const format = a.format === "json" ? "json" : "jsonl";
1590
+ const statusFilter = typeof a.status === "string" ? a.status : undefined;
1591
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
1592
+ const allRecords = await jobStore.listJobs();
1593
+ const filtered = allRecords.filter((r) => {
1594
+ if (r.startedAt && new Date(r.startedAt).getTime() < cutoff)
1595
+ return false;
1596
+ if (!r.startedAt)
1597
+ return false;
1598
+ if (statusFilter && r.status !== statusFilter)
1599
+ return false;
1600
+ return true;
1601
+ });
1602
+ const records = filtered.map((r) => {
1603
+ let durationSeconds = null;
1604
+ if (r.startedAt && r.finishedAt) {
1605
+ durationSeconds = Math.round((new Date(r.finishedAt).getTime() - new Date(r.startedAt).getTime()) / 1000);
1606
+ }
1607
+ return {
1608
+ id: r.id,
1609
+ status: r.status,
1610
+ repo_url: r.repoUrl,
1611
+ task: (r.task ?? "").slice(0, 500),
1612
+ started_at: r.startedAt ?? null,
1613
+ finished_at: r.finishedAt ?? null,
1614
+ exit_code: r.exitCode ?? null,
1615
+ output_lines: r.outputLineCount ?? 0,
1616
+ score: r.score ?? null,
1617
+ duration_seconds: durationSeconds,
1618
+ };
1619
+ });
1620
+ const text = format === "json"
1621
+ ? JSON.stringify(records)
1622
+ : records.map((r) => JSON.stringify(r)).join("\n");
1623
+ return {
1624
+ content: [{ type: "text", text }],
1625
+ };
1626
+ }
1627
+ case "get_cost_report": {
1628
+ logger.info("[mcp] get_cost_report");
1629
+ const days = typeof a.days === "number" ? a.days : 30;
1630
+ const groupBy = a.group_by === "day" ? "day" : a.group_by === "status" ? "status" : "repo";
1631
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
1632
+ const allRecords = await jobStore.listJobs();
1633
+ const filtered = allRecords.filter((r) => {
1634
+ if (!r.startedAt)
1635
+ return false;
1636
+ return new Date(r.startedAt).getTime() >= cutoff;
1637
+ });
1638
+ const groups = new Map();
1639
+ for (const r of filtered) {
1640
+ let key;
1641
+ if (groupBy === "day") {
1642
+ key = r.startedAt ? r.startedAt.slice(0, 10) : "unknown";
1643
+ }
1644
+ else if (groupBy === "status") {
1645
+ key = r.status;
1646
+ }
1647
+ else {
1648
+ key = r.repoUrl;
1649
+ }
1650
+ const g = groups.get(key) ?? { total_usd: 0, job_count: 0, scores: [] };
1651
+ g.total_usd += r.costUsd ?? 0;
1652
+ g.job_count += 1;
1653
+ if (r.score != null)
1654
+ g.scores.push(r.score);
1655
+ groups.set(key, g);
1656
+ }
1657
+ const summary = Array.from(groups.entries())
1658
+ .map(([key, g]) => ({
1659
+ group: key,
1660
+ total_usd: Math.round(g.total_usd * 10000) / 10000,
1661
+ job_count: g.job_count,
1662
+ avg_cost_usd: g.job_count > 0 ? Math.round((g.total_usd / g.job_count) * 10000) / 10000 : 0,
1663
+ avg_score: g.scores.length > 0 ? Math.round((g.scores.reduce((s, v) => s + v, 0) / g.scores.length) * 1000) / 1000 : null,
1664
+ }))
1665
+ .sort((a, b) => b.total_usd - a.total_usd);
1666
+ return {
1667
+ content: [{
1668
+ type: "text",
1669
+ text: JSON.stringify({ group_by: groupBy, days, total_groups: summary.length, summary }),
1670
+ }],
1671
+ };
1672
+ }
1673
+ case "search_jobs": {
1674
+ logger.info("[mcp] search_jobs", { query: a.query });
1675
+ const query = (a.query ?? "").toLowerCase();
1676
+ const days = typeof a.days === "number" ? a.days : 30;
1677
+ const statusFilter = typeof a.status === "string" ? a.status : undefined;
1678
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
1679
+ if (!query) {
1680
+ return {
1681
+ content: [{ type: "text", text: JSON.stringify({ error: "query is required" }) }],
1682
+ };
1683
+ }
1684
+ const allRecords = await jobStore.listJobs();
1685
+ const matches = allRecords
1686
+ .filter((r) => {
1687
+ if (!r.startedAt)
1688
+ return false;
1689
+ if (new Date(r.startedAt).getTime() < cutoff)
1690
+ return false;
1691
+ if (statusFilter && r.status !== statusFilter)
1692
+ return false;
1693
+ return (r.task ?? "").toLowerCase().includes(query);
1694
+ })
1695
+ .map((r) => {
1696
+ const task = r.task ?? "";
1697
+ const idx = task.toLowerCase().indexOf(query);
1698
+ const start = Math.max(0, idx - 50);
1699
+ const end = Math.min(task.length, idx + query.length + 50);
1700
+ const snippet = (start > 0 ? "…" : "") + task.slice(start, end) + (end < task.length ? "…" : "");
1701
+ return {
1702
+ id: r.id,
1703
+ status: r.status,
1704
+ repo_url: r.repoUrl,
1705
+ started_at: r.startedAt ?? null,
1706
+ score: r.score ?? null,
1707
+ task_snippet: snippet,
1708
+ };
1709
+ });
1710
+ return {
1711
+ content: [{
1712
+ type: "text",
1713
+ text: JSON.stringify({ query: a.query, days, total: matches.length, matches }),
1714
+ }],
1715
+ };
1716
+ }
1550
1717
  default:
1551
1718
  throw new Error(`Unknown tool: ${name}`);
1552
1719
  }
@@ -1569,6 +1736,18 @@ await manager.init();
1569
1736
  await seedBuiltinProfiles(profileStore);
1570
1737
  await coordinator.start();
1571
1738
  await cronEngine.start();
1739
+ // Startup summary — logged after all engines are initialized
1740
+ logger.info("[cc-agent] started", { version: PKG_VERSION, namespace });
1741
+ const _startupJobs = manager.list();
1742
+ const _jobCounts = {};
1743
+ for (const j of _startupJobs)
1744
+ _jobCounts[j.status] = (_jobCounts[j.status] ?? 0) + 1;
1745
+ logger.info("[cc-agent] startup", { jobs_total: _startupJobs.length, ..._jobCounts });
1746
+ const _startupCrons = await cronEngine.listCrons();
1747
+ logger.info("[cc-agent] startup", { crons_loaded: _startupCrons.length });
1748
+ for (const _cron of _startupCrons) {
1749
+ logger.info("[cron] registered", { id: _cron.id, schedule: _cron.schedule, intervalMs: _cron.intervalMs });
1750
+ }
1572
1751
  metaAgentManager.startPoller();
1573
1752
  const transport = new StdioServerTransport();
1574
1753
  await server.connect(transport);