code-context-control 2.37.0__tar.gz → 2.38.1__tar.gz

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 (218) hide show
  1. {code_context_control-2.37.0/code_context_control.egg-info → code_context_control-2.38.1}/PKG-INFO +5 -1
  2. {code_context_control-2.37.0 → code_context_control-2.38.1}/README.md +4 -0
  3. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/c3.py +1 -1
  4. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/guide/oracle.html +1 -0
  5. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/mcp_server.py +23 -8
  6. {code_context_control-2.37.0 → code_context_control-2.38.1/code_context_control.egg-info}/PKG-INFO +5 -1
  7. {code_context_control-2.37.0 → code_context_control-2.38.1}/code_context_control.egg-info/SOURCES.txt +3 -0
  8. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/oracle.html +108 -0
  9. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/oracle_server.py +36 -1
  10. code_context_control-2.38.1/oracle/services/activity_reporter.py +256 -0
  11. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/chat_engine.py +18 -0
  12. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/tool_registry.py +22 -0
  13. {code_context_control-2.37.0 → code_context_control-2.38.1}/pyproject.toml +1 -1
  14. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/embedding_index.py +26 -2
  15. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/vector_store.py +28 -2
  16. code_context_control-2.38.1/tests/test_activity_reporter.py +130 -0
  17. code_context_control-2.38.1/tests/test_lazy_store_init.py +77 -0
  18. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_oracle_discovery_api.py +11 -0
  19. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_tool_registry.py +9 -0
  20. {code_context_control-2.37.0 → code_context_control-2.38.1}/LICENSE +0 -0
  21. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/__init__.py +0 -0
  22. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/_hook_utils.py +0 -0
  23. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/commands/__init__.py +0 -0
  24. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/commands/common.py +0 -0
  25. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/commands/parser.py +0 -0
  26. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/docs.html +0 -0
  27. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/edits.html +0 -0
  28. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/guide/bitbucket.html +0 -0
  29. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/guide/getting-started.html +0 -0
  30. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/guide/index.html +0 -0
  31. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/guide/shared.css +0 -0
  32. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/guide/tools.html +0 -0
  33. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/guide/workflow.html +0 -0
  34. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hook_auto_snapshot.py +0 -0
  35. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hook_c3_signal.py +0 -0
  36. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hook_c3read.py +0 -0
  37. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hook_edit_ledger.py +0 -0
  38. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hook_edit_unlock.py +0 -0
  39. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hook_filter.py +0 -0
  40. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hook_ghost_files.py +0 -0
  41. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hook_pretool_enforce.py +0 -0
  42. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hook_read.py +0 -0
  43. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hook_session_stats.py +0 -0
  44. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hook_terse_advisor.py +0 -0
  45. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hub.html +0 -0
  46. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/hub_server.py +0 -0
  47. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/mcp_proxy.py +0 -0
  48. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/server.py +0 -0
  49. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/__init__.py +0 -0
  50. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/_helpers.py +0 -0
  51. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/agent.py +0 -0
  52. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/bitbucket.py +0 -0
  53. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/compress.py +0 -0
  54. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/delegate.py +0 -0
  55. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/edit.py +0 -0
  56. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/edits.py +0 -0
  57. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/filter.py +0 -0
  58. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/impact.py +0 -0
  59. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/memory.py +0 -0
  60. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/project.py +0 -0
  61. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/read.py +0 -0
  62. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/search.py +0 -0
  63. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/session.py +0 -0
  64. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/shell.py +0 -0
  65. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/status.py +0 -0
  66. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/tools/validate.py +0 -0
  67. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/api.js +0 -0
  68. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/app.js +0 -0
  69. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/components/bitbucket.js +0 -0
  70. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/components/chat.js +0 -0
  71. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/components/dashboard.js +0 -0
  72. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/components/edits.js +0 -0
  73. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/components/instructions.js +0 -0
  74. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/components/memory.js +0 -0
  75. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/components/sessions.js +0 -0
  76. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/components/settings.js +0 -0
  77. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/components/sidebar.js +0 -0
  78. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/icons.js +0 -0
  79. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/shared.js +0 -0
  80. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui/theme.js +0 -0
  81. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui.html +0 -0
  82. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui_legacy.html +0 -0
  83. {code_context_control-2.37.0 → code_context_control-2.38.1}/cli/ui_nano.html +0 -0
  84. {code_context_control-2.37.0 → code_context_control-2.38.1}/code_context_control.egg-info/dependency_links.txt +0 -0
  85. {code_context_control-2.37.0 → code_context_control-2.38.1}/code_context_control.egg-info/entry_points.txt +0 -0
  86. {code_context_control-2.37.0 → code_context_control-2.38.1}/code_context_control.egg-info/requires.txt +0 -0
  87. {code_context_control-2.37.0 → code_context_control-2.38.1}/code_context_control.egg-info/top_level.txt +0 -0
  88. {code_context_control-2.37.0 → code_context_control-2.38.1}/core/__init__.py +0 -0
  89. {code_context_control-2.37.0 → code_context_control-2.38.1}/core/config.py +0 -0
  90. {code_context_control-2.37.0 → code_context_control-2.38.1}/core/ide.py +0 -0
  91. {code_context_control-2.37.0 → code_context_control-2.38.1}/core/mcp_toml.py +0 -0
  92. {code_context_control-2.37.0 → code_context_control-2.38.1}/core/web_security.py +0 -0
  93. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/__init__.py +0 -0
  94. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/config.py +0 -0
  95. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/mcp_oracle.py +0 -0
  96. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/__init__.py +0 -0
  97. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/api_auth.py +0 -0
  98. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/c3_bridge.py +0 -0
  99. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/chat_store.py +0 -0
  100. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/cross_memory.py +0 -0
  101. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/federated_graph.py +0 -0
  102. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/health_checker.py +0 -0
  103. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/insight_engine.py +0 -0
  104. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/memory_reader.py +0 -0
  105. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/memory_writer.py +0 -0
  106. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/ollama_bridge.py +0 -0
  107. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/project_scanner.py +0 -0
  108. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/review_agent.py +0 -0
  109. {code_context_control-2.37.0 → code_context_control-2.38.1}/oracle/services/tool_executor.py +0 -0
  110. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/__init__.py +0 -0
  111. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/activity_log.py +0 -0
  112. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/agent_base.py +0 -0
  113. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/agents.py +0 -0
  114. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/auto_memory.py +0 -0
  115. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/bench/__init__.py +0 -0
  116. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/bench/external/__init__.py +0 -0
  117. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/bench/external/aider_polyglot.py +0 -0
  118. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/bench/external/swe_bench.py +0 -0
  119. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/benchmark_dashboard.py +0 -0
  120. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/bitbucket_client.py +0 -0
  121. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/bitbucket_credentials.py +0 -0
  122. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/claude_md.py +0 -0
  123. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/compressor.py +0 -0
  124. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/context_snapshot.py +0 -0
  125. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/conversation_store.py +0 -0
  126. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/doc_index.py +0 -0
  127. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/e2e_benchmark.py +0 -0
  128. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/e2e_evaluator.py +0 -0
  129. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/e2e_tasks.py +0 -0
  130. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/edit_ledger.py +0 -0
  131. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/error_reporting.py +0 -0
  132. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/file_memory.py +0 -0
  133. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/git_context.py +0 -0
  134. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/hub_service.py +0 -0
  135. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/indexer.py +0 -0
  136. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/memory.py +0 -0
  137. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/memory_consolidator.py +0 -0
  138. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/memory_graph.py +0 -0
  139. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/memory_grounder.py +0 -0
  140. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/memory_scorer.py +0 -0
  141. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/metrics.py +0 -0
  142. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/notifications.py +0 -0
  143. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/ollama_client.py +0 -0
  144. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/output_filter.py +0 -0
  145. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/parser.py +0 -0
  146. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/project_manager.py +0 -0
  147. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/project_runtime.py +0 -0
  148. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/protocol.py +0 -0
  149. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/proxy_state.py +0 -0
  150. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/retrieval_broker.py +0 -0
  151. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/router.py +0 -0
  152. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/runtime.py +0 -0
  153. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/session_benchmark.py +0 -0
  154. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/session_manager.py +0 -0
  155. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/session_preloader.py +0 -0
  156. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/text_index.py +0 -0
  157. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/tool_classifier.py +0 -0
  158. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/transcript_index.py +0 -0
  159. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/validation_cache.py +0 -0
  160. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/version_tracker.py +0 -0
  161. {code_context_control-2.37.0 → code_context_control-2.38.1}/services/watcher.py +0 -0
  162. {code_context_control-2.37.0 → code_context_control-2.38.1}/setup.cfg +0 -0
  163. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_aider_polyglot.py +0 -0
  164. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_bitbucket_cli_smoke.py +0 -0
  165. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_bitbucket_client.py +0 -0
  166. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_bitbucket_credentials.py +0 -0
  167. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_bitbucket_tool.py +0 -0
  168. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_c3_shell.py +0 -0
  169. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_claude_md_merge.py +0 -0
  170. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_cli_smoke.py +0 -0
  171. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_e2e_benchmark.py +0 -0
  172. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_edit_normalization.py +0 -0
  173. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_enforcement_flip.py +0 -0
  174. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_federated_graph.py +0 -0
  175. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_ghost_files.py +0 -0
  176. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_git_branch_awareness.py +0 -0
  177. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_hub_server_smoke.py +0 -0
  178. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_install_mcp_entrypoint.py +0 -0
  179. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_mcp_host_guard.py +0 -0
  180. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_mcp_server_smoke.py +0 -0
  181. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_mcp_toml.py +0 -0
  182. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_memory_graph_api.py +0 -0
  183. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_memory_system.py +0 -0
  184. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_notification_discipline.py +0 -0
  185. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_oracle_api_auth.py +0 -0
  186. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_oracle_apikey_api.py +0 -0
  187. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_output_filter.py +0 -0
  188. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_permissions.py +0 -0
  189. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_project_manager.py +0 -0
  190. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_project_manager_merge.py +0 -0
  191. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_project_tool.py +0 -0
  192. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_read_coercion.py +0 -0
  193. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_session_benchmark.py +0 -0
  194. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_session_budget.py +0 -0
  195. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_shell_robustness.py +0 -0
  196. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_swe_bench.py +0 -0
  197. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_upgrade_and_version.py +0 -0
  198. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_validate.py +0 -0
  199. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_web_security.py +0 -0
  200. {code_context_control-2.37.0 → code_context_control-2.38.1}/tests/test_windows_reliability.py +0 -0
  201. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/__init__.py +0 -0
  202. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/backend.py +0 -0
  203. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/main.py +0 -0
  204. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/__init__.py +0 -0
  205. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/benchmark_view.py +0 -0
  206. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/claudemd_view.py +0 -0
  207. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/compress_view.py +0 -0
  208. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/index_view.py +0 -0
  209. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/init_view.py +0 -0
  210. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/mcp_view.py +0 -0
  211. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/optimize_view.py +0 -0
  212. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/pipe_view.py +0 -0
  213. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/projects_view.py +0 -0
  214. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/search_view.py +0 -0
  215. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/session_view.py +0 -0
  216. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/stats.py +0 -0
  217. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/screens/ui_view.py +0 -0
  218. {code_context_control-2.37.0 → code_context_control-2.38.1}/tui/theme.tcss +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.37.0
