code-context-control 2.46.1__py3-none-any.whl → 2.49.1__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 (39) hide show
  1. cli/c3.py +11 -3
  2. cli/commands/parser.py +10 -1
  3. {code_context_control-2.46.1.dist-info → code_context_control-2.49.1.dist-info}/METADATA +1 -1
  4. {code_context_control-2.46.1.dist-info → code_context_control-2.49.1.dist-info}/RECORD +38 -19
  5. oracle/config.py +12 -0
  6. oracle/oracle_server.py +125 -20
  7. oracle/oracle_ui.html +1217 -0
  8. oracle/services/c3_bridge.py +194 -59
  9. oracle/services/chat_engine.py +453 -141
  10. oracle/services/federated_graph.py +52 -2
  11. oracle/services/local_session.py +54 -0
  12. oracle/services/ollama_bridge.py +87 -7
  13. oracle/services/project_scanner.py +60 -6
  14. oracle/services/review_agent.py +84 -1
  15. oracle/services/tool_registry.py +82 -3
  16. oracle/ui/activity.js +101 -0
  17. oracle/ui/agents.js +108 -0
  18. oracle/ui/app.js +11 -0
  19. oracle/ui/busy.js +102 -0
  20. oracle/ui/chat/conversations.js +153 -0
  21. oracle/ui/chat/input.js +284 -0
  22. oracle/ui/chat/markdown.js +33 -0
  23. oracle/ui/chat/send.js +281 -0
  24. oracle/ui/chat/stream_renderer.js +767 -0
  25. oracle/ui/chat/toolbar.js +270 -0
  26. oracle/ui/core.js +60 -0
  27. oracle/ui/crossgraph.js +184 -0
  28. oracle/ui/header.js +75 -0
  29. oracle/ui/insights.js +52 -0
  30. oracle/ui/projects.js +245 -0
  31. oracle/ui/settings.js +186 -0
  32. oracle/ui/suggestions.js +47 -0
  33. oracle/ui/theme_tabs.js +68 -0
  34. services/project_runtime.py +16 -3
  35. oracle/oracle.html +0 -4136
  36. {code_context_control-2.46.1.dist-info → code_context_control-2.49.1.dist-info}/WHEEL +0 -0
  37. {code_context_control-2.46.1.dist-info → code_context_control-2.49.1.dist-info}/entry_points.txt +0 -0
  38. {code_context_control-2.46.1.dist-info → code_context_control-2.49.1.dist-info}/licenses/LICENSE +0 -0
  39. {code_context_control-2.46.1.dist-info → code_context_control-2.49.1.dist-info}/top_level.txt +0 -0
cli/c3.py CHANGED
@@ -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.46.1"
88
+ __version__ = "2.49.1"
89
89
 
90
90
 
91
91
  def _command_deps() -> CommandDeps:
@@ -5632,10 +5632,18 @@ def _bb_cmd_set_default(args, project_path: str) -> None:
5632
5632
 
5633
5633
 
5634
5634
  def cmd_oracle(args):
5635
- """Oracle Discovery API key + connection management."""
5635
+ """Oracle dashboard server + Discovery API key management."""
5636
5636
  sub = getattr(args, "oracle_cmd", None)
5637
+ if sub in ("serve", "start"):
5638
+ # Lazy import: run_oracle builds all Oracle services (matches
5639
+ # cmd_hub's deferred-import style so bare `c3` stays fast).
5640
+ from oracle.oracle_server import run_oracle
5641
+ run_oracle(port=getattr(args, "port", None),
5642
+ open_browser=not getattr(args, "no_browser", False))
5643
+ return
5637
5644
  if sub != "api":
5638
- print("Usage: c3 oracle api {info,key,rotate,clear}")
5645
+ print("Usage: c3 oracle {serve,api} — serve: launch the dashboard; "
5646
+ "api {info,key,rotate,clear}: manage the Discovery key")
5639
5647
  return
5640
5648
 
5641
5649
  from oracle.config import load_config
cli/commands/parser.py CHANGED
@@ -357,9 +357,18 @@ def build_parser(version: str, parse_cli_ide_arg):
357
357
  # ── Oracle Discovery API (v2.32.0) ──────────────────────────────────
358
358
  p_oracle = subparsers.add_parser(
359
359
  "oracle",
360
- help="Oracle Discovery API key + connection management",
360
+ help="Oracle dashboard server + Discovery API key management",
361
361
  )
362
362
  or_subs = p_oracle.add_subparsers(dest="oracle_cmd")
