marvisx-cli 0.1.0__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 (587) hide show
  1. core/api/__init__.py +0 -0
  2. core/api/agents/__init__.py +0 -0
  3. core/api/agents/session_health.py +59 -0
  4. core/api/agents/session_manager.py +206 -0
  5. core/api/bin/marvisx-state-hook.py +182 -0
  6. core/api/config.py +533 -0
  7. core/api/db.py +1516 -0
  8. core/api/dependencies/__init__.py +0 -0
  9. core/api/dependencies/tenant.py +34 -0
  10. core/api/main.py +1641 -0
  11. core/api/mcp/__init__.py +8 -0
  12. core/api/mcp/_adapter.py +184 -0
  13. core/api/mcp/server.py +58 -0
  14. core/api/mcp/tools/__init__.py +59 -0
  15. core/api/mcp/tools/brain.py +599 -0
  16. core/api/mcp/tools/graph.py +380 -0
  17. core/api/mcp/tools/handoffs.py +112 -0
  18. core/api/mcp/tools/ingest.py +326 -0
  19. core/api/mcp/tools/learnings.py +144 -0
  20. core/api/mcp/tools/projects.py +99 -0
  21. core/api/mcp/tools/pull_requests.py +173 -0
  22. core/api/mcp/tools/safety.py +111 -0
  23. core/api/mcp/tools/search.py +79 -0
  24. core/api/mcp/tools/tasks.py +258 -0
  25. core/api/middleware/__init__.py +0 -0
  26. core/api/middleware/tool_call_audit.py +111 -0
  27. core/api/models/__init__.py +346 -0
  28. core/api/models/auth.py +48 -0
  29. core/api/models/brain.py +1006 -0
  30. core/api/models/common.py +76 -0
  31. core/api/models/costs.py +91 -0
  32. core/api/models/graph.py +66 -0
  33. core/api/models/graph_cosmo.py +125 -0
  34. core/api/models/graph_pr_impact.py +257 -0
  35. core/api/models/graph_ux.py +141 -0
  36. core/api/models/inbox.py +230 -0
  37. core/api/models/ingest_keys.py +108 -0
  38. core/api/models/kg.py +41 -0
  39. core/api/models/llm_config.py +56 -0
  40. core/api/models/monitoring.py +234 -0
  41. core/api/models/projects.py +161 -0
  42. core/api/models/search.py +42 -0
  43. core/api/models/sessions.py +322 -0
  44. core/api/models/tasks.py +184 -0
  45. core/api/models/teams.py +63 -0
  46. core/api/models/users.py +184 -0
  47. core/api/observability/__init__.py +0 -0
  48. core/api/observability/tracing.py +92 -0
  49. core/api/paths.py +26 -0
  50. core/api/rate_limit.py +24 -0
  51. core/api/rbac.py +112 -0
  52. core/api/routers/__init__.py +0 -0
  53. core/api/routers/_adapter.py +24 -0
  54. core/api/routers/admin_pr_impact.py +230 -0
  55. core/api/routers/admin_settings.py +147 -0
  56. core/api/routers/agent.py +1079 -0
  57. core/api/routers/agent_tokens.py +276 -0
  58. core/api/routers/app_settings.py +112 -0
  59. core/api/routers/audit.py +89 -0
  60. core/api/routers/auth.py +586 -0
  61. core/api/routers/bench.py +161 -0
  62. core/api/routers/brain.py +881 -0
  63. core/api/routers/brain_directions.py +527 -0
  64. core/api/routers/ci_checks.py +140 -0
  65. core/api/routers/comments.py +273 -0
  66. core/api/routers/costs.py +148 -0
  67. core/api/routers/docs_coverage.py +217 -0
  68. core/api/routers/docs_governance.py +63 -0
  69. core/api/routers/documents.py +318 -0
  70. core/api/routers/files.py +163 -0
  71. core/api/routers/finder.py +987 -0
  72. core/api/routers/graph.py +836 -0
  73. core/api/routers/handoffs.py +156 -0
  74. core/api/routers/inbox.py +496 -0
  75. core/api/routers/ingest_api_keys.py +205 -0
  76. core/api/routers/ingest_triage.py +1227 -0
  77. core/api/routers/judge.py +306 -0
  78. core/api/routers/kg.py +336 -0
  79. core/api/routers/learnings.py +253 -0
  80. core/api/routers/llm_config.py +130 -0
  81. core/api/routers/monitoring.py +347 -0
  82. core/api/routers/notifications.py +125 -0
  83. core/api/routers/pr_impact.py +315 -0
  84. core/api/routers/projects.py +1061 -0
  85. core/api/routers/pull_requests.py +312 -0
  86. core/api/routers/push.py +67 -0
  87. core/api/routers/raci.py +228 -0
  88. core/api/routers/search.py +125 -0
  89. core/api/routers/sessions.py +3100 -0
  90. core/api/routers/settings.py +90 -0
  91. core/api/routers/share_repo.py +68 -0
  92. core/api/routers/status_updates.py +96 -0
  93. core/api/routers/tags.py +45 -0
  94. core/api/routers/tasks.py +526 -0
  95. core/api/routers/teams.py +425 -0
  96. core/api/routers/terminal.py +105 -0
  97. core/api/routers/users.py +331 -0
  98. core/api/routers/webhooks.py +330 -0
  99. core/api/runtime_settings.py +84 -0
  100. core/api/security.py +652 -0
  101. core/api/services/__init__.py +0 -0
  102. core/api/services/audit.py +58 -0
  103. core/api/services/auto_approval.py +82 -0
  104. core/api/services/brain/__init__.py +52 -0
  105. core/api/services/brain/baseline.py +230 -0
  106. core/api/services/brain/capabilities.py +75 -0
  107. core/api/services/brain/cascade_rollup.py +388 -0
  108. core/api/services/brain/compound_bridge.py +215 -0
  109. core/api/services/brain/cycle.py +1242 -0
  110. core/api/services/brain/cycle_snapshot.py +371 -0
  111. core/api/services/brain/digest_collector.py +147 -0
  112. core/api/services/brain/direction.py +421 -0
  113. core/api/services/brain/drift.py +356 -0
  114. core/api/services/brain/drift_router.py +409 -0
  115. core/api/services/brain/edge_metrics.py +79 -0
  116. core/api/services/brain/events_reader.py +222 -0
  117. core/api/services/brain/findings.py +1379 -0
  118. core/api/services/brain/findings_reader.py +1006 -0
  119. core/api/services/brain/jobs.py +733 -0
  120. core/api/services/brain/journal.py +206 -0
  121. core/api/services/brain/knowledge_forms.py +92 -0
  122. core/api/services/brain/llm/__init__.py +37 -0
  123. core/api/services/brain/llm/_runner.py +62 -0
  124. core/api/services/brain/llm/base.py +70 -0
  125. core/api/services/brain/llm/cache.py +99 -0
  126. core/api/services/brain/llm/constants.py +46 -0
  127. core/api/services/brain/llm/direction_alignment.py +289 -0
  128. core/api/services/brain/llm/factory.py +132 -0
  129. core/api/services/brain/llm/finding_reasoning.py +98 -0
  130. core/api/services/brain/llm/finding_summary.py +92 -0
  131. core/api/services/brain/llm/grounding.py +46 -0
  132. core/api/services/brain/llm/journal_polish.py +96 -0
  133. core/api/services/brain/llm/local_gateway.py +426 -0
  134. core/api/services/brain/llm/parsers.py +71 -0
  135. core/api/services/brain/llm/router_glue.py +422 -0
  136. core/api/services/brain/memory_ops.py +1677 -0
  137. core/api/services/brain/models.py +140 -0
  138. core/api/services/brain/owner_hint.py +211 -0
  139. core/api/services/brain/recap.py +307 -0
  140. core/api/services/brain/rules/__init__.py +65 -0
  141. core/api/services/brain/rules/_signals.py +205 -0
  142. core/api/services/brain/rules/dr1_activity_without_status.py +108 -0
  143. core/api/services/brain/rules/dr2_decision_without_adr.py +110 -0
  144. core/api/services/brain/rules/dr3_stale_open_loop.py +127 -0
  145. core/api/services/brain/rules/dr4_docs_governance_drift.py +79 -0
  146. core/api/services/brain/rules/dr5_playbook_changed.py +99 -0
  147. core/api/services/brain/rules/dr6_external_update_unpropagated.py +100 -0
  148. core/api/services/brain/rules/dr7_claimed_decision_gap.py +123 -0
  149. core/api/services/brain/rules/dr8_direction_misalignment.py +230 -0
  150. core/api/services/brain/runs_reader.py +485 -0
  151. core/api/services/brain/scope.py +79 -0
  152. core/api/services/brain/sources/__init__.py +46 -0
  153. core/api/services/brain/sources/base.py +86 -0
  154. core/api/services/brain/sources/git_kg.py +393 -0
  155. core/api/services/brain/sources/handoffs.py +157 -0
  156. core/api/services/brain/sources/ingestor.py +130 -0
  157. core/api/services/brain/sources/learnings.py +121 -0
  158. core/api/services/brain/sources/pir_tasks.py +245 -0
  159. core/api/services/brain/watermarks.py +147 -0
  160. core/api/services/brain/ws_emitter.py +170 -0
  161. core/api/services/cc_tasks_reader.py +76 -0
  162. core/api/services/ci_service.py +263 -0
  163. core/api/services/claude_metrics.py +796 -0
  164. core/api/services/codex_metrics.py +364 -0
  165. core/api/services/conversation_reader.py +102 -0
  166. core/api/services/cost_service.py +243 -0
  167. core/api/services/crypto.py +147 -0
  168. core/api/services/docs_governance/__init__.py +1 -0
  169. core/api/services/docs_governance/confidence.py +230 -0
  170. core/api/services/docs_governance/config.py +87 -0
  171. core/api/services/docs_governance/enrichment.py +65 -0
  172. core/api/services/docs_governance/frontmatter_validator.py +83 -0
  173. core/api/services/docs_governance/hard_gates.py +221 -0
  174. core/api/services/docs_governance/triage_orchestrator.py +98 -0
  175. core/api/services/embedding_internal.py +395 -0
  176. core/api/services/embedding_service.py +832 -0
  177. core/api/services/event_dispatcher.py +167 -0
  178. core/api/services/events.py +70 -0
  179. core/api/services/git_ops.py +621 -0
  180. core/api/services/graph_cosmo_service.py +440 -0
  181. core/api/services/graph_ranker.py +306 -0
  182. core/api/services/graph_service.py +1589 -0
  183. core/api/services/inbox.py +800 -0
  184. core/api/services/inbox_digest.py +221 -0
  185. core/api/services/inbox_digest_deep_research.py +80 -0
  186. core/api/services/inbox_digest_jobs.py +595 -0
  187. core/api/services/inbox_gmail_sync.py +167 -0
  188. core/api/services/inbox_llm_classifier.py +906 -0
  189. core/api/services/inbox_source_identity.py +116 -0
  190. core/api/services/inbox_sources.py +456 -0
  191. core/api/services/inbox_taxonomy.py +195 -0
  192. core/api/services/inbox_tldr.py +1079 -0
  193. core/api/services/inbox_triage.py +899 -0
  194. core/api/services/ingest/__init__.py +13 -0
  195. core/api/services/ingest/api_key_auth.py +136 -0
  196. core/api/services/ingest/auto_approve.py +120 -0
  197. core/api/services/ingest/classifier.py +138 -0
  198. core/api/services/ingest/confidence.py +173 -0
  199. core/api/services/ingest/dispatch.py +88 -0
  200. core/api/services/ingest/embedding_router.py +272 -0
  201. core/api/services/ingest/events.py +33 -0
  202. core/api/services/ingest/ignore_patterns.py +79 -0
  203. core/api/services/ingest/image_probe.py +218 -0
  204. core/api/services/ingest/ingress.py +263 -0
  205. core/api/services/ingest/insert_saga.py +793 -0
  206. core/api/services/ingest/llm/__init__.py +13 -0
  207. core/api/services/ingest/llm/anthropic_haiku.py +23 -0
  208. core/api/services/ingest/llm/base.py +59 -0
  209. core/api/services/ingest/llm/byok_provider.py +130 -0
  210. core/api/services/ingest/llm/classification_context.py +301 -0
  211. core/api/services/ingest/llm/config_store.py +246 -0
  212. core/api/services/ingest/llm/factory.py +24 -0
  213. core/api/services/ingest/llm/kg_enricher.py +306 -0
  214. core/api/services/ingest/llm/local_gateway.py +821 -0
  215. core/api/services/ingest/llm/local_vllm.py +23 -0
  216. core/api/services/ingest/llm/openai_nano.py +349 -0
  217. core/api/services/ingest/lock_advisory.py +57 -0
  218. core/api/services/ingest/parser_router.py +1756 -0
  219. core/api/services/ingest/parsers/__init__.py +1 -0
  220. core/api/services/ingest/parsers/docling_parser.py +142 -0
  221. core/api/services/ingest/parsers/docparse_gateway.py +178 -0
  222. core/api/services/ingest/parsers/docx_parser.py +127 -0
  223. core/api/services/ingest/parsers/folder_unpacker.py +85 -0
  224. core/api/services/ingest/parsers/gateway_aux.py +147 -0
  225. core/api/services/ingest/parsers/image_parser.py +251 -0
  226. core/api/services/ingest/parsers/internal_markdown.py +89 -0
  227. core/api/services/ingest/parsers/ocr_gateway.py +117 -0
  228. core/api/services/ingest/parsers/ocr_pdf_parser.py +112 -0
  229. core/api/services/ingest/parsers/pdf_types.py +13 -0
  230. core/api/services/ingest/parsers/transcript_parser.py +445 -0
  231. core/api/services/ingest/parsers/vision_gateway.py +186 -0
  232. core/api/services/ingest/parsers/xlsx_parser.py +91 -0
  233. core/api/services/ingest/parsers/zip_unpacker.py +126 -0
  234. core/api/services/ingest/preflight.py +393 -0
  235. core/api/services/ingest/retry_voyage.py +88 -0
  236. core/api/services/ingest/routing_policy.py +307 -0
  237. core/api/services/ingest/serializers/__init__.py +1 -0
  238. core/api/services/ingest/serializers/xlsx_to_markdown.py +80 -0
  239. core/api/services/ingest/skip_log.py +74 -0
  240. core/api/services/ingest/watcher.py +637 -0
  241. core/api/services/kg/__init__.py +0 -0
  242. core/api/services/kg/audit.py +49 -0
  243. core/api/services/kg/hybrid_search.py +691 -0
  244. core/api/services/kg/lens.py +339 -0
  245. core/api/services/kg/pr_impact.py +770 -0
  246. core/api/services/kg/queries.py +152 -0
  247. core/api/services/kg/ranking.py +89 -0
  248. core/api/services/kg/rrf.py +143 -0
  249. core/api/services/kg_watcher_control.py +161 -0
  250. core/api/services/local_llm/__init__.py +19 -0
  251. core/api/services/local_llm/async_client.py +385 -0
  252. core/api/services/local_llm/client.py +173 -0
  253. core/api/services/local_llm/url_validator.py +44 -0
  254. core/api/services/metrics_collector.py +646 -0
  255. core/api/services/metrics_providers.py +65 -0
  256. core/api/services/model_registry.py +266 -0
  257. core/api/services/model_router.py +137 -0
  258. core/api/services/n8n_client.py +77 -0
  259. core/api/services/newsletter_llm_gateway.py +66 -0
  260. core/api/services/notification_service.py +134 -0
  261. core/api/services/openai_responses.py +55 -0
  262. core/api/services/opencode_metrics.py +375 -0
  263. core/api/services/opencode_sessions.py +173 -0
  264. core/api/services/pii_redactor.py +138 -0
  265. core/api/services/pr_impact_pipeline/__init__.py +21 -0
  266. core/api/services/pr_impact_pipeline/differ.py +421 -0
  267. core/api/services/pr_impact_pipeline/dispatcher.py +415 -0
  268. core/api/services/pr_impact_pipeline/gc.py +93 -0
  269. core/api/services/pr_impact_pipeline/languages.py +192 -0
  270. core/api/services/pr_impact_pipeline/parser.py +178 -0
  271. core/api/services/pr_impact_pipeline/writer.py +394 -0
  272. core/api/services/pr_service.py +1393 -0
  273. core/api/services/project_paths.py +70 -0
  274. core/api/services/project_status_updates.py +265 -0
  275. core/api/services/providers.py +276 -0
  276. core/api/services/push_service.py +170 -0
  277. core/api/services/reminder_service.py +89 -0
  278. core/api/services/runas.py +41 -0
  279. core/api/services/salience_service.py +69 -0
  280. core/api/services/security_collector.py +281 -0
  281. core/api/services/session_catalog.py +385 -0
  282. core/api/services/session_metrics_service.py +301 -0
  283. core/api/services/session_ops.py +272 -0
  284. core/api/services/session_state.py +173 -0
  285. core/api/services/share_links.py +222 -0
  286. core/api/services/task_transitions.py +146 -0
  287. core/api/services/terminal_metrics.py +462 -0
  288. core/api/services/terminal_metrics_dump.py +203 -0
  289. core/api/services/tmux.py +1205 -0
  290. core/api/services/webhook_service.py +422 -0
  291. core/api/services/workspace_sync.py +164 -0
  292. core/api/templates/__init__.py +1 -0
  293. core/api/templates/markdown_share.py +164 -0
  294. core/api/terminal.py +1031 -0
  295. core/api/tests/__init__.py +0 -0
  296. core/api/tests/test_agent_facing_auth_dependencies.py +132 -0
  297. core/api/tests/test_audit_permissions.py +133 -0
  298. core/api/tests/test_backfill_session_conversations.py +90 -0
  299. core/api/tests/test_backfill_working_seconds_msg.py +129 -0
  300. core/api/tests/test_claude_metrics.py +326 -0
  301. core/api/tests/test_codex_metrics.py +189 -0
  302. core/api/tests/test_finder_paths.py +74 -0
  303. core/api/tests/test_git_ops_merge.py +155 -0
  304. core/api/tests/test_learnings_check_search.py +81 -0
  305. core/api/tests/test_metrics_providers.py +133 -0
  306. core/api/tests/test_migration_087.py +164 -0
  307. core/api/tests/test_migration_088.py +94 -0
  308. core/api/tests/test_migration_089.py +116 -0
  309. core/api/tests/test_openai_responses.py +24 -0
  310. core/api/tests/test_opencode_metrics.py +740 -0
  311. core/api/tests/test_opencode_sessions.py +321 -0
  312. core/api/tests/test_pr_workflow_e2e.py +457 -0
  313. core/api/tests/test_projects_handoffs.py +31 -0
  314. core/api/tests/test_providers.py +138 -0
  315. core/api/tests/test_safety_bridge.py +347 -0
  316. core/api/tests/test_session_catalog.py +142 -0
  317. core/api/tests/test_session_conversations.py +512 -0
  318. core/api/tests/test_session_metrics_service.py +270 -0
  319. core/api/tests/test_session_resume_paths.py +548 -0
  320. core/api/tests/test_session_theme_mode_migration.py +56 -0
  321. core/api/tests/test_sessions_rbac.py +131 -0
  322. core/api/tests/test_share_edit.py +398 -0
  323. core/api/tests/test_share_repo.py +200 -0
  324. core/api/tests/test_terminal_session_manager.py +98 -0
  325. core/api/tests/test_terminal_upload.py +34 -0
  326. core/api/tests/test_tmux.py +272 -0
  327. core/api/tests/test_workspace_sync.py +186 -0
  328. core/api/tests/test_ws_ticket_in_memory.py +73 -0
  329. core/api/use_cases/__init__.py +11 -0
  330. core/api/use_cases/_context.py +89 -0
  331. core/api/use_cases/_errors.py +62 -0
  332. core/api/use_cases/_roles.py +16 -0
  333. core/api/use_cases/audit.py +171 -0
  334. core/api/use_cases/brain.py +1232 -0
  335. core/api/use_cases/costs.py +249 -0
  336. core/api/use_cases/graph.py +1153 -0
  337. core/api/use_cases/handoffs.py +506 -0
  338. core/api/use_cases/ingest_triage.py +1229 -0
  339. core/api/use_cases/learnings.py +538 -0
  340. core/api/use_cases/projects.py +705 -0
  341. core/api/use_cases/pull_requests.py +415 -0
  342. core/api/use_cases/search.py +926 -0
  343. core/api/use_cases/tasks.py +1495 -0
  344. core/api/visibility.py +141 -0
  345. core/cli/__init__.py +5 -0
  346. core/cli/_index_source.py +632 -0
  347. core/cli/_runtime_ctx.py +160 -0
  348. core/cli/_transmute.py +241 -0
  349. core/cli/marvis_doctor.py +704 -0
  350. core/cli/marvis_feedback.py +396 -0
  351. core/cli/marvis_governance.py +315 -0
  352. core/cli/marvis_hooks.py +515 -0
  353. core/cli/marvis_init.py +757 -0
  354. core/cli/marvis_mcp.py +401 -0
  355. core/cli/marvis_runtime.py +855 -0
  356. core/cli/marvis_telemetry.py +228 -0
  357. core/scripts/_drift_check.py +716 -0
  358. core/scripts/_frontmatter.py +66 -0
  359. core/scripts/_graph_writer.py +189 -0
  360. core/scripts/ast_parser.py +1553 -0
  361. core/scripts/install_hooks/__init__.py +1 -0
  362. core/scripts/install_hooks/_config.sh +109 -0
  363. core/scripts/install_hooks/block-dangerous-bash.sh +23 -0
  364. core/scripts/install_hooks/block-db-direct-write.sh +23 -0
  365. core/scripts/install_hooks/block-push-no-task.sh +23 -0
  366. core/scripts/install_hooks/block-staging-to-prod.sh +23 -0
  367. core/scripts/install_hooks/block-subtree-push.sh +23 -0
  368. core/scripts/install_hooks/config.json +53 -0
  369. core/scripts/install_hooks/enforce-no-merge-main.sh +23 -0
  370. core/scripts/install_hooks/enforce-worktree.sh +23 -0
  371. core/scripts/install_hooks/quality-gate.sh +170 -0
  372. core/scripts/install_hooks/safety_bridge.py +968 -0
  373. core/scripts/install_hooks/secret-scan.sh +23 -0
  374. core/scripts/migrate_spike_node_ids.py +122 -0
  375. core/scripts/populate_artifacts.py +2198 -0
  376. core/scripts/populate_cross_project.py +2457 -0
  377. core/scripts/populate_inbox_nodes.py +357 -0
  378. core/scripts/populate_pr_impact.py +267 -0
  379. core/scripts/populate_project_nodes.py +603 -0
  380. core/scripts/populate_touch_counter.py +337 -0
  381. core/scripts/reparse_failed.py +57 -0
  382. core/scripts/safety_bridge.py +968 -0
  383. core/telemetry/__init__.py +9 -0
  384. core/telemetry/client.py +405 -0
  385. core/telemetry/schema.py +122 -0
  386. core/wizard/__init__.py +65 -0
  387. core/wizard/byok_vault.py +147 -0
  388. core/wizard/defaults.py +58 -0
  389. core/wizard/state.py +117 -0
  390. core/wizard/steps.py +70 -0
  391. core/wizard/validation.py +136 -0
  392. marvisx_cli-0.1.0.dist-info/METADATA +201 -0
  393. marvisx_cli-0.1.0.dist-info/RECORD +587 -0
  394. marvisx_cli-0.1.0.dist-info/WHEEL +5 -0
  395. marvisx_cli-0.1.0.dist-info/entry_points.txt +3 -0
  396. marvisx_cli-0.1.0.dist-info/licenses/LICENSE +98 -0
  397. marvisx_cli-0.1.0.dist-info/top_level.txt +3 -0
  398. migrations/001_initial.sql +33 -0
  399. migrations/002_tasks.sql +30 -0
  400. migrations/003_session_management.sql +7 -0
  401. migrations/004_projects_comments.sql +65 -0
  402. migrations/005_session_intelligence.sql +15 -0
  403. migrations/006_settings.sql +12 -0
  404. migrations/007_task_scoring.sql +12 -0
  405. migrations/008_cost_tracking.sql +31 -0
  406. migrations/009_session_card_metrics.sql +3 -0
  407. migrations/010_monitoring.sql +55 -0
  408. migrations/012_agent_api.sql +21 -0
  409. migrations/013_session_complete.sql +8 -0
  410. migrations/015_pull_requests.sql +43 -0
  411. migrations/015_pull_requests_down.sql +5 -0
  412. migrations/016_users_raci.sql +116 -0
  413. migrations/017_task_cost_entries.sql +87 -0
  414. migrations/018_agents.sql +73 -0
  415. migrations/018_agents_down.sql +13 -0
  416. migrations/019_review_feedback.sql +18 -0
  417. migrations/020_pr_commit_sha.sql +4 -0
  418. migrations/021_webhook_events.sql +18 -0
  419. migrations/022_devx_agent_managed.sql +11 -0
  420. migrations/022_devx_agent_managed_down.sql +6 -0
  421. migrations/023_devx_p1_gate.sql +7 -0
  422. migrations/023_devx_p1_gate_down.sql +3 -0
  423. migrations/024_chat_messages.sql +16 -0
  424. migrations/024_pr_conversation_id.sql +8 -0
  425. migrations/024_task_indexes.sql +21 -0
  426. migrations/024_task_indexes_down.sql +7 -0
  427. migrations/025_audit_log.sql +17 -0
  428. migrations/026_agent_tokens.sql +20 -0
  429. migrations/027_teams_auth_phase_b.sql +35 -0
  430. migrations/028_learnings.sql +23 -0
  431. migrations/029_team_roles.sql +14 -0
  432. migrations/030_finder_pins.sql +10 -0
  433. migrations/031_pr_deploy_status.sql +9 -0
  434. migrations/032_task_reminders.sql +7 -0
  435. migrations/033_events_retry_count.sql +6 -0
  436. migrations/033_session_owner.sql +9 -0
  437. migrations/034_notifications.sql +38 -0
  438. migrations/035_shared_links.sql +15 -0
  439. migrations/036_session_index_upgrade.sql +29 -0
  440. migrations/037_pr_approval.sql +15 -0
  441. migrations/038_pr_submitted_by.sql +6 -0
  442. migrations/039_push_subscriptions.sql +17 -0
  443. migrations/040_semantic_search.sql +16 -0
  444. migrations/041_workspaces.sql +63 -0
  445. migrations/042_oidc_providers.sql +24 -0
  446. migrations/043_ci_checks.sql +31 -0
  447. migrations/044_agent_metrics.sql +30 -0
  448. migrations/045_documents_doc_type.sql +5 -0
  449. migrations/046_salience.sql +13 -0
  450. migrations/047_seed_missing_agents.sql +6 -0
  451. migrations/048_fix_agent_paths_roles.sql +5 -0
  452. migrations/049_agent_role_and_learnings_schema.sql +3 -0
  453. migrations/050_session_provider.sql +2 -0
  454. migrations/051_session_launch_profile.sql +4 -0
  455. migrations/052_session_theme_mode.sql +2 -0
  456. migrations/052_task_kind.sql +4 -0
  457. migrations/053_inbox_items.sql +31 -0
  458. migrations/054_inbox_triage_contract.sql +30 -0
  459. migrations/055_inbox_topic_treatment.sql +12 -0
  460. migrations/056_inbox_treatment_read_save.sql +57 -0
  461. migrations/057_session_theme_mode_backfill.sql +4 -0
  462. migrations/058_inbox_item_status_lifecycle.sql +13 -0
  463. migrations/059_inbox_tldr_and_source_scores.sql +18 -0
  464. migrations/060_newsletter.sql +16 -0
  465. migrations/061_inbox_redesign.sql +69 -0
  466. migrations/062_fix_inbox_sources_backfill.sql +37 -0
  467. migrations/063_task_completion_mode.sql +23 -0
  468. migrations/064_judge_mode_setting.sql +4 -0
  469. migrations/065_knowledge_graph_spike.sql +40 -0
  470. migrations/066_digest_ranking_inputs.sql +10 -0
  471. migrations/066_kg_artifact_nodes.sql +129 -0
  472. migrations/067_inbox_digest_selections.sql +28 -0
  473. migrations/067_kg_temporal.sql +53 -0
  474. migrations/068_inbox_digest_app_settings.sql +9 -0
  475. migrations/068_kg_touch_counter.sql +52 -0
  476. migrations/069_kg_doc_types.sql +117 -0
  477. migrations/070_digest_ranking_inputs_recovery.sql +3 -0
  478. migrations/071_inbox_digest_selections_recovery.sql +3 -0
  479. migrations/072_inbox_digest_app_settings_recovery.sql +3 -0
  480. migrations/073_kg_cross_project.sql +216 -0
  481. migrations/073_kg_cross_project_down.sql +77 -0
  482. migrations/074_kg_infra_types.sql +208 -0
  483. migrations/074_kg_infra_types_down.sql +80 -0
  484. migrations/075_kg_file_state_recovery.sql +35 -0
  485. migrations/075_kg_file_state_recovery_down.sql +5 -0
  486. migrations/076_kg_watcher_state.sql +33 -0
  487. migrations/076_kg_watcher_state_down.sql +3 -0
  488. migrations/077_kg_doc_types_extend.sql +226 -0
  489. migrations/077_kg_doc_types_extend_down.sql +80 -0
  490. migrations/078_kg_fts5.sql +102 -0
  491. migrations/078_kg_fts5_down.sql +14 -0
  492. migrations/079_kg_missing_indexes.sql +31 -0
  493. migrations/079_kg_missing_indexes_down.sql +10 -0
  494. migrations/080_kg_fts5_extended.sql +232 -0
  495. migrations/080_kg_fts5_extended_down.sql +25 -0
  496. migrations/081_kg_lens_indexes.sql +9 -0
  497. migrations/081_kg_lens_indexes_down.sql +3 -0
  498. migrations/082_kg_pins.sql +26 -0
  499. migrations/082_kg_pins_down.sql +14 -0
  500. migrations/083_kg_graph_nodes_degree.sql +20 -0
  501. migrations/083_kg_graph_nodes_degree_down.sql +15 -0
  502. migrations/084_drop_legacy_scheduler_tables.sql +58 -0
  503. migrations/084_drop_legacy_scheduler_tables_down.sql +112 -0
  504. migrations/085_kg_edge_resolves_to.sql +142 -0
  505. migrations/085_kg_edge_resolves_to_down.sql +66 -0
  506. migrations/086_project_status_updates_feed.sql +20 -0
  507. migrations/086_project_status_updates_feed_down.sql +36 -0
  508. migrations/087_session_metrics_dual.sql +50 -0
  509. migrations/087_session_metrics_dual_down.sql +21 -0
  510. migrations/088_rename_context_pct_legacy.sql +23 -0
  511. migrations/088_rename_context_pct_legacy_down.sql +8 -0
  512. migrations/089_session_metrics_equivalent_cost.sql +26 -0
  513. migrations/089_session_metrics_equivalent_cost_down.sql +11 -0
  514. migrations/090_kg_inbox_node_type.sql +26 -0
  515. migrations/090_kg_inbox_node_type_down.sql +20 -0
  516. migrations/091_kg_inbox_node_type_check.sql +265 -0
  517. migrations/091_kg_inbox_node_type_check_down.sql +129 -0
  518. migrations/092_sessions_activity_state_ts.sql +29 -0
  519. migrations/092_sessions_activity_state_ts_down.sql +14 -0
  520. migrations/093_sessions_activity_state_column.sql +29 -0
  521. migrations/093_sessions_activity_state_column_down.sql +10 -0
  522. migrations/094_ingest_pending.sql +55 -0
  523. migrations/094_ingest_pending_down.sql +15 -0
  524. migrations/095_kg_intent_first.sql +77 -0
  525. migrations/095_kg_intent_first_down.sql +25 -0
  526. migrations/096_kg_xlsx_artifact_prefix.sql +17 -0
  527. migrations/096_kg_xlsx_artifact_prefix_down.sql +11 -0
  528. migrations/097_ingest_change_history.sql +37 -0
  529. migrations/097_ingest_change_history_down.sql +13 -0
  530. migrations/098_kg_node_type_business.sql +254 -0
  531. migrations/098_kg_node_type_business_down.sql +195 -0
  532. migrations/099_kg_edges_restore_weight.sql +58 -0
  533. migrations/099_kg_edges_restore_weight_down.sql +12 -0
  534. migrations/100_kg_enriched_at.sql +25 -0
  535. migrations/100_kg_enriched_at_down.sql +12 -0
  536. migrations/101_local_llm_shadow_comparisons.sql +66 -0
  537. migrations/101_local_llm_shadow_comparisons_down.sql +15 -0
  538. migrations/102_promote_llm_costs.sql +69 -0
  539. migrations/102_promote_llm_costs_down.sql +19 -0
  540. migrations/103_ingest_skipped_log.sql +46 -0
  541. migrations/103_ingest_skipped_log_down.sql +15 -0
  542. migrations/120_docs_governance.sql +50 -0
  543. migrations/120_docs_governance_down.sql +11 -0
  544. migrations/121_notification_event_fk_cleanup.sql +21 -0
  545. migrations/121_notification_event_fk_cleanup_down.sql +10 -0
  546. migrations/122_docs_drift_history.sql +34 -0
  547. migrations/122_docs_drift_history_down.sql +15 -0
  548. migrations/123_ingest_parser_waiting_status.sql +69 -0
  549. migrations/123_ingest_parser_waiting_status_down.sql +69 -0
  550. migrations/124_heypocket_recordings.sql +63 -0
  551. migrations/124_heypocket_recordings_down.sql +13 -0
  552. migrations/125_kg_node_type_record.sql +219 -0
  553. migrations/125_kg_node_type_record_down.sql +205 -0
  554. migrations/126_ingest_terminal_upload_source_kind.sql +69 -0
  555. migrations/126_ingest_terminal_upload_source_kind_down.sql +69 -0
  556. migrations/127_brain_v1_substrate.sql +200 -0
  557. migrations/127_brain_v1_substrate_down.sql +32 -0
  558. migrations/128_brain_drift_signals.sql +157 -0
  559. migrations/128_brain_drift_signals_down.sql +23 -0
  560. migrations/129_brain_memory_operations.sql +232 -0
  561. migrations/129_brain_memory_operations_down.sql +27 -0
  562. migrations/130_brain_findings.sql +258 -0
  563. migrations/130_brain_findings_down.sql +29 -0
  564. migrations/132_kg_pr_modifies.sql +242 -0
  565. migrations/132_kg_pr_modifies_down.sql +99 -0
  566. migrations/133_brain_v1_2_direction_schema.sql +476 -0
  567. migrations/133_brain_v1_2_direction_schema_down.sql +273 -0
  568. migrations/134_brain_journal_narrative_polished.sql +8 -0
  569. migrations/134_brain_journal_narrative_polished_down.sql +6 -0
  570. migrations/135_kg_edges_provider.sql +21 -0
  571. migrations/136_documents_fts.sql +56 -0
  572. migrations/137_promote_llm_costs.sql +59 -0
  573. migrations/137_promote_llm_costs_down.sql +19 -0
  574. migrations/138_ingest_api_keys.sql +39 -0
  575. migrations/138_ingest_api_keys_down.sql +8 -0
  576. migrations/139_ingest_pending_ingress.sql +91 -0
  577. migrations/139_ingest_pending_ingress_down.sql +73 -0
  578. migrations/140_ingest_idempotency_quota.sql +45 -0
  579. migrations/140_ingest_idempotency_quota_down.sql +9 -0
  580. migrations/141_ingest_pending_metadata.sql +16 -0
  581. migrations/141_ingest_pending_metadata_down.sql +7 -0
  582. migrations/142_llm_function_config.sql +36 -0
  583. migrations/142_llm_function_config_down.sql +8 -0
  584. migrations/143_kg_code_embeddings.sql +25 -0
  585. migrations/143_kg_code_embeddings_down.sql +5 -0
  586. migrations/__init__.py +4 -0
  587. projects/_template/project.yaml +46 -0