3
+ Version: 2.38.1
4
4
  Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
5
5
  Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -334,6 +334,10 @@ Only **read** and **safe-action** tools are exposed (no code editing); requests
334
334
  default. Generate, rotate, and copy the token from the dashboard's **Settings →
335
335
  Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).
336
336
 
337
+ As of v2.38.0 the Oracle also reports a **cross-project activity digest** — sessions,
338
+ tool calls, edits, git mutations, and token/cost for a day — via the `activity_report`
339
+ discovery tool, the `GET /api/activity/digest` endpoint, and the dashboard's **Activity** tab.
340
+
337
341
  ---
338
342
 
339
343
  ## Tiered local AI (optional)
@@ -272,6 +272,10 @@ Only **read** and **safe-action** tools are exposed (no code editing); requests
272
272
  default. Generate, rotate, and copy the token from the dashboard's **Settings →
273
273
  Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).
274
274
 
275
+ As of v2.38.0 the Oracle also reports a **cross-project activity digest** — sessions,
276
+ tool calls, edits, git mutations, and token/cost for a day — via the `activity_report`
277
+ discovery tool, the `GET /api/activity/digest` endpoint, and the dashboard's **Activity** tab.
278
+
275
279
  ---
276
280
 
277
281
  ## Tiered local AI (optional)
