codex-flow 2.1.13__py3-none-any.whl

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.
Files changed (113) hide show
  1. codex_flow/__init__.py +28 -0
  2. codex_flow/__main__.py +9 -0
  3. codex_flow/cli.py +242 -0
  4. codex_flow/data/LICENSE +21 -0
  5. codex_flow/data/README.en.md +303 -0
  6. codex_flow/data/README.md +305 -0
  7. codex_flow/data/VERSION +1 -0
  8. codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
  9. codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
  10. codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
  11. codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
  12. codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
  13. codex_flow/data/apps/macos-overlay/README.en.md +121 -0
  14. codex_flow/data/apps/macos-overlay/README.md +123 -0
  15. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
  16. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
  17. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
  18. codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
  19. codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
  20. codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
  21. codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
  22. codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
  23. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
  24. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
  25. codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
  26. codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
  27. codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
  28. codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
  29. codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
  30. codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
  31. codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
  32. codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
  33. codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
  34. codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
  35. codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
  36. codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
  37. codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
  38. codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
  39. codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
  40. codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
  41. codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
  42. codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
  43. codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
  44. codex_flow/data/apps/macos-overlay/build.sh +75 -0
  45. codex_flow/data/benchmark/corpus.json +103 -0
  46. codex_flow/data/benchmark/manifest.example.json +41 -0
  47. codex_flow/data/benchmark/manifest.schema.json +137 -0
  48. codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
  49. codex_flow/data/benchmark/profiles.json +90 -0
  50. codex_flow/data/benchmark/schema.json +77 -0
  51. codex_flow/data/benchmark/tasks.json +50 -0
  52. codex_flow/data/completions/codex-flow.bash +34 -0
  53. codex_flow/data/completions/codex-flow.zsh +52 -0
  54. codex_flow/data/glama.json +6 -0
  55. codex_flow/data/install-release.ps1 +126 -0
  56. codex_flow/data/install-release.sh +155 -0
  57. codex_flow/data/install.ps1 +349 -0
  58. codex_flow/data/install.sh +362 -0
  59. codex_flow/data/policy/benchmark.toml +49 -0
  60. codex_flow/data/policy/defaults.toml +70 -0
  61. codex_flow/data/scripts/analyze-benchmark.py +510 -0
  62. codex_flow/data/scripts/benchmark-local.py +171 -0
  63. codex_flow/data/scripts/check-recommendation.py +277 -0
  64. codex_flow/data/scripts/doctor.py +449 -0
  65. codex_flow/data/scripts/generate-release-manifest.py +74 -0
  66. codex_flow/data/scripts/localization.py +192 -0
  67. codex_flow/data/scripts/manage-hooks.py +448 -0
  68. codex_flow/data/scripts/manage-instructions.py +389 -0
  69. codex_flow/data/scripts/manage-shell.py +151 -0
  70. codex_flow/data/scripts/materialize-corpus.py +193 -0
  71. codex_flow/data/scripts/menu.py +646 -0
  72. codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
  73. codex_flow/data/scripts/package-release.py +132 -0
  74. codex_flow/data/scripts/render-benchmark-report.py +292 -0
  75. codex_flow/data/scripts/run-benchmark.py +829 -0
  76. codex_flow/data/scripts/strategies/__init__.py +28 -0
  77. codex_flow/data/scripts/strategies/balanced.py +115 -0
  78. codex_flow/data/scripts/strategies/base.py +363 -0
  79. codex_flow/data/scripts/strategies/efficient.py +158 -0
  80. codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
  81. codex_flow/data/scripts/strategies/quality.py +209 -0
  82. codex_flow/data/scripts/strategies/speed.py +108 -0
  83. codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
  84. codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
  85. codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
  86. codex_flow/data/scripts/strategy_runtime.py +1091 -0
  87. codex_flow/data/scripts/telemetry.py +400 -0
  88. codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
  89. codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
  90. codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
  91. codex_flow/data/scripts/telemetry_core/common.py +421 -0
  92. codex_flow/data/scripts/telemetry_core/latency.py +593 -0
  93. codex_flow/data/scripts/telemetry_core/query.py +427 -0
  94. codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
  95. codex_flow/data/scripts/telemetry_core/render.py +460 -0
  96. codex_flow/data/scripts/telemetry_core/repair.py +223 -0
  97. codex_flow/data/scripts/ui.py +266 -0
  98. codex_flow/data/scripts/update-homebrew-formula.py +146 -0
  99. codex_flow/data/scripts/update_runtime_config.py +134 -0
  100. codex_flow/data/scripts/updater.py +1718 -0
  101. codex_flow/data/smithery.yaml +18 -0
  102. codex_flow/data/templates/agents/worker-explorer.toml +24 -0
  103. codex_flow/data/templates/agents/worker-implementer.toml +49 -0
  104. codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
  105. codex_flow/data/templates/flow-pilot-instructions.md +35 -0
  106. codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
  107. codex_flow/mcp.py +35 -0
  108. codex_flow-2.1.13.dist-info/METADATA +342 -0
  109. codex_flow-2.1.13.dist-info/RECORD +113 -0
  110. codex_flow-2.1.13.dist-info/WHEEL +5 -0
  111. codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
  112. codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
  113. codex_flow-2.1.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,458 @@
