@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0

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 (287) hide show
  1. package/README.md +132 -0
  2. package/ciel-runtime-menu.py +56 -6
  3. package/ciel_runtime.py +9379 -35909
  4. package/ciel_runtime_support/advisor_client.py +193 -0
  5. package/ciel_runtime_support/advisor_policy.py +320 -0
  6. package/ciel_runtime_support/advisor_refinement.py +160 -0
  7. package/ciel_runtime_support/advisor_request_builder.py +261 -0
  8. package/ciel_runtime_support/agy_installer.py +169 -0
  9. package/ciel_runtime_support/agy_mcp_restore.py +182 -0
  10. package/ciel_runtime_support/anthropic_model_policy.py +186 -0
  11. package/ciel_runtime_support/anthropic_response_writer.py +255 -0
  12. package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
  13. package/ciel_runtime_support/api_key_cooldown.py +159 -0
  14. package/ciel_runtime_support/architecture.py +488 -1
  15. package/ciel_runtime_support/architecture_budget.py +42 -0
  16. package/ciel_runtime_support/channel_backlog.py +90 -0
  17. package/ciel_runtime_support/channel_cli.py +119 -0
  18. package/ciel_runtime_support/channel_compact_injection.py +82 -0
  19. package/ciel_runtime_support/channel_compact_poll.py +67 -0
  20. package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
  21. package/ciel_runtime_support/channel_config_service.py +281 -0
  22. package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
  23. package/ciel_runtime_support/channel_connection_registry.py +128 -0
  24. package/ciel_runtime_support/channel_connection_worker.py +284 -0
  25. package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
  26. package/ciel_runtime_support/channel_cursor_repository.py +89 -0
  27. package/ciel_runtime_support/channel_cursor_service.py +178 -0
  28. package/ciel_runtime_support/channel_event_identity.py +212 -0
  29. package/ciel_runtime_support/channel_event_projection.py +315 -0
  30. package/ciel_runtime_support/channel_inflight.py +127 -0
  31. package/ciel_runtime_support/channel_injection.py +115 -0
  32. package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
  33. package/ciel_runtime_support/channel_launch_policy.py +180 -0
  34. package/ciel_runtime_support/channel_llm_context.py +156 -0
  35. package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
  36. package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
  37. package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
  38. package/ciel_runtime_support/channel_mcp_tools.py +240 -0
  39. package/ciel_runtime_support/channel_mcp_transport.py +394 -0
  40. package/ciel_runtime_support/channel_message_dedupe.py +65 -0
  41. package/ciel_runtime_support/channel_message_policy.py +256 -0
  42. package/ciel_runtime_support/channel_message_prompt.py +305 -0
  43. package/ciel_runtime_support/channel_message_repository.py +234 -0
  44. package/ciel_runtime_support/channel_notification_projection.py +217 -0
  45. package/ciel_runtime_support/channel_panel.py +162 -0
  46. package/ciel_runtime_support/channel_pending_injection.py +209 -0
  47. package/ciel_runtime_support/channel_pending_poll.py +109 -0
  48. package/ciel_runtime_support/channel_probe_cache.py +433 -0
  49. package/ciel_runtime_support/channel_probe_report.py +101 -0
  50. package/ciel_runtime_support/channel_runtime_environment.py +181 -0
  51. package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
  52. package/ciel_runtime_support/channel_session_repository.py +90 -0
  53. package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
  54. package/ciel_runtime_support/channel_terminal_input.py +277 -0
  55. package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
  56. package/ciel_runtime_support/channel_tool_context.py +166 -0
  57. package/ciel_runtime_support/channel_transcript.py +414 -0
  58. package/ciel_runtime_support/channel_transcript_repository.py +96 -0
  59. package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
  60. package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
  61. package/ciel_runtime_support/chat_files.py +138 -0
  62. package/ciel_runtime_support/chat_http_controller.py +235 -0
  63. package/ciel_runtime_support/claude_environment.py +375 -0
  64. package/ciel_runtime_support/claude_router.py +247 -193
  65. package/ciel_runtime_support/cli_dispatch.py +792 -0
  66. package/ciel_runtime_support/cli_parser.py +165 -0
  67. package/ciel_runtime_support/cli_usage.py +100 -0
  68. package/ciel_runtime_support/codex_app_server.py +20 -5
  69. package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
  70. package/ciel_runtime_support/codex_cli.py +42 -6
  71. package/ciel_runtime_support/codex_config.py +323 -0
  72. package/ciel_runtime_support/codex_launch_configuration.py +240 -0
  73. package/ciel_runtime_support/codex_launch_policy.py +66 -0
  74. package/ciel_runtime_support/codex_mcp_integration.py +195 -0
  75. package/ciel_runtime_support/codex_mcp_restore.py +304 -0
  76. package/ciel_runtime_support/codex_model_catalog.py +133 -0
  77. package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
  78. package/ciel_runtime_support/codex_router.py +147 -1
  79. package/ciel_runtime_support/codex_session_repository.py +115 -0
  80. package/ciel_runtime_support/codex_session_selection.py +114 -0
  81. package/ciel_runtime_support/command_asset_installer.py +103 -0
  82. package/ciel_runtime_support/compatibility_probe.py +295 -0
  83. package/ciel_runtime_support/compatibility_protocol.py +251 -0
  84. package/ciel_runtime_support/compatibility_runtime.py +166 -0
  85. package/ciel_runtime_support/compatibility_test.py +370 -0
  86. package/ciel_runtime_support/config_migrations.py +307 -0
  87. package/ciel_runtime_support/config_repository.py +175 -0
  88. package/ciel_runtime_support/config_value_codec.py +64 -0
  89. package/ciel_runtime_support/configuration_cli.py +374 -0
  90. package/ciel_runtime_support/context_compaction.py +280 -0
  91. package/ciel_runtime_support/context_setup.py +208 -0
  92. package/ciel_runtime_support/context_summary_policy.py +392 -0
  93. package/ciel_runtime_support/credential_cli.py +104 -0
  94. package/ciel_runtime_support/credential_management.py +261 -0
  95. package/ciel_runtime_support/credentials.py +269 -0
  96. package/ciel_runtime_support/executable_discovery.py +141 -0
  97. package/ciel_runtime_support/github_copilot_oauth.py +335 -0
  98. package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
  99. package/ciel_runtime_support/header_forwarding.py +73 -0
  100. package/ciel_runtime_support/headless_config.py +221 -0
  101. package/ciel_runtime_support/http_response.py +129 -0
  102. package/ciel_runtime_support/install_diagnostics.py +149 -0
  103. package/ciel_runtime_support/kimi_identity.py +123 -0
  104. package/ciel_runtime_support/launch_diagnostics.py +204 -0
  105. package/ciel_runtime_support/launch_state.py +127 -0
  106. package/ciel_runtime_support/live_api_key_controller.py +58 -0
  107. package/ciel_runtime_support/llm_config_http.py +148 -0
  108. package/ciel_runtime_support/llm_option_config.py +259 -0
  109. package/ciel_runtime_support/llm_presentation_data.py +447 -0
  110. package/ciel_runtime_support/llm_presets.py +773 -0
  111. package/ciel_runtime_support/lm_studio_runtime.py +401 -0
  112. package/ciel_runtime_support/managed_mcp_config.py +144 -0
  113. package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
  114. package/ciel_runtime_support/managed_service_cleanup.py +89 -0
  115. package/ciel_runtime_support/mcp_config_reader.py +230 -0
  116. package/ciel_runtime_support/mcp_http_proxy.py +607 -0
  117. package/ciel_runtime_support/mcp_inventory.py +59 -0
  118. package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
  119. package/ciel_runtime_support/mcp_probe_codec.py +136 -0
  120. package/ciel_runtime_support/mcp_probe_transport.py +328 -0
  121. package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
  122. package/ciel_runtime_support/mcp_proxy_config.py +107 -0
  123. package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
  124. package/ciel_runtime_support/mcp_proxy_process.py +560 -0
  125. package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
  126. package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
  127. package/ciel_runtime_support/mcp_transport.py +146 -0
  128. package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
  129. package/ciel_runtime_support/model_catalog_projection.py +61 -0
  130. package/ciel_runtime_support/model_context_hints.py +109 -0
  131. package/ciel_runtime_support/model_panel.py +147 -0
  132. package/ciel_runtime_support/model_registry_repository.py +231 -0
  133. package/ciel_runtime_support/npm_runtime.py +191 -0
  134. package/ciel_runtime_support/ollama_catalog.py +462 -0
  135. package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
  136. package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
  137. package/ciel_runtime_support/ollama_context_sync.py +87 -0
  138. package/ciel_runtime_support/ollama_forwarding.py +449 -0
  139. package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
  140. package/ciel_runtime_support/openai_chat_router.py +64 -0
  141. package/ciel_runtime_support/openai_forwarding.py +194 -0
  142. package/ciel_runtime_support/openai_responses_router.py +291 -0
  143. package/ciel_runtime_support/openai_responses_stream.py +135 -0
  144. package/ciel_runtime_support/output_budget.py +89 -0
  145. package/ciel_runtime_support/package_lifecycle.py +217 -0
  146. package/ciel_runtime_support/plan_artifact_controller.py +104 -0
  147. package/ciel_runtime_support/prelaunch.py +959 -0
  148. package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
  149. package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
  150. package/ciel_runtime_support/prelaunch_terminal.py +764 -0
  151. package/ciel_runtime_support/process_control.py +708 -0
  152. package/ciel_runtime_support/prompt_compaction.py +322 -0
  153. package/ciel_runtime_support/prompt_injection.py +176 -0
  154. package/ciel_runtime_support/protocols/__init__.py +24 -0
  155. package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
  156. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
  157. package/ciel_runtime_support/protocols/chat_projection.py +315 -0
  158. package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
  159. package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
  160. package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
  161. package/ciel_runtime_support/protocols/ollama_response.py +231 -0
  162. package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
  163. package/ciel_runtime_support/protocols/openai_responses.py +271 -0
  164. package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
  165. package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
  166. package/ciel_runtime_support/provider_adapters.py +160 -0
  167. package/ciel_runtime_support/provider_catalog_sources.py +316 -0
  168. package/ciel_runtime_support/provider_choice.py +201 -0
  169. package/ciel_runtime_support/provider_compatibility.py +165 -0
  170. package/ciel_runtime_support/provider_config_mutations.py +361 -0
  171. package/ciel_runtime_support/provider_configuration_service.py +162 -0
  172. package/ciel_runtime_support/provider_context.py +308 -0
  173. package/ciel_runtime_support/provider_contract_projection.py +73 -0
  174. package/ciel_runtime_support/provider_descriptor.py +82 -0
  175. package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
  176. package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
  177. package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
  178. package/ciel_runtime_support/provider_limits.py +457 -0
  179. package/ciel_runtime_support/provider_model_identity.py +139 -0
  180. package/ciel_runtime_support/provider_model_selection.py +431 -0
  181. package/ciel_runtime_support/provider_model_specs.py +142 -0
  182. package/ciel_runtime_support/provider_models.py +263 -0
  183. package/ciel_runtime_support/provider_network.py +176 -0
  184. package/ciel_runtime_support/provider_option_cli.py +238 -0
  185. package/ciel_runtime_support/provider_option_panel.py +275 -0
  186. package/ciel_runtime_support/provider_option_status.py +192 -0
  187. package/ciel_runtime_support/provider_policy.py +101 -0
  188. package/ciel_runtime_support/provider_query_policy.py +67 -0
  189. package/ciel_runtime_support/provider_readiness.py +112 -0
  190. package/ciel_runtime_support/provider_request_access.py +131 -0
  191. package/ciel_runtime_support/provider_request_builder.py +250 -0
  192. package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
  193. package/ciel_runtime_support/provider_runtime_info.py +113 -0
  194. package/ciel_runtime_support/provider_runtime_modes.py +150 -0
  195. package/ciel_runtime_support/provider_sampling_policy.py +46 -0
  196. package/ciel_runtime_support/provider_status.py +145 -0
  197. package/ciel_runtime_support/provider_timeout_policy.py +184 -0
  198. package/ciel_runtime_support/provider_tool_policy.py +145 -0
  199. package/ciel_runtime_support/providers/__init__.py +65 -0
  200. package/ciel_runtime_support/providers/anthropic.py +160 -0
  201. package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
  202. package/ciel_runtime_support/providers/base.py +232 -0
  203. package/ciel_runtime_support/providers/catalog.py +326 -0
  204. package/ciel_runtime_support/providers/cloud.py +194 -0
  205. package/ciel_runtime_support/providers/constants.py +54 -0
  206. package/ciel_runtime_support/providers/deepseek.py +115 -0
  207. package/ciel_runtime_support/providers/fireworks.py +158 -0
  208. package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
  209. package/ciel_runtime_support/providers/kimi.py +297 -0
  210. package/ciel_runtime_support/providers/lm_studio.py +76 -0
  211. package/ciel_runtime_support/providers/meta.py +257 -0
  212. package/ciel_runtime_support/providers/native.py +194 -0
  213. package/ciel_runtime_support/providers/nim.py +69 -0
  214. package/ciel_runtime_support/providers/nvidia.py +158 -0
  215. package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
  216. package/ciel_runtime_support/providers/ollama.py +181 -0
  217. package/ciel_runtime_support/providers/ollama_context.py +195 -0
  218. package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
  219. package/ciel_runtime_support/providers/opencode.py +220 -0
  220. package/ciel_runtime_support/providers/opencode_go.py +37 -0
  221. package/ciel_runtime_support/providers/openrouter.py +57 -0
  222. package/ciel_runtime_support/providers/vllm.py +65 -0
  223. package/ciel_runtime_support/providers/zai.py +119 -0
  224. package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
  225. package/ciel_runtime_support/rate_limit_policy.py +117 -0
  226. package/ciel_runtime_support/rate_limit_repository.py +154 -0
  227. package/ciel_runtime_support/registry.py +46 -0
  228. package/ciel_runtime_support/request_shortcuts.py +253 -0
  229. package/ciel_runtime_support/request_trace.py +323 -0
  230. package/ciel_runtime_support/response_collection.py +209 -0
  231. package/ciel_runtime_support/router_access.py +238 -0
  232. package/ciel_runtime_support/router_client_lifecycle.py +366 -0
  233. package/ciel_runtime_support/router_health_policy.py +101 -0
  234. package/ciel_runtime_support/router_http.py +513 -0
  235. package/ciel_runtime_support/router_process_lifecycle.py +401 -0
  236. package/ciel_runtime_support/router_rate_limit_service.py +285 -0
  237. package/ciel_runtime_support/router_server_runtime.py +103 -0
  238. package/ciel_runtime_support/router_shortcuts.py +201 -0
  239. package/ciel_runtime_support/routing_fallback.py +73 -0
  240. package/ciel_runtime_support/runtime_activity_repository.py +143 -0
  241. package/ciel_runtime_support/runtime_adapters.py +104 -0
  242. package/ciel_runtime_support/runtime_command_factory.py +73 -0
  243. package/ciel_runtime_support/runtime_compatibility.py +50 -0
  244. package/ciel_runtime_support/runtime_constants.py +178 -0
  245. package/ciel_runtime_support/runtime_launch.py +1602 -0
  246. package/ciel_runtime_support/runtime_llm_options.py +312 -0
  247. package/ciel_runtime_support/runtime_logging.py +161 -0
  248. package/ciel_runtime_support/runtime_paths.py +157 -0
  249. package/ciel_runtime_support/runtime_restart.py +84 -0
  250. package/ciel_runtime_support/runtime_upgrade.py +149 -0
  251. package/ciel_runtime_support/secure_json_repository.py +55 -0
  252. package/ciel_runtime_support/session_import.py +356 -0
  253. package/ciel_runtime_support/settings_repository.py +8 -0
  254. package/ciel_runtime_support/slash_command_assets.py +211 -0
  255. package/ciel_runtime_support/sse_stream.py +57 -0
  256. package/ciel_runtime_support/sse_trace.py +225 -0
  257. package/ciel_runtime_support/statusline_script.py +593 -0
  258. package/ciel_runtime_support/statusline_settings.py +53 -0
  259. package/ciel_runtime_support/stream_chunk_policy.py +18 -0
  260. package/ciel_runtime_support/streaming_anthropic.py +1955 -0
  261. package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
  262. package/ciel_runtime_support/terminal_platform_io.py +127 -0
  263. package/ciel_runtime_support/timeout_profile.py +196 -0
  264. package/ciel_runtime_support/tool_dialects.py +85 -0
  265. package/ciel_runtime_support/tool_exposure_policy.py +63 -0
  266. package/ciel_runtime_support/tool_guard_hooks.py +218 -0
  267. package/ciel_runtime_support/tool_request_projection.py +96 -0
  268. package/ciel_runtime_support/tool_schema.py +483 -0
  269. package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
  270. package/ciel_runtime_support/ui_text.py +266 -0
  271. package/ciel_runtime_support/upstream_error_policy.py +104 -0
  272. package/ciel_runtime_support/upstream_retry.py +419 -0
  273. package/ciel_runtime_support/upstream_stream_io.py +106 -0
  274. package/ciel_runtime_support/usage_events.py +96 -0
  275. package/ciel_runtime_support/visible_stream_filters.py +130 -0
  276. package/ciel_runtime_support/web_endpoints.py +447 -0
  277. package/ciel_runtime_support/web_ui.py +915 -0
  278. package/ciel_runtime_support/web_ui_controller.py +189 -0
  279. package/ciel_runtime_support/windows_console_input.py +137 -0
  280. package/ciel_runtime_support/windows_console_mode.py +112 -0
  281. package/docs/Architecture.md +54 -0
  282. package/docs/Configuration.md +17 -0
  283. package/docs/Module-Map.md +1093 -26
  284. package/docs/Providers.md +46 -1
  285. package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
  286. package/npm-bin/run-ciel-runtime.js +22 -2
  287. package/package.json +9 -2