@@ -85,7 +85,7 @@ console = Console() if HAS_RICH else None
85
85
  # Config
86
86
  CONFIG_DIR = ".c3"
87
87
  CONFIG_FILE = ".c3/config.json"
88
- __version__ = "2.37.0"
88
+ __version__ = "2.38.1"
89
89
 
90
90
 
91
91
  def _command_deps() -> CommandDeps:
@@ -269,6 +269,7 @@ curl -X POST \
269
269
  <tr><td class="grp-read"><code>c3_status</code></td> <td>Project health / budget / sessions overview</td></tr>
270
270
  <tr><td class="grp-read"><code>c3_memory_query</code></td> <td>Read-only memory query (recall/list/score/graph/trends)</td></tr>
271
271
  <tr><td class="grp-read"><code>c3_edits</code> / <code>c3_edits_cross</code></td><td>Query the edit ledger (one / all projects)</td></tr>
272
+ <tr><td class="grp-read"><code>activity_report</code></td><td>Cross-project daily digest: sessions, tool calls, edits, git mutations, token/cost (optional LLM narration)</td></tr>
272
273
  </tbody>
273
274
  </table>
274
275
 
@@ -120,12 +120,19 @@ async def lifespan(server):
120
120
  services.indexer.build_index()
121
121
  except Exception:
122
122
  pass
123
- # After code index is built, build embedding index
124
- if services.embedding_index and services.embedding_index.ready:
123
+ # After code index is built, build embedding index. build() lazily
124
+ # inits its chromadb/ollama backends, kept off the handshake path.
125
+ if services.embedding_index:
125
126
  try:
126
127
  services.embedding_index.build(services.indexer)
127
128
  except Exception:
128
129
  pass
130
+ # Warm SLTM vector store so the first memory call isn't slow.
131
+ if services.vector_store:
132
+ try:
133
+ services.vector_store.warm()
134
+ except Exception:
135
+ pass
129
136
  # Build doc index for Local RAG Pipeline
130
137
  if services.doc_index:
131
138
  try:
@@ -136,15 +143,23 @@ async def lifespan(server):
136
143
  threading.Thread(target=_bg_build, daemon=True, name="c3-initial-index").start()
137
144
  else:
138
145
  services.indexer._load_index()
139
- # Build/update embedding index in background
140
- if services.embedding_index and services.embedding_index.ready:
146
+ # Build/update embedding index + warm SLTM in background. Deferred off
147
+ # the handshake path: build()/warm() lazily init the heavy backends, so
148
+ # this must NOT gate on .ready synchronously here.
149
+ if services.embedding_index or services.vector_store:
141
150
  import threading
142
151
 
143
152
  def _bg_embed():
