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,421 @@
1
+ """Shared constants, policy configuration, locking, and formatting helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import tempfile
8
+ import time
9
+ import unicodedata
10
+ from contextlib import contextmanager
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ CODEX_HOME = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex"))
15
+ STATE_ROOT = CODEX_HOME / "codex-flow" / "telemetry"
16
+ RUNS_DIR = STATE_ROOT / "runs"
17
+ LAST_FILE = STATE_ROOT / "last.json"
18
+ QUOTA_LEDGER_DB = STATE_ROOT / "quota_ledger.db"
19
+ QUOTA_SUMMARY_FILE = STATE_ROOT / "quota_summary.json"
20
+ QUOTA_LOCK_KEY = "quota_ledger"
21
+ WORKER_INDEX_FILE = STATE_ROOT / "worker-index.json"
22
+ SESSION_INDEX_FILE = CODEX_HOME / "session_index.jsonl"
23
+ TIMEOUT = float(os.environ.get("CODEX_FLOW_TELEMETRY_TIMEOUT", "3.0"))
24
+ LOCK_TIMEOUT = float(os.environ.get("CODEX_FLOW_TELEMETRY_LOCK_TIMEOUT", "2.0"))
25
+ DEFAULT_RETENTION_DAYS = 30
26
+
27
+
28
+ def display_width(s: str) -> int:
29
+ """Calculate terminal display width considering fullwidth/CJK characters."""
30
+ width = 0
31
+ for ch in s:
32
+ if unicodedata.east_asian_width(ch) in ("W", "F"):
33
+ width += 2
34
+ else:
35
+ width += 1
36
+ return width
37
+
38
+
39
+ def truncate_display(s: str, max_width: int) -> str:
40
+ """Truncate a string to fit max_width visual columns."""
41
+ cur = 0
42
+ res: list[str] = []
43
+ for ch in s:
44
+ w = 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
45
+ if cur + w > max_width:
46
+ break
47
+ res.append(ch)
48
+ cur += w
49
+ return "".join(res)
50
+
51
+
52
+ def pad_display(s: str, target_width: int, align: str = "left") -> str:
53
+ """Pad string to target display width."""
54
+ w = display_width(s)
55
+ pad = max(0, target_width - w)
56
+ if align == "right":
57
+ return (" " * pad) + s
58
+ return s + (" " * pad)
59
+
60
+
61
+ def policy_value(section_name: str, key_name: str) -> str | None:
62
+ """Read one simple TOML scalar without adding a Python-version dependency."""
63
+ path = CODEX_HOME / "codex-flow.toml"
64
+ try:
65
+ lines = path.read_text(encoding="utf-8").splitlines()
66
+ except (OSError, UnicodeError):
67
+ return None
68
+ section: str | None = None
69
+ for raw in lines:
70
+ line = raw.split("#", 1)[0].strip()
71
+ if not line:
72
+ continue
73
+ if line.startswith("[") and line.endswith("]"):
74
+ section = line[1:-1].strip()
75
+ continue
76
+ if section != section_name or "=" not in line:
77
+ continue
78
+ key, value = (part.strip() for part in line.split("=", 1))
79
+ if key != key_name:
80
+ continue
81
+ return value.strip().strip('"')
82
+ return None
83
+
84
+
85
+ def policy_bool(section_name: str, key_name: str, default: bool) -> bool:
86
+ value = policy_value(section_name, key_name)
87
+ if value is None:
88
+ return default
89
+ normalized = value.lower()
90
+ if normalized == "true":
91
+ return True
92
+ if normalized == "false":
93
+ return False
94
+ return default
95
+
96
+
97
+ def policy_int(section_name: str, key_name: str, default: int) -> int:
98
+ value = policy_value(section_name, key_name)
99
+ try:
100
+ parsed = int(value) if value is not None else default
101
+ except (TypeError, ValueError):
102
+ return default
103
+ return parsed if parsed > 0 else default
104
+
105
+
106
+ def telemetry_notifications_enabled() -> bool:
107
+ value = os.environ.get("CODEX_FLOW_TELEMETRY_NOTIFICATIONS")
108
+ if value is not None:
109
+ normalized = value.strip().lower()
110
+ if normalized in {"0", "false", "no", "off"}:
111
+ return False
112
+ if normalized in {"1", "true", "yes", "on"}:
113
+ return True
114
+ return policy_bool("telemetry", "notifications", False)
115
+
116
+
117
+ def telemetry_retention_days() -> int:
118
+ value = os.environ.get("CODEX_FLOW_TELEMETRY_RETENTION_DAYS")
119
+ if value is not None:
120
+ try:
121
+ parsed = int(value)
122
+ if parsed > 0:
123
+ return parsed
124
+ except ValueError:
125
+ pass
126
+ return policy_int("telemetry", "retention_days", DEFAULT_RETENTION_DAYS)
127
+
128
+
129
+ def now_ms() -> int:
130
+ return int(time.time() * 1000)
131
+
132
+
133
+ def atomic_json(path: Path, value: Any) -> None:
134
+ path.parent.mkdir(parents=True, exist_ok=True)
135
+ fd, tmp = tempfile.mkstemp(prefix=path.name + ".", dir=str(path.parent))
136
+ try:
137
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
138
+ json.dump(value, handle, ensure_ascii=False, indent=2, sort_keys=True)
139
+ handle.write("\n")
140
+ os.replace(tmp, path)
141
+ finally:
142
+ if os.path.exists(tmp):
143
+ os.unlink(tmp)
144
+
145
+
146
+ @contextmanager
147
+ def state_lock(key: str):
148
+ STATE_ROOT.mkdir(parents=True, exist_ok=True)
149
+ lock = STATE_ROOT / ("." + key.replace("/", "_") + ".lock")
150
+ deadline = time.monotonic() + LOCK_TIMEOUT
151
+ acquired = False
152
+ while True:
153
+ try:
154
+ lock.mkdir()
155
+ acquired = True
156
+ break
157
+ except FileExistsError:
158
+ if time.monotonic() >= deadline:
159
+ break
160
+ time.sleep(0.025)
161
+ try:
162
+ yield acquired
163
+ finally:
164
+ if acquired:
165
+ try:
166
+ lock.rmdir()
167
+ except OSError:
168
+ pass
169
+
170
+
171
+ def read_json_object(path: Path) -> dict[str, Any] | None:
172
+ try:
173
+ value = json.loads(path.read_text(encoding="utf-8"))
174
+ except (OSError, UnicodeError, json.JSONDecodeError):
175
+ return None
176
+ return value if isinstance(value, dict) else None
177
+
178
+
179
+ def numeric_ms(value: Any) -> int | None:
180
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
181
+ return int(value)
182
+ return None
183
+
184
+
185
+ def safe_key_part(value: Any) -> str:
186
+ return str(value).replace("/", "_").replace("\\", "_")
187
+
188
+
189
+ def run_key(event: dict[str, Any]) -> str:
190
+ session = safe_key_part(event.get("session_id") or "unknown")
191
+ turn = safe_key_part(event.get("turn_id") or "unknown")
192
+ return f"{session}--{turn}"
193
+
194
+
195
+ def run_path_for_key(key: str) -> Path:
196
+ return RUNS_DIR / f"{safe_key_part(key)}.json"
197
+
198
+
199
+ def run_path(event: dict[str, Any]) -> Path:
200
+ return run_path_for_key(run_key(event))
201
+
202
+
203
+ def load_run(event: dict[str, Any], key: str | None = None) -> dict[str, Any]:
204
+ resolved_key = key or run_key(event)
205
+ path = run_path_for_key(resolved_key)
206
+ existing = read_json_object(path) if path.exists() else None
207
+ if existing is not None:
208
+ return existing
209
+ return {
210
+ "schema_version": 1,
211
+ "session_id": event.get("session_id"),
212
+ "turn_id": event.get("turn_id"),
213
+ "cwd": event.get("cwd"),
214
+ "parent": {"model": event.get("model")},
215
+ "workers": {},
216
+ "started_at_ms": now_ms(),
217
+ }
218
+
219
+
220
+ def iter_run_files() -> list[Path]:
221
+ try:
222
+ return sorted(path for path in RUNS_DIR.glob("*.json") if path.is_file())
223
+ except OSError:
224
+ return []
225
+
226
+
227
+ def load_worker_index() -> dict[str, Any]:
228
+ value = read_json_object(WORKER_INDEX_FILE)
229
+ if value is None:
230
+ return {"schema_version": 1, "workers": {}}
231
+ workers = value.get("workers")
232
+ if not isinstance(workers, dict):
233
+ value["workers"] = {}
234
+ return value
235
+
236
+
237
+ def worker_index_entry(
238
+ agent_id: str, session_id: Any, worker_turn_id: Any = None
239
+ ) -> dict[str, Any] | None:
240
+ if not agent_id or agent_id == "unknown":
241
+ return None
242
+ index = load_worker_index()
243
+ workers = index.get("workers")
244
+ entry = workers.get(agent_id) if isinstance(workers, dict) else None
245
+ if not isinstance(entry, dict):
246
+ return None
247
+ if str(entry.get("session_id") or "") != str(session_id or ""):
248
+ return None
249
+ key = entry.get("run_key")
250
+ if worker_turn_id is not None:
251
+ target = str(worker_turn_id)
252
+ executions = entry.get("executions")
253
+ if isinstance(executions, dict):
254
+ execution = executions.get(target)
255
+ if isinstance(execution, dict):
256
+ execution_key = execution.get("run_key") or execution.get("parent_key")
257
+ if isinstance(execution_key, str) and run_path_for_key(execution_key).is_file():
258
+ result = dict(entry)
259
+ result["run_key"] = execution_key
260
+ result["parent_key"] = execution_key
261
+ result["parent_turn_id"] = execution.get("parent_turn_id")
262
+ result["execution"] = execution
263
+ return result
264
+ elif isinstance(executions, list):
265
+ for execution in executions:
266
+ if not isinstance(execution, dict) or str(execution.get("turn_id") or "") != target:
267
+ continue
268
+ execution_key = execution.get("run_key") or execution.get("parent_key")
269
+ if isinstance(execution_key, str) and run_path_for_key(execution_key).is_file():
270
+ result = dict(entry)
271
+ result["run_key"] = execution_key
272
+ result["parent_key"] = execution_key
273
+ result["parent_turn_id"] = execution.get("parent_turn_id")
274
+ result["execution"] = execution
275
+ return result
276
+ # A legacy index entry has no child-turn identity. Returning it here
277
+ # would silently route a delayed stop to whichever parent was latest.
278
+ return None
279
+ if not isinstance(key, str) or not run_path_for_key(key).is_file():
280
+ return None
281
+ return entry
282
+
283
+
284
+ def remember_worker_parent(
285
+ agent_id: str,
286
+ session_id: Any,
287
+ parent_key: str,
288
+ parent_turn_id: Any,
289
+ started_at_ms: Any,
290
+ worker_turn_id: Any = None,
291
+ finished_at_ms: Any = None,
292
+ transcript_path: Any = None,
293
+ ) -> None:
294
+ if not agent_id or agent_id == "unknown":
295
+ return
296
+ with state_lock("worker-index") as acquired:
297
+ if not acquired:
298
+ return
299
+ index = load_worker_index()
300
+ workers = index.setdefault("workers", {})
301
+ if not isinstance(workers, dict):
302
+ workers = {}
303
+ index["workers"] = workers
304
+ existing = workers.get(agent_id)
305
+ new_started = numeric_ms(started_at_ms)
306
+ if not isinstance(existing, dict) or str(existing.get("session_id") or "") != str(session_id or ""):
307
+ existing = None
308
+ entry = dict(existing) if existing is not None else {
309
+ "agent_id": agent_id,
310
+ "session_id": session_id,
311
+ }
312
+ old_started = numeric_ms(entry.get("started_at_ms"))
313
+ # Keep the legacy top-level pointer useful for a newly-started worker,
314
+ # while retaining every child execution for exact late-stop routing.
315
+ if old_started is None or (new_started is not None and new_started >= old_started):
316
+ entry.update(
317
+ {
318
+ "parent_key": parent_key,
319
+ "parent_turn_id": parent_turn_id,
320
+ "run_key": parent_key,
321
+ "started_at_ms": new_started,
322
+ }
323
+ )
324
+ executions = entry.get("executions")
325
+ if not isinstance(executions, dict):
326
+ executions = {}
327
+ entry["executions"] = executions
328
+ if worker_turn_id is not None:
329
+ execution_id = str(worker_turn_id)
330
+ execution = executions.get(execution_id)
331
+ if not isinstance(execution, dict):
332
+ execution = {"turn_id": worker_turn_id}
333
+ execution["run_key"] = parent_key
334
+ execution["parent_key"] = parent_key
335
+ execution["parent_turn_id"] = parent_turn_id
336
+ previous_started = numeric_ms(execution.get("started_at_ms"))
337
+ if previous_started is None or (new_started is not None and new_started < previous_started):
338
+ execution["started_at_ms"] = new_started
339
+ new_finished = numeric_ms(finished_at_ms)
340
+ previous_finished = numeric_ms(execution.get("finished_at_ms"))
341
+ if new_finished is not None and (previous_finished is None or new_finished > previous_finished):
342
+ execution["finished_at_ms"] = new_finished
343
+ if transcript_path is not None:
344
+ execution["transcript_path"] = transcript_path
345
+ execution["updated_at_ms"] = now_ms()
346
+ executions[execution_id] = execution
347
+ entry["updated_at_ms"] = now_ms()
348
+ workers[agent_id] = entry
349
+ atomic_json(WORKER_INDEX_FILE, index)
350
+
351
+
352
+ def text_value(value: Any) -> str | None:
353
+ if not isinstance(value, str):
354
+ return None
355
+ value = value.strip()
356
+ return value or None
357
+
358
+
359
+ def compact_text(value: Any, limit: int = 52) -> str | None:
360
+ if value is None:
361
+ return None
362
+ text = " ".join(str(value).split())
363
+ if not text:
364
+ return None
365
+ if len(text) <= limit:
366
+ return text
367
+ return text[: max(1, limit - 1)] + "…"
368
+
369
+
370
+ def fmt_tokens(value: Any) -> str:
371
+ if not isinstance(value, (int, float)):
372
+ return "n/a"
373
+ value = int(value)
374
+ if value >= 1_000_000:
375
+ return f"{value / 1_000_000:.2f}m"
376
+ if value >= 1_000:
377
+ return f"{value / 1_000:.1f}k"
378
+ return str(value)
379
+
380
+
381
+ def window_label(minutes: Any) -> str:
382
+ if minutes == 300:
383
+ return "5h"
384
+ if minutes == 10080:
385
+ return "7d"
386
+ if isinstance(minutes, int) and minutes > 0:
387
+ if minutes % 1440 == 0:
388
+ return f"{minutes // 1440}d"
389
+ return f"{minutes}m"
390
+ return "quota"
391
+
392
+
393
+ def fmt_percent(value: Any) -> str | None:
394
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
395
+ return f"{value:g}"
396
+ if value is None:
397
+ return None
398
+ return str(value)
399
+
400
+
401
+ def fmt_local_timestamp(value: Any, *, milliseconds: bool = False) -> str | None:
402
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
403
+ return None
404
+ timestamp = float(value) / 1000 if milliseconds else float(value)
405
+ try:
406
+ return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp))
407
+ except (OverflowError, OSError, ValueError):
408
+ return None
409
+
410
+
411
+ def fmt_duration_ms(value: Any) -> str | None:
412
+ if not isinstance(value, (int, float)) or isinstance(value, bool):
413
+ return None
414
+ total_seconds = max(0, int(round(float(value) / 1000)))
415
+ hours, remainder = divmod(total_seconds, 3600)
416
+ minutes, seconds = divmod(remainder, 60)
417
+ if hours:
418
+ return f"{hours}h {minutes}m {seconds}s"
419
+ if minutes:
420
+ return f"{minutes}m {seconds}s"
421
+ return f"{seconds}s"