@@ -0,0 +1,593 @@
1
+ """Embedded statusline program distributed with Ciel Runtime."""
2
+
3
+ STATUSLINE_SCRIPT = r'''#!/usr/bin/env python3
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import re
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+
12
+ HOME = Path.home()
13
+
14
+
15
+ def default_config_dir():
16
+ configured = os.environ.get("CIEL_RUNTIME_CONFIG_DIR")
17
+ if configured:
18
+ return Path(configured)
19
+ if os.name == "nt":
20
+ root = os.environ.get("APPDATA") or os.environ.get("LOCALAPPDATA")
21
+ if root:
22
+ return Path(root) / "ciel-runtime"
23
+ return HOME / "AppData" / "Roaming" / "ciel-runtime"
24
+ return HOME / ".config" / "ciel-runtime"
25
+
26
+
27
+ CONFIG_DIR = default_config_dir()
28
+ CONFIG_PATH = CONFIG_DIR / "config.json"
29
+ STATE_PATH = CONFIG_DIR / "rate-limit-state.json"
30
+ ACTIVITY_PATH = CONFIG_DIR / "router-activity.json"
31
+ COMPACT_ACTIVITY_PATH = CONFIG_DIR / "context-compact-activity.json"
32
+ CONTEXT_PATH = CONFIG_DIR / "context-usage.json"
33
+ CHAT_MESSAGES_PATH = CONFIG_DIR / "chat-messages.jsonl"
34
+ CHANNEL_LLM_CURSOR_PATH = CONFIG_DIR / "channel-llm-cursor.json"
35
+ CHANNEL_LLM_CLEAR_FLOOR_PATH = CONFIG_DIR / "channel-llm-clear-floor.json"
36
+ PALETTE = (203, 209, 215, 221, 229, 187, 151, 116, 111, 147, 183, 219)
37
+
38
+
39
+ def load_json(path, default):
40
+ try:
41
+ value = json.loads(path.read_text(encoding="utf-8"))
42
+ return value if isinstance(value, type(default)) else default
43
+ except Exception:
44
+ return default
45
+
46
+
47
+ def _status_meaningful_key(value):
48
+ text = str(value or "").strip()
49
+ if not text:
50
+ return False
51
+ return text.lower() not in {"none", "null", "unset", "missing", "not set"}
52
+
53
+
54
+ def _status_parse_api_key_list(value):
55
+ if value is None:
56
+ return []
57
+ if isinstance(value, str):
58
+ items = re.split(r"[\r\n,;]+", value)
59
+ elif isinstance(value, (list, tuple, set)):
60
+ items = []
61
+ for item in value:
62
+ items.extend(_status_parse_api_key_list(item))
63
+ else:
64
+ items = [value]
65
+ keys = []
66
+ seen = set()
67
+ for item in items:
68
+ key = str(item or "").strip()
69
+ if not _status_meaningful_key(key) or key in seen:
70
+ continue
71
+ keys.append(key)
72
+ seen.add(key)
73
+ return keys
74
+
75
+
76
+ def _status_provider_api_keys(pcfg):
77
+ keys = []
78
+ if isinstance(pcfg, dict):
79
+ keys.extend(_status_parse_api_key_list(pcfg.get("api_keys")))
80
+ keys.extend(_status_parse_api_key_list(pcfg.get("api_key")))
81
+ out = []
82
+ seen = set()
83
+ for key in keys:
84
+ if key in seen:
85
+ continue
86
+ out.append(key)
87
+ seen.add(key)
88
+ return out
89
+
90
+
91
+ def _status_provider_rotation_name(provider, pcfg):
92
+ base = str((pcfg or {}).get("base_url") or "").rstrip("/")
93
+ return f"{provider}:{base}" if base else f"{provider}:"
94
+
95
+
96
+ def _status_key_cooldown_summary(provider, pcfg, state, now):
97
+ keys = _status_provider_api_keys(pcfg)
98
+ if len(keys) <= 1 or not isinstance(state, dict):
99
+ return ""
100
+ prefix = _status_provider_rotation_name(provider, pcfg) + ":__key__:"
101
+ live = 0
102
+ next_until = 0.0
103
+ cooling = 0
104
+ for key in keys:
105
+ digest = hashlib.sha256(str(key).encode("utf-8")).hexdigest()[:12]
106
+ entry = state.get(prefix + digest)
107
+ until = 0.0
108
+ if isinstance(entry, dict):
109
+ try:
110
+ until = float(entry.get("cooldown_until") or 0.0)
111
+ except Exception:
112
+ until = 0.0
113
+ if until > now:
114
+ cooling += 1
115
+ next_until = until if not next_until else min(next_until, until)
116
+ else:
117
+ live += 1
118
+ if not cooling:
119
+ return f"RL {live}/{len(keys)}"
120
+ wait_s = max(0.0, next_until - now) if next_until else 0.0
121
+ if wait_s >= 3600:
122
+ wait_text = f"{wait_s / 3600.0:.1f}h"
123
+ elif wait_s >= 60:
124
+ wait_text = f"{wait_s / 60.0:.0f}m"
125
+ else:
126
+ wait_text = f"{wait_s:.0f}s"
127
+ text = f"RL {live}/{len(keys)} next {wait_text}"
128
+ if live == 0 and len(keys) > 1:
129
+ text = f"RL 0/{len(keys)} next {wait_text}"
130
+ return text
131
+
132
+
133
+ def color(text):
134
+ if os.environ.get("CIEL_RUNTIME_STATUSLINE_ANSI", "1").lower() in ("0", "false", "no"):
135
+ return text
136
+ phase = int(time.monotonic() * 8)
137
+ out = []
138
+ for i, ch in enumerate(text):
139
+ if ch.isspace():
140
+ out.append(ch)
141
+ else:
142
+ out.append(f"\033[1;38;5;{PALETTE[(phase + i) % len(PALETTE)]}m{ch}\033[0m")
143
+ return "".join(out)
144
+
145
+
146
+ def gray(text):
147
+ if os.environ.get("CIEL_RUNTIME_STATUSLINE_ANSI", "1").lower() in ("0", "false", "no"):
148
+ return text
149
+ return f"\033[38;5;245m{text}\033[0m"
150
+
151
+
152
+ def token_text(value):
153
+ try:
154
+ return f"{int(value):,} tok"
155
+ except Exception:
156
+ return f"{value} tok"
157
+
158
+
159
+ def token_part(value, muted=False):
160
+ text = token_text(value)
161
+ return gray(text) if muted else color(text)
162
+
163
+
164
+ def _status_positive_int(value):
165
+ try:
166
+ number = int(value)
167
+ return number if number > 0 else 0
168
+ except Exception:
169
+ return 0
170
+
171
+
172
+ def status_config_context_limit(provider, pcfg):
173
+ if not isinstance(pcfg, dict):
174
+ return 0
175
+ if provider in ("ollama", "ollama-cloud"):
176
+ fixed = str(pcfg.get("num_ctx") or "").strip().lower()
177
+ if fixed and fixed not in ("auto", "0", "false", "off", "none", "unset"):
178
+ parsed = _status_positive_int(fixed)
179
+ if parsed:
180
+ return parsed
181
+ return (
182
+ _status_positive_int(pcfg.get("num_ctx_max"))
183
+ or _status_positive_int(pcfg.get("model_context_max"))
184
+ or _status_positive_int(pcfg.get("context_window"))
185
+ or _status_positive_int(pcfg.get("max_model_len"))
186
+ )
187
+ return _status_positive_int(pcfg.get("context_window")) or _status_positive_int(pcfg.get("max_model_len"))
188
+
189
+
190
+ def context_status_text(context, provider, model, expected_limit=0):
191
+ if not isinstance(context, dict):
192
+ return ""
193
+ if str(context.get("provider") or "") != str(provider or ""):
194
+ return ""
195
+ try:
196
+ age = time.time() - float(context.get("updated_at") or 0)
197
+ except Exception:
198
+ age = 999999
199
+ if age < 0 or age > 3600:
200
+ return ""
201
+ try:
202
+ tokens = int(context.get("tokens") or 0)
203
+ except Exception:
204
+ tokens = 0
205
+ if tokens <= 0:
206
+ return ""
207
+ limit = _status_positive_int(expected_limit) or _status_positive_int(context.get("context_limit"))
208
+ if limit > 0:
209
+ pct = (tokens / limit) * 100.0
210
+ return f"ctx {tokens:,}/{limit:,} tok ({pct:.1f}%)"
211
+ return f"ctx {tokens:,} tok"
212
+
213
+
214
+ def _as_int(value, default=0):
215
+ try:
216
+ return int(value)
217
+ except Exception:
218
+ return default
219
+
220
+
221
+ def session_context_status_text(session):
222
+ if not isinstance(session, dict):
223
+ return ""
224
+ context_window = session.get("context_window")
225
+ if not isinstance(context_window, dict):
226
+ return ""
227
+ usage = context_window.get("current_usage")
228
+ if not isinstance(usage, dict):
229
+ return ""
230
+ tokens = (
231
+ _as_int(usage.get("input_tokens"))
232
+ + _as_int(usage.get("cache_creation_input_tokens"))
233
+ + _as_int(usage.get("cache_read_input_tokens"))
234
+ )
235
+ if tokens <= 0:
236
+ tokens = _as_int(context_window.get("total_input_tokens"))
237
+ if tokens <= 0:
238
+ return ""
239
+ limit = _as_int(context_window.get("context_window_size"))
240
+ pct = context_window.get("used_percentage")
241
+ if limit > 0:
242
+ try:
243
+ pct_value = float(pct) if pct is not None else (tokens / limit) * 100.0
244
+ except Exception:
245
+ pct_value = (tokens / limit) * 100.0
246
+ return f"ctx {tokens:,}/{limit:,} tok ({pct_value:.1f}%)"
247
+ return f"ctx {tokens:,} tok"
248
+
249
+
250
+ def _status_as_string_list(value):
251
+ if value is None:
252
+ return []
253
+ if isinstance(value, str):
254
+ text = value.strip()
255
+ if not text:
256
+ return []
257
+ try:
258
+ parsed = json.loads(text)
259
+ if isinstance(parsed, list):
260
+ return _status_as_string_list(parsed)
261
+ except Exception:
262
+ pass
263
+ return [text]
264
+ if isinstance(value, (list, tuple, set)):
265
+ out = []
266
+ for item in value:
267
+ out.extend(_status_as_string_list(item))
268
+ return out
269
+ return [str(value).strip()] if str(value).strip() else []
270
+
271
+
272
+ def _status_channel_message_has_external_provenance(message):
273
+ meta = message.get("meta") if isinstance(message.get("meta"), dict) else {}
274
+ for key in (
275
+ "mcp_server",
276
+ "mcp_method",
277
+ "mcp_json",
278
+ "sse_source",
279
+ "sse_event",
280
+ "sse_json",
281
+ "stream_id",
282
+ "sse_id",
283
+ "cursor",
284
+ "event_id",
285
+ "message_id",
286
+ "source_message_id",
287
+ "rpc_id",
288
+ ):
289
+ value = meta.get(key)
290
+ if value is not None and str(value).strip():
291
+ return True
292
+ return False
293
+
294
+
295
+ def _status_channel_message_skip_reason(message):
296
+ visibility = str(message.get("visibility") or "user").strip().lower()
297
+ if visibility in {"hidden", "internal", "transport", "control", "system"}:
298
+ return f"visibility_{visibility}"
299
+ recipients = {item.strip().lower() for item in _status_as_string_list(message.get("recipients"))}
300
+ if "internal" in recipients:
301
+ return "recipient_internal"
302
+ delivery = _status_as_string_list(message.get("delivery"))
303
+ if delivery:
304
+ normalized_delivery = {item.strip().lower() for item in delivery}
305
+ if not ({"all", "*", "llm"} & normalized_delivery):
306
+ return "delivery_not_llm"
307
+ meta = message.get("meta") if isinstance(message.get("meta"), dict) else {}
308
+ meta_kind = str(meta.get("kind") or meta.get("type") or meta.get("event_type") or meta.get("eventType") or meta.get("event") or meta.get("status") or "").strip().lower()
309
+ if meta_kind in {
310
+ "agent_presence",
311
+ "check-in",
312
+ "checkin",
313
+ "checkins",
314
+ "checked_in",
315
+ "colleague_presence",
316
+ "connection",
317
+ "connected",
318
+ "disconnect",
319
+ "disconnected",
320
+ "endpoint",
321
+ "heartbeat",
322
+ "initialized",
323
+ "init",
324
+ "keepalive",
325
+ "ping",
326
+ "pong",
327
+ "presence",
328
+ "ready",
329
+ "status",
330
+ "system",
331
+ "user_presence",
332
+ }:
333
+ return meta_kind
334
+ body = re.sub(r"\s+", " ", str(message.get("message") or "")).strip().lower()
335
+ kind = str(message.get("kind") or "").strip().lower()
336
+ if not body:
337
+ return "empty"
338
+ if kind in {"connection", "connected", "heartbeat", "keepalive"}:
339
+ return kind
340
+ if re.fullmatch(r"[a-z0-9_.:-]{1,80}\.(ws|sse)\.connected", body):
341
+ return "transport_connected"
342
+ if not delivery and not _status_channel_message_has_external_provenance(message):
343
+ return "unscoped_channel_message"
344
+ return ""
345
+
346
+
347
+ def channel_pending_status_count():
348
+ cursor = load_json(CHANNEL_LLM_CURSOR_PATH, {})
349
+ last_id = _as_int(cursor.get("last_id") if isinstance(cursor, dict) else 0)
350
+ count = 0
351
+ try:
352
+ if not CHAT_MESSAGES_PATH.exists():
353
+ return 0
354
+ with CHAT_MESSAGES_PATH.open("r", encoding="utf-8") as f:
355
+ for line in f:
356
+ try:
357
+ item = json.loads(line)
358
+ item_id = int(item.get("id") or 0)
359
+ except Exception:
360
+ continue
361
+ if item_id <= last_id:
362
+ continue
363
+ if _status_channel_message_skip_reason(item):
364
+ continue
365
+ count += 1
366
+ if count >= 999:
367
+ return count
368
+ except Exception:
369
+ return 0
370
+ return count
371
+
372
+
373
+ def is_ciel_runtime_session(session):
374
+ if os.environ.get("CIEL_RUNTIME_PROVIDER") or os.environ.get("CIEL_RUNTIME_MODEL_ALIAS"):
375
+ return True
376
+ if os.environ.get("CIEL_RUNTIME_STATUSLINE_FORCE", "").lower() in ("1", "true", "yes", "on"):
377
+ return True
378
+ model_name = ((session.get("model") or {}).get("display_name") if isinstance(session.get("model"), dict) else None) or ""
379
+ return str(model_name).startswith("ciel-runtime-")
380
+
381
+
382
+ def display_capacity(rpm):
383
+ if rpm <= 1:
384
+ return rpm
385
+ reserve = 1 if rpm <= 20 else max(1, int((rpm * 0.05) + 0.999))
386
+ return max(1, rpm - reserve)
387
+
388
+
389
+ def main():
390
+ try:
391
+ session = json.load(sys.stdin)
392
+ if not isinstance(session, dict):
393
+ session = {}
394
+ except Exception:
395
+ session = {}
396
+ if not is_ciel_runtime_session(session):
397
+ return
398
+ cfg = load_json(CONFIG_PATH, {})
399
+ providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
400
+ provider = str(cfg.get("current_provider") or "")
401
+ pcfg = providers.get(provider) if isinstance(providers.get(provider), dict) else {}
402
+ router_debug_external = bool(cfg.get("router_debug_external_access", False))
403
+ rpm_status = bool(pcfg.get("rate_limit_status", False))
404
+ migrations = cfg.get("migrations") if isinstance(cfg.get("migrations"), dict) else {}
405
+ if not migrations.get("rate_limit_defaults_off_20260526"):
406
+ if (
407
+ provider in ("ollama", "ollama-cloud", "nvidia-hosted", "self-hosted-nim")
408
+ and str(pcfg.get("rate_limit_rpm", "")).strip() == "40"
409
+ ):
410
+ rpm_status = False
411
+ elif provider == "lm-studio" and str(pcfg.get("rate_limit_rpm", "")).strip() == "0":
412
+ rpm_status = False
413
+ model = str(pcfg.get("current_model") or "")
414
+ raw_rpm = pcfg.get("rate_limit_rpm", 0)
415
+ try:
416
+ rpm = int(raw_rpm)
417
+ except Exception:
418
+ rpm = 0
419
+ state = load_json(STATE_PATH, {})
420
+ activity = load_json(ACTIVITY_PATH, {})
421
+ compact_activity = load_json(COMPACT_ACTIVITY_PATH, {})
422
+ context = load_json(CONTEXT_PATH, {})
423
+ now = time.time()
424
+ key = f"{provider}:__global__" if provider else ""
425
+ entry = state.get(key) if key else None
426
+ if not isinstance(entry, dict):
427
+ legacy_key = f"{provider}:{model}" if provider and model else ""
428
+ entry = state.get(legacy_key) if legacy_key else None
429
+ if not isinstance(entry, dict):
430
+ prefix = f"{provider}:"
431
+ candidates = [(k, v) for k, v in state.items() if isinstance(k, str) and k.startswith(prefix) and ":__key__:" not in k and isinstance(v, dict)]
432
+ if not candidates:
433
+ candidates = [(k, v) for k, v in state.items() if isinstance(v, dict)]
434
+ if candidates:
435
+ key, entry = max(candidates, key=lambda item: float(item[1].get("updated_at") or 0))
436
+ timestamps = entry.get("timestamps") if isinstance(entry, dict) else []
437
+ if isinstance(entry, dict):
438
+ try:
439
+ rpm = int(entry.get("rpm") or rpm)
440
+ except Exception:
441
+ pass
442
+ try:
443
+ last_wait = float(entry.get("last_wait") or 0.0) if isinstance(entry, dict) else 0.0
444
+ except Exception:
445
+ last_wait = 0.0
446
+ try:
447
+ penalty_until = float(entry.get("penalty_until") or 0.0) if isinstance(entry, dict) else 0.0
448
+ except Exception:
449
+ penalty_until = 0.0
450
+ try:
451
+ updated_at = float(entry.get("updated_at") or 0.0) if isinstance(entry, dict) else 0.0
452
+ except Exception:
453
+ updated_at = 0.0
454
+ server_remaining = entry.get("server_remaining") if isinstance(entry, dict) else None
455
+ server_reset_seconds = entry.get("server_reset_seconds") if isinstance(entry, dict) else None
456
+ server_rpm = entry.get("server_rpm") if isinstance(entry, dict) else None
457
+ server_max_concurrent = entry.get("server_max_concurrent") if isinstance(entry, dict) else None
458
+ server_active = entry.get("server_active") if isinstance(entry, dict) else None
459
+ server_queue_limit = entry.get("server_queue_limit") if isinstance(entry, dict) else None
460
+ server_queued = entry.get("server_queued") if isinstance(entry, dict) else None
461
+ used = len([ts for ts in (timestamps or []) if isinstance(ts, (int, float)) and 0.0 <= now - float(ts) < 60.0])
462
+ model_name = ((session.get("model") or {}).get("display_name") if isinstance(session.get("model"), dict) else None) or model or "model"
463
+ current_dir = ((session.get("workspace") or {}).get("current_dir") if isinstance(session.get("workspace"), dict) else None) or session.get("cwd") or ""
464
+ dir_name = Path(current_dir).name if current_dir else ""
465
+ left = f"[{model_name}]"
466
+ if dir_name:
467
+ left += f" {dir_name}"
468
+ status_parts = []
469
+ expected_ctx_limit = status_config_context_limit(provider, pcfg)
470
+ router_ctx_text = context_status_text(context, provider, model, expected_ctx_limit)
471
+ session_ctx_text = session_context_status_text(session)
472
+ ctx_text = router_ctx_text or session_ctx_text
473
+ if ctx_text:
474
+ status_parts.append(gray(ctx_text))
475
+ channel_pending = channel_pending_status_count()
476
+ if channel_pending > 0:
477
+ status_parts.append(color(f"channel queue {channel_pending}"))
478
+ if router_debug_external:
479
+ status_parts.append(color("debug external"))
480
+ if rpm_status:
481
+ key_rl_text = _status_key_cooldown_summary(provider, pcfg, state, now)
482
+ if key_rl_text:
483
+ rpm_text = key_rl_text
484
+ elif rpm > 0:
485
+ shown_limit = display_capacity(rpm)
486
+ shown_used = min(used, shown_limit)
487
+ rpm_text = f"RPM used: {shown_used}/{shown_limit}"
488
+ else:
489
+ rpm_text = f"RPM used: {used}/min (unmanaged)"
490
+ if server_rpm or server_remaining is not None or server_reset_seconds is not None:
491
+ parts = []
492
+ if server_remaining is not None:
493
+ parts.append(f"remaining {server_remaining}")
494
+ if server_rpm:
495
+ parts.append(f"limit {server_rpm}")
496
+ try:
497
+ if server_reset_seconds is not None and float(server_reset_seconds) > 0:
498
+ parts.append(f"reset {float(server_reset_seconds):.0f}s")
499
+ except Exception:
500
+ pass
501
+ if parts:
502
+ rpm_text += " | server " + ", ".join(parts)
503
+ if server_max_concurrent is not None or server_active is not None:
504
+ try:
505
+ active_text = "?" if server_active is None else str(int(server_active))
506
+ max_text = "?" if server_max_concurrent is None else str(int(server_max_concurrent))
507
+ rpm_text += f" | conc {active_text}/{max_text}"
508
+ except Exception:
509
+ pass
510
+ if server_queue_limit is not None or server_queued is not None:
511
+ try:
512
+ queued_text = "?" if server_queued is None else str(int(server_queued))
513
+ limit_text = "?" if server_queue_limit is None else str(int(server_queue_limit))
514
+ rpm_text += f" | q {queued_text}/{limit_text}"
515
+ except Exception:
516
+ pass
517
+ if penalty_until > now:
518
+ rpm_text += f" | wait {max(0.0, penalty_until - now):.0f}s"
519
+ elif last_wait >= 0.5 and 0.0 <= now - updated_at < 60.0:
520
+ rpm_text += f" | wait {last_wait:.1f}s"
521
+ status_parts.append(color(rpm_text))
522
+ activity_text = ""
523
+ if isinstance(activity, dict):
524
+ try:
525
+ age = now - float(activity.get("updated_at") or 0)
526
+ except Exception:
527
+ age = 999999
528
+ if 0 <= age < 180:
529
+ event = str(activity.get("event") or "")
530
+ if event == "retry":
531
+ activity_text = color(f"retry {activity.get('attempt')}/{activity.get('total')}")
532
+ wait = activity.get("wait")
533
+ try:
534
+ if rpm_status and wait is not None and float(wait) > 0:
535
+ activity_text += " " + color(f"wait {float(wait):.0f}s")
536
+ except Exception:
537
+ pass
538
+ tokens = activity.get("tokens")
539
+ if tokens:
540
+ activity_text += " " + color("last input") + " " + token_part(tokens, muted=rpm_status)
541
+ elif event == "request":
542
+ tokens = activity.get("tokens")
543
+ activity_text = color(f"upstream {age:.0f}s")
544
+ if tokens:
545
+ activity_text += " " + token_part(tokens, muted=rpm_status)
546
+ output_tokens = activity.get("output_tokens")
547
+ if output_tokens:
548
+ activity_text += " " + color("->") + " " + token_part(output_tokens, muted=rpm_status)
549
+ chunks = activity.get("chunks")
550
+ if chunks:
551
+ try:
552
+ activity_text += " " + color(f"({int(chunks):,} chunks)")
553
+ except Exception:
554
+ activity_text += " " + color(f"({chunks} chunks)")
555
+ elif event in ("success", "error"):
556
+ activity_text = color(f"{event} {age:.0f}s")
557
+ if activity_text:
558
+ status_parts.append(activity_text)
559
+ compact_text = ""
560
+ if isinstance(compact_activity, dict):
561
+ try:
562
+ compact_age = now - float(compact_activity.get("updated_at") or 0)
563
+ except Exception:
564
+ compact_age = 999999
565
+ if 0 <= compact_age < 180:
566
+ try:
567
+ compact_chunks = int(compact_activity.get("chunks") or 0)
568
+ except Exception:
569
+ compact_chunks = 0
570
+ try:
571
+ parallel_sessions = int(compact_activity.get("parallel_sessions") or 1)
572
+ except Exception:
573
+ parallel_sessions = 1
574
+ if compact_chunks > 0:
575
+ compact_text = color(f"compact {compact_chunks:,} chunks")
576
+ if parallel_sessions > 1:
577
+ compact_text += " " + color(f"parallel {parallel_sessions:,}/{compact_chunks:,}")
578
+ elif str(compact_activity.get("event") or "") == "compact":
579
+ compact_text = color("compact")
580
+ if compact_text:
581
+ status_parts.append(compact_text)
582
+ status_text = " | ".join(status_parts)
583
+ if status_text:
584
+ print(f"{left} | {status_text}")
585
+ else:
586
+ print(left)
587
+
588
+
589
+ if __name__ == "__main__":
590
+ main()
591
+
592
+ '''
593
+
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shlex
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Callable
8
+
9
+ from ciel_runtime_support.settings_repository import JsonSettingsRepository
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class StatusLineServices:
14
+ repository: JsonSettingsRepository
15
+ warn: Callable[[str], None]
16
+ chmod: Callable[[Path, int], None] = os.chmod
17
+
18
+
19
+ def install_statusline_settings(
20
+ script_path: Path,
21
+ script: str,
22
+ python_executable: str,
23
+ services: StatusLineServices,
24
+ ) -> None:
25
+ try:
26
+ script_path.parent.mkdir(parents=True, exist_ok=True)
27
+ if not script_path.exists() or script_path.read_text(encoding="utf-8") != script:
28
+ script_path.write_text(script, encoding="utf-8")
29
+ try:
30
+ services.chmod(script_path, 0o700)
31
+ except Exception as exc:
32
+ services.warn(
33
+ f"could not restrict status line script permissions ({type(exc).__name__}: {exc})."
34
+ )
35
+ settings = services.repository.load("statusline")
36
+ if settings is None:
37
+ services.warn(
38
+ f"could not read {services.repository.path}; status line was not installed."
39
+ )
40
+ return
41
+ command = f"{shlex.quote(python_executable)} {shlex.quote(str(script_path))}"
42
+ current = settings.get("statusLine")
43
+ if isinstance(current, dict) and current.get("command") == command:
44
+ return
45
+ settings["statusLine"] = {
46
+ "type": "command",
47
+ "command": command,
48
+ "padding": 0,
49
+ "refreshInterval": 1000,
50
+ }
51
+ services.repository.save(settings, "statusline")
52
+ except Exception as exc:
53
+ services.warn(f"could not install status line ({type(exc).__name__}: {exc}).")
@@ -0,0 +1,18 @@
1
+ """Pure buffering policy for word-boundary streaming."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def split_word_buffer(
7
+ buffer: str, *, force: bool = False, max_buffer: int = 64
8
+ ) -> tuple[str, str]:
9
+ if not buffer:
10
+ return "", ""
11
+ if force:
12
+ return buffer, ""
13
+ for index in range(len(buffer) - 1, -1, -1):
14
+ if buffer[index].isspace():
15
+ return buffer[: index + 1], buffer[index + 1 :]
16
+ if len(buffer) >= max_buffer:
17
+ return buffer, ""
18
+ return "", buffer