144
- try:
145
- services.embedding_index.build(services.indexer)
146
- except Exception:
147
- pass
153
+ if services.embedding_index:
154
+ try:
155
+ services.embedding_index.build(services.indexer)
156
+ except Exception:
157
+ pass
158
+ if services.vector_store:
159
+ try:
160
+ services.vector_store.warm()
161
+ except Exception:
162
+ pass
148
163
 
149
164
  threading.Thread(target=_bg_embed, daemon=True, name="c3-embed-index").start()
150
165
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.37.0
3
+ Version: 2.38.1
4
4
  Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer.
5
5
  Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -334,6 +334,10 @@ Only **read** and **safe-action** tools are exposed (no code editing); requests
334
334
  default. Generate, rotate, and copy the token from the dashboard's **Settings →
335
335
  Discovery API** tab. See the [Oracle Discovery API guide](oracle-guide/discovery-api.md).
336
336
 
337
+ As of v2.38.0 the Oracle also reports a **cross-project activity digest** — sessions,
338
+ tool calls, edits, git mutations, and token/cost for a day — via the `activity_report`
339
+ discovery tool, the `GET /api/activity/digest` endpoint, and the dashboard's **Activity** tab.
340
+
337
341
  ---
338
342
 
339
343
  ## Tiered local AI (optional)
@@ -84,6 +84,7 @@ oracle/mcp_oracle.py
84
84
  oracle/oracle.html
85
85
  oracle/oracle_server.py
86
86
  oracle/services/__init__.py
87
+ oracle/services/activity_reporter.py
87
88
  oracle/services/api_auth.py
88
89
  oracle/services/c3_bridge.py
89
90
  oracle/services/chat_engine.py
@@ -153,6 +154,7 @@ services/bench/__init__.py
153
154
  services/bench/external/__init__.py
154
155
  services/bench/external/aider_polyglot.py
155
156
  services/bench/external/swe_bench.py
157
+ tests/test_activity_reporter.py
156
158
  tests/test_aider_polyglot.py
157
159
  tests/test_bitbucket_cli_smoke.py
158
160
  tests/test_bitbucket_client.py
@@ -169,6 +171,7 @@ tests/test_ghost_files.py
169
171
  tests/test_git_branch_awareness.py
170
172
  tests/test_hub_server_smoke.py
171
173
  tests/test_install_mcp_entrypoint.py
174
+ tests/test_lazy_store_init.py
172
175
  tests/test_mcp_host_guard.py
173
176
  tests/test_mcp_server_smoke.py
174
177
  tests/test_mcp_toml.py
@@ -841,6 +841,7 @@
841
841
  <div class="tab active" data-tab="chat">Chat</div>
842
842
  <div class="tab" data-tab="projects">Projects</div>
843
843
  <div class="tab" data-tab="insights">Insights</div>
844
+ <div class="tab" data-tab="activity">Activity</div>
844
845
  <div class="tab" data-tab="crossgraph">Cross-Graph</div>
845
846
  <div class="tab" data-tab="suggestions">Suggestions</div>
846
847
  <div class="tab" data-tab="agents">Team / Agents</div>
@@ -975,6 +976,27 @@
975
976
  <div id="insightsEmpty" class="empty" style="display:none">No insights yet. Click "Generate" with 2+ projects.</div>
976
977
  </div>
977
978
 
979
+ <!-- ── Activity ── -->
980
+ <div class="panel" id="panel-activity">
981
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:10px">
982
+ <div>
983
+ <h2 style="font-size:16px;font-weight:600">Activity Digest</h2>
984
+ <p style="font-size:12px;color:var(--text2);margin-top:4px">Cross-project sessions, tool calls, edits, git mutations and token/cost for one day (UTC).</p>
985
+ </div>
986
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
987
+ <input type="date" id="actDate" style="padding:4px 6px" onchange="loadActivity()">
988
+ <label style="font-size:11px;color:var(--text2);display:flex;align-items:center;gap:6px">
989
+ <input type="checkbox" id="actNarrate"> Narrate
990
+ </label>
991
+ <button class="btn btn-primary btn-sm" id="btnLoadActivity" onclick="loadActivity()">Refresh</button>
992
+ </div>
993
+ </div>
994
+ <div id="actSummary" style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:14px"></div>
995
+ <div id="actNarrative" class="card" style="display:none;margin-bottom:14px"></div>
996
+ <div id="actProjects"></div>
997
+ <div id="actEmpty" class="empty" style="display:none">No activity recorded for this day.</div>
998
+ </div>
999
+
978
1000
  <!-- ── Cross-Graph ── -->
979
1001
  <div class="panel" id="panel-crossgraph">
980
1002
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:10px">
@@ -1279,6 +1301,9 @@ document.querySelectorAll('.tab').forEach(tab => {
1279
1301
  } else {
1280
1302
  mainEl.classList.remove('wide');
1281
1303
  }
1304
+ if (tab.dataset.tab === 'activity' && !window._actLoaded) {
1305
+ loadActivity(); window._actLoaded = true;
1306
+ }
1282
1307
  });
1283
1308
  });
1284
1309
 
@@ -1899,6 +1924,89 @@ async function dismissInsight(id) {
1899
1924
  }, { successMsg: 'Insight dismissed', silent: false });
