mcp-efficiency-engine 0.1.3 → 0.1.5

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.
@@ -26,7 +26,7 @@ jobs:
26
26
  - name: Setup Node.js
27
27
  uses: actions/setup-node@v4
28
28
  with:
29
- node-version: 20
29
+ node-version: 22
30
30
  registry-url: https://registry.npmjs.org
31
31
 
32
32
  - name: Resolve publish version
@@ -94,14 +94,23 @@ jobs:
94
94
  env:
95
95
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
96
96
 
97
- - name: Sync package.json version to repository
97
+ - name: Record package.json sync requirement
98
98
  if: steps.version.outputs.changed == 'true'
99
99
  run: |
100
- git config user.name "github-actions[bot]"
101
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
102
- git add package.json
103
- git commit -m "chore(release): sync npm version to ${{ steps.version.outputs.tag }} [skip ci]"
104
- git push
100
+ echo "package.json was bumped in CI to publish an available npm version (${{ steps.version.outputs.tag }})."
101
+ echo "Repository sync is skipped because main is protected and requires PR-based updates."
102
+ echo "Open a follow-up PR to align package.json with npm published version."
103
+
104
+ - name: Release summary
105
+ run: |
106
+ echo "## NPM Release" >> "$GITHUB_STEP_SUMMARY"
107
+ echo "- mode: ${{ steps.version.outputs.mode }}" >> "$GITHUB_STEP_SUMMARY"
108
+ echo "- published: ${{ steps.version.outputs.tag }}" >> "$GITHUB_STEP_SUMMARY"
109
+ if [ "${{ steps.version.outputs.changed }}" = "true" ]; then
110
+ echo "- package.json sync: skipped (protected branch, requires PR)" >> "$GITHUB_STEP_SUMMARY"
111
+ else
112
+ echo "- package.json sync: not required" >> "$GITHUB_STEP_SUMMARY"
113
+ fi
105
114
 
106
115
  - name: Create GitHub release
107
116
  uses: softprops/action-gh-release@v2
package/ARCHITECTURE.md CHANGED
@@ -7,21 +7,27 @@ flowchart TD
7
7
  O --> A[Agents Layer]
8
8
 
9
9
  A --> DEV[backend]
10
+ A --> FE[frontend-agent]
10
11
  A --> LEG[legacy]
11
12
  A --> DBA[dba]
13
+ A --> UX[ux-ui]
12
14
  A --> RAG[rag-local]
13
15
  A --> AZ[rag-azure]
14
16
  A --> IOT[iot]
15
17
  A --> COM[community-manager]
18
+ A --> WIKI[wiki-agent]
16
19
  A --> SNAP[snapshot]
17
20
 
18
21
  DEV --> TS[Token Saver Policy]
22
+ FE --> TS
19
23
  LEG --> TS
20
24
  DBA --> TS
25
+ UX --> TS
21
26
  RAG --> TS
22
27
  AZ --> TS
23
28
  IOT --> TS
24
29
  COM --> TS
30
+ WIKI --> TS
25
31
  SNAP --> TS
26
32
 
27
33
  TS --> CG[CodeGraph]
@@ -55,3 +61,12 @@ Token Saver optimiza qué contexto se usa.
55
61
  Routing decide qué motor se usa.
56
62
  Observability mide si todo funciona.