1
+ """Read-only adapter from FlowPilot telemetry to MCP tool/resource values.
2
+
3
+ This module deliberately uses the structured telemetry query helpers instead of
4
+ the telemetry CLI, hook collector, or app-server client. It is safe to import
5
+ from a long-running process: importing it does not start any subprocesses or
6
+ write telemetry state.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import math
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional, Tuple
17
+
18
+
19
+ TOOL_NAME = "flowpilot_get_telemetry"
20
+ RESOURCE_URI = "ui://flowpilot/telemetry-pip.html"
21
+ RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"
22
+ WIDGET_FILENAME = "widget.html"
23
+ _MAX_WIDGET_BYTES = 1024 * 1024
24
+
25
+
26
+ # ``telemetry_core`` lives under scripts/ in the checkout. Keep this import
27
+ # local to the repository layout so the service can be started from any cwd.
28
+ _MODULE_DIR = Path(__file__).resolve().parent
29
+ _REPOSITORY_ROOT = _MODULE_DIR.parents[1]
30
+ _SCRIPTS_DIR = _REPOSITORY_ROOT / "scripts"
31
+ if str(_SCRIPTS_DIR) not in sys.path:
32
+ sys.path.insert(0, str(_SCRIPTS_DIR))
33
+
34
+ _TELEMETRY_IMPORT_ERROR: Optional[BaseException] = None
35
+ try:
36
+ from telemetry_core import ( # type: ignore
37
+ fmt_duration_ms,
38
+ fmt_local_timestamp,
39
+ fmt_tokens,
40
+ list_runs,
41
+ load_run,
42
+ resolve_run_target,
43
+ run_context,
44
+ run_key,
45
+ )
46
+ except Exception as exc: # pragma: no cover - only used by broken installs
47
+ # The MCP process should remain a harmless, healthy local service even if
48
+ # telemetry is unavailable in an incomplete checkout or installation.
49
+ _TELEMETRY_IMPORT_ERROR = exc
50
+ fmt_duration_ms = None # type: ignore
51
+ fmt_local_timestamp = None # type: ignore
52
+ fmt_tokens = None # type: ignore
53
+ list_runs = None # type: ignore
54
+ load_run = None # type: ignore
55
+ resolve_run_target = None # type: ignore
56
+ run_context = None # type: ignore
57
+ run_key = None # type: ignore
58
+
59
+
60
+ class WidgetResourceError(IOError):
61
+ """Raised when the one allowed MCP Apps resource cannot be read."""
62
+
63
+
64
+ def _is_number(value: Any) -> bool:
65
+ return (
66
+ isinstance(value, (int, float))
67
+ and not isinstance(value, bool)
68
+ and math.isfinite(float(value))
69
+ )
70
+
71
+
72
+ def _number(value: Any) -> Optional[float]:
73
+ return float(value) if _is_number(value) else None
74
+
75
+
76
+ def _integer(value: Any) -> Optional[int]:
77
+ if not _is_number(value):
78
+ return None
79
+ return int(value)
80
+
81
+
82
+ def _text(value: Any, limit: int = 128) -> Optional[str]:
83
+ if value is None:
84
+ return None
85
+ value = " ".join(str(value).split())
86
+ if not value:
87
+ return None
88
+ return value[:limit]
89
+
90
+
91
+ def _empty_quota() -> Dict[str, Any]:
92
+ return {"used": [], "remaining": [], "windows": []}
93
+
94
+
95
+ def unavailable_telemetry() -> Dict[str, Any]:
96
+ """Return the stable shape used when telemetry data is not available."""
97
+
98
+ return {
99
+ "run_id": None,
100
+ "session": None,
101
+ "project": None,
102
+ "branch": None,
103
+ "status": "unavailable",
104
+ "started_at": None,
105
+ "finished_at": None,
106
+ "duration_seconds": None,
107
+ "duration_text": None,
108
+ "participants": [],
109
+ "parent": {"model": None, "reasoning_effort": None, "total_tokens": None},
110
+ "parent_model": None,
111
+ "parent_reasoning_effort": None,
112
+ "total_tokens": None,
113
+ "quota": _empty_quota(),
114
+ "skills_used": [],
115
+ "tools_used": [],
116
+ "trajectory": [],
117
+ "summary": {},
118
+ }
119
+
120
+
121
+ def _safe_context(run: Dict[str, Any]) -> Tuple[Optional[str], Optional[str], Optional[str]]:
122
+ """Get display context without falling back to a thread preview.
123
+
124
+ ``run_context`` is the canonical project/branch formatter, but its session
125
+ fallback includes ``thread.preview``. A preview can contain user text, so
126
+ remove it before calling the shared helper.
127
+ """
128
+
129
+ if run_context is None:
130
+ return None, None, None
131
+ safe_run = dict(run)
132
+ thread = run.get("thread")
133
+ if isinstance(thread, dict):
134
+ safe_thread = dict(thread)
135
+ safe_thread.pop("preview", None)
136
+ safe_run["thread"] = safe_thread
137
+ try:
138
+ return run_context(safe_run)
139
+ except Exception:
140
+ return None, None, None
141
+
142
+
143
+ def _run_identifier(run: Dict[str, Any]) -> Optional[str]:
144
+ explicit = _text(run.get("run_id") or run.get("id"))
145
+ if explicit:
146
+ return explicit
147
+ session_id = _text(run.get("session_id"))
148
+ turn_id = _text(run.get("turn_id"))
149
+ if session_id and turn_id:
150
+ # This mirrors telemetry_core.run_key while avoiding a local path or
151
+ # any other storage detail in the MCP response.
152
+ if run_key is not None:
153
+ try:
154
+ return _text(run_key(run))
155
+ except Exception:
156
+ pass
157
+ return "%s--%s" % (session_id.replace("/", "_"), turn_id.replace("/", "_"))
158
+ return session_id or turn_id
159
+
160
+
161
+ def _status(run: Dict[str, Any]) -> str:
162
+ value = _text(run.get("status"), limit=32)
163
+ if value:
164
+ return value
165
+ return "completed" if _integer(run.get("finished_at_ms")) is not None else "running"
166
+
167
+
168
+ def _usage(participant: Any) -> Optional[Dict[str, Any]]:
169
+ if not isinstance(participant, dict):
170
+ return None
171
+ value = participant.get("usage_delta")
172
+ if isinstance(value, dict):
173
+ return value
174
+ value = participant.get("usage")
175
+ return value if isinstance(value, dict) else None
176
+
177
+
178
+ def _participant(
179
+ participant: Any,
180
+ role: str,
181
+ participant_status: str,
182
+ ) -> Dict[str, Any]:
183
+ if not isinstance(participant, dict):
184
+ participant = {}
185
+ usage = _usage(participant)
186
+ return {
187
+ "role": role,
188
+ "agent_id": _text(participant.get("agent_id")),
189
+ "agent_type": _text(participant.get("agent_type")),
190
+ "model": _text(participant.get("model")),
191
+ "reasoning_effort": _text(
192
+ participant.get("reasoning_effort") or participant.get("reasoningEffort")
193
+ ),
194
+ "status": _text(participant.get("status"), limit=32) or participant_status,
195
+ "conclusion": _text(participant.get("conclusion"), limit=1000),
196
+ "total_tokens": _integer(usage.get("total_tokens")) if usage else None,
197
+ }
198
+
199
+
200
+ def _total_tokens(participants: List[Dict[str, Any]]) -> Optional[int]:
201
+ """Sum tokens only when every participant has an exact total.
202
+
203
+ A partial sum would look authoritative in a dashboard while hiding a
204
+ missing worker measurement, so the adapter follows telemetry's
205
+ all-participants aggregation semantics and returns null instead.
206
+ """
207
+
208
+ values = [item.get("total_tokens") for item in participants]
209
+ if not values or any(not isinstance(value, int) for value in values):
210
+ return None
211
+ return sum(values)
212
+
213
+
214
+ def _quota(run: Dict[str, Any]) -> Dict[str, Any]:
215
+ # quota_after is the current snapshot for a completed run. An in-flight
216
+ # run has only quota_before; exposing that snapshot is more useful than
217
+ # inventing a current value, and the source is kept explicit in each item.
218
+ source = run.get("quota_after")
219
+ source_name = "after"
220
+ if not isinstance(source, list) or not source:
221
+ source = run.get("quota_before")
222
+ source_name = "before"
223
+ if not isinstance(source, list):
224
+ source = []
225
+
226
+ used: List[Dict[str, Any]] = []
227
+ remaining: List[Dict[str, Any]] = []
228
+ windows: List[Dict[str, Any]] = []
229
+ for raw in source:
230
+ if not isinstance(raw, dict):
231
+ continue
232
+ used_percent = _number(raw.get("used_percent"))
233
+ remaining_percent = (
234
+ max(0.0, 100.0 - used_percent) if used_percent is not None else None
235
+ )
236
+ duration = _integer(raw.get("window_duration_mins"))
237
+ resets_at = raw.get("resets_at") if _is_number(raw.get("resets_at")) else None
238
+ slot = _text(raw.get("slot"), limit=32)
239
+ common = {
240
+ "slot": slot,
241
+ "window_duration_mins": duration,
242
+ "resets_at": resets_at,
243
+ "source": source_name,
244
+ }
245
+ used.append(dict(common, percent=used_percent))
246
+ remaining.append(dict(common, percent=remaining_percent))
247
+ windows.append(
248
+ dict(
249
+ common,
250
+ used_percent=used_percent,
251
+ remaining_percent=remaining_percent,
252
+ )
253
+ )
254
+ return {"used": used, "remaining": remaining, "windows": windows}
255
+
256
+
257
+ def telemetry_for_target(target: str = "last") -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
258
+ """Read one run through telemetry_core without invoking collection code."""
259
+
260
+ if resolve_run_target is None:
261
+ return None, "FlowPilot telemetry is unavailable."
262
+ try:
263
+ run, identifier = resolve_run_target(target or "last")
264
+ except Exception:
265
+ return None, "FlowPilot telemetry is unavailable."
266
+ if not isinstance(run, dict):
267
+ return None, _text(identifier) or "No FlowPilot telemetry run recorded yet."
268
+ return run, _text(identifier)
269
+
270
+
271
+ def build_telemetry(run: Dict[str, Any], identifier: Optional[str] = None) -> Dict[str, Any]:
272
+ """Convert one internal run record into the public, transcript-free shape."""
273
+
274
+ status = _status(run)
275
+ session, project, branch = _safe_context(run)
276
+ if not session:
277
+ session = _text(run.get("session_id"))
278
+
279
+ started_ms = _integer(run.get("started_at_ms"))
280
+ finished_ms = _integer(run.get("finished_at_ms"))
281
+ duration_ms: Optional[int] = None
282
+ if started_ms is not None and finished_ms is not None and finished_ms >= started_ms:
283
+ duration_ms = finished_ms - started_ms
284
+
285
+ parent = run.get("parent") if isinstance(run.get("parent"), dict) else {}
286
+ parent_item = _participant(parent, "parent", status)
287
+ participants = [parent_item]
288
+ workers = run.get("workers")
289
+ if isinstance(workers, dict):
290
+ for worker in workers.values():
291
+ if isinstance(worker, dict):
292
+ participants.append(
293
+ _participant(worker, "worker", status)
294
+ )
295
+
296
+ parent_data = {
297
+ "model": parent_item.get("model"),
298
+ "reasoning_effort": parent_item.get("reasoning_effort"),
299
+ "total_tokens": parent_item.get("total_tokens"),
300
+ }
301
+ result: Dict[str, Any] = {
302
+ "run_id": _run_identifier(run) or identifier,
303
+ "session": session,
304
+ "project": project,
305
+ "branch": branch,
306
+ "status": status,
307
+ "started_at": (
308
+ fmt_local_timestamp(started_ms, milliseconds=True)
309
+ if fmt_local_timestamp is not None
310
+ else None
311
+ ),
312
+ "finished_at": (
313
+ fmt_local_timestamp(finished_ms, milliseconds=True)
314
+ if fmt_local_timestamp is not None
315
+ else None
316
+ ),
317
+ "duration_seconds": (
318
+ duration_ms / 1000.0 if duration_ms is not None else None
319
+ ),
320
+ "duration_text": (
321
+ fmt_duration_ms(duration_ms)
322
+ if duration_ms is not None and fmt_duration_ms is not None
323
+ else None
324
+ ),
325
+ "participants": participants,
326
+ "parent": parent_data,
327
+ "parent_model": parent_data["model"],
328
+ "parent_reasoning_effort": parent_data["reasoning_effort"],
329
+ "total_tokens": _total_tokens(participants),
330
+ "quota": _quota(run),
331
+ "skills_used": run.get("skills_used") or [],
332
+ "tools_used": run.get("tools_used") or [],
333
+ "trajectory": run.get("trajectory") or [],
334
+ "summary": run.get("summary_info") or {},
335
+ }
336
+ return result
337
+
338
+
339
+ def get_telemetry(arguments: Optional[Dict[str, Any]] = None) -> Tuple[Dict[str, Any], str, bool]:
340
+ """Return ``(structured_content, text, is_error)`` for tools/call."""
341
+
342
+ args = arguments if isinstance(arguments, dict) else {}
343
+ target = args.get("target")
344
+ if target is None:
345
+ target = args.get("run_id")
346
+ if isinstance(target, dict):
347
+ target = target.get("run_id") or target.get("target")
348
+ if target is None or target == "":
349
+ target = "last"
350
+ if not isinstance(target, str):
351
+ return unavailable_telemetry(), "Telemetry target must be a run id or 'last'.", True
352
+
353
+ run, message = telemetry_for_target(target)
354
+ if run is None:
355
+ text = message or "No FlowPilot telemetry run recorded yet."
356
+ return unavailable_telemetry(), text, False
357
+
358
+ structured = build_telemetry(run, message)
359
+ status = structured.get("status") or "unavailable"
360
+ project = structured.get("project") or "unknown project"
361
+ tokens = structured.get("total_tokens")
362
+ token_text = str(tokens) if isinstance(tokens, int) else "token usage unavailable"
363
+ text = "FlowPilot run %s: %s · %s · %s tokens." % (
364
+ structured.get("run_id") or "unknown",
365
+ status,
366
+ project,
367
+ token_text,
368
+ )
369
+ return structured, text, False
370
+
371
+
372
+ def tool_result(arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
373
+ """Build an MCP tool result with both human-readable and structured data."""
374
+
375
+ try:
376
+ structured, text, is_error = get_telemetry(arguments)
377
+ except Exception:
378
+ structured = unavailable_telemetry()
379
+ text = "FlowPilot telemetry is temporarily unavailable."
380
+ is_error = True
381
+ result: Dict[str, Any] = {
382
+ "content": [{"type": "text", "text": text}],
383
+ "structuredContent": structured,
384
+ }
385
+ if is_error:
386
+ result["isError"] = True
387
+ return result
388
+
389
+
390
+ def read_widget() -> str:
391
+ """Read exactly the fixed widget resource and reject symlink escapes."""
392
+
393
+ module_dir = os.path.realpath(str(_MODULE_DIR))
394
+ widget_path = os.path.join(module_dir, WIDGET_FILENAME)
395
+ if os.path.realpath(widget_path) != widget_path:
396
+ raise WidgetResourceError("The telemetry widget resource is invalid.")
397
+ try:
398
+ with open(widget_path, "rb") as stream:
399
+ data = stream.read(_MAX_WIDGET_BYTES + 1)
400
+ except OSError as exc:
401
+ raise WidgetResourceError("The telemetry widget resource is unavailable.") from exc
402
+ if len(data) > _MAX_WIDGET_BYTES:
403
+ raise WidgetResourceError("The telemetry widget resource is too large.")
404
+ try:
405
+ return data.decode("utf-8")
406
+ except UnicodeDecodeError as exc:
407
+ raise WidgetResourceError("The telemetry widget resource is not valid UTF-8.") from exc
408
+
409
+
410
+ def resource_descriptor() -> Dict[str, Any]:
411
+ return {
412
+ "uri": RESOURCE_URI,
413
+ "name": "flowpilot-telemetry-pip",
414
+ "title": "FlowPilot telemetry",
415
+ "description": "Read-only FlowPilot run telemetry dashboard.",
416
+ "mimeType": RESOURCE_MIME_TYPE,
417
+ }
418
+
419
+
420
+ def tool_descriptor() -> Dict[str, Any]:
421
+ return {
422
+ "name": TOOL_NAME,
423
+ "title": "FlowPilot telemetry",
424
+ "description": (
425
+ "Read the latest FlowPilot telemetry run, or a run by run_id. "
426
+ "This tool is read-only and never starts an app-server."
427
+ ),
428
+ "inputSchema": {
429
+ "type": "object",
430
+ "properties": {
431
+ "target": {
432
+ "type": "string",
433
+ "description": "Optional run id; omit or use 'last' for the latest run.",
434
+ },
435
+ "run_id": {
436
+ "type": "string",
437
+ "description": "Optional alias for target when selecting a run.",
438
+ },
439
+ },
440
+ "additionalProperties": False,
441
+ },
442
+ "_meta": {"ui": {"resourceUri": RESOURCE_URI}},
443
+ }
444
+
445
+
446
+ __all__ = [
447
+ "RESOURCE_MIME_TYPE",
448
+ "RESOURCE_URI",
449
+ "TOOL_NAME",
450
+ "WidgetResourceError",
451
+ "build_telemetry",
452
+ "get_telemetry",
453
+ "read_widget",
454
+ "resource_descriptor",
455
+ "tool_descriptor",
456
+ "tool_result",
457
+ "unavailable_telemetry",
458
+ ]