1900
1925
  }
1901
1926
 
1927
+ // ═══════════════════════════════════════════════════════════
1928
+ // ── Activity Digest ──
1929
+ // ═══════════════════════════════════════════════════════════
1930
+ async function loadActivity() {
1931
+ const dateEl = document.getElementById('actDate');
1932
+ if (dateEl && !dateEl.value) dateEl.value = new Date().toISOString().slice(0, 10);
1933
+ const date = dateEl ? dateEl.value : '';
1934
+ const narrate = document.getElementById('actNarrate')?.checked ? 'true' : 'false';
1935
+ const btn = document.getElementById('btnLoadActivity');
1936
+ if (btn) btn.disabled = true;
1937
+ try {
1938
+ const qs = new URLSearchParams({ date, narrate }).toString();
1939
+ renderActivity(await api('/api/activity/digest?' + qs));
1940
+ } catch (e) {
1941
+ renderActivity({ error: (e && e.message) || 'Failed to load activity.' });
1942
+ } finally {
1943
+ if (btn) btn.disabled = false;
1944
+ }
1945
+ }
1946
+
1947
+ function _actCard(label, value) {
1948
+ return `<div class="card" style="flex:1;min-width:110px;text-align:center">
1949
+ <div style="font-size:22px;font-weight:700">${value}</div>
1950
+ <div style="font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:.04em">${label}</div>
1951
+ </div>`;
1952
+ }
1953
+
1954
+ function renderActivity(data) {
1955
+ const summary = document.getElementById('actSummary');
1956
+ const projects = document.getElementById('actProjects');
1957
+ const empty = document.getElementById('actEmpty');
1958
+ const narrEl = document.getElementById('actNarrative');
1959
+ if (!data || data.error) {
1960
+ summary.innerHTML = ''; projects.innerHTML = ''; narrEl.style.display = 'none';
1961
+ empty.style.display = ''; empty.textContent = (data && data.error) || 'Failed to load activity.';
1962
+ return;
1963
+ }
1964
+ const t = data.totals || {};
1965
+ summary.innerHTML = [
1966
+ _actCard('Projects', t.projects_active || 0),
1967
+ _actCard('Sessions', t.sessions || 0),
1968
+ _actCard('Tool Calls', t.tool_calls || 0),
1969
+ _actCard('Edits', t.edits || 0),
1970
+ _actCard('Git', t.git_mutations || 0),
1971
+ _actCard('Cost $', (t.cost_usd || 0).toFixed(2)),
1972
+ ].join('');
1973
+
1974
+ if (data.narrative) {
1975
+ narrEl.style.display = '';
1976
+ narrEl.innerHTML = `<div style="font-size:11px;color:var(--text2);text-transform:uppercase;margin-bottom:6px">${esc(data.window && data.window.label || '')}</div>
1977
+ <p style="font-size:13px;line-height:1.6;white-space:pre-wrap">${esc(data.narrative)}</p>`;
1978
+ } else {
1979
+ narrEl.style.display = 'none';
1980
+ }
1981
+
1982
+ const rows = data.projects || [];
1983
+ if (!rows.length) {
1984
+ projects.innerHTML = ''; empty.style.display = '';
1985
+ empty.textContent = 'No activity recorded for ' + (data.window && data.window.label || 'this day') + '.';
1986
+ return;
1987
+ }
1988
+ empty.style.display = 'none';
1989
+ projects.innerHTML = `
1990
+ <table style="width:100%;border-collapse:collapse;font-size:13px">
1991
+ <thead><tr style="text-align:left;color:var(--text2);font-size:11px;text-transform:uppercase">
1992
+ <th style="padding:6px">Project</th><th style="padding:6px">Sessions</th>
1993
+ <th style="padding:6px">Tools</th><th style="padding:6px">Edits</th>
1994
+ <th style="padding:6px">Git</th><th style="padding:6px">Tokens</th>
1995
+ <th style="padding:6px">Cost $</th><th style="padding:6px">Last activity</th>
1996
+ </tr></thead>
1997
+ <tbody>${rows.map(p => `<tr style="border-top:1px solid var(--border)">
1998
+ <td style="padding:6px;font-weight:600">${esc(p.name)}</td>
1999
+ <td style="padding:6px">${(p.sessions || []).length}</td>
2000
+ <td style="padding:6px">${p.tool_calls || 0}</td>
2001
+ <td style="padding:6px">${p.edits || 0}</td>
2002
+ <td style="padding:6px">${p.git_mutations || 0}</td>
2003
+ <td style="padding:6px">${(((p.tokens || {}).input || 0) + ((p.tokens || {}).output || 0)).toLocaleString()}</td>
2004
+ <td style="padding:6px">${(p.cost_usd || 0).toFixed(2)}</td>
2005
+ <td style="padding:6px;color:var(--text2);font-size:11px">${esc((p.last_activity || '').replace('T', ' ').slice(0, 19))}</td>
2006
+ </tr>`).join('')}</tbody>
2007
+ </table>`;
2008
+ }
2009
+
1902
2010
  // ═══════════════════════════════════════════════════════════
1903
2011
  // ── Suggestions ──