57
63
  ```
64
+
65
+ ## Routing base (resumen)
66
+
67
+ - `backend` y `frontend-agent` -> `CodeGraph`
68
+ - `legacy` -> `GitNexus`
69
+ - `dba`, `ux-ui`, `rag-local`, `community-manager` -> `Graphify`
70
+ - `rag-azure` -> `Azure RAG Builder`
71
+ - `wiki-agent` -> `CodeGraph` (fallback `Graphify`)
72
+ - `snapshot` -> `Repomix`
@@ -85,7 +85,30 @@ Esto significa:
85
85
  - las fuentes se conservan cuando sean obligatorias,
86
86
  - el sistema solo aumenta detalle si el usuario o el caso lo requiere.
87
87
 
88
- ## 7. Telemetry Engine
88
+ ## 7. Instalación desde npm (rápido)
89
+
90
+ Instalación recomendada en proyecto host:
91
+
92
+ ```powershell
93
+ npm install mcp-efficiency-engine
94
+ ```
95
+
96
+ Si el entorno bloquea scripts de npm (`allow-scripts`), ejecutar manualmente:
97
+
98
+ ```powershell
99
+ npx mcp-efficiency-engine install
100
+ npx mcp-efficiency-engine validate -PortableMode
101
+ ```
102
+
103
+ Validación de contenido publicado en npm:
104
+
105
+ ```powershell
106
+ npm pack --dry-run
107
+ ```
108
+
109
+ Debe listar, como mínimo, `bin/`, `scripts/`, `.github/`, `README.md`, `AGENTS.md`, `ARCHITECTURE.md` y `FINAL_USAGE_GUIDE.md`.
110
+
111
+ ## 8. Telemetry Engine
89
112
 
90
113
  El sistema incluye un engine de telemetría desacoplado con collector único.
91
114
 
package/README.md CHANGED
@@ -118,6 +118,7 @@ Contrato global en `AGENTS.md`:
118
118
  - `rag-azure` -> `Azure RAG Builder`
119
119
  - `iot` -> `GitNexus/CodeGraph + Graphify`
120
120
  - `community-manager` -> `Graphify`
121
+ - `wiki-agent` -> `CodeGraph` (fallback `Graphify`)
121
122
  - `snapshot` -> `Repomix`
122
123
 
123
124
  ## Motores Y Herramientas
@@ -252,6 +253,11 @@ Comportamiento del hook:
252
253
  - `scripts/learning/iteration-value-report.py`
253
254
  - `scripts/ops/publish-langsmith-kpis.py` (best effort)
254
255
 
256
+ Notas operativas recientes:
257
+
258
+ - `mcpee` envuelve scripts PowerShell con `scripts/ops/trace-command.py` para emitir trazas por comando (`mcpee.*`) en el collector.
259
+ - `scripts/ops/publish-langsmith-kpis.py` agrega snapshots locales de flujos, coste y tokens antes de publicar KPI runs en LangSmith.
260
+
255
261
  Artefactos/resultados:
256
262
 
257
263
  - AutoDocs actualizado en `autodocs/generated` y `autodocs/site`
package/bin/mcpee.js CHANGED
@@ -60,6 +60,7 @@ function resolveInvocationCandidates() {
60
60
  function runPowerShellScript(scriptRelativePath, forwardedArgs) {
61
61
  const executionRoot = resolveExecutionRoot(scriptRelativePath);
62
62
  const scriptPath = path.join(executionRoot, scriptRelativePath);
63
+ const tracerScriptPath = path.join(repoRoot, "scripts", "ops", "trace-command.py");
63
64
  if (!fs.existsSync(scriptPath)) {
64
65
  process.stderr.write(`Script no encontrado: ${scriptRelativePath}\n`);
65
66
  return 1;
@@ -67,6 +68,43 @@ function runPowerShellScript(scriptRelativePath, forwardedArgs) {
67
68
 
68
69
  const shellCandidates = resolveInvocationCandidates();
69
70
  for (const shellCommand of shellCandidates) {
71
+ if (fs.existsSync(tracerScriptPath)) {
72
+ const operation = `mcpee.${path.basename(scriptRelativePath, path.extname(scriptRelativePath))}`;
73
+ const tracedResult = spawnSync(
74
+ "py",
75
+ [
76
+ "-3",
77
+ tracerScriptPath,
78
+ "--operation",
79
+ operation,
80
+ "--session-id",
81
+ "mcpee-cli",
82
+ "--cwd",
83
+ executionRoot,
84
+ "--",
85
+ shellCommand,
86
+ "-NoProfile",
87
+ "-ExecutionPolicy",
88
+ "Bypass",
89
+ "-File",
90
+ scriptPath,
91
+ ...forwardedArgs,
92
+ ],
93
+ {
94
+ cwd: repoRoot,
95
+ stdio: "inherit",
96
+ },
97
+ );
98
+
99
+ if (!tracedResult.error || tracedResult.error.code !== "ENOENT") {
100
+ if (tracedResult.error) {
101
+ process.stderr.write(`${tracedResult.error.message}\n`);
102
+ return 1;
103
+ }
104
+ return tracedResult.status ?? 0;
105
+ }
106
+ }
107
+
70
108
  const result = spawnSync(
71
109
  shellCommand,
72
110
  ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath, ...forwardedArgs],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-efficiency-engine",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Motor de orquestacion para agentes MCP con routing por dominio y bootstrap portable.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -51,7 +51,7 @@
51
51
  "hi": "node ./bin/mcpee.js hi",
52
52
  "bye": "node ./bin/mcpee.js bye",
53
53
  "intake": "node ./bin/mcpee.js intake",
54
- "langsmith:kpis": "py -3 ./scripts/ops/publish-langsmith-kpis.py",
54
+ "langsmith:kpis": "py -3 ./scripts/ops/trace-command.py --operation mcpee.langsmith-kpis --session-id mcpee-cli --cwd . -- py -3 ./scripts/ops/publish-langsmith-kpis.py",
55
55
  "pack:check": "npm pack --dry-run"
56
56
  },
57
57
  "engines": {
@@ -4,6 +4,7 @@ import json
4
4
  import os
5
5
  import sys
6
6
  import uuid
7
+ from collections import Counter
7
8
  from datetime import datetime, timezone
8
9
  from pathlib import Path
9
10
  from typing import Any
@@ -25,6 +26,28 @@ def _read_json(path: Path) -> dict[str, Any]:
25
26
  return payload if isinstance(payload, dict) else {}
26
27
 
27
28
 
29
+ def _read_jsonl(path: Path) -> list[dict[str, Any]]:
30
+ if not path.exists():
31
+ return []
32
+ rows: list[dict[str, Any]] = []
33
+ for line in path.read_text(encoding="utf-8").splitlines():
34
+ raw = line.strip()
35
+ if not raw:
36
+ continue
37
+ try:
38
+ obj = json.loads(raw)
39
+ except Exception:
40
+ continue
41
+ if isinstance(obj, dict):
42
+ rows.append(obj)
43
+ return rows
44
+
45
+
46
+ def _write_json(path: Path, payload: dict[str, Any]) -> None:
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
49
+
50
+
28
51
  def _iso_now() -> datetime:
29
52
  return datetime.now(timezone.utc)
30
53
 
@@ -48,6 +71,161 @@ def _flatten_kpis(payload: dict[str, Any]) -> dict[str, Any]:
48
71
  return flat
49
72
 
50
73
 
74
+ def _build_telemetry_snapshot(repo_root: Path) -> dict[str, Any]:
75
+ traces_path = repo_root / ".telemetry" / "traces.jsonl"
76
+ metrics_path = repo_root / ".telemetry" / "metrics.jsonl"
77
+ traces = _read_jsonl(traces_path)
78
+ metrics = _read_jsonl(metrics_path)
79
+
80
+ execution_ids: set[str] = set()
81
+ operations = Counter()
82
+ sessions = Counter()
83
+ engines = Counter()
84
+ agents = Counter()
85
+ warnings = 0
86
+ errors = 0
87
+ fallback_count = 0
88
+ grounded_count = 0
89
+ routing_count = 0
90
+ durations: list[float] = []
91
+ exporter_failures = Counter()
92
+
93
+ for row in traces:
94
+ context = row.get("context", {}) if isinstance(row.get("context"), dict) else {}
95
+ payload = row.get("payload", {}) if isinstance(row.get("payload"), dict) else {}
96
+ event_name = str(row.get("event_name", ""))
97
+ level = str(row.get("level", "INFO")).upper()
98
+
99
+ execution_id = str(context.get("execution_id", "")).strip()
100
+ if execution_id:
101
+ execution_ids.add(execution_id)
102
+
103
+ session_id = str(context.get("session_id", "")).strip()
104
+ if session_id:
105
+ sessions[session_id] += 1
106
+
107
+ if level == "WARNING":
108
+ warnings += 1
109
+ if level == "ERROR":
110
+ errors += 1
111
+
112
+ if event_name == "ExecutionStarted":
113
+ op = str(payload.get("operation", "")).strip() or "unknown"
114
+ operations[op] += 1
115
+
116
+ if event_name == "RoutingResolved":
117
+ routing_count += 1
118
+ engine = str(payload.get("engine", "")).strip() or "unknown"
119
+ agent = str(payload.get("agent", "")).strip() or "unknown"
120
+ engines[engine] += 1
121
+ agents[agent] += 1
122
+ if bool(payload.get("fallback", False)):
123
+ fallback_count += 1
124
+ if bool(payload.get("grounded", False)):
125
+ grounded_count += 1
126
+
127
+ if event_name == "SpanFinished":
128
+ try:
129
+ durations.append(float(payload.get("duration_ms") or 0.0))
130
+ except Exception:
131
+ pass
132
+
133
+ if event_name == "WarningGenerated" and str(payload.get("warning", "")) == "exporter_failed":
134
+ exporter = str(payload.get("exporter", "unknown")).strip() or "unknown"
135
+ exporter_failures[exporter] += 1
136
+
137
+ metric_totals = {
138
+ "tokens_input": 0.0,
139
+ "tokens_output": 0.0,
140
+ "total_tokens": 0.0,
141
+ "estimated_cost": 0.0,
142
+ "efficiency_score_samples": 0,
143
+ "efficiency_score_avg": 0.0,
144
+ }
145
+ efficiency_sum = 0.0
146
+ for row in metrics:
147
+ payload = row.get("payload", {}) if isinstance(row.get("payload"), dict) else {}
148
+ if str(row.get("event_name", "")) != "MetricCalculated":
149
+ continue
150
+ name = str(payload.get("name", "")).strip()
151
+ try:
152
+ value = float(payload.get("value") or 0.0)
153
+ except Exception:
154
+ continue
155
+
156
+ if name in {"tokens_input", "tokens_output", "total_tokens", "estimated_cost"}:
157
+ metric_totals[name] += value
158
+ elif name == "efficiency_score":
159
+ efficiency_sum += value
160
+ metric_totals["efficiency_score_samples"] += 1
161
+
162
+ samples = int(metric_totals["efficiency_score_samples"])
163
+ if samples > 0:
164
+ metric_totals["efficiency_score_avg"] = round(efficiency_sum / samples, 4)
165
+
166
+ avg_duration = round(sum(durations) / len(durations), 4) if durations else 0.0
167
+ p95_duration = 0.0
168
+ if durations:
169
+ sorted_d = sorted(durations)
170
+ idx = max(0, min(len(sorted_d) - 1, int(round(0.95 * (len(sorted_d) - 1)))))
171
+ p95_duration = round(sorted_d[idx], 4)
172
+
173
+ fallback_rate = round((fallback_count / routing_count), 4) if routing_count else 0.0
174
+ grounded_rate = round((grounded_count / routing_count), 4) if routing_count else 0.0
175
+
176
+ return {
177
+ "timestamp": _iso_now().isoformat(),
178
+ "source": {
179
+ "traces": str(traces_path),
180
+ "metrics": str(metrics_path),
181
+ },
182
+ "totals": {
183
+ "trace_records": len(traces),
184
+ "metric_records": len(metrics),
185
+ "executions": len(execution_ids),
186
+ "warnings": warnings,
187
+ "errors": errors,
188
+ },
189
+ "kpis": {
190
+ "routing_fallback_rate": fallback_rate,
191
+ "routing_grounded_rate": grounded_rate,
192
+ "avg_span_duration_ms": avg_duration,
193
+ "p95_span_duration_ms": p95_duration,
194
+ "tokens_input": int(metric_totals["tokens_input"]),
195
+ "tokens_output": int(metric_totals["tokens_output"]),
196
+ "total_tokens": int(metric_totals["total_tokens"]),
197
+ "total_cost_usd": round(float(metric_totals["estimated_cost"]), 6),
198
+ "efficiency_score_avg": metric_totals["efficiency_score_avg"],
199
+ },
200
+ "breakdown": {
201
+ "operation": dict(operations),
202
+ "session": dict(sessions),
203
+ "engine": dict(engines),
204
+ "agent": dict(agents),
205
+ "exporter_failures": dict(exporter_failures),
206
+ },
207
+ }
208
+
209
+
210
+ def _build_chat_usage_snapshot(repo_root: Path) -> dict[str, Any]:
211
+ chat_path = repo_root / "observability" / "evals" / "chat-token-usage-report.json"
212
+ payload = _read_json(chat_path)
213
+ totals = payload.get("totals", {}) if isinstance(payload.get("totals", {}), dict) else {}
214
+ budget = payload.get("budget", {}) if isinstance(payload.get("budget", {}), dict) else {}
215
+ return {
216
+ "timestamp": payload.get("timestamp"),
217
+ "source": str(chat_path),
218
+ "kpis": {
219
+ "input_tokens": int(totals.get("input_tokens", 0) or 0),
220
+ "output_tokens": int(totals.get("output_tokens", 0) or 0),
221
+ "total_tokens": int(totals.get("total_tokens", 0) or 0),
222
+ "copilot_credits": float(totals.get("copilot_credits", 0.0) or 0.0),
223
+ "budget_utilization_rate": float(budget.get("budget_utilization_rate", 0.0) or 0.0),
224
+ },
225
+ "raw": payload,
226
+ }
227
+
228
+
51
229
  def _publish_run(
52
230
  client: Any,
53
231
  *,
@@ -104,12 +282,18 @@ def _publish_run(
104
282
 
105
283
 
106
284
  def _publish_feedback_metrics(client: Any, *, run_id: str, metrics: dict[str, Any], source: str) -> None:
285
+ min_score = -99999.9999
286
+ max_score = 99999.9999
107
287
  for key, raw_value in metrics.items():
108
288
  try:
109
289
  value = float(raw_value)
110
290
  except Exception:
111
291
  continue
112
292
 
293
+ if value < min_score or value > max_score:
294
+ # LangSmith feedback score has bounded range.
295
+ continue
296
+
113
297
  try:
114
298
  client.create_feedback(
115
299
  run_id=run_id,
@@ -152,9 +336,38 @@ def main() -> int:
152
336
 
153
337
  learning_path = repo_root / "observability" / "evals" / "learning-loop-report.json"
154
338
  value_path = repo_root / "observability" / "evals" / "iteration-value-report.json"
339
+ telemetry_snapshot_path = repo_root / "observability" / "evals" / "telemetry-flow-cost-token-report.json"
340
+ chat_snapshot_path = repo_root / "observability" / "evals" / "chat-token-usage-report.json"
155
341
 
156
342
  learning = _read_json(learning_path)
157
343
  value = _read_json(value_path)
344
+ telemetry_snapshot = _build_telemetry_snapshot(repo_root)
345
+ _write_json(telemetry_snapshot_path, telemetry_snapshot)
346
+ chat_snapshot = _build_chat_usage_snapshot(repo_root)
347
+
348
+ project_traceability_path = (
349
+ repo_root
350
+ / "projects"
351
+ / "techriders"
352
+ / "analysis_mcpee"
353
+ / f"telemetry-recovery-{datetime.now(timezone.utc).strftime('%Y-%m-%d')}.json"
354
+ )
355
+ unified_snapshot = {
356
+ "timestamp": _iso_now().isoformat(),
357
+ "host_project": host_project,
358
+ "telemetry_scope": telemetry_scope,
359
+ "sources": {
360
+ "learning": str(learning_path),
361
+ "iteration_value": str(value_path),
362
+ "telemetry_snapshot": str(telemetry_snapshot_path),
363
+ "chat_usage": str(chat_snapshot_path),
364
+ },
365
+ "learning": learning,
366
+ "iteration_value": value,
367
+ "telemetry": telemetry_snapshot,
368
+ "chat_usage": chat_snapshot,
369
+ }
370
+ _write_json(project_traceability_path, unified_snapshot)
158
371
 
159
372
  published = 0
160
373
 
@@ -288,10 +501,92 @@ def main() -> int:
288
501
  _publish_feedback_metrics(client, run_id=run_id, metrics=alignment_feedback, source="alignment")
289
502
  published += 1
290
503
 
504
+ telemetry_kpis = telemetry_snapshot.get("kpis", {}) if isinstance(telemetry_snapshot.get("kpis", {}), dict) else {}
505
+ if telemetry_kpis:
506
+ telemetry_flat_kpis = _flatten_kpis(telemetry_kpis)
507
+ run_id = _publish_run(
508
+ client,
509
+ project=cfg.langsmith.project,
510
+ name="KPI::TelemetryFlowCostToken",
511
+ inputs={
512
+ "source_file": str(telemetry_snapshot_path),
513
+ "timestamp": telemetry_snapshot.get("timestamp"),
514
+ },
515
+ outputs={
516
+ "kpis": telemetry_kpis,
517
+ **telemetry_flat_kpis,
518
+ "totals": telemetry_snapshot.get("totals", {}),
519
+ "breakdown": telemetry_snapshot.get("breakdown", {}),
520
+ },
521
+ tags=[
522
+ "mcpee",
523
+ "kpi",
524
+ "telemetry",
525
+ "cost-token-flow",
526
+ "dashboard",
527
+ f"scope:{telemetry_scope}",
528
+ f"host:{host_slug}",
529
+ ],
530
+ metadata={
531
+ "kpi_source": "telemetry-flow-cost-token",
532
+ "kpi_kind": "snapshot",
533
+ "host_project": host_project,
534
+ "host_project_slug": host_slug,
535
+ "telemetry_scope": telemetry_scope,
536
+ **telemetry_flat_kpis,
537
+ },
538
+ total_cost=float(telemetry_kpis.get("total_cost_usd", 0.0) or 0.0),
539
+ total_tokens=int(telemetry_kpis.get("total_tokens", 0) or 0),
540
+ run_type="llm",
541
+ )
542
+ _publish_feedback_metrics(client, run_id=run_id, metrics=telemetry_flat_kpis, source="telemetry-flow-cost-token")
543
+ published += 1
544
+
545
+ chat_kpis = chat_snapshot.get("kpis", {}) if isinstance(chat_snapshot.get("kpis", {}), dict) else {}
546
+ if chat_kpis:
547
+ chat_flat_kpis = _flatten_kpis(chat_kpis)
548
+ run_id = _publish_run(
549
+ client,
550
+ project=cfg.langsmith.project,
551
+ name="KPI::ChatTokenUsage",
552
+ inputs={
553
+ "source_file": str(chat_snapshot_path),
554
+ "timestamp": chat_snapshot.get("timestamp"),
555
+ },
556
+ outputs={
557
+ "kpis": chat_kpis,
558
+ **chat_flat_kpis,
559
+ "budget": (chat_snapshot.get("raw", {}) or {}).get("budget", {}),
560
+ },
561
+ tags=[
562
+ "mcpee",
563
+ "kpi",
564
+ "chat-usage",
565
+ "cost-token-flow",
566
+ "dashboard",
567
+ f"scope:{telemetry_scope}",
568
+ f"host:{host_slug}",
569
+ ],
570
+ metadata={
571
+ "kpi_source": "chat-token-usage",
572
+ "kpi_kind": "snapshot",
573
+ "host_project": host_project,
574
+ "host_project_slug": host_slug,
575
+ "telemetry_scope": telemetry_scope,
576
+ **chat_flat_kpis,
577
+ },
578
+ total_tokens=int(chat_kpis.get("total_tokens", 0) or 0),
579
+ run_type="llm",
580
+ )
581
+ _publish_feedback_metrics(client, run_id=run_id, metrics=chat_flat_kpis, source="chat-token-usage")
582
+ published += 1
583
+
291
584
  print(
292
585
  f"Published KPI runs: {published} to project '{cfg.langsmith.project}' "
293
586
  f"(host_project='{host_project}', scope='{telemetry_scope}')."
294
587
  )
588
+ print(f"Local telemetry snapshot: {telemetry_snapshot_path}")
589
+ print(f"Project traceability snapshot: {project_traceability_path}")
295
590
  if published == 0:
296
591
  print("No local KPI report JSON files were found in observability/evals.")
297
592
 
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def _bootstrap_repo_root() -> Path:
10
+ repo_root = Path(__file__).resolve().parents[2]
11
+ if str(repo_root) not in sys.path:
12
+ sys.path.insert(0, str(repo_root))
13
+ return repo_root
14
+
15
+
16
+ def _parse_args() -> argparse.Namespace:
17
+ parser = argparse.ArgumentParser(description="Run a command wrapped with telemetry execution/span.")
18
+ parser.add_argument("--operation", required=True, help="Telemetry operation name.")
19
+ parser.add_argument("--session-id", default="mcpee-cli", help="Telemetry session identifier.")
20
+ parser.add_argument("--cwd", default=".", help="Working directory for the command.")
21
+ parser.add_argument("command", nargs=argparse.REMAINDER, help="Command to execute after --.")
22
+ return parser.parse_args()
23
+
24
+
25
+ def _normalize_command(raw: list[str]) -> list[str]:
26
+ cmd = list(raw)
27
+ if cmd and cmd[0] == "--":
28
+ cmd = cmd[1:]
29
+ return cmd
30
+
31
+
32
+ def main() -> int:
33
+ args = _parse_args()
34
+ command = _normalize_command(args.command)
35
+ if not command:
36
+ print("trace-command.py: missing command to execute", file=sys.stderr)
37
+ return 2
38
+
39
+ repo_root = _bootstrap_repo_root()
40
+ from telemetry import build_telemetry_collector
41
+
42
+ collector = build_telemetry_collector(repo_root)
43
+ cwd = Path(args.cwd).resolve()
44
+ exit_code = 0
45
+
46
+ try:
47
+ with collector.start_execution(operation=args.operation, session_id=args.session_id, model="mcpee-cli"):
48
+ with collector.start_span(
49
+ name="subprocess",
50
+ kind="TOOL",
51
+ attributes={
52
+ "cwd": str(cwd),
53
+ "command": command[0],
54
+ "args_count": len(command) - 1,
55
+ },
56
+ ):
57
+ completed = subprocess.run(command, cwd=str(cwd), check=False)
58
+ exit_code = int(completed.returncode)
59
+ collector.record_metric("subprocess_exit_code", float(exit_code), unit="code")
60
+ if exit_code != 0:
61
+ raise RuntimeError(f"command failed with exit code {exit_code}")
62
+ except RuntimeError:
63
+ pass
64
+
65
+ return exit_code
66
+
67
+
68
+ if __name__ == "__main__":
69
+ raise SystemExit(main())