@@ -0,0 +1,364 @@
1
+ """Codex JSONL metrics provider.
2
+
3
+ Codex CLI stores session transcripts under ~/.codex/sessions/YYYY/MM/DD/*.jsonl.
4
+ The JSONL includes a session_meta event with the stable session id and periodic
5
+ token_count events with current context and cumulative token usage.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import os
12
+ import re
13
+ from dataclasses import dataclass
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+ from typing import Iterable
17
+
18
+ from core.api.services.claude_metrics import SessionMetrics
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ CODEX_SESSIONS_DIR = Path.home() / ".codex" / "sessions"
23
+ _WORKSPACE_ROOT = os.environ.get("MARVIS_WORKSPACE_ROOT", str(Path.home() / "workspace"))
24
+ _PROC_ROOT = Path("/proc")
25
+ _CODEX_DETECT_GRACE_SECONDS = 5.0
26
+ _CODEX_DETECT_WINDOW_SECONDS = 60.0
27
+ _SESSION_ID_RE = re.compile(
28
+ r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$",
29
+ re.IGNORECASE,
30
+ )
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class CodexSessionMeta:
35
+ session_id: str
36
+ timestamp: datetime
37
+ cwd: str | None
38
+ path: Path
39
+
40
+
41
+ def _parse_timestamp(value: str | None) -> datetime | None:
42
+ if not value:
43
+ return None
44
+ try:
45
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
46
+ except ValueError:
47
+ return None
48
+ if parsed.tzinfo is None:
49
+ return parsed.replace(tzinfo=timezone.utc)
50
+ return parsed.astimezone(timezone.utc)
51
+
52
+
53
+ def _iter_session_files() -> Iterable[Path]:
54
+ if not CODEX_SESSIONS_DIR.is_dir():
55
+ return ()
56
+ return CODEX_SESSIONS_DIR.glob("**/*.jsonl")
57
+
58
+
59
+ def _session_id_from_path(path: Path) -> str | None:
60
+ match = _SESSION_ID_RE.search(path.stem)
61
+ return match.group(1).lower() if match else None
62
+
63
+
64
+ def _is_codex_session_path(path: Path) -> bool:
65
+ if path.suffix != ".jsonl":
66
+ return False
67
+ try:
68
+ path.resolve(strict=False).relative_to(
69
+ CODEX_SESSIONS_DIR.resolve(strict=False)
70
+ )
71
+ except (OSError, ValueError):
72
+ return False
73
+ return True
74
+
75
+
76
+ def _read_meta(path: Path) -> CodexSessionMeta | None:
77
+ try:
78
+ with path.open("r", encoding="utf-8", errors="replace") as f:
79
+ for line in f:
80
+ try:
81
+ obj = json.loads(line)
82
+ except json.JSONDecodeError:
83
+ continue
84
+ if obj.get("type") != "session_meta":
85
+ continue
86
+ payload = obj.get("payload") or {}
87
+ session_id = payload.get("id")
88
+ timestamp = _parse_timestamp(payload.get("timestamp"))
89
+ if not session_id or timestamp is None:
90
+ return None
91
+ return CodexSessionMeta(
92
+ session_id=session_id,
93
+ timestamp=timestamp,
94
+ cwd=payload.get("cwd"),
95
+ path=path,
96
+ )
97
+ except OSError:
98
+ return None
99
+ return None
100
+
101
+
102
+ def find_session_path(session_id: str) -> Path | None:
103
+ """Find the JSONL file for a Codex session id."""
104
+ if not session_id or not CODEX_SESSIONS_DIR.is_dir():
105
+ return None
106
+ for path in _iter_session_files():
107
+ if session_id in path.stem:
108
+ return path
109
+ for path in _iter_session_files():
110
+ meta = _read_meta(path)
111
+ if meta and meta.session_id == session_id:
112
+ return path
113
+ return None
114
+
115
+
116
+ def _iter_process_tree_pids(root_pid: int) -> Iterable[int]:
117
+ """Yield root_pid and descendants using Linux procfs child lists."""
118
+ seen: set[int] = set()
119
+ stack = [root_pid]
120
+ while stack:
121
+ pid = stack.pop()
122
+ if pid in seen or pid <= 0:
123
+ continue
124
+ seen.add(pid)
125
+ yield pid
126
+
127
+ children_path = _PROC_ROOT / str(pid) / "task" / str(pid) / "children"
128
+ try:
129
+ children = children_path.read_text(encoding="utf-8").split()
130
+ except OSError:
131
+ continue
132
+ for child in children:
133
+ try:
134
+ stack.append(int(child))
135
+ except ValueError:
136
+ continue
137
+
138
+
139
+ def _iter_open_session_files(pid: int) -> Iterable[Path]:
140
+ fd_dir = _PROC_ROOT / str(pid) / "fd"
141
+ try:
142
+ fds = list(fd_dir.iterdir())
143
+ except OSError:
144
+ return ()
145
+
146
+ paths: list[Path] = []
147
+ for fd_path in fds:
148
+ try:
149
+ target = fd_path.readlink()
150
+ except OSError:
151
+ continue
152
+ target_text = str(target)
153
+ if target_text.endswith(" (deleted)"):
154
+ target_text = target_text.removesuffix(" (deleted)")
155
+ target_path = Path(target_text)
156
+ if not target_path.is_absolute() or not _is_codex_session_path(target_path):
157
+ continue
158
+ paths.append(target_path)
159
+ return paths
160
+
161
+
162
+ def detect_codex_for_process(
163
+ pid: int | None,
164
+ already_linked: Iterable[str] = (),
165
+ ) -> str | None:
166
+ """Detect the Codex session id from a live process tree's open JSONL fd.
167
+
168
+ Resumed Codex sessions keep writing to their original JSONL, whose
169
+ session_meta timestamp can be days older than the tmux pane. Inspecting the
170
+ live process fds is the stable correlation path for those sessions.
171
+ """
172
+ if pid is None or pid <= 0:
173
+ return None
174
+
175
+ linked = set(already_linked)
176
+ seen_paths: set[Path] = set()
177
+ candidates: list[tuple[float, str]] = []
178
+ for process_pid in _iter_process_tree_pids(pid):
179
+ for path in _iter_open_session_files(process_pid):
180
+ try:
181
+ canonical = path.resolve(strict=False)
182
+ except OSError:
183
+ canonical = path
184
+ if canonical in seen_paths:
185
+ continue
186
+ seen_paths.add(canonical)
187
+
188
+ meta = _read_meta(path)
189
+ session_id = meta.session_id if meta else _session_id_from_path(path)
190
+ if not session_id or session_id in linked:
191
+ continue
192
+ try:
193
+ mtime = path.stat().st_mtime
194
+ except OSError:
195
+ mtime = 0.0
196
+ candidates.append((mtime, session_id))
197
+
198
+ candidates.sort(reverse=True)
199
+ return candidates[0][1] if candidates else None
200
+
201
+
202
+ def detect_codex_for_session(
203
+ pane_start_epoch: float | None,
204
+ cwd: str | None = None,
205
+ already_linked: Iterable[str] = (),
206
+ ) -> str | None:
207
+ """Detect the Codex session id that started shortly after a tmux pane.
208
+
209
+ Codex JSONL files do not currently record the tmux session name. The safest
210
+ stable correlation available is the launch timestamp: Console-created Codex
211
+ sessions write session_meta within a few seconds of tmux creation. The match
212
+ window is intentionally bounded to avoid stealing a later unrelated Codex
213
+ session from an idle pane that never received a prompt.
214
+ """
215
+ if pane_start_epoch is None:
216
+ return None
217
+ linked = set(already_linked)
218
+ lower = pane_start_epoch - _CODEX_DETECT_GRACE_SECONDS
219
+ upper = pane_start_epoch + _CODEX_DETECT_WINDOW_SECONDS
220
+ candidates: list[CodexSessionMeta] = []
221
+ for path in _iter_session_files():
222
+ meta = _read_meta(path)
223
+ if not meta or meta.session_id in linked:
224
+ continue
225
+ started = meta.timestamp.timestamp()
226
+ if lower <= started <= upper:
227
+ # Codex currently launches from the shared workspace. If a future
228
+ # version records a provider-specific cwd, prefer exact matches but
229
+ # do not reject workspace-root sessions.
230
+ if (
231
+ cwd
232
+ and meta.cwd
233
+ and meta.cwd != cwd
234
+ and meta.cwd != _WORKSPACE_ROOT
235
+ ):
236
+ continue
237
+ candidates.append(meta)
238
+ candidates.sort(key=lambda item: item.timestamp)
239
+ return candidates[0].session_id if candidates else None
240
+
241
+
242
+ def parse_session_file(path: Path) -> SessionMetrics | None:
243
+ if not path.exists():
244
+ return None
245
+
246
+ session_id: str | None = None
247
+ model: str | None = None
248
+ first_ts: str | None = None
249
+ last_ts: str | None = None
250
+ message_count = 0
251
+ last_token_info: dict | None = None
252
+
253
+ try:
254
+ with path.open("r", encoding="utf-8", errors="replace") as f:
255
+ for line in f:
256
+ try:
257
+ obj = json.loads(line)
258
+ except json.JSONDecodeError:
259
+ continue
260
+
261
+ obj_ts = obj.get("timestamp")
262
+ if obj_ts and first_ts is None:
263
+ first_ts = obj_ts
264
+ if obj_ts:
265
+ last_ts = obj_ts
266
+
267
+ obj_type = obj.get("type")
268
+ payload = obj.get("payload") or {}
269
+ if obj_type == "session_meta":
270
+ session_id = payload.get("id") or session_id
271
+ first_ts = payload.get("timestamp") or first_ts
272
+ elif obj_type == "turn_context":
273
+ model = payload.get("model") or model
274
+ elif obj_type == "event_msg":
275
+ event_type = payload.get("type")
276
+ if event_type == "agent_message":
277
+ message_count += 1
278
+ elif event_type == "token_count":
279
+ info = payload.get("info")
280
+ if isinstance(info, dict):
281
+ last_token_info = info
282
+ except OSError:
283
+ return None
284
+
285
+ if not session_id:
286
+ meta = _read_meta(path)
287
+ session_id = (
288
+ meta.session_id if meta else (_session_id_from_path(path) or path.stem)
289
+ )
290
+
291
+ if not last_token_info:
292
+ return SessionMetrics(
293
+ conversation_id=session_id,
294
+ model=model,
295
+ message_count=message_count,
296
+ first_timestamp=first_ts,
297
+ last_timestamp=last_ts,
298
+ )
299
+
300
+ total_usage = last_token_info.get("total_token_usage") or {}
301
+ last_usage = last_token_info.get("last_token_usage") or {}
302
+ input_tokens = int(total_usage.get("input_tokens") or 0)
303
+ output_tokens = int(total_usage.get("output_tokens") or 0)
304
+ cache_read_tokens = int(total_usage.get("cached_input_tokens") or 0)
305
+ reasoning_tokens = int(total_usage.get("reasoning_output_tokens") or 0)
306
+
307
+ window = int(last_token_info.get("model_context_window") or 0)
308
+ last_total = int(last_usage.get("total_tokens") or 0)
309
+ if window > 0 and last_total > 0:
310
+ context_pct = round(min((last_total / window) * 100, 100.0), 1)
311
+ else:
312
+ context_pct = 0.0
313
+
314
+ duration_minutes = 0.0
315
+ first_dt = _parse_timestamp(first_ts)
316
+ last_dt = _parse_timestamp(last_ts)
317
+ if first_dt and last_dt and last_dt >= first_dt:
318
+ duration_minutes = round((last_dt - first_dt).total_seconds() / 60, 1)
319
+
320
+ return SessionMetrics(
321
+ conversation_id=session_id,
322
+ model=model,
323
+ context_pct=context_pct,
324
+ cost_usd=0.0,
325
+ message_count=message_count,
326
+ input_tokens=input_tokens,
327
+ output_tokens=output_tokens,
328
+ cache_read_tokens=cache_read_tokens,
329
+ cache_write_tokens=0,
330
+ first_timestamp=first_ts,
331
+ last_timestamp=last_ts,
332
+ duration_minutes=duration_minutes,
333
+ context_pct_real=context_pct,
334
+ context_pct_scaled=None,
335
+ cost_conversation_usd=0.0,
336
+ cost_session_usd=None,
337
+ reasoning_tokens=reasoning_tokens,
338
+ working_seconds_msg=None,
339
+ pricing_version=None,
340
+ cost_conversation_equivalent_usd=None,
341
+ cost_session_equivalent_usd=None,
342
+ cost_equivalent_pricing_version=None,
343
+ )
344
+
345
+
346
+ class CodexMetricsProvider:
347
+ name = "codex"
348
+
349
+ def parse_session(
350
+ self,
351
+ session_id: str,
352
+ cwd: str | None = None,
353
+ ) -> SessionMetrics | None:
354
+ _ = cwd
355
+ path = find_session_path(session_id)
356
+ return parse_session_file(path) if path else None
357
+
358
+ def get_last_context_pct(
359
+ self,
360
+ session_id: str,
361
+ cwd: str | None = None,
362
+ ) -> float | None:
363
+ metrics = self.parse_session(session_id, cwd)
364
+ return metrics.context_pct_real if metrics else None
@@ -0,0 +1,102 @@
1
+ # api/services/conversation_reader.py
2
+ # v2.1.0 - 2026-03-04 - Extract AskUserQuestion tool_use blocks for DevX classifier
3
+
4
+ import asyncio
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+
9
+ CLAUDE_PROJECTS_PATH = Path.home() / ".claude" / "projects"
10
+ _UUID_RE = re.compile(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', re.IGNORECASE)
11
+
12
+
13
+ def _validate_conversation_id(conversation_id: str) -> bool:
14
+ """Validates that conversation_id is a safe UUID-like string (no path traversal)."""
15
+ return bool(_UUID_RE.match(conversation_id))
16
+
17
+
18
+ def _find_jsonl_path(conversation_id: str) -> Path | None:
19
+ """Finds JSONL path safely using direct path construction (no rglob)."""
20
+ if not CLAUDE_PROJECTS_PATH.exists():
21
+ return None
22
+ for project_dir in CLAUDE_PROJECTS_PATH.iterdir():
23
+ if not project_dir.is_dir():
24
+ continue
25
+ candidate = project_dir / f"{conversation_id}.jsonl"
26
+ if candidate.exists() and CLAUDE_PROJECTS_PATH in candidate.resolve().parents:
27
+ return candidate
28
+ return None
29
+
30
+
31
+ def _read_conversation_sync(
32
+ jsonl_path: Path,
33
+ limit: int = 20,
34
+ role: str | None = None,
35
+ since: str | None = None,
36
+ ) -> list[dict]:
37
+ """Sync: reads JSONL tail, filters, returns last N messages."""
38
+ messages = []
39
+ file_size = jsonl_path.stat().st_size
40
+ tail_bytes = min(file_size, 524288) # 512KB tail
41
+
42
+ with jsonl_path.open('rb') as f:
43
+ if tail_bytes < file_size:
44
+ f.seek(file_size - tail_bytes)
45
+ f.readline() # skip partial first line
46
+ raw_lines = f.readlines()
47
+
48
+ for raw_line in raw_lines:
49
+ try:
50
+ line = raw_line.decode('utf-8', errors='replace')
51
+ obj = json.loads(line)
52
+ except (json.JSONDecodeError, UnicodeDecodeError):
53
+ continue
54
+ if obj.get("type") not in ("user", "assistant"):
55
+ continue
56
+ if role and obj.get("type") != role:
57
+ continue
58
+ if since and obj.get("timestamp", "") < since:
59
+ continue
60
+ msg = obj.get("message", {})
61
+ content = msg.get("content", "")
62
+ text = ""
63
+ if isinstance(content, list):
64
+ for c in content:
65
+ if isinstance(c, dict) and c.get("type") == "text":
66
+ text += c.get("text", "")
67
+ elif isinstance(c, dict) and c.get("type") == "tool_use" and c.get("name") == "AskUserQuestion":
68
+ for q in c.get("input", {}).get("questions", []):
69
+ question = q.get("question", "")
70
+ opts = q.get("options", [])
71
+ opt_lines = [f"{i}. {o.get('label', '')}" for i, o in enumerate(opts, 1)]
72
+ text += f"[AskUserQuestion] {question}\n" + "\n".join(opt_lines) + "\n"
73
+ else:
74
+ text = str(content)
75
+ messages.append({
76
+ "role": obj.get("type"),
77
+ "text": text[:2000],
78
+ "timestamp": obj.get("timestamp"),
79
+ })
80
+ return messages[-limit:]
81
+
82
+
83
+ async def read_conversation(
84
+ conversation_id: str,
85
+ limit: int = 20,
86
+ role: str | None = None,
87
+ since: str | None = None,
88
+ ) -> list[dict] | None:
89
+ """Async wrapper: finds JSONL file safely, reads conversation messages."""
90
+ if not _validate_conversation_id(conversation_id):
91
+ raise ValueError(f"Invalid conversation_id format: {conversation_id!r}")
92
+ jsonl_path = _find_jsonl_path(conversation_id)
93
+ if jsonl_path is None:
94
+ return None
95
+ loop = asyncio.get_running_loop()
96
+ try:
97
+ return await asyncio.wait_for(
98
+ loop.run_in_executor(None, _read_conversation_sync, jsonl_path, limit, role, since),
99
+ timeout=30.0,
100
+ )
101
+ except TimeoutError:
102
+ return []