1904
2012
  // ═══════════════════════════════════════════════════════════
@@ -21,6 +21,7 @@ from flask import Flask, Response, jsonify, request, send_from_directory # noqa
21
21
  from oracle.config import ORACLE_DIR, load_config, save_config # noqa: E402
22
22
  from oracle.mcp_oracle import mcp_url, start_mcp_thread # noqa: E402
23
23
  from oracle.services import api_auth # noqa: E402
24
+ from oracle.services.activity_reporter import ActivityReporter # noqa: E402
24
25
  from oracle.services.api_auth import extract_bearer # noqa: E402
25
26
  from oracle.services.api_auth import verify as verify_api_key # noqa: E402
26
27
  from oracle.services.c3_bridge import C3Bridge # noqa: E402
@@ -57,10 +58,11 @@ _chat_engine: ChatEngine | None = None
57
58
  _c3_bridge: C3Bridge | None = None
58
59
  _federated: FederatedGraph | None = None
59
60
  _tool_registry: ToolRegistry | None = None
61
+ _activity_reporter: ActivityReporter | None = None
60
62
 
61
63
 
62
64
  def _init_services():
63
- global _cfg, _bridge, _scanner, _reader, _checker, _writer, _cross_memory, _engine, _agent, _model_verified, _chat_store, _chat_engine, _c3_bridge, _federated, _tool_registry
65
+ global _cfg, _bridge, _scanner, _reader, _checker, _writer, _cross_memory, _engine, _agent, _model_verified, _chat_store, _chat_engine, _c3_bridge, _federated, _tool_registry, _activity_reporter
64
66
  _cfg = load_config()
65
67
  _bridge = OllamaBridge(
66
68
  base_url=_cfg.get("ollama_base_url", "https://ollama.com"),
@@ -97,6 +99,7 @@ def _init_services():
97
99
  interval=int(_cfg.get("review_interval_seconds", 1800)),
98
100
  federated_graph=_federated,
99
101
  )
102
+ _activity_reporter = ActivityReporter(scanner=_scanner, ollama_bridge=_bridge)
100
103
  _chat_engine = ChatEngine(
101
104
  bridge=_bridge,
102
105
  reader=_reader,
@@ -107,6 +110,7 @@ def _init_services():
107
110
  scanner=_scanner,
108
111
  store=_chat_store,
109
112
  c3_bridge=_c3_bridge,
113
+ activity_reporter=_activity_reporter,
110
114
  )