363
+ or_serve = or_subs.add_parser(
364
+ "serve",
365
+ aliases=["start"],
366
+ help="Launch the Oracle dashboard server (REST + MCP discovery endpoints)",
367
+ )
368
+ or_serve.add_argument("--port", type=int, default=None,
369
+ help="Server port (default: config 'port', 3331)")
370
+ or_serve.add_argument("--no-browser", action="store_true",
371
+ help="Don't open the browser")
363
372
  or_api = or_subs.add_parser(
364
373
  "api",
365
374
  help="Show connection info / manage the Discovery API key",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.46.1
3
+ Version: 2.49.1
4
4
  Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer — and version the configs that shape your agent (CLAUDE.md, skills, hooks, MCP).
5
5
  Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  cli/__init__.py,sha256=ec66drCZGNMRU4V6ov0zVhYZph1us12Vn8OvG_LJyRY,22
2
2
  cli/_hook_utils.py,sha256=zlquaQm0dvwdaZewD6YnFyF_LBzhmfFUUhL_8UbBKGA,11749
3
- cli/c3.py,sha256=Sx8ySlKiRJsdWwmBjhzaca8gEOWE4CLHangvpeRiGek,304129
3
+ cli/c3.py,sha256=gQY5eY2DO1gk7xSa4734VLHLi_6aFwxd32HBUjGBbZQ,304587
4
4
  cli/docs.html,sha256=CC_-4PaQxfjNXbcqcVuU5rzdbP1NosYJxFRiXZV4joI,143645
5
5
  cli/edits.html,sha256=UjAhoCmBmQ89cklGvJqzC6eyNP2tc8H6T-e01DVkLvE,43418
6
6
  cli/hook_artifact.py,sha256=Se1CNBfoBFyvJQlRmYdNtdRXIkQp9zaF0O6ndRMo7ts,2198
@@ -27,7 +27,7 @@ cli/ui_legacy.html,sha256=cI8tC6RKmE2NIJOcsu7CY-zT4VznjcbD6NTjxb_fvUY,378460
27
27
  cli/ui_nano.html,sha256=UAwQ6bbTOXAoGq191AZ7slhngR9edJSa3IhqpynveDg,27740
28
28
  cli/commands/__init__.py,sha256=0Z8MABNzwSFJGT4Xv9R5AJVR8XxraTsuVTz5b0bShmo,38
29
29
  cli/commands/common.py,sha256=fHZWzkd4OLl3vPWTTgaI5vaqMi3Ma3XqAu1Ds_-51_4,11299
30
- cli/commands/parser.py,sha256=Opci6sxpmp0fND1XI77blOYjf04rwGCpL5lwd6y33U8,24923
30
+ cli/commands/parser.py,sha256=inkJuMro1kkpKj8lpiWRlNYPQ_DC4REF-m6Ut1CAkmA,25355
31
31
  cli/guide/bitbucket.html,sha256=5HrLDm6Ue-AJZ81bqWaSp2nSfxaRdEjHFY29P7plLXA,33350
32
32
  cli/guide/getting-started.html,sha256=F3AaF2HVfeg6eKO25p_vPogr_i1hvkjIZVggIJFWTl0,18829
33
33
  cli/guide/index.html,sha256=XIpgQN0XbLJzL-Atou65NU5Irj_I-w4mbERU9WfZJ0E,21223
@@ -93,34 +93,53 @@ cli/ui/components/sessions.js,sha256=FIKtil76B8tCkAmcFV7hlj6GQ_DCJK2jCzvEmdK7NBE
93
93
  cli/ui/components/settings.js,sha256=8LVTV2TQl9tcRXhXbtBEJOCBdiyk-x2QASoVYZUAuEA,71442
94
94
  cli/ui/components/sidebar.js,sha256=cAY_jwYB-o1X_wWn__VXlG4IegVObuE3NmVsuFWqxtg,7417
95
95
  cli/ui/components/tasks.js,sha256=OxLhspzzEQjxxtSEpOpugO2KptD4xNrimf_xM0wiVv8,12752
96
- code_context_control-2.46.1.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
96
+ code_context_control-2.49.1.dist-info/licenses/LICENSE,sha256=l8Kh5QCNWNvR6kIt8L0BUZvc2LAFiHv2c-FnsGnUZf4,11301
97
97
  core/__init__.py,sha256=TSDCEcM4V7gcZVM3w2ykJaqEUch4Dkon-rivV17T73s,2501
98
98
  core/config.py,sha256=9R8bIcHhjSPdrfcCeMPnS4YgLCyRNWwlt_Z75PZcBDE,14780
99
99
  core/ide.py,sha256=9LzsDVK2LL8RVpL40l6oNGiasZ3D8OCU_9i9A0gJKBo,6876
100
100
  core/mcp_toml.py,sha256=wfc7c6X7lIW5YpdWFnRK0qQ0M46v2t8j6WzcqfTKhaE,5627
101
101
  core/web_security.py,sha256=HwgK5aBATfvRIeO_4gWgzK77gKwGuzVlmiuNGm4gGxI,7884
102
102
  oracle/__init__.py,sha256=-OTD7Jh4mUMA4QgPGthPLWXttgZLpkIPhGQ87ZfHBx0,63
103
- oracle/config.py,sha256=ErjH6Y_F51jGXpYo_4boGhdIk-AIo3rDH3xDwUGs7B8,3193
103
+ oracle/config.py,sha256=4geiSXhwXbKvy2EaKibKxpg8DgUVJWkNeSMcpQFOo_s,3913
104
104
  oracle/mcp_oracle.py,sha256=B5B-qSxi37outsuRHn1Ns6Gj8e7W593rmL34LMzuE8M,8980
105
- oracle/oracle.html,sha256=fWs60EW0ZOBMyC_XwB0SWN_LpjI8S2eACt23pJeUI7g,190791
106
- oracle/oracle_server.py,sha256=dxoycgeJvVezGPuoharv1uMa0BUIBfHJ5QqQWUDsuk4,37617
105
+ oracle/oracle_server.py,sha256=zDywXKQC8Yxq8HrwpBEIJvanr-OKkO62eQZCVGGKcTI,41589
106
+ oracle/oracle_ui.html,sha256=qML_m7GY-iPEhQOiczDd8T6IEVtqcrjwEoudK9f9C5U,68441
107
107
  oracle/services/__init__.py,sha256=Nb4POd1_YIwLVYsGfr-DiK-iKTelkU0fh9m7wjeLQHA,23
108
108
  oracle/services/activity_reporter.py,sha256=ZAxNFyUy7QRP28anQLRbKEwm5SGG97KIQYFQDLjYqlM,11729
109
109
  oracle/services/api_auth.py,sha256=1PW3pG--1DJb_F6qMhP3gBTYHxxPE2bsHmVmIhC81Y8,3566
110
- oracle/services/c3_bridge.py,sha256=TgYnX9tJvzpXx6B2eC3EhJkqoKoRUaGJw0nrBidLKVA,10752
111
- oracle/services/chat_engine.py,sha256=yKJcqtLNuwDy_sMVOBZk1R7uAH6d9QboPjfAbekqhSs,50500
110
+ oracle/services/c3_bridge.py,sha256=RqSw1tOoUDXA21fKDHmZ7s7qrIj3jiDYq8j8OvbxshA,17919
111
+ oracle/services/chat_engine.py,sha256=5vSPjjsaA1Swbs9Ob4bS60SRgTa58YQV8pzzxcXCTbc,66126
112
112
  oracle/services/chat_store.py,sha256=mizKwDyFGESKh63V-XgyNd3jQVrN2JCQc-7u8z3_1F4,6252
113
113
  oracle/services/cross_memory.py,sha256=F8JeYkEFSwQL3iGS9KV1onaMSFj3opr6f3nCxwvznV0,5484
114
- oracle/services/federated_graph.py,sha256=F808OkuqAbKXHqugP0PfQRLk-KrROLfjSLGLBVb6p-c,17735
114
+ oracle/services/federated_graph.py,sha256=I9FWedcht6-FU2hoIlMHlXyjpwrspKueIN-jEYek6vg,19947
115
115
  oracle/services/health_checker.py,sha256=SlsW4nxP5u8gHJY9FpeaZoC9_K-NhoiHr2Q7JvGMvdY,4359
116
116
  oracle/services/insight_engine.py,sha256=YBZX28P-ME-nJ02cebFIVH0WvIGebFnQQCt8paXri4Y,11417
117
+ oracle/services/local_session.py,sha256=f6qzHPZPtn1XIV5wNnlJ85BY3tApAh2NoYdtaIFpMDs,1887
117
118
  oracle/services/memory_reader.py,sha256=rSEmoIXQu2JkolHJIQWpn0piPJpyuf1KZvW_9liQwkU,3727
118
119
  oracle/services/memory_writer.py,sha256=hpvYsxjhl293jEjGcRKfjG5njUaAOXKqbmJaUu1WdUU,6997
119
- oracle/services/ollama_bridge.py,sha256=d0458HTaQO9m-Ur4bRIt9izxbJFDj1Dbea9sfo7MRCU,12958
120
- oracle/services/project_scanner.py,sha256=SGHYKU00fm57L5VyDuet-CAACfrhUZY2xvIlZR27aj4,3142
121
- oracle/services/review_agent.py,sha256=99PQ1oKxRDo_4COMOy3CJOawqk08MRJ4cS66_w7SWCo,7720
120
+ oracle/services/ollama_bridge.py,sha256=T3Xif3OXsjtsJvsNXhr_iRlB3fr9lbFipuA_1ngK7jQ,16705
121
+ oracle/services/project_scanner.py,sha256=gHpXihRZSu2gXCp4vvOWp5gMAsmfINE9ESiqVF1uV3A,5587
122
+ oracle/services/review_agent.py,sha256=m_fzoHowpiibf0_iz3dF72LWSrwB_FbDd1am-SbeWXg,11273
122
123
  oracle/services/tool_executor.py,sha256=xtAkBWkclh_FwOgzprjGkyRIV8-A7Lc3bz0UFPShrv0,1113
123
- oracle/services/tool_registry.py,sha256=OSTl_ngQTO-JJRgcXvNeM6A5WDxTLfcCslafZjmolpA,19950
124
+ oracle/services/tool_registry.py,sha256=fHXgQ2Y7T8JdjrxYgAnOlYRANtve5SikkEuf4b86i94,25686
125
+ oracle/ui/activity.js,sha256=zbJjqkY-tTQS4YKcdSEr2Z5zAbwbOIPyTNzldQSPU7I,4830
126
+ oracle/ui/agents.js,sha256=RNnsGUVg5DlRYiv-Ovzb3W4yyP4z0qTX3o7Y1_BgZpo,4538
127
+ oracle/ui/app.js,sha256=fyfFL7YNbl_z1WSUOMXScp4qcavuFVg5cnA7D1i3zYU,795
128
+ oracle/ui/busy.js,sha256=ePFGqEheHpn-E971pN_9DbQvvkxD-d2V7Efbw68bpo0,4122
129
+ oracle/ui/core.js,sha256=M6gt_AjxKLoZ8sEYsHT1n4-qR4F7qMSe80Quls1hgL8,3214
130
+ oracle/ui/crossgraph.js,sha256=7yBCciuEzZ0LYspWMO0xTe_GuPQ_NccnIeRrqEq5Qxs,9234
131
+ oracle/ui/header.js,sha256=KA-vZ-KIVEDZ1RejMSXX76t3Zno1r7q85xQWzMQizvw,3598
132
+ oracle/ui/insights.js,sha256=zACyfGz9-4y2eFd6OM1IE-L_HyL1wnqo46nJDyRx7-M,2408
133
+ oracle/ui/projects.js,sha256=ULE6Py1iULVmRGy0mCnEAxKNwHvn-baUYlNDF5xGYM8,13002
134
+ oracle/ui/settings.js,sha256=4p4CB_5GFic1uHeiDHtN7yB5Ma0dLN5HAVDQ_04QbGo,7889
135
+ oracle/ui/suggestions.js,sha256=ItQTlyd6LS_QxvbJmNMYeHY3cl0lSRgBdR53XKkBpP0,2332
136
+ oracle/ui/theme_tabs.js,sha256=sIFnBLgI_Hp7c-GRZrWN2v4pvRBJUnIhhwGG91qydKI,3347
137
+ oracle/ui/chat/conversations.js,sha256=FOpXNrIeiJUR9VueBYEuLml-VfJfuHo05ZVkT-x7XRI,5907
138
+ oracle/ui/chat/input.js,sha256=9m48OZzmhCygND24pjZikOBzEXhOK6YqwWt4d5bjKbs,9329
139
+ oracle/ui/chat/markdown.js,sha256=14Le4i88756PQvsl2ZuKGMuRw1FDuEPXr_oi2U6kLFM,1508
140
+ oracle/ui/chat/send.js,sha256=k2qGN_asb7FTjcLa204Zjq4X_QWHIKHQnI33IEuxvaQ,11558
141
+ oracle/ui/chat/stream_renderer.js,sha256=RZndI2mu5xbzZF-4NG9jeOqXTcmIqfZqxgkrBvTzWmk,29861
142
+ oracle/ui/chat/toolbar.js,sha256=ZDexIIpGwsQ6XsY4Tyo082rJoofdD_JBTV97-mLQUVM,12027
124
143
  services/__init__.py,sha256=3Kn4cZweLm7at8wFdBdZ-Zwo8hHcnVIsmY5f29nzi2Y,116
125
144
  services/activity_log.py,sha256=PNGeEYlw1Aux-mRU4pKCQdbsSuzLvOg7MYiq-cb4n8g,4473
126
145
  services/agent_base.py,sha256=a-gdSd_jtZtbjXo1WS8CnWCagXgKaGZd5ShcG6s0kT4,4809
@@ -158,7 +177,7 @@ services/ollama_client.py,sha256=UiC5Abca622-ac-4goCkAjwo7iUye9g9JGFMN6Ea08M,798
158
177
  services/output_filter.py,sha256=lknQHs38JlRtXU4Ogoru6sveNokSKBQPfZXhf-fE0xM,21736
159
178
  services/parser.py,sha256=CPoIN1FmX7kK-55FnC3jvRy3NLjlnf1armTaD6o7GBg,55203
160
179
  services/project_manager.py,sha256=LaNWqBeVMx6wcB6iiOQyRWSWhS-JXzfDApJmHFmTCy8,36477
161
- services/project_runtime.py,sha256=e4qDsYbjsWbm7RyAUOPdKbPXFoO4OBApnlQM69Tw-bE,10982
180
+ services/project_runtime.py,sha256=yyftX48BFjq4B2KcR4KGszDAAa4xR6gn6j0SgTyro1s,11603
162
181
  services/protocol.py,sha256=E0xxs43j-YSOBWalKHbX_wwqphQgDjqUjyyvTW0s6II,12247
163
182
  services/proxy_state.py,sha256=u5rd0k6CrOsywZA8FpRu_hMLwhR0TAJhZjy5MdWbCGc,6107
164
183
  services/retention.py,sha256=I2_RV233kWBBXox5rc_w-1h1aPua93o9huuPf-pJVuE,18629
@@ -200,8 +219,8 @@ tui/screens/search_view.py,sha256=MMHjVdlk3HZSuDBSvq8IGrqv_Mh5Us6YqXQ80bcWSMk,19
200
219
  tui/screens/session_view.py,sha256=eZ1eDwHTvPOck1wCCviixtOaCxIkBT_95ytNNNriGNA,5991
201
220
  tui/screens/stats.py,sha256=p81PjzdaIv7hllb8f45-rlVe4lJZwSdIMqu7e86_u5s,6223
202
221
  tui/screens/ui_view.py,sha256=1QJCgLh2YfgWIpvzRG1KOGXYEaOYX6ojN61Azjf2oX0,2125
203
- code_context_control-2.46.1.dist-info/METADATA,sha256=Gqn5nbiiyphjNm0oz2ohgTxveh5KQyzUDVd14POqUxw,23785
204
- code_context_control-2.46.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
205
- code_context_control-2.46.1.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
206
- code_context_control-2.46.1.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
207
- code_context_control-2.46.1.dist-info/RECORD,,
222
+ code_context_control-2.49.1.dist-info/METADATA,sha256=nYHYs-fLmzcaQ-VP7YRq7MKu6r9i4fnq3rEPCVlx5UU,23785
223
+ code_context_control-2.49.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
224
+ code_context_control-2.49.1.dist-info/entry_points.txt,sha256=7kX_WUsDCF2hbXzvbNyscyaBb9AeA-DJY5v_5hN0DlU,93
225
+ code_context_control-2.49.1.dist-info/top_level.txt,sha256=wRt41zBybVF3qAiNXHz9BURbkKvUvfhmWWtKMhaw6eE,29
226
+ code_context_control-2.49.1.dist-info/RECORD,,
oracle/config.py CHANGED
@@ -18,12 +18,21 @@ DEFAULTS = {
18
18
  "mcp_port": 3332, # discovery MCP transport port (loopback)
19
19
  "ollama_base_url": "https://ollama.com",
20
20
  "ollama_api_key": "",
21
+ "llm_cache_ttl_sec": 86400, # disk cache TTL for generate() responses
21
22
  "model": "gemma4:31b-cloud",
22
23
  "hub_url": "http://localhost:3330",
24
+ "scanner_ttl_seconds": 20,
23
25
  "review_interval_seconds": 1800,
24
26
  "review_enabled": True,
27
+ # ── Scheduled activity digest (runs inside the review loop) ──
28
+ "digest_enabled": False, # off = current behavior (on-demand only)
29
+ "digest_interval_seconds": 86400, # daily cadence once enabled
30
+ "digest_narrate": False, # LLM prose costs a cloud call; opt-in
31
+ "digest_notify_file": "", # "" = disabled; else JSONL sink path
32
+ "digest_retention_days": 14, # prune stored digests
25
33
  "auto_open_browser": True,
26
34
  "theme": "dark",
35
+ "ui_last_tab": "chat", # persisted UI preference (hub parity)
27
36
  "max_facts_per_analysis": 100,
28
37
  "insight_confidence_threshold": 0.5,
29
38
  "log_level": "INFO",
@@ -39,6 +48,7 @@ DEFAULTS = {
39
48
  "description": "Expert in system architecture, design patterns, and cross-project structure. Best for high-level analysis.",
40
49
  "system_prompt": "You are the Architect. Focus on structural integrity, design patterns, and the big picture. Provide high-level recommendations before diving into code.",
41
50
  "model": "gemma4:31b-cloud",
51
+ "backend": "ollama",
42
52
  "active": True
43
53
  },
44
54
  {
@@ -47,6 +57,7 @@ DEFAULTS = {
47
57
  "description": "Specializes in deep code analysis, bug hunting, and tracing execution paths.",
48
58
  "system_prompt": "You are the Code Explorer. Be incredibly precise, cite specific lines of code, and focus on the technical implementation details. Trace logic thoroughly.",
49
59
  "model": "gemma4:31b-cloud",
60
+ "backend": "ollama",
50
61
  "active": True
51
62
  },
52
63
  {
@@ -55,6 +66,7 @@ DEFAULTS = {
55
66
  "description": "Focuses on analyzing project memory, facts, and insights.",
56
67
  "system_prompt": "You are the Memory Analyst. Rely heavily on memory tools and facts to spot trends. Connect current issues to past context.",
57
68
  "model": "gemma4:31b-cloud",
69
+ "backend": "ollama",
58
70
  "active": True
59
71
  }
60
72
  ],
oracle/oracle_server.py CHANGED
@@ -3,7 +3,6 @@
3
3
  import atexit
4
4
  import json
5
5
  import logging
6
- import os
7
6
  import socket
8
7
  import sys
9
8
  import threading
@@ -16,11 +15,11 @@ _PROJECT_ROOT = Path(__file__).resolve().parent.parent
16
15
  if str(_PROJECT_ROOT) not in sys.path:
17
16
  sys.path.insert(0, str(_PROJECT_ROOT))
18
17
 
19
- from flask import Flask, Response, jsonify, request, send_from_directory # noqa: E402
18
+ from flask import Flask, Response, jsonify, request # noqa: E402
20
19
 
21
20
  from oracle.config import ORACLE_DIR, load_config, save_config # noqa: E402
22
21
  from oracle.mcp_oracle import mcp_url, start_mcp_thread # noqa: E402
23
- from oracle.services import api_auth # noqa: E402
22
+ from oracle.services import api_auth, local_session # noqa: E402
24
23
  from oracle.services.activity_reporter import ActivityReporter # noqa: E402
25
24
  from oracle.services.api_auth import extract_bearer # noqa: E402
26
25
  from oracle.services.api_auth import verify as verify_api_key # noqa: E402
@@ -68,6 +67,7 @@ def _init_services():
68
67
  base_url=_cfg.get("ollama_base_url", "https://ollama.com"),
69
68
  model=_cfg.get("model", "gemma4:31b-cloud"),
70
69
  api_key=_cfg.get("ollama_api_key", ""),
70
+ cache_ttl_sec=int(_cfg.get("llm_cache_ttl_sec", 86400)),
71
71
  )
72
72
  # Verify model works on startup (background thread to avoid blocking)
73
73
  def _verify():
@@ -80,7 +80,10 @@ def _init_services():
80
80
  _model_verified = False
81
81
  logging.getLogger("oracle").warning("Ollama unreachable — model not verified")
82
82
  threading.Thread(target=_verify, daemon=True, name="oracle-model-verify").start()
83
- _scanner = ProjectScanner(hub_url=_cfg.get("hub_url", "http://localhost:3330"))
83
+ _scanner = ProjectScanner(
84
+ hub_url=_cfg.get("hub_url", "http://localhost:3330"),
85
+ ttl=float(_cfg.get("scanner_ttl_seconds", 20)),
86
+ )
84
87
  _reader = MemoryReader()
85
88
  _checker = HealthChecker(_reader)
86
89
  _writer = MemoryWriter()
@@ -89,6 +92,8 @@ def _init_services():
89
92
  _chat_store = ChatStore()
90
93
  _c3_bridge = C3Bridge(scanner=_scanner)
91
94
  _federated = FederatedGraph(reader=_reader, cross_memory=_cross_memory, ollama_bridge=_bridge)
95
+ # Reporter before ReviewAgent: the review loop emits the scheduled digest.
96
+ _activity_reporter = ActivityReporter(scanner=_scanner, ollama_bridge=_bridge)
92
97
  _agent = ReviewAgent(
93
98
  scanner=_scanner,
94
99
  reader=_reader,
@@ -98,8 +103,8 @@ def _init_services():
98
103
  writer=_writer,
99
104
  interval=int(_cfg.get("review_interval_seconds", 1800)),
100
105
  federated_graph=_federated,
106
+ activity_reporter=_activity_reporter,
101
107
  )
102
- _activity_reporter = ActivityReporter(scanner=_scanner, ollama_bridge=_bridge)
103
108
  _chat_engine = ChatEngine(
104
109
  bridge=_bridge,
105
110
  reader=_reader,
@@ -122,9 +127,10 @@ def _init_services():
122
127
  # ── CORS ──────────────────────────────────────────────────
123
128
  # Localhost security guard + scoped CORS (replaces the previous wildcard CORS).
124
129
  # Host-header allowlist + Origin/Referer CSRF guard. Bearer auth on
125
- # /api/discovery/* (see _discovery_auth_guard below) still applies on top; this
126
- # guard also protects the ungated endpoints (config, chat, /api/apikey) from
127
- # cross-origin reads notably the raw Discovery token returned by api_apikey_get.
130
+ # /api/discovery/* (see _discovery_auth_guard below) and the local write gate
131
+ # (_local_write_guard: session cookie or Bearer on every other mutating
132
+ # /api/* call) still apply on top; this guard blocks cross-origin browsers,
133
+ # the write gate blocks unauthenticated local processes.
128
134
  from core.web_security import (
129
135
  allowed_hostnames as _allowed_hostnames,
130
136
  )
@@ -155,10 +161,90 @@ def _discovery_auth_guard():
155
161
  return None
156
162
 
157
163
 
164
+ # ── Local write gate ──────────────────────────────────────
165
+ @app.before_request
166
+ def _local_write_guard():
167
+ """Auth gate for mutating local API calls (everything except Discovery).
168
+
169
+ Requires either the per-boot dashboard session cookie (issued on ``GET /``
170
+ to loopback browsers) or the Discovery Bearer token. Default-deny: any
171
+ future mutating ``/api/*`` endpoint is covered automatically. Closes the
172
+ rotate-then-read kill chain — previously any local process could POST
173
+ /api/apikey/rotate unauthenticated and read the fresh token, defeating
174
+ the Bearer gates on /api/config and /api/discovery/*.
175
+ """
176
+ path = request.path or ""
177
+ if not path.startswith("/api/") or path.startswith("/api/discovery"):
178
+ return None
179
+ if request.method in ("GET", "HEAD", "OPTIONS"):
180
+ return None
181
+ if verify_api_key(extract_bearer(request.headers.get("Authorization"))):
182
+ return None
183
+ if local_session.verify(request.cookies.get(local_session.COOKIE_NAME)):
184
+ return None
185
+ return jsonify({"error": "unauthorized"}), 401
186
+
187
+
158
188
  # ── Static ────────────────────────────────────────────────
189
+
190
+ # JS load order for the concatenated Oracle UI build (mirrors cli/hub_server.py).
191
+ # One shared script scope: function declarations hoist across files, and
192
+ # app.js (the init IIFE) must stay LAST.
193
+ _ORACLE_JS_FILES = [
194
+ "ui/core.js",
195
+ "ui/busy.js",
196
+ "ui/theme_tabs.js",
197
+ "ui/crossgraph.js",
198
+ "ui/header.js",
199
+ "ui/projects.js",
200
+ "ui/insights.js",
201
+ "ui/activity.js",
202
+ "ui/suggestions.js",
203
+ "ui/settings.js",
204
+ "ui/agents.js",
205
+ "ui/chat/markdown.js",
206
+ "ui/chat/conversations.js",
207
+ "ui/chat/stream_renderer.js",
208
+ "ui/chat/toolbar.js",
209
+ "ui/chat/input.js",
210
+ "ui/chat/send.js",
211
+ "ui/app.js",
212
+ ]
213
+
214
+
215
+ def _build_oracle_html() -> str:
216
+ """Concatenate oracle_ui.html shell + all JS module files into one response."""
217
+ oracle_dir = Path(__file__).parent
218
+ shell_path = oracle_dir / "oracle_ui.html"
219
+ if not shell_path.exists():
220
+ return "<h1>Oracle UI not found.</h1>"
221
+
222
+ shell = shell_path.read_text(encoding="utf-8")
223
+
224
+ js_parts = []
225
+ for rel in _ORACLE_JS_FILES:
226
+ js_path = oracle_dir / rel
227
+ if js_path.exists():
228
+ js_parts.append(f"// ═══ {rel} ═══\n" + js_path.read_text(encoding="utf-8"))
229
+
230
+ return shell.replace("/* __C3_ORACLE_SCRIPTS__ */", "\n\n".join(js_parts))
231
+
232
+
233
+ # Cache the built HTML (built on first request; cleared on server restart).
234
+ _oracle_html_cache: str | None = None
235
+
236
+
159
237
  @app.route("/")
160
238
  def index():
161
- return send_from_directory(os.path.dirname(__file__), "oracle.html")
239
+ global _oracle_html_cache
240
+ if _oracle_html_cache is None:
241
+ _oracle_html_cache = _build_oracle_html()
242
+ resp = Response(_oracle_html_cache, mimetype="text/html")
243
+ # Issue the dashboard session cookie only to loopback browsers; remote
244
+ # viewers (LAN bind) can read GET dashboards but cannot mutate.
245
+ if local_session.is_loopback(request.remote_addr):
246
+ local_session.attach_cookie(resp)
247
+ return resp
162
248
 
163
249
 
164
250
  # ── Health ────────────────────────────────────────────────
@@ -176,6 +262,7 @@ def api_health():
176
262
  return jsonify({
177
263
  "status": "ok",
178
264
  "service": "c3-oracle",
265
+ "version": _c3_version(),
179
266
  "model": _cfg.get("model", "gemma4:31b-cloud"),
180
267
  "ollama_available": ollama_ok,
181
268
  "model_verified": _model_verified,
@@ -206,11 +293,9 @@ _CONFIG_SETTABLE_KEYS = frozenset(_CONFIG_DEFAULTS.keys())
206
293
  @app.route("/api/config", methods=["POST"])
207
294
  def api_config_set():
208
295
  global _cfg
209
- # Require the Bearer token: this endpoint can disable Discovery auth or
210
- # repoint ollama_base_url, so it must never be reachable unauthenticated.
211
- token = extract_bearer(request.headers.get("Authorization"))
212
- if not verify_api_key(token):
213
- return jsonify({"error": "unauthorized"}), 401
296
+ # Auth is enforced by _local_write_guard (session cookie or Bearer): this
297
+ # endpoint can disable Discovery auth or repoint ollama_base_url, so it
298
+ # must never be reachable unauthenticated.
214
299
  body = request.get_json(silent=True) or {}
215
300
  if not isinstance(body, dict):
216
301
  return jsonify({"error": "body must be a JSON object"}), 400
@@ -258,7 +343,8 @@ def api_projects():
258
343
 
259
344
  @app.route("/api/projects/scan", methods=["POST"])
260
345
  def api_projects_scan():
261
- projects = _scanner.discover() if _scanner else []
346
+ # Explicit Scan action bypasses the scanner's TTL cache.
347
+ projects = _scanner.discover(force=True) if _scanner else []
262
348
  return jsonify({"scanned": len(projects), "projects": projects})
263
349
 
264
350
 
@@ -635,6 +721,23 @@ def api_chat_conversation_state(conv_id):
635
721
 
636
722
 
637
723
  # ── Activity digest (Oracle UI) ───────────────────────────
724
+ @app.route("/api/activity/digest/latest", methods=["GET"])
725
+ def api_activity_digest_latest():
726
+ """Most recent SCHEDULED digest (written by the review loop), or null.
727
+
728
+ On-demand digests via /api/activity/digest are not persisted here; this
729
+ serves ~/.c3/oracle/activity_digests/latest.json.
730
+ """
731
+ latest = ORACLE_DIR / "activity_digests" / "latest.json"
732
+ try:
733
+ if latest.is_file():
734
+ return Response(latest.read_text(encoding="utf-8"),
735
+ mimetype="application/json")
736
+ except Exception as e:
737
+ return jsonify({"error": str(e)}), 500
738
+ return jsonify({"digest": None, "generated_at": None})
739
+
740
+
638
741
  @app.route("/api/activity/digest", methods=["GET"])
639
742
  def api_activity_digest():
640
743
  """Cross-project activity digest for the Oracle UI.
@@ -740,7 +843,7 @@ def api_discovery_mcp_info():
740
843
  })
741
844
 
742
845
 
743
- # ── Discovery API key management (local dashboard unauthenticated, loopback) ──
846
+ # ── Discovery API key management (mutations gated by _local_write_guard) ──
744
847
  def _apikey_status(reveal: bool = False) -> dict:
745
848
  """Status payload for the Discovery API token + connection info.
746
849
 
@@ -786,12 +889,14 @@ def _apikey_status(reveal: bool = False) -> dict:
786
889
  def api_apikey_get():
787
890
  """Return Discovery API token status + connection info.
788
891
 
789
- The unmasked token is only included when the caller presents a valid Bearer
790
- token; otherwise only the masked form is returned (never the raw key over
791
- HTTP unauthenticated).
892
+ The unmasked token is only included when the caller presents a valid
893
+ Bearer token or the dashboard session cookie; otherwise only the masked
894
+ form is returned (never the raw key over HTTP unauthenticated).
792
895
  """
793
896
  try:
794
- reveal = verify_api_key(extract_bearer(request.headers.get("Authorization")))
897
+ reveal = verify_api_key(
898
+ extract_bearer(request.headers.get("Authorization"))
899
+ ) or local_session.verify(request.cookies.get(local_session.COOKIE_NAME))
795
900
  return jsonify(_apikey_status(reveal=reveal))
796
901
  except Exception as e:
797
902
  return jsonify({"error": str(e)}), 500