@arcbridge/mcp-server 0.1.1 → 0.1.3
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 +9 -1
- package/dist/index.js +308 -9
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @arcbridge/mcp-server
|
|
2
2
|
|
|
3
|
-
MCP server for ArcBridge — exposes
|
|
3
|
+
MCP server for ArcBridge — exposes 29 architecture tools to AI coding agents via the [Model Context Protocol](https://modelcontextprotocol.io).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -88,6 +88,14 @@ Restart your AI agent (Claude Code, etc.) and approve the MCP server when prompt
|
|
|
88
88
|
| `arcbridge_verify_scenarios` | Run linked tests for quality scenarios |
|
|
89
89
|
| `arcbridge_run_role_check` | Run a role's quality checks against code |
|
|
90
90
|
|
|
91
|
+
### Metrics
|
|
92
|
+
|
|
93
|
+
| Tool | Description |
|
|
94
|
+
|------|-------------|
|
|
95
|
+
| `arcbridge_record_activity` | Record agent activity — model, tokens, cost, duration, quality snapshot |
|
|
96
|
+
| `arcbridge_get_metrics` | Query and aggregate activity by model, task, phase, tool, or day |
|
|
97
|
+
| `arcbridge_export_metrics` | Export metrics to JSON, CSV, or Markdown for git commits |
|
|
98
|
+
|
|
91
99
|
## How It Works
|
|
92
100
|
|
|
93
101
|
The server communicates over stdio using the MCP protocol. Each tool call receives a `target_dir` parameter pointing to an ArcBridge-initialized project. The server manages a SQLite database (`.arcbridge/index.db`) that caches architecture docs, indexed symbols, and planning state.
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/suppress-warnings.ts
|
|
4
|
+
import { suppressSqliteWarning } from "@arcbridge/core";
|
|
5
|
+
suppressSqliteWarning();
|
|
6
|
+
|
|
3
7
|
// src/index.ts
|
|
4
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
9
|
|
|
@@ -149,7 +153,7 @@ import { refreshFromDocs } from "@arcbridge/core";
|
|
|
149
153
|
// src/helpers.ts
|
|
150
154
|
import { join as join2 } from "path";
|
|
151
155
|
import { existsSync as existsSync2 } from "fs";
|
|
152
|
-
import { openDatabase } from "@arcbridge/core";
|
|
156
|
+
import { openDatabase, migrate } from "@arcbridge/core";
|
|
153
157
|
function ensureDb(ctx, targetDir) {
|
|
154
158
|
if (ctx.db) return ctx.db;
|
|
155
159
|
const dbPath = join2(targetDir, ".arcbridge", "index.db");
|
|
@@ -157,6 +161,7 @@ function ensureDb(ctx, targetDir) {
|
|
|
157
161
|
return null;
|
|
158
162
|
}
|
|
159
163
|
ctx.db = openDatabase(dbPath);
|
|
164
|
+
migrate(ctx.db);
|
|
160
165
|
ctx.projectRoot = targetDir;
|
|
161
166
|
return ctx.db;
|
|
162
167
|
}
|
|
@@ -762,6 +767,42 @@ function registerGetCurrentTasks(server, ctx) {
|
|
|
762
767
|
// src/tools/update-task.ts
|
|
763
768
|
import { z as z8 } from "zod";
|
|
764
769
|
import { syncTaskToYaml } from "@arcbridge/core";
|
|
770
|
+
|
|
771
|
+
// src/auto-record.ts
|
|
772
|
+
import { loadConfig, insertActivity } from "@arcbridge/core";
|
|
773
|
+
var configCache = /* @__PURE__ */ new Map();
|
|
774
|
+
var CACHE_TTL_MS = 3e4;
|
|
775
|
+
function isAutoRecordEnabled(projectRoot) {
|
|
776
|
+
const cached = configCache.get(projectRoot);
|
|
777
|
+
if (cached && Date.now() - cached.loadedAt < CACHE_TTL_MS) {
|
|
778
|
+
return cached.autoRecord;
|
|
779
|
+
}
|
|
780
|
+
const { config } = loadConfig(projectRoot);
|
|
781
|
+
const autoRecord2 = config?.metrics?.auto_record ?? false;
|
|
782
|
+
configCache.set(projectRoot, { autoRecord: autoRecord2, loadedAt: Date.now() });
|
|
783
|
+
return autoRecord2;
|
|
784
|
+
}
|
|
785
|
+
function autoRecord(db, projectRoot, params) {
|
|
786
|
+
try {
|
|
787
|
+
if (!isAutoRecordEnabled(projectRoot)) return;
|
|
788
|
+
insertActivity(db, {
|
|
789
|
+
toolName: params.toolName,
|
|
790
|
+
action: params.action,
|
|
791
|
+
taskId: params.taskId,
|
|
792
|
+
phaseId: params.phaseId,
|
|
793
|
+
durationMs: params.durationMs,
|
|
794
|
+
driftCount: params.driftCount,
|
|
795
|
+
driftErrors: params.driftErrors,
|
|
796
|
+
testPassCount: params.testPassCount,
|
|
797
|
+
testFailCount: params.testFailCount,
|
|
798
|
+
lintClean: params.lintClean,
|
|
799
|
+
typecheckClean: params.typecheckClean
|
|
800
|
+
});
|
|
801
|
+
} catch {
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/tools/update-task.ts
|
|
765
806
|
function registerUpdateTask(server, ctx) {
|
|
766
807
|
server.tool(
|
|
767
808
|
"arcbridge_update_task",
|
|
@@ -773,6 +814,7 @@ function registerUpdateTask(server, ctx) {
|
|
|
773
814
|
notes: z8.string().optional().describe("Optional notes about the status change")
|
|
774
815
|
},
|
|
775
816
|
async (params) => {
|
|
817
|
+
const start = Date.now();
|
|
776
818
|
const db = ensureDb(ctx, params.target_dir);
|
|
777
819
|
if (!db) return notInitialized();
|
|
778
820
|
const task = db.prepare("SELECT id, title, status, phase_id FROM tasks WHERE id = ?").get(params.task_id);
|
|
@@ -828,6 +870,13 @@ function registerUpdateTask(server, ctx) {
|
|
|
828
870
|
);
|
|
829
871
|
}
|
|
830
872
|
}
|
|
873
|
+
autoRecord(db, params.target_dir, {
|
|
874
|
+
toolName: "arcbridge_update_task",
|
|
875
|
+
action: `${task.id}: ${oldStatus} \u2192 ${params.status}`,
|
|
876
|
+
taskId: params.task_id,
|
|
877
|
+
phaseId: task.phase_id,
|
|
878
|
+
durationMs: Date.now() - start
|
|
879
|
+
});
|
|
831
880
|
return {
|
|
832
881
|
content: [{ type: "text", text: lines.join("\n") }]
|
|
833
882
|
};
|
|
@@ -1018,7 +1067,7 @@ import { indexProject as indexProject2, refreshFromDocs as refreshFromDocs4 } fr
|
|
|
1018
1067
|
function registerReindex(server, ctx) {
|
|
1019
1068
|
server.tool(
|
|
1020
1069
|
"arcbridge_reindex",
|
|
1021
|
-
"Re-index
|
|
1070
|
+
"Re-index the project: refreshes architecture docs from arc42/YAML files, then reindexes code symbols (TypeScript & C#/.NET). This is the first step of the sync pipeline \u2014 use it to pick up manual doc edits and code changes.",
|
|
1022
1071
|
{
|
|
1023
1072
|
target_dir: z11.string().describe("Absolute path to the project directory"),
|
|
1024
1073
|
tsconfig_path: z11.string().optional().describe("Override tsconfig.json path (default: auto-detect). Only used for TypeScript projects."),
|
|
@@ -1026,6 +1075,7 @@ function registerReindex(server, ctx) {
|
|
|
1026
1075
|
language: z11.enum(["typescript", "csharp", "auto"]).optional().describe("Project language. 'auto' detects from project files (default: 'auto')")
|
|
1027
1076
|
},
|
|
1028
1077
|
async (params) => {
|
|
1078
|
+
const start = Date.now();
|
|
1029
1079
|
const db = ensureDb(ctx, params.target_dir);
|
|
1030
1080
|
if (!db) return notInitialized();
|
|
1031
1081
|
try {
|
|
@@ -1049,6 +1099,11 @@ function registerReindex(server, ctx) {
|
|
|
1049
1099
|
`- **Routes analyzed:** ${result.routesAnalyzed}`,
|
|
1050
1100
|
`- **Duration:** ${result.durationMs}ms`
|
|
1051
1101
|
];
|
|
1102
|
+
autoRecord(db, params.target_dir, {
|
|
1103
|
+
toolName: "arcbridge_reindex",
|
|
1104
|
+
action: `${result.symbolsIndexed} symbols, ${result.filesProcessed} files`,
|
|
1105
|
+
durationMs: Date.now() - start
|
|
1106
|
+
});
|
|
1052
1107
|
return textResult(lines.join("\n"));
|
|
1053
1108
|
} catch (err) {
|
|
1054
1109
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1760,6 +1815,7 @@ function registerCheckDrift(server, ctx) {
|
|
|
1760
1815
|
persist: z18.boolean().default(true).describe("Write findings to drift_log table (default: true)")
|
|
1761
1816
|
},
|
|
1762
1817
|
async (params) => {
|
|
1818
|
+
const start = Date.now();
|
|
1763
1819
|
const db = ensureDb(ctx, params.target_dir);
|
|
1764
1820
|
if (!db) return notInitialized();
|
|
1765
1821
|
const entries = detectDrift(db);
|
|
@@ -1815,6 +1871,13 @@ function registerCheckDrift(server, ctx) {
|
|
|
1815
1871
|
""
|
|
1816
1872
|
);
|
|
1817
1873
|
}
|
|
1874
|
+
autoRecord(db, params.target_dir, {
|
|
1875
|
+
toolName: "arcbridge_check_drift",
|
|
1876
|
+
action: `${entries.length} issues (${errors} errors)`,
|
|
1877
|
+
driftCount: entries.length,
|
|
1878
|
+
driftErrors: errors,
|
|
1879
|
+
durationMs: Date.now() - start
|
|
1880
|
+
});
|
|
1818
1881
|
return textResult(lines.join("\n"));
|
|
1819
1882
|
}
|
|
1820
1883
|
);
|
|
@@ -2652,9 +2715,10 @@ import {
|
|
|
2652
2715
|
inferTaskStatuses,
|
|
2653
2716
|
applyInferences,
|
|
2654
2717
|
verifyScenarios,
|
|
2655
|
-
loadConfig,
|
|
2718
|
+
loadConfig as loadConfig2,
|
|
2656
2719
|
refreshFromDocs as refreshFromDocs5,
|
|
2657
|
-
syncPhaseToYaml
|
|
2720
|
+
syncPhaseToYaml,
|
|
2721
|
+
transaction
|
|
2658
2722
|
} from "@arcbridge/core";
|
|
2659
2723
|
function registerCompletePhase(server, ctx) {
|
|
2660
2724
|
server.tool(
|
|
@@ -2668,6 +2732,7 @@ function registerCompletePhase(server, ctx) {
|
|
|
2668
2732
|
run_tests: z23.boolean().default(false).describe("Run linked tests for quality scenarios before checking the quality gate")
|
|
2669
2733
|
},
|
|
2670
2734
|
async (params) => {
|
|
2735
|
+
const start = Date.now();
|
|
2671
2736
|
const db = ensureDb(ctx, params.target_dir);
|
|
2672
2737
|
if (!db) return notInitialized();
|
|
2673
2738
|
refreshFromDocs5(db, params.target_dir);
|
|
@@ -2715,7 +2780,7 @@ function registerCompletePhase(server, ctx) {
|
|
|
2715
2780
|
const projectRoot = ctx.projectRoot ?? params.target_dir;
|
|
2716
2781
|
let testCommand = "npx vitest run";
|
|
2717
2782
|
let timeoutMs = 6e4;
|
|
2718
|
-
const configResult =
|
|
2783
|
+
const configResult = loadConfig2(params.target_dir);
|
|
2719
2784
|
if (configResult.config) {
|
|
2720
2785
|
testCommand = configResult.config.testing.test_command;
|
|
2721
2786
|
timeoutMs = configResult.config.testing.timeout_ms;
|
|
@@ -2810,7 +2875,7 @@ function registerCompletePhase(server, ctx) {
|
|
|
2810
2875
|
const nextPhase = db.prepare(
|
|
2811
2876
|
"SELECT id, name FROM phases WHERE phase_number = ? AND status = 'planned'"
|
|
2812
2877
|
).get(phase.phase_number + 1);
|
|
2813
|
-
|
|
2878
|
+
transaction(db, () => {
|
|
2814
2879
|
db.prepare(
|
|
2815
2880
|
"UPDATE phases SET status = 'complete', completed_at = ?, gate_status = ? WHERE id = ?"
|
|
2816
2881
|
).run(now, gateStatus, phase.id);
|
|
@@ -2820,7 +2885,6 @@ function registerCompletePhase(server, ctx) {
|
|
|
2820
2885
|
).run(now, nextPhase.id);
|
|
2821
2886
|
}
|
|
2822
2887
|
});
|
|
2823
|
-
transition();
|
|
2824
2888
|
const projectRoot = ctx.projectRoot ?? params.target_dir;
|
|
2825
2889
|
syncPhaseToYaml(projectRoot, phase.id, "complete", void 0, now);
|
|
2826
2890
|
if (nextPhase) {
|
|
@@ -2868,6 +2932,12 @@ function registerCompletePhase(server, ctx) {
|
|
|
2868
2932
|
"Resolve the issues above before completing this phase."
|
|
2869
2933
|
);
|
|
2870
2934
|
}
|
|
2935
|
+
autoRecord(db, params.target_dir, {
|
|
2936
|
+
toolName: "arcbridge_complete_phase",
|
|
2937
|
+
action: `${phase.name}: ${allPass ? "PASSED" : "BLOCKED"}`,
|
|
2938
|
+
phaseId: params.phase_id,
|
|
2939
|
+
durationMs: Date.now() - start
|
|
2940
|
+
});
|
|
2871
2941
|
return textResult(lines.join("\n"));
|
|
2872
2942
|
}
|
|
2873
2943
|
);
|
|
@@ -3188,7 +3258,7 @@ function getRoleDefinition(roleId) {
|
|
|
3188
3258
|
|
|
3189
3259
|
// src/tools/verify-scenarios.ts
|
|
3190
3260
|
import { z as z25 } from "zod";
|
|
3191
|
-
import { verifyScenarios as verifyScenarios2, loadConfig as
|
|
3261
|
+
import { verifyScenarios as verifyScenarios2, loadConfig as loadConfig3 } from "@arcbridge/core";
|
|
3192
3262
|
function registerVerifyScenarios(server, ctx) {
|
|
3193
3263
|
server.tool(
|
|
3194
3264
|
"arcbridge_verify_scenarios",
|
|
@@ -3207,7 +3277,7 @@ function registerVerifyScenarios(server, ctx) {
|
|
|
3207
3277
|
if (!db) return notInitialized();
|
|
3208
3278
|
let testCommand = "npx vitest run";
|
|
3209
3279
|
let timeoutMs = 6e4;
|
|
3210
|
-
const configResult =
|
|
3280
|
+
const configResult = loadConfig3(params.target_dir);
|
|
3211
3281
|
if (configResult.config) {
|
|
3212
3282
|
testCommand = configResult.config.testing.test_command;
|
|
3213
3283
|
timeoutMs = configResult.config.testing.timeout_ms;
|
|
@@ -3702,6 +3772,232 @@ function runCustomRoleCheck(db, lines, roleDef) {
|
|
|
3702
3772
|
appendDriftSection(db, lines);
|
|
3703
3773
|
}
|
|
3704
3774
|
|
|
3775
|
+
// src/tools/record-activity.ts
|
|
3776
|
+
import { z as z27 } from "zod";
|
|
3777
|
+
import { insertActivity as insertActivity2, getSessionTotals } from "@arcbridge/core";
|
|
3778
|
+
function registerRecordActivity(server, ctx) {
|
|
3779
|
+
server.tool(
|
|
3780
|
+
"arcbridge_record_activity",
|
|
3781
|
+
"Record agent activity \u2014 model, tokens, cost, duration, and optional quality snapshot. Use this to track what work was done and measure agent performance.",
|
|
3782
|
+
{
|
|
3783
|
+
target_dir: z27.string().describe("Absolute path to the project directory"),
|
|
3784
|
+
tool_name: z27.string().describe("Name of the tool or action performed (e.g., 'arcbridge_update_task', 'code_edit')"),
|
|
3785
|
+
action: z27.string().optional().describe("Human-readable label (e.g., 'implement login form')"),
|
|
3786
|
+
model: z27.string().optional().describe("Model identifier (e.g., 'claude-sonnet-4-20250514')"),
|
|
3787
|
+
agent_role: z27.string().optional().describe("Active ArcBridge role (e.g., 'implementer')"),
|
|
3788
|
+
task_id: z27.string().optional().describe("Associated task ID"),
|
|
3789
|
+
phase_id: z27.string().optional().describe("Associated phase ID"),
|
|
3790
|
+
input_tokens: z27.number().int().nonnegative().optional().describe("Input/prompt tokens"),
|
|
3791
|
+
output_tokens: z27.number().int().nonnegative().optional().describe("Output/completion tokens"),
|
|
3792
|
+
total_tokens: z27.number().int().nonnegative().optional().describe("Total tokens (auto-computed if input+output given)"),
|
|
3793
|
+
cost_usd: z27.number().nonnegative().optional().describe("Estimated cost in USD"),
|
|
3794
|
+
duration_ms: z27.number().int().nonnegative().optional().describe("Wall-clock duration in ms"),
|
|
3795
|
+
drift_count: z27.number().int().nonnegative().optional().describe("Current drift count"),
|
|
3796
|
+
drift_errors: z27.number().int().nonnegative().optional().describe("Current drift errors"),
|
|
3797
|
+
test_pass_count: z27.number().int().nonnegative().optional().describe("Passing tests"),
|
|
3798
|
+
test_fail_count: z27.number().int().nonnegative().optional().describe("Failing tests"),
|
|
3799
|
+
lint_clean: z27.boolean().optional().describe("Whether lint passes cleanly"),
|
|
3800
|
+
typecheck_clean: z27.boolean().optional().describe("Whether typecheck passes cleanly"),
|
|
3801
|
+
notes: z27.string().optional().describe("Free-form notes"),
|
|
3802
|
+
metadata: z27.record(z27.unknown()).optional().describe("Additional key-value metadata")
|
|
3803
|
+
},
|
|
3804
|
+
async (params) => {
|
|
3805
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
3806
|
+
if (!db) return notInitialized();
|
|
3807
|
+
const rowId = insertActivity2(db, {
|
|
3808
|
+
toolName: params.tool_name,
|
|
3809
|
+
action: params.action,
|
|
3810
|
+
model: params.model,
|
|
3811
|
+
agentRole: params.agent_role,
|
|
3812
|
+
taskId: params.task_id,
|
|
3813
|
+
phaseId: params.phase_id,
|
|
3814
|
+
inputTokens: params.input_tokens,
|
|
3815
|
+
outputTokens: params.output_tokens,
|
|
3816
|
+
totalTokens: params.total_tokens,
|
|
3817
|
+
costUsd: params.cost_usd,
|
|
3818
|
+
durationMs: params.duration_ms,
|
|
3819
|
+
driftCount: params.drift_count,
|
|
3820
|
+
driftErrors: params.drift_errors,
|
|
3821
|
+
testPassCount: params.test_pass_count,
|
|
3822
|
+
testFailCount: params.test_fail_count,
|
|
3823
|
+
lintClean: params.lint_clean,
|
|
3824
|
+
typecheckClean: params.typecheck_clean,
|
|
3825
|
+
notes: params.notes,
|
|
3826
|
+
metadata: params.metadata
|
|
3827
|
+
});
|
|
3828
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3829
|
+
const totals = getSessionTotals(db, today, params.model);
|
|
3830
|
+
const totalTokens = params.total_tokens ?? (params.input_tokens != null && params.output_tokens != null ? params.input_tokens + params.output_tokens : null);
|
|
3831
|
+
const lines = [
|
|
3832
|
+
`# Activity Recorded (#${rowId})`,
|
|
3833
|
+
"",
|
|
3834
|
+
`- **Tool:** ${params.tool_name}`
|
|
3835
|
+
];
|
|
3836
|
+
if (params.action) lines.push(`- **Action:** ${params.action}`);
|
|
3837
|
+
if (params.model) lines.push(`- **Model:** ${params.model}`);
|
|
3838
|
+
if (totalTokens != null) {
|
|
3839
|
+
const detail = params.input_tokens != null && params.output_tokens != null ? `${params.input_tokens.toLocaleString()} in / ${params.output_tokens.toLocaleString()} out (${totalTokens.toLocaleString()} total)` : `${totalTokens.toLocaleString()} total`;
|
|
3840
|
+
lines.push(`- **Tokens:** ${detail}`);
|
|
3841
|
+
}
|
|
3842
|
+
if (params.cost_usd != null) lines.push(`- **Cost:** $${params.cost_usd.toFixed(4)}`);
|
|
3843
|
+
if (params.duration_ms != null) lines.push(`- **Duration:** ${params.duration_ms.toLocaleString()}ms`);
|
|
3844
|
+
lines.push(
|
|
3845
|
+
"",
|
|
3846
|
+
`## Session Totals (today${params.model ? `, ${params.model}` : ""})`,
|
|
3847
|
+
`- **Total cost:** $${totals.totalCost.toFixed(4)}`,
|
|
3848
|
+
`- **Total tokens:** ${totals.totalTokens.toLocaleString()}`,
|
|
3849
|
+
`- **Activities recorded:** ${totals.activityCount}`
|
|
3850
|
+
);
|
|
3851
|
+
return textResult(lines.join("\n"));
|
|
3852
|
+
}
|
|
3853
|
+
);
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3856
|
+
// src/tools/get-metrics.ts
|
|
3857
|
+
import { z as z28 } from "zod";
|
|
3858
|
+
import { queryMetrics } from "@arcbridge/core";
|
|
3859
|
+
function registerGetMetrics(server, ctx) {
|
|
3860
|
+
server.tool(
|
|
3861
|
+
"arcbridge_get_metrics",
|
|
3862
|
+
"Query agent activity metrics \u2014 filter by model, task, phase, or time range. Group by model/task/phase/tool/day for aggregated views.",
|
|
3863
|
+
{
|
|
3864
|
+
target_dir: z28.string().describe("Absolute path to the project directory"),
|
|
3865
|
+
task_id: z28.string().optional().describe("Filter by task ID"),
|
|
3866
|
+
phase_id: z28.string().optional().describe("Filter by phase ID"),
|
|
3867
|
+
model: z28.string().optional().describe("Filter by model name"),
|
|
3868
|
+
agent_role: z28.string().optional().describe("Filter by agent role"),
|
|
3869
|
+
tool_name: z28.string().optional().describe("Filter by tool name"),
|
|
3870
|
+
since: z28.string().optional().describe("ISO 8601 timestamp \u2014 activity after this time"),
|
|
3871
|
+
until: z28.string().optional().describe("ISO 8601 timestamp \u2014 activity before this time"),
|
|
3872
|
+
group_by: z28.enum(["model", "task", "phase", "tool", "day", "none"]).default("none").describe("Group results for aggregation"),
|
|
3873
|
+
limit: z28.number().int().min(1).max(500).default(50).describe("Max rows in detail view (group_by=none)")
|
|
3874
|
+
},
|
|
3875
|
+
async (params) => {
|
|
3876
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
3877
|
+
if (!db) return notInitialized();
|
|
3878
|
+
const result = queryMetrics(db, {
|
|
3879
|
+
taskId: params.task_id,
|
|
3880
|
+
phaseId: params.phase_id,
|
|
3881
|
+
model: params.model,
|
|
3882
|
+
agentRole: params.agent_role,
|
|
3883
|
+
toolName: params.tool_name,
|
|
3884
|
+
since: params.since,
|
|
3885
|
+
until: params.until,
|
|
3886
|
+
groupBy: params.group_by,
|
|
3887
|
+
limit: params.limit
|
|
3888
|
+
});
|
|
3889
|
+
if (result.totals.activityCount === 0) {
|
|
3890
|
+
return textResult("No agent activity recorded yet. Use `arcbridge_record_activity` to log agent work.");
|
|
3891
|
+
}
|
|
3892
|
+
const lines = [];
|
|
3893
|
+
if (result.grouped) {
|
|
3894
|
+
const rows = result.rows;
|
|
3895
|
+
lines.push(
|
|
3896
|
+
`# Agent Metrics (grouped by ${params.group_by})`,
|
|
3897
|
+
"",
|
|
3898
|
+
`| ${capitalize(params.group_by)} | Activities | Total Tokens | Avg Tokens | Total Cost | Avg Duration |`,
|
|
3899
|
+
`|${"-".repeat(20)}|-----------|-------------|-----------|-----------|-------------|`
|
|
3900
|
+
);
|
|
3901
|
+
for (const r of rows) {
|
|
3902
|
+
lines.push(
|
|
3903
|
+
`| ${mdCell(r.groupKey)} | ${r.activityCount} | ${r.sumTokens?.toLocaleString() ?? "-"} | ${r.avgTokens?.toLocaleString() ?? "-"} | ${r.sumCost != null ? "$" + r.sumCost.toFixed(4) : "-"} | ${r.avgDuration != null ? Math.round(r.avgDuration) + "ms" : "-"} |`
|
|
3904
|
+
);
|
|
3905
|
+
}
|
|
3906
|
+
} else {
|
|
3907
|
+
const rows = result.rows;
|
|
3908
|
+
lines.push(
|
|
3909
|
+
"# Agent Activity (recent)",
|
|
3910
|
+
"",
|
|
3911
|
+
"| Time | Tool | Action | Model | Tokens | Cost | Duration |",
|
|
3912
|
+
"|------|------|--------|-------|--------|------|----------|"
|
|
3913
|
+
);
|
|
3914
|
+
for (const r of rows) {
|
|
3915
|
+
lines.push(
|
|
3916
|
+
`| ${r.recorded_at.slice(0, 19)} | ${mdCell(r.tool_name)} | ${mdCell(r.action)} | ${mdCell(r.model)} | ${r.total_tokens?.toLocaleString() ?? ""} | ${r.cost_usd != null ? "$" + r.cost_usd.toFixed(4) : ""} | ${r.duration_ms != null ? r.duration_ms.toLocaleString() + "ms" : ""} |`
|
|
3917
|
+
);
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
const q = result.qualitySnapshot;
|
|
3921
|
+
if (q.capturedAt) {
|
|
3922
|
+
lines.push(
|
|
3923
|
+
"",
|
|
3924
|
+
"## Latest Quality Snapshot",
|
|
3925
|
+
""
|
|
3926
|
+
);
|
|
3927
|
+
if (q.driftCount != null) lines.push(`- **Drift:** ${q.driftCount} issues (${q.driftErrors ?? 0} errors)`);
|
|
3928
|
+
if (q.testPassCount != null) lines.push(`- **Tests:** ${q.testPassCount} pass / ${q.testFailCount ?? 0} fail`);
|
|
3929
|
+
if (q.lintClean != null) lines.push(`- **Lint:** ${q.lintClean ? "clean" : "errors"}`);
|
|
3930
|
+
if (q.typecheckClean != null) lines.push(`- **Typecheck:** ${q.typecheckClean ? "clean" : "errors"}`);
|
|
3931
|
+
}
|
|
3932
|
+
lines.push(
|
|
3933
|
+
"",
|
|
3934
|
+
"## Totals",
|
|
3935
|
+
"",
|
|
3936
|
+
`- **Activities:** ${result.totals.activityCount}`,
|
|
3937
|
+
`- **Total cost:** $${result.totals.totalCost.toFixed(4)}`,
|
|
3938
|
+
`- **Total tokens:** ${result.totals.totalTokens.toLocaleString()}`
|
|
3939
|
+
);
|
|
3940
|
+
if (result.timeSpan) {
|
|
3941
|
+
lines.push(`- **Time span:** ${result.timeSpan.first.slice(0, 10)} \u2192 ${result.timeSpan.last.slice(0, 10)}`);
|
|
3942
|
+
}
|
|
3943
|
+
return textResult(lines.join("\n"));
|
|
3944
|
+
}
|
|
3945
|
+
);
|
|
3946
|
+
}
|
|
3947
|
+
function capitalize(s) {
|
|
3948
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
3949
|
+
}
|
|
3950
|
+
function mdCell(val) {
|
|
3951
|
+
if (val == null) return "";
|
|
3952
|
+
return val.replace(/\|/g, "\\|").replace(/\r?\n|\r/g, " ");
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
// src/tools/export-metrics.ts
|
|
3956
|
+
import { z as z29 } from "zod";
|
|
3957
|
+
import { exportMetrics } from "@arcbridge/core";
|
|
3958
|
+
function registerExportMetrics(server, ctx) {
|
|
3959
|
+
server.tool(
|
|
3960
|
+
"arcbridge_export_metrics",
|
|
3961
|
+
"Export agent activity metrics to a file (JSON, CSV, or Markdown) in .arcbridge/metrics/ for git commits or reporting.",
|
|
3962
|
+
{
|
|
3963
|
+
target_dir: z29.string().describe("Absolute path to the project directory"),
|
|
3964
|
+
format: z29.enum(["json", "csv", "markdown"]).default("json").describe("Export format"),
|
|
3965
|
+
task_id: z29.string().optional().describe("Filter by task ID"),
|
|
3966
|
+
phase_id: z29.string().optional().describe("Filter by phase ID"),
|
|
3967
|
+
model: z29.string().optional().describe("Filter by model name"),
|
|
3968
|
+
agent_role: z29.string().optional().describe("Filter by agent role"),
|
|
3969
|
+
tool_name: z29.string().optional().describe("Filter by tool name"),
|
|
3970
|
+
since: z29.string().optional().describe("ISO 8601 \u2014 activity after this time"),
|
|
3971
|
+
until: z29.string().optional().describe("ISO 8601 \u2014 activity before this time"),
|
|
3972
|
+
max_rows: z29.number().int().min(1).default(1e5).describe("Maximum rows to export (default: 100,000)")
|
|
3973
|
+
},
|
|
3974
|
+
async (params) => {
|
|
3975
|
+
const db = ensureDb(ctx, params.target_dir);
|
|
3976
|
+
if (!db) return notInitialized();
|
|
3977
|
+
const filePath = exportMetrics(
|
|
3978
|
+
db,
|
|
3979
|
+
params.target_dir,
|
|
3980
|
+
params.format,
|
|
3981
|
+
{
|
|
3982
|
+
taskId: params.task_id,
|
|
3983
|
+
phaseId: params.phase_id,
|
|
3984
|
+
model: params.model,
|
|
3985
|
+
agentRole: params.agent_role,
|
|
3986
|
+
toolName: params.tool_name,
|
|
3987
|
+
since: params.since,
|
|
3988
|
+
until: params.until
|
|
3989
|
+
},
|
|
3990
|
+
params.max_rows
|
|
3991
|
+
);
|
|
3992
|
+
return textResult(
|
|
3993
|
+
`Metrics exported to: ${filePath}
|
|
3994
|
+
|
|
3995
|
+
You can commit this file to preserve the activity record in git.`
|
|
3996
|
+
);
|
|
3997
|
+
}
|
|
3998
|
+
);
|
|
3999
|
+
}
|
|
4000
|
+
|
|
3705
4001
|
// src/server.ts
|
|
3706
4002
|
var require2 = createRequire(import.meta.url);
|
|
3707
4003
|
var { version } = require2("../package.json");
|
|
@@ -3737,6 +4033,9 @@ function createArcBridgeServer() {
|
|
|
3737
4033
|
registerActivateRole(server, ctx);
|
|
3738
4034
|
registerVerifyScenarios(server, ctx);
|
|
3739
4035
|
registerRunRoleCheck(server, ctx);
|
|
4036
|
+
registerRecordActivity(server, ctx);
|
|
4037
|
+
registerGetMetrics(server, ctx);
|
|
4038
|
+
registerExportMetrics(server, ctx);
|
|
3740
4039
|
return server;
|
|
3741
4040
|
}
|
|
3742
4041
|
|