111
115
  _tool_registry = ToolRegistry(
112
116
  ToolExecutor(_chat_engine),
@@ -608,6 +612,26 @@ def api_chat_conversation_state(conv_id):
608
612
  return jsonify({"state": _chat_store.get_state(conv_id)})
609
613
 
610
614
 
615
+ # ── Activity digest (Oracle UI) ───────────────────────────
616
+ @app.route("/api/activity/digest", methods=["GET"])
617
+ def api_activity_digest():
618
+ """Cross-project activity digest for the Oracle UI.
619
+
620
+ Query params: date=YYYY-MM-DD, since, until, project (single-project path),
621
+ narrate=true|false. Defaults to today (UTC) across all registered projects.
622
+ """
623
+ if not _activity_reporter:
624
+ return jsonify({"error": "not initialized"}), 500
625
+ narrate = str(request.args.get("narrate", "")).lower() in ("1", "true", "yes", "on")
626
+ return jsonify(_activity_reporter.report(
627
+ date=request.args.get("date", ""),
628
+ since=request.args.get("since", ""),
629
+ until=request.args.get("until", ""),
630
+ project_path=request.args.get("project", ""),
631
+ narrate=narrate,
632
+ ))
633
+
634
+
611
635
  # ── Discovery API (external LLM tool surface) ─────────────
612
636
  @app.route("/api/discovery/tools", methods=["GET"])
613
637
  def api_discovery_tools():
@@ -801,8 +825,19 @@ def _is_oracle_running(port: int) -> bool:
801
825
  return False
802
826
 
803
827
 
828
+ def _force_utf8_console() -> None:
829
+ """Make stdout/stderr UTF-8 so banner/log output can't crash on legacy
830
+ Windows code pages (cp1252 raises UnicodeEncodeError on chars like '→')."""
831
+ for stream in (sys.stdout, sys.stderr):
832
+ try:
833
+ stream.reconfigure(encoding="utf-8", errors="replace")
834
+ except (AttributeError, ValueError, OSError):
835
+ pass
836
+
837
+
804
838
  def run_oracle(port: int = None, open_browser: bool = None):
805
839
  """Main entry point for Oracle server."""
840
+ _force_utf8_console()
806
841
  _init_services()
807
842
 
808
843
  cfg = load_config()
@@ -0,0 +1,256 @@
1
+ """Cross-project activity reporting for the Oracle.
2
+
3
+ Aggregates per-project C3 activity (sessions, tool calls, edits, git mutations,
4
+ token/cost) into a single daily digest. Reads the same ``.c3`` JSONL artifacts
5
+ the local UI uses, directly per project — no C3Runtime build required (mirrors
6
+ how ``MemoryReader`` / ``ProjectManager`` read project data straight off disk).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+
16
+ from services.activity_log import ActivityLog
17
+ from services.edit_ledger import EditLedger
18
+ from services.session_manager import SessionManager
19
+
20
+ log = logging.getLogger("oracle")
21
+
22
+ # Lexicographic sentinels for an open-ended window (ISO-8601 keeps string order).
23
+ _MIN_TS = "0000-01-01T00:00:00"
24
+ _MAX_TS = "9999-12-31T23:59:59"
25
+
26
+ _NARRATE_SYSTEM = (
27
+ "You are C3's activity reporter. Given a JSON digest of a developer's work "
28
+ "across their projects for a time window, write a concise, friendly summary "
29
+ "(3-6 sentences). Lead with the headline (busiest project and the totals), "
30
+ "then call out notable specifics. Use ONLY the numbers provided — never "
31
+ "invent data. If everything is zero, say it was a quiet period."
32
+ )
33
+
34
+
35
+ class ActivityReporter:
36
+ """Builds cross-project (or single-project) activity digests.
37
+
38
+ Construct with an Oracle ``ProjectScanner`` and, optionally, an
39
+ ``OllamaBridge`` for prose narration (``narrate=True``).
40
+ """
41
+
42
+ def __init__(self, scanner, ollama_bridge=None):
43
+ self.scanner = scanner
44
+ self.ollama_bridge = ollama_bridge
45
+
46
+ # ── Public API ───────────────────────────────────────────────
47
+
48
+ def report(self, date: str = "", since: str = "", until: str = "",
49
+ project_path: str = "", narrate: bool = False) -> dict:
50
+ """Aggregate activity into a digest dict.
51
+
52
+ date: UTC day ``YYYY-MM-DD`` (default today). since/until: ISO bounds
53
+ that override ``date``. project_path: limit to one project (else all
54
+ registered projects with a ``.c3`` dir). narrate: add an LLM prose
55
+ summary (best-effort; never fails the structured result).
56
+ """
57
+ lo, hi, window = self._resolve_window(date, since, until)
58
+
59
+ proj_reports = []
60
+ for proj in self._target_projects(project_path):
61
+ pr = self._report_project(proj, lo, hi)
62
+ if (pr["tool_calls"] or pr["edits"] or pr["git_mutations"]
63
+ or pr["sessions"] or pr["decisions"]):
64
+ proj_reports.append(pr)
65
+
66
+ digest = {
67
+ "window": window,
68
+ "totals": self._aggregate_totals(proj_reports),
69
+ "projects": proj_reports,
70
+ "narrative": None,
71
+ }
72
+ if narrate:
73
+ digest["narrative"] = self._narrate(digest)
74
+ return digest
75
+
76
+ # ── Window resolution ────────────────────────────────────────
77
+
78
+ @staticmethod
79
+ def _resolve_window(date: str, since: str, until: str):
80
+ """Return (lo, hi, window_dict).
81
+
82
+ Bounds are naive (no tz suffix) on purpose: the date/time prefix
83
+ dominates lexicographically, so the same bounds correctly window both
84
+ naive ledger timestamps and tz-aware ``+00:00`` activity timestamps.
85
+ """
86
+ if since or until:
87
+ lo = since or _MIN_TS
88
+ hi = until or _MAX_TS
89
+ window = {
90
+ "since": since or None,
91
+ "until": until or None,
92
+ "label": f"{since or 'beginning'} → {until or 'now'}",
93
+ "tz": "as-given",
94
+ }
95
+ return lo, hi, window
96
+
97
+ day = date.strip() if date else datetime.now(timezone.utc).strftime("%Y-%m-%d")
98
+ lo = f"{day}T00:00:00"
99
+ hi = f"{day}T23:59:59.999999"
100
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
101
+ window = {
102
+ "since": lo,
103
+ "until": hi,
104
+ "label": f"{day}{' (today)' if day == today else ''}, UTC",
105
+ "tz": "UTC",
106
+ }
107
+ return lo, hi, window
108
+
109
+ # ── Project selection ────────────────────────────────────────
110
+
111
+ def _target_projects(self, project_path: str) -> list[dict]:
112
+ if project_path:
113
+ p = Path(project_path)
114
+ return [{"path": str(p), "name": p.name, "has_c3": (p / ".c3").is_dir()}]
115
+ return [p for p in self.scanner.discover() if p.get("has_c3")]
116
+
117
+ # ── Per-project aggregation ──────────────────────────────────
118
+
119
+ def _report_project(self, proj: dict, lo: str, hi: str) -> dict:
120
+ path = proj.get("path", "")
121
+ base = {
122
+ "name": proj.get("name") or Path(path).name,
123
+ "path": path,
124
+ "sessions": [],
125
+ "tool_calls": 0,
126
+ "edits": 0,
127
+ "git_mutations": 0,
128
+ "decisions": 0,
129
+ "events": {},
130
+ "tokens": {"input": 0, "output": 0},
131
+ "cost_usd": 0.0,
132
+ "first_activity": None,
133
+ "last_activity": None,
134
+ }
135
+ # Guard: only read projects that already have a .c3 dir — never create
136
+ # one as a side effect (ActivityLog/SessionManager mkdir on init).
137
+ if not path or not (Path(path) / ".c3").is_dir():
138
+ return base
139
+
140
+ timestamps: list[str] = []
141
+
142
+ # Activity log → per-type event counts.
143
+ try:
144
+ counts: dict[str, int] = {}
145
+ for e in ActivityLog(path).get_recent(limit=10000, since=lo, until=hi):
146
+ etype = e.get("type", "unknown")
147
+ counts[etype] = counts.get(etype, 0) + 1
148
+ if e.get("timestamp"):
149
+ timestamps.append(e["timestamp"])
150
+ base["events"] = counts
151
+ base["tool_calls"] = counts.get("tool_call", 0)
152
+ base["decisions"] = counts.get("decision", 0)
153
+ except Exception as exc: # pragma: no cover - defensive
154
+ log.debug("activity_log read failed for %s: %s", path, exc)
155
+
156
+ try:
157
+ sm = SessionManager(path)
158
+ # Sessions started within the window.
159
+ for s in sm.list_sessions(100):
160
+ started = s.get("started", "")
161
+ if started and lo <= started <= hi:
162
+ base["sessions"].append({
163
+ "id": s.get("id"),
164
+ "started": started,
165
+ "ended": s.get("ended", ""),
166
+ "description": s.get("description", ""),
167
+ "tool_calls": s.get("tool_calls", 0),
168
+ "duration": s.get("duration", ""),
169
+ })
170
+ timestamps.append(started)
171
+ # Token / cost from hook-captured session stats.
172
+ for st in sm.get_session_stats(500):
173
+ ts = st.get("ts", "")
174
+ if ts and lo <= ts <= hi:
175
+ base["tokens"]["input"] += int(st.get("input_tokens", 0) or 0)
176
+ base["tokens"]["output"] += int(st.get("output_tokens", 0) or 0)
177
+ base["cost_usd"] += float(st.get("cost_usd", 0) or 0)
178
+ except Exception as exc: # pragma: no cover - defensive
179
+ log.debug("session read failed for %s: %s", path, exc)
180
+
181
+ # Edit ledger → edits vs git mutations (get_history filters >= lo only).
182
+ try:
183
+ for en in EditLedger(path).get_history(since=lo, limit=10000):
184
+ ts = en.get("timestamp", "")
185
+ if ts and ts > hi:
186
+ continue
187
+ if en.get("change_type") == "shell_git":
188
+ base["git_mutations"] += 1
189
+ else:
190
+ base["edits"] += 1
191
+ if ts:
192
+ timestamps.append(ts)
193
+ except Exception as exc: # pragma: no cover - defensive
194
+ log.debug("edit ledger read failed for %s: %s", path, exc)
195
+
196
+ base["cost_usd"] = round(base["cost_usd"], 4)
197
+ if timestamps:
198
+ timestamps.sort()
199
+ base["first_activity"] = timestamps[0]
200
+ base["last_activity"] = timestamps[-1]
201
+ return base
202
+
203
+ @staticmethod
204
+ def _aggregate_totals(reports: list[dict]) -> dict:
205
+ totals = {
206
+ "projects_active": len(reports),
207
+ "sessions": 0,
208
+ "tool_calls": 0,
209
+ "edits": 0,
210
+ "git_mutations": 0,
211
+ "decisions": 0,
212
+ "input_tokens": 0,
213
+ "output_tokens": 0,
214
+ "cost_usd": 0.0,
215
+ }
216
+ for r in reports:
217
+ totals["sessions"] += len(r["sessions"])
218
+ totals["tool_calls"] += r["tool_calls"]
219
+ totals["edits"] += r["edits"]
220
+ totals["git_mutations"] += r["git_mutations"]
221
+ totals["decisions"] += r["decisions"]
222
+ totals["input_tokens"] += r["tokens"]["input"]
223
+ totals["output_tokens"] += r["tokens"]["output"]
224
+ totals["cost_usd"] += r["cost_usd"]
225
+ totals["cost_usd"] = round(totals["cost_usd"], 4)
226
+ return totals
227
+
228
+ # ── Narration (best-effort) ──────────────────────────────────
229
+
230
+ def _narrate(self, digest: dict) -> str | None:
231
+ if self.ollama_bridge is None:
232
+ digest["narrative_error"] = "No Ollama bridge configured."
233
+ return None
234
+ payload = {
235
+ "window": digest["window"]["label"],
236
+ "totals": digest["totals"],
237
+ "projects": [
238
+ {
239
+ "name": p["name"],
240
+ "tool_calls": p["tool_calls"],
241
+ "edits": p["edits"],
242
+ "git_mutations": p["git_mutations"],
243
+ "sessions": len(p["sessions"]),
244
+ "cost_usd": p["cost_usd"],
245
+ }
246
+ for p in digest["projects"]
247
+ ],
248
+ }
249
+ prompt = "Summarize this developer activity digest:\n\n" + json.dumps(payload, indent=2)
250
+ try:
251
+ text = self.ollama_bridge.generate(prompt, system=_NARRATE_SYSTEM)
252
+ return (text or "").strip() or None
253
+ except Exception as exc:
254
+ log.warning("activity narration failed: %s", exc)
255
+ digest["narrative_error"] = str(exc)
256
+ return None