@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,84 @@
1
+ """Restart the active Ciel Runtime installation after self-update."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, MutableMapping, Sequence
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ def running_from_npm_package(path: Path, environ: MutableMapping[str, str]) -> bool:
11
+ if environ.get("CIEL_RUNTIME_NPM_MODE") is not None:
12
+ return True
13
+ normalized = str(path.resolve(strict=False)).replace("\\", "/")
14
+ return "/node_modules/@oneciel-ai/ciel-runtime/" in normalized
15
+
16
+
17
+ def forced_upgrade_environment(environ: MutableMapping[str, str]) -> dict[str, str]:
18
+ result = dict(environ)
19
+ result.update(CI="1", NPM_CONFIG_YES="true", npm_config_yes="true")
20
+ result.setdefault("NPM_CONFIG_UPDATE_NOTIFIER", "false")
21
+ result.setdefault("npm_config_update_notifier", "false")
22
+ return result
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class RuntimeRestartSettings:
27
+ argv: Sequence[str]
28
+ python_executable: str
29
+ environ: MutableMapping[str, str]
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class RuntimeRestartPorts:
34
+ current_package_root: Callable[[], Path | None]
35
+ global_package_root: Callable[[str], Path | None]
36
+ find_executable: Callable[[str], str | None]
37
+ execv: Callable[[str, list[str]], object]
38
+ call: Callable[..., int]
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class RuntimeRestartService:
43
+ settings: RuntimeRestartSettings
44
+ ports: RuntimeRestartPorts
45
+
46
+ def user_args(self) -> list[str]:
47
+ arguments = list(self.settings.argv[1:])
48
+ return arguments[1:] if arguments and arguments[0] == "cli" else arguments
49
+
50
+ def restart(self, npm: str, package_root: Path | None = None) -> None:
51
+ self.settings.environ["CIEL_RUNTIME_SKIP_SELF_UPDATE"] = "1"
52
+ user_args = self.user_args()
53
+ root = (
54
+ package_root
55
+ or self.ports.current_package_root()
56
+ or self.ports.global_package_root(npm)
57
+ )
58
+ package_script = root / "ciel_runtime.py" if root else None
59
+ if package_script and package_script.exists():
60
+ self.ports.execv(
61
+ self.settings.python_executable,
62
+ [self.settings.python_executable, str(package_script), "cli", *user_args],
63
+ )
64
+ return
65
+ launcher = self.ports.find_executable("ciel-runtime")
66
+ if launcher:
67
+ raise SystemExit(
68
+ self.ports.call(
69
+ [launcher, *user_args], env=dict(self.settings.environ)
70
+ )
71
+ )
72
+ self.ports.execv(
73
+ self.settings.python_executable,
74
+ [self.settings.python_executable, *self.settings.argv],
75
+ )
76
+
77
+
78
+ __all__ = [
79
+ "RuntimeRestartPorts",
80
+ "RuntimeRestartService",
81
+ "RuntimeRestartSettings",
82
+ "forced_upgrade_environment",
83
+ "running_from_npm_package",
84
+ ]
@@ -0,0 +1,149 @@
1
+ """Coordinated quiet upgrades for the runtime CLI toolchain."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Mapping
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class RuntimeUpgradeSettings:
12
+ runtime_version: str
13
+ environ: Mapping[str, str]
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class RuntimeUpgradeNpmPorts:
18
+ find_executable: Callable[[str], str | None]
19
+ latest_version: Callable[[str, str], str]
20
+ version_newer: Callable[[str, str], bool]
21
+ current_package_root: Callable[[], Path | None]
22
+ package_prefix: Callable[[Path], Path | None]
23
+ current_prefix: Callable[[], Path | None]
24
+ global_install_command: Callable[[str, str, Path | None], list[str]]
25
+ runtime_install_command: Callable[[str | None, str, Path | None], list[str]]
26
+ run_command: Callable[[list[str], float], tuple[int, str]]
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class RuntimeUpgradeToolPorts:
31
+ claude_version: Callable[[str], str]
32
+ codex_version: Callable[[str], str]
33
+ install_claude: Callable[[], str | None]
34
+ install_codex: Callable[[], str | None]
35
+ install_agy: Callable[[], str | None]
36
+ update_agy: Callable[[str, bool], str]
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class RuntimeUpgradeService:
41
+ settings: RuntimeUpgradeSettings
42
+ npm: RuntimeUpgradeNpmPorts
43
+ tools: RuntimeUpgradeToolPorts
44
+ output: Callable[[str], None]
45
+
46
+ def ciel_runtime(self) -> int:
47
+ npm = self.npm.find_executable("npm")
48
+ if not npm:
49
+ self.output("Ciel Runtime update skipped: npm was not found.")
50
+ return 1
51
+ package_spec = "@oneciel-ai/ciel-runtime@latest"
52
+ latest = self.npm.latest_version(npm, package_spec)
53
+ if latest and not self.npm.version_newer(
54
+ latest, self.settings.runtime_version
55
+ ):
56
+ self.output(
57
+ f"Ciel Runtime is up to date ({self.settings.runtime_version})."
58
+ )
59
+ return 0
60
+ self.output(f"Updating Ciel Runtime to {latest or 'latest'}...")
61
+ package_root = self.npm.current_package_root()
62
+ install_prefix = self.npm.package_prefix(package_root) if package_root else None
63
+ if install_prefix is not None:
64
+ self.output(f"Updating current Ciel Runtime install prefix: {install_prefix}")
65
+ return self._run_install(
66
+ self.npm.global_install_command(npm, package_spec, install_prefix),
67
+ "Ciel Runtime",
68
+ install_prefix,
69
+ )
70
+
71
+ def claude(self) -> int:
72
+ claude = self.npm.find_executable("claude")
73
+ if not claude:
74
+ return 0 if self.tools.install_claude() else 1
75
+ current = self.tools.claude_version(claude)
76
+ npm = self.npm.find_executable("npm")
77
+ package_spec = self.settings.environ.get(
78
+ "CIEL_RUNTIME_CLAUDE_CODE_PACKAGE", "@anthropic-ai/claude-code@latest"
79
+ )
80
+ latest = self.npm.latest_version(npm, package_spec) if npm else ""
81
+ if current and latest and not self.npm.version_newer(latest, current):
82
+ self.output(f"Claude Code is up to date ({current}).")
83
+ return 0
84
+ self.output(f"Updating Claude Code ({current or 'unknown'} -> {latest or 'latest'})...")
85
+ install_prefix = self.npm.current_prefix()
86
+ if install_prefix is not None:
87
+ self.output(f"Updating Claude Code in active npm prefix: {install_prefix}")
88
+ return self._run_install(
89
+ self.npm.runtime_install_command(npm, package_spec, install_prefix),
90
+ "Claude Code",
91
+ )
92
+
93
+ def codex(self) -> int:
94
+ codex = self.npm.find_executable("codex")
95
+ if not codex:
96
+ return 0 if self.tools.install_codex() else 1
97
+ current = self.tools.codex_version(codex)
98
+ npm = self.npm.find_executable("npm")
99
+ if not npm:
100
+ self.output("Codex update skipped: npm was not found.")
101
+ return 1
102
+ package_spec = self.settings.environ.get(
103
+ "CIEL_RUNTIME_CODEX_PACKAGE", "@openai/codex@latest"
104
+ )
105
+ latest = self.npm.latest_version(npm, package_spec)
106
+ if current and latest and not self.npm.version_newer(latest, current):
107
+ self.output(f"Codex is up to date ({current}).")
108
+ return 0
109
+ self.output(f"Updating Codex ({current or 'unknown'} -> {latest or 'latest'})...")
110
+ install_prefix = self.npm.current_prefix()
111
+ if install_prefix is not None:
112
+ self.output(f"Updating Codex in active npm prefix: {install_prefix}")
113
+ return self._run_install(
114
+ self.npm.runtime_install_command(npm, package_spec, install_prefix),
115
+ "Codex",
116
+ )
117
+
118
+ def agy(self) -> int:
119
+ agy = self.npm.find_executable("agy")
120
+ if not agy:
121
+ return 0 if self.tools.install_agy() else 1
122
+ return 0 if self.tools.update_agy(agy, True) else 1
123
+
124
+ def _run_install(
125
+ self,
126
+ command: list[str],
127
+ label: str,
128
+ install_prefix: Path | None = None,
129
+ ) -> int:
130
+ return_code, output = self.npm.run_command(command, 300.0)
131
+ if output:
132
+ self.output(output)
133
+ if return_code != 0:
134
+ self.output(f"{label} update failed ({return_code}).")
135
+ if install_prefix is not None:
136
+ self.output(
137
+ f"Update targeted the active install prefix ({install_prefix}). "
138
+ "If this prefix is not writable, reinstall or update with the "
139
+ "permissions used for that prefix."
140
+ )
141
+ return return_code
142
+
143
+
144
+ __all__ = [
145
+ "RuntimeUpgradeNpmPorts",
146
+ "RuntimeUpgradeService",
147
+ "RuntimeUpgradeSettings",
148
+ "RuntimeUpgradeToolPorts",
149
+ ]
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import time
8
+ from typing import Any, Callable
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class SecureJsonEffects:
13
+ log: Callable[[str, str], None]
14
+ chmod: Callable[[Path, int], None] = os.chmod
15
+ process_id: Callable[[], int] = os.getpid
16
+ time_ns: Callable[[], int] = time.time_ns
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class SecureJsonRepository:
21
+ path: Path
22
+ effects: SecureJsonEffects
23
+
24
+ def load(self, purpose: str) -> dict[str, Any] | None:
25
+ if not self.path.exists():
26
+ return {}
27
+ try:
28
+ value = json.loads(self.path.read_text(encoding="utf-8"))
29
+ except (OSError, UnicodeError, json.JSONDecodeError, TypeError) as exc:
30
+ self.effects.log(
31
+ "WARN",
32
+ f"secure_json_read_failed purpose={purpose} path={self.path} "
33
+ f"error={type(exc).__name__}: {exc}",
34
+ )
35
+ return None
36
+ return value if isinstance(value, dict) else {}
37
+
38
+ def save(self, value: dict[str, Any], purpose: str) -> None:
39
+ self.path.parent.mkdir(parents=True, exist_ok=True)
40
+ temporary_path = self.path.with_name(
41
+ f"{self.path.name}.{self.effects.process_id()}.{self.effects.time_ns()}.tmp"
42
+ )
43
+ temporary_path.write_text(
44
+ json.dumps(value, indent=2, ensure_ascii=False) + "\n",
45
+ encoding="utf-8",
46
+ )
47
+ try:
48
+ self.effects.chmod(temporary_path, 0o600)
49
+ except OSError as exc:
50
+ self.effects.log(
51
+ "WARN",
52
+ f"secure_json_chmod_failed purpose={purpose} path={temporary_path} "
53
+ f"error={type(exc).__name__}: {exc}",
54
+ )
55
+ temporary_path.replace(self.path)
@@ -0,0 +1,356 @@
1
+ """Cross-runtime transcript import repository and application service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import uuid
9
+ from collections.abc import Callable, Mapping
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from ciel_runtime_support.channel_transcript import (
15
+ content_text,
16
+ is_assistant_message,
17
+ user_text,
18
+ )
19
+
20
+
21
+ def normalize_import_source(value: str) -> str:
22
+ normalized = re.sub(r"[^a-z0-9]+", "", str(value or "").strip().lower())
23
+ if normalized in {"codex", "openai", "codexcli"}:
24
+ return "codex"
25
+ if normalized in {"claude", "claudecode", "anthropic"}:
26
+ return "claude"
27
+ return ""
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class ImportSessionLimits:
32
+ max_bytes: int
33
+ max_chars: int
34
+
35
+
36
+ class ImportSessionRepository:
37
+ def __init__(
38
+ self,
39
+ home: Path,
40
+ environment: Mapping[str, str],
41
+ limits: ImportSessionLimits,
42
+ ) -> None:
43
+ self.home = home
44
+ self.environment = environment
45
+ self.limits = limits
46
+
47
+ def latest(self, source: str) -> Path | None:
48
+ if source == "claude":
49
+ root, pattern = self.home / ".claude" / "projects", "*/*.jsonl"
50
+ elif source == "codex":
51
+ codex_home = Path(
52
+ self.environment.get("CODEX_HOME") or (self.home / ".codex")
53
+ ).expanduser()
54
+ root, pattern = codex_home / "sessions", "**/*.jsonl"
55
+ else:
56
+ return None
57
+ return self._latest_under(root, pattern)
58
+
59
+ @staticmethod
60
+ def _latest_under(root: Path, pattern: str) -> Path | None:
61
+ latest: Path | None = None
62
+ latest_mtime = -1.0
63
+ try:
64
+ paths = root.glob(pattern)
65
+ except (OSError, ValueError):
66
+ return None
67
+ for path in paths:
68
+ try:
69
+ mtime = path.stat().st_mtime if path.is_file() else -1.0
70
+ except OSError:
71
+ continue
72
+ if mtime > latest_mtime:
73
+ latest = path
74
+ latest_mtime = mtime
75
+ return latest
76
+
77
+ def resolve(self, source: str, path_text: str) -> tuple[Path | None, str]:
78
+ raw = str(path_text or "").strip()
79
+ if not raw:
80
+ latest = self.latest(source)
81
+ if latest is None:
82
+ return None, (
83
+ f"No {source} transcript path was provided and no recent "
84
+ f"{source} transcript was found."
85
+ )
86
+ return latest, ""
87
+ if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://", raw):
88
+ return None, "ImportSession only reads local transcript files, not URLs."
89
+ path = Path(os.path.expandvars(raw)).expanduser()
90
+ if path.is_dir():
91
+ latest = self._latest_under(path, "**/*.jsonl")
92
+ if latest is None:
93
+ return None, f"No .jsonl transcript files found under {path}."
94
+ return latest, ""
95
+ if not path.exists():
96
+ return None, f"Transcript file not found: {path}"
97
+ if not path.is_file():
98
+ return None, f"Transcript path is not a regular file: {path}"
99
+ return path, ""
100
+
101
+ def read(self, source: str, path: Path) -> tuple[str, dict[str, Any]]:
102
+ size = path.stat().st_size
103
+ truncated_bytes = size > self.limits.max_bytes
104
+ with path.open("rb") as stream:
105
+ if truncated_bytes:
106
+ stream.seek(max(0, size - self.limits.max_bytes))
107
+ raw = stream.read(self.limits.max_bytes)
108
+ text = raw.decode("utf-8", errors="replace")
109
+ lines: list[str] = []
110
+ parsed_jsonl = False
111
+ for raw_line in text.splitlines():
112
+ try:
113
+ record = json.loads(raw_line.strip())
114
+ except (TypeError, ValueError):
115
+ continue
116
+ if not isinstance(record, dict):
117
+ continue
118
+ parsed_jsonl = True
119
+ line = import_record_line(record)
120
+ if line:
121
+ lines.append(line)
122
+ imported = "\n\n".join(lines).strip() if parsed_jsonl else text.strip()
123
+ truncated_chars = len(imported) > self.limits.max_chars
124
+ if truncated_chars:
125
+ imported = imported[-self.limits.max_chars :].lstrip()
126
+ return imported, {
127
+ "source": source,
128
+ "path": str(path),
129
+ "bytes": size,
130
+ "bytes_read": len(raw),
131
+ "byte_truncated": truncated_bytes,
132
+ "char_truncated": truncated_chars,
133
+ "jsonl": parsed_jsonl,
134
+ "records": len(lines),
135
+ }
136
+
137
+
138
+ def import_tool_text(record: dict[str, Any]) -> str:
139
+ record_type = str(record.get("type") or "")
140
+ payload = record.get("payload")
141
+ payload_object = payload if isinstance(payload, dict) else {}
142
+ payload_type = str(payload_object.get("type") or "")
143
+ call_types = {"function_call", "custom_tool_call", "local_shell_call"}
144
+ result_types = {
145
+ "function_call_output",
146
+ "custom_tool_call_output",
147
+ "local_shell_call_output",
148
+ }
149
+ if record_type == "response_item" and payload_type in call_types:
150
+ name = str(
151
+ payload_object.get("name")
152
+ or payload_object.get("tool_name")
153
+ or payload_type
154
+ ).strip()
155
+ arguments = payload_object.get("arguments")
156
+ if not isinstance(arguments, str):
157
+ arguments = (
158
+ json.dumps(arguments, ensure_ascii=False, default=str)
159
+ if arguments is not None
160
+ else ""
161
+ )
162
+ return f"tool_call {name}: {arguments}".strip()
163
+ if record_type == "response_item" and payload_type in result_types:
164
+ text = content_text(
165
+ payload_object.get("output")
166
+ or payload_object.get("content")
167
+ or payload_object.get("message")
168
+ )
169
+ return f"tool_result: {text}".strip() if text else "tool_result"
170
+ message = record.get("message")
171
+ message_object = message if isinstance(message, dict) else {}
172
+ content = message_object.get("content")
173
+ if not isinstance(content, list):
174
+ return ""
175
+ parts: list[str] = []
176
+ for block in content:
177
+ if not isinstance(block, dict):
178
+ continue
179
+ if block.get("type") == "tool_use":
180
+ arguments = json.dumps(
181
+ block.get("input") or {}, ensure_ascii=False, default=str
182
+ )
183
+ parts.append(f"tool_call {block.get('name') or 'tool'}: {arguments}")
184
+ elif block.get("type") == "tool_result":
185
+ parts.append(f"tool_result: {content_text(block.get('content'))}".strip())
186
+ return "\n".join(part for part in parts if part)
187
+
188
+
189
+ def import_record_line(record: dict[str, Any]) -> str:
190
+ text = user_text(record).strip()
191
+ if text:
192
+ return "User: " + text
193
+ if is_assistant_message(record):
194
+ message = record.get("message")
195
+ message_object = message if isinstance(message, dict) else {}
196
+ payload = record.get("payload")
197
+ payload_object = payload if isinstance(payload, dict) else {}
198
+ text = content_text(message_object.get("content")).strip()
199
+ if not text:
200
+ text = content_text(
201
+ payload_object.get("content") or payload_object.get("message")
202
+ ).strip()
203
+ if text:
204
+ return "Assistant: " + text
205
+ tool_text = import_tool_text(record).strip()
206
+ return "Tool: " + tool_text if tool_text else ""
207
+
208
+
209
+ class ImportSessionService:
210
+ def __init__(
211
+ self,
212
+ repository: ImportSessionRepository,
213
+ parse_args: Callable[[dict[str, Any]], tuple[str, str]],
214
+ ) -> None:
215
+ self.repository = repository
216
+ self.parse_args = parse_args
217
+
218
+ def response_text(self, client_runtime: str, body: dict[str, Any]) -> str:
219
+ raw_source, raw_path = self.parse_args(body)
220
+ source = normalize_import_source(raw_source)
221
+ if not source:
222
+ return (
223
+ "Usage: `/ImportSession Codex [transcript-path]` or "
224
+ "`/ImportSession Claude [transcript-path]`."
225
+ )
226
+ if client_runtime == "claude" and source == "claude":
227
+ return (
228
+ "Claude transcript import into a Claude session is blocked. "
229
+ "Use Claude Code's native resume/continue flow for Claude-to-Claude "
230
+ "sessions. Use ImportSession for cross-runtime import, for example "
231
+ "`/ImportSession Codex <path>`."
232
+ )
233
+ path, error = self.repository.resolve(source, raw_path)
234
+ if error or path is None:
235
+ return f"ImportSession failed: {error}"
236
+ try:
237
+ imported, metadata = self.repository.read(source, path)
238
+ except (OSError, UnicodeError, ValueError) as exc:
239
+ return (
240
+ f"ImportSession failed while reading {path}: "
241
+ f"{type(exc).__name__}: {exc}"
242
+ )
243
+ if not imported:
244
+ return f"ImportSession found no importable transcript content in {path}."
245
+ header = self._header(client_runtime, source, path, metadata)
246
+ return "\n".join(
247
+ [
248
+ *header,
249
+ "",
250
+ "Imported transcript context:",
251
+ f'<ciel-runtime-imported-session source="{source}" path="{path}">',
252
+ imported,
253
+ "</ciel-runtime-imported-session>",
254
+ ]
255
+ )
256
+
257
+ def _header(
258
+ self,
259
+ client_runtime: str,
260
+ source: str,
261
+ path: Path,
262
+ metadata: dict[str, Any],
263
+ ) -> list[str]:
264
+ header = [
265
+ "Ciel Runtime ImportSession completed.",
266
+ f"- current client: {client_runtime}",
267
+ f"- source transcript format: {source}",
268
+ f"- transcript: {path}",
269
+ f"- parsed jsonl: {str(bool(metadata.get('jsonl'))).lower()}",
270
+ f"- imported records: {metadata.get('records')}",
271
+ ]
272
+ if metadata.get("byte_truncated"):
273
+ header.append(
274
+ f"- warning: read tail only: {metadata.get('bytes_read'):,}/"
275
+ f"{metadata.get('bytes'):,} bytes"
276
+ )
277
+ if metadata.get("char_truncated"):
278
+ header.append(
279
+ f"- warning: trimmed to last {self.repository.limits.max_chars:,} chars"
280
+ )
281
+ return header
282
+
283
+
284
+ @dataclass(frozen=True, slots=True)
285
+ class ImportSessionHttpPorts:
286
+ is_request: Callable[[dict[str, Any]], bool]
287
+ response_text: Callable[[str, dict[str, Any]], str]
288
+ load_config: Callable[[], dict[str, Any]]
289
+ current_alias: Callable[[dict[str, Any]], str]
290
+ current_provider: Callable[[dict[str, Any]], tuple[str, dict[str, Any]]]
291
+ estimate_tokens: Callable[[Any], int]
292
+ write_openai: Callable[..., Any]
293
+ write_anthropic: Callable[..., Any]
294
+ publish_event: Callable[..., Any]
295
+
296
+
297
+ @dataclass(frozen=True, slots=True)
298
+ class ImportSessionHttpController:
299
+ ports: ImportSessionHttpPorts
300
+
301
+ def handle(
302
+ self,
303
+ handler: Any,
304
+ body: dict[str, Any],
305
+ *,
306
+ client_runtime: str,
307
+ response_format: str = "anthropic",
308
+ source_body: dict[str, Any] | None = None,
309
+ ) -> bool:
310
+ if not self.ports.is_request(body):
311
+ return False
312
+ text = self.ports.response_text(client_runtime, body)
313
+ config = self.ports.load_config()
314
+ model = str(body.get("model") or self.ports.current_alias(config))
315
+ source = source_body or body
316
+ stream = bool(source.get("stream", True))
317
+ if response_format == "openai":
318
+ message = {
319
+ "id": f"msg_ciel_runtime_import_{uuid.uuid4().hex[:12]}",
320
+ "type": "message",
321
+ "role": "assistant",
322
+ "model": model,
323
+ "content": [{"type": "text", "text": text}],
324
+ "stop_reason": "end_turn",
325
+ "usage": {
326
+ "input_tokens": 1,
327
+ "output_tokens": max(1, self.ports.estimate_tokens({"text": text})),
328
+ },
329
+ }
330
+ self.ports.write_openai(
331
+ handler, message, source_body=source, stream=stream
332
+ )
333
+ else:
334
+ self.ports.write_anthropic(handler, model, text, stream)
335
+ provider = self.ports.current_provider(config)[0]
336
+ self.ports.publish_event(
337
+ level="info",
338
+ category="import_session.short_circuit",
339
+ message="ImportSession request handled locally",
340
+ provider=provider,
341
+ model=model,
342
+ data={"client_runtime": client_runtime},
343
+ )
344
+ return True
345
+
346
+
347
+ __all__ = [
348
+ "ImportSessionHttpController",
349
+ "ImportSessionHttpPorts",
350
+ "ImportSessionLimits",
351
+ "ImportSessionRepository",
352
+ "ImportSessionService",
353
+ "import_record_line",
354
+ "import_tool_text",
355
+ "normalize_import_source",
356
+ ]
@@ -0,0 +1,8 @@
1
+ from ciel_runtime_support.secure_json_repository import SecureJsonEffects, SecureJsonRepository
2
+
3
+
4
+ SettingsFileEffects = SecureJsonEffects
5
+ JsonSettingsRepository = SecureJsonRepository
6
+
7
+
8
+ __all__ = ["JsonSettingsRepository", "SettingsFileEffects"]