@remodex/rmx 1.0.2

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 (588) hide show
  1. package/AGENTS_INSTALL.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +584 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-CZqebSPQ.css +1 -0
  11. package/gui/dist/assets/index-CkETtt7P.js +71 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
  14. package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
  15. package/gui/dist/icons.svg +24 -0
  16. package/gui/dist/index.html +25 -0
  17. package/gui/dist/logo.png +0 -0
  18. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  19. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  20. package/gui/dist/provider-icons/claude-color.svg +1 -0
  21. package/gui/dist/provider-icons/cline-color.svg +16 -0
  22. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  23. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  24. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  25. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  26. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  27. package/gui/dist/provider-icons/discord.svg +1 -0
  28. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  29. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok.svg +1 -0
  34. package/gui/dist/provider-icons/groq-color.svg +1 -0
  35. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  36. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  37. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  38. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  39. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  40. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  41. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  42. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  43. package/gui/dist/provider-icons/openai.svg +1 -0
  44. package/gui/dist/provider-icons/opencode.svg +2 -0
  45. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  46. package/gui/dist/provider-icons/pi.svg +21 -0
  47. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  48. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  49. package/gui/dist/provider-icons/telegram.svg +1 -0
  50. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  51. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  52. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  53. package/package.json +118 -0
  54. package/src/AGENTS.md +28 -0
  55. package/src/adapters/anthropic-image-guard.ts +251 -0
  56. package/src/adapters/anthropic-image-normalize.ts +518 -0
  57. package/src/adapters/anthropic.ts +1205 -0
  58. package/src/adapters/azure.ts +36 -0
  59. package/src/adapters/base.ts +83 -0
  60. package/src/adapters/client-fingerprint.ts +59 -0
  61. package/src/adapters/command-code.ts +453 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/cursor-errors.ts +165 -0
  65. package/src/adapters/cursor/discovery.ts +276 -0
  66. package/src/adapters/cursor/effort-map.ts +139 -0
  67. package/src/adapters/cursor/exec-policy.ts +88 -0
  68. package/src/adapters/cursor/framing.ts +250 -0
  69. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  70. package/src/adapters/cursor/kv-store.ts +52 -0
  71. package/src/adapters/cursor/live-models.ts +153 -0
  72. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  73. package/src/adapters/cursor/live-transport.ts +1235 -0
  74. package/src/adapters/cursor/mcp-config.ts +42 -0
  75. package/src/adapters/cursor/mcp-manager.ts +333 -0
  76. package/src/adapters/cursor/message-mapper.ts +49 -0
  77. package/src/adapters/cursor/native-exec-common.ts +59 -0
  78. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  79. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  80. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  81. package/src/adapters/cursor/native-exec-network.ts +43 -0
  82. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  83. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  84. package/src/adapters/cursor/native-exec.ts +604 -0
  85. package/src/adapters/cursor/protobuf-events.ts +735 -0
  86. package/src/adapters/cursor/protobuf-request.ts +719 -0
  87. package/src/adapters/cursor/request-builder.ts +280 -0
  88. package/src/adapters/cursor/thread-continuity.ts +67 -0
  89. package/src/adapters/cursor/tool-definitions.ts +621 -0
  90. package/src/adapters/cursor/transport-retry.ts +132 -0
  91. package/src/adapters/cursor/transport.ts +57 -0
  92. package/src/adapters/cursor/types.ts +59 -0
  93. package/src/adapters/cursor.ts +196 -0
  94. package/src/adapters/google-antigravity-replay.ts +520 -0
  95. package/src/adapters/google-antigravity-wire.ts +140 -0
  96. package/src/adapters/google-errors.ts +85 -0
  97. package/src/adapters/google-http.ts +100 -0
  98. package/src/adapters/google-tool-schema.ts +173 -0
  99. package/src/adapters/google-truncation.ts +24 -0
  100. package/src/adapters/google-wire-compiler.ts +232 -0
  101. package/src/adapters/google.ts +859 -0
  102. package/src/adapters/identity.ts +77 -0
  103. package/src/adapters/image.ts +23 -0
  104. package/src/adapters/kiro-constants.ts +16 -0
  105. package/src/adapters/kiro-errors.ts +208 -0
  106. package/src/adapters/kiro-events.ts +197 -0
  107. package/src/adapters/kiro-images.ts +129 -0
  108. package/src/adapters/kiro-retry.ts +312 -0
  109. package/src/adapters/kiro-thinking.ts +104 -0
  110. package/src/adapters/kiro-tool-fallback.ts +36 -0
  111. package/src/adapters/kiro-tools.ts +224 -0
  112. package/src/adapters/kiro-truncation.ts +33 -0
  113. package/src/adapters/kiro-wire.ts +129 -0
  114. package/src/adapters/kiro.ts +1924 -0
  115. package/src/adapters/mimo-free.ts +263 -0
  116. package/src/adapters/openai-chat.ts +1265 -0
  117. package/src/adapters/openai-responses.ts +1309 -0
  118. package/src/adapters/run-turn-queue.ts +114 -0
  119. package/src/adapters/tool-catalog-nudge.ts +71 -0
  120. package/src/adapters/upstream-http-error.ts +48 -0
  121. package/src/android-remote/assets.ts +218 -0
  122. package/src/android-remote/attachments.ts +168 -0
  123. package/src/android-remote/auth.ts +237 -0
  124. package/src/android-remote/cloudflare-provisioning.ts +409 -0
  125. package/src/android-remote/cloudflare-secret.ts +106 -0
  126. package/src/android-remote/cloudflare-tunnel.ts +488 -0
  127. package/src/android-remote/cloudflared.ts +286 -0
  128. package/src/android-remote/codex-app-server.ts +565 -0
  129. package/src/android-remote/desktop-history-page.ts +936 -0
  130. package/src/android-remote/desktop-ipc.ts +3643 -0
  131. package/src/android-remote/desktop-ownership-store.ts +98 -0
  132. package/src/android-remote/desktop-project-registration.ts +129 -0
  133. package/src/android-remote/desktop-session-stream.ts +1110 -0
  134. package/src/android-remote/desktop-workspace-state.ts +443 -0
  135. package/src/android-remote/file-change-parser.ts +112 -0
  136. package/src/android-remote/gateway.ts +9108 -0
  137. package/src/android-remote/mutation-store.ts +299 -0
  138. package/src/android-remote/projection.ts +1780 -0
  139. package/src/android-remote/queued-turn-store.ts +248 -0
  140. package/src/android-remote/session-command-recovery.ts +1384 -0
  141. package/src/android-remote/store.ts +466 -0
  142. package/src/android-remote/thread-reconciliation.ts +133 -0
  143. package/src/android-remote/thread-source-paths.ts +307 -0
  144. package/src/android-remote/thread-stream.ts +546 -0
  145. package/src/android-remote/turn-activity.ts +235 -0
  146. package/src/android-remote/user-message-identity.ts +94 -0
  147. package/src/bridge.ts +1793 -0
  148. package/src/chat/inbound.ts +295 -0
  149. package/src/chat/outbound.ts +821 -0
  150. package/src/claude/agents-inject.ts +267 -0
  151. package/src/claude/alias.ts +149 -0
  152. package/src/claude/auth-detect.ts +229 -0
  153. package/src/claude/auth-mode-migration.ts +32 -0
  154. package/src/claude/auth-mode.ts +62 -0
  155. package/src/claude/context-windows.ts +189 -0
  156. package/src/claude/desktop-3p-guard.ts +35 -0
  157. package/src/claude/desktop-3p-paths.ts +84 -0
  158. package/src/claude/desktop-3p.ts +601 -0
  159. package/src/claude/desktop-health.ts +26 -0
  160. package/src/claude/desktop-profile.ts +263 -0
  161. package/src/claude/gateway-cache.ts +70 -0
  162. package/src/claude/inbound-debug.ts +163 -0
  163. package/src/claude/inbound.ts +519 -0
  164. package/src/claude/model-info.ts +154 -0
  165. package/src/claude/outbound.ts +898 -0
  166. package/src/cli/access.ts +108 -0
  167. package/src/cli/account-api.ts +296 -0
  168. package/src/cli/account-auth.ts +250 -0
  169. package/src/cli/account-catalog-refresh.ts +14 -0
  170. package/src/cli/account-extended.ts +476 -0
  171. package/src/cli/account-main.ts +317 -0
  172. package/src/cli/account.ts +297 -0
  173. package/src/cli/agent-driven.ts +70 -0
  174. package/src/cli/agent.ts +184 -0
  175. package/src/cli/catalog-prewarm.ts +27 -0
  176. package/src/cli/claude-desktop.ts +211 -0
  177. package/src/cli/claude.ts +302 -0
  178. package/src/cli/codex-shim-autorestore.ts +45 -0
  179. package/src/cli/codex-shim-readiness.ts +69 -0
  180. package/src/cli/combo.ts +124 -0
  181. package/src/cli/config-command.ts +183 -0
  182. package/src/cli/debug.ts +228 -0
  183. package/src/cli/desktop-first-run.ts +25 -0
  184. package/src/cli/doctor.ts +1022 -0
  185. package/src/cli/export-command.ts +201 -0
  186. package/src/cli/help.ts +370 -0
  187. package/src/cli/index.ts +1565 -0
  188. package/src/cli/init.ts +224 -0
  189. package/src/cli/integrations.ts +225 -0
  190. package/src/cli/interactive-confirm.ts +133 -0
  191. package/src/cli/internal-dispatch.ts +35 -0
  192. package/src/cli/launcher-context.ts +77 -0
  193. package/src/cli/models-runtime.ts +224 -0
  194. package/src/cli/models.ts +340 -0
  195. package/src/cli/observe.ts +170 -0
  196. package/src/cli/opencode.ts +587 -0
  197. package/src/cli/provider-runtime.ts +179 -0
  198. package/src/cli/provider.ts +476 -0
  199. package/src/cli/ready.ts +301 -0
  200. package/src/cli/route-policy.ts +92 -0
  201. package/src/cli/runtime-api.ts +328 -0
  202. package/src/cli/star-prompt.ts +211 -0
  203. package/src/cli/status-oauth.ts +78 -0
  204. package/src/cli/status.ts +321 -0
  205. package/src/cli/system-command.ts +196 -0
  206. package/src/cli/system-restart-client.ts +146 -0
  207. package/src/cli/tray-proxy.ts +205 -0
  208. package/src/cli/v2.ts +200 -0
  209. package/src/cli.ts +10 -0
  210. package/src/clients/config-export.ts +1109 -0
  211. package/src/codex/account-id.ts +34 -0
  212. package/src/codex/account-label.ts +34 -0
  213. package/src/codex/account-lifecycle.ts +172 -0
  214. package/src/codex/account-namespace-match.ts +63 -0
  215. package/src/codex/account-namespaces.ts +195 -0
  216. package/src/codex/account-pause.ts +20 -0
  217. package/src/codex/account-priority.ts +83 -0
  218. package/src/codex/account-runtime-state.ts +31 -0
  219. package/src/codex/account-store.ts +517 -0
  220. package/src/codex/account-usability.ts +40 -0
  221. package/src/codex/admission.ts +263 -0
  222. package/src/codex/app-server-processes.ts +799 -0
  223. package/src/codex/auth-api.ts +2098 -0
  224. package/src/codex/auth-collision.ts +107 -0
  225. package/src/codex/auth-context.ts +480 -0
  226. package/src/codex/autostart-health.ts +156 -0
  227. package/src/codex/catalog/account-models.ts +67 -0
  228. package/src/codex/catalog/aggregation.ts +471 -0
  229. package/src/codex/catalog/bundled.ts +533 -0
  230. package/src/codex/catalog/effort.ts +432 -0
  231. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  232. package/src/codex/catalog/kinds.ts +2 -0
  233. package/src/codex/catalog/metadata.ts +287 -0
  234. package/src/codex/catalog/native-models.ts +7 -0
  235. package/src/codex/catalog/parsing.ts +503 -0
  236. package/src/codex/catalog/provider-fetch.ts +2267 -0
  237. package/src/codex/catalog/sync.ts +1606 -0
  238. package/src/codex/catalog-admission.ts +197 -0
  239. package/src/codex/catalog-refresh-status.ts +87 -0
  240. package/src/codex/catalog-write-serialization.ts +242 -0
  241. package/src/codex/catalog.ts +15 -0
  242. package/src/codex/codex-write-lock.ts +384 -0
  243. package/src/codex/convergence-types.ts +593 -0
  244. package/src/codex/convergence.ts +580 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/desktop-client-processes.ts +521 -0
  249. package/src/codex/exec-invocation.ts +22 -0
  250. package/src/codex/features.ts +1091 -0
  251. package/src/codex/generation.ts +202 -0
  252. package/src/codex/history-job.ts +347 -0
  253. package/src/codex/history-lock.ts +242 -0
  254. package/src/codex/history-migration-guardian.ts +115 -0
  255. package/src/codex/history-provider.ts +1075 -0
  256. package/src/codex/history-transition.ts +105 -0
  257. package/src/codex/history-worker.ts +204 -0
  258. package/src/codex/home.ts +206 -0
  259. package/src/codex/inject-coordination.ts +257 -0
  260. package/src/codex/inject.ts +1857 -0
  261. package/src/codex/injected-marker.ts +79 -0
  262. package/src/codex/integration-record.ts +266 -0
  263. package/src/codex/internal/catalog-writer.ts +203 -0
  264. package/src/codex/internal/history-writer.ts +105 -0
  265. package/src/codex/journal.ts +172 -0
  266. package/src/codex/main-account-cache.ts +56 -0
  267. package/src/codex/main-account.ts +40 -0
  268. package/src/codex/management-convergence.ts +114 -0
  269. package/src/codex/model-cache.ts +267 -0
  270. package/src/codex/native-main-admission.ts +47 -0
  271. package/src/codex/native-main-auth-temp.ts +187 -0
  272. package/src/codex/native-main-claim.ts +167 -0
  273. package/src/codex/native-main-lock-file.ts +162 -0
  274. package/src/codex/native-main-owner.ts +329 -0
  275. package/src/codex/native-profile-api.ts +247 -0
  276. package/src/codex/native-profile-manager.ts +1531 -0
  277. package/src/codex/native-profile-processes.ts +121 -0
  278. package/src/codex/native-profile-recovery.ts +99 -0
  279. package/src/codex/native-profile-stage-store.ts +387 -0
  280. package/src/codex/native-profile-startup.ts +348 -0
  281. package/src/codex/native-profile-store.ts +855 -0
  282. package/src/codex/native-profile-types.ts +120 -0
  283. package/src/codex/native-residue.ts +691 -0
  284. package/src/codex/paths.ts +78 -0
  285. package/src/codex/plugins-doctor.ts +242 -0
  286. package/src/codex/pool-rotation.ts +295 -0
  287. package/src/codex/project-config-warnings.ts +426 -0
  288. package/src/codex/prompt-journal.ts +311 -0
  289. package/src/codex/prompt-layers.ts +967 -0
  290. package/src/codex/prompt-lock.ts +143 -0
  291. package/src/codex/provider-adoption.ts +242 -0
  292. package/src/codex/quota-rejection.ts +224 -0
  293. package/src/codex/quota.ts +494 -0
  294. package/src/codex/refresh.ts +60 -0
  295. package/src/codex/routing.ts +1855 -0
  296. package/src/codex/runtime.ts +659 -0
  297. package/src/codex/shim.ts +1215 -0
  298. package/src/codex/subagent-defaults.ts +557 -0
  299. package/src/codex/subagent-model-fallback.ts +560 -0
  300. package/src/codex/sync.ts +238 -0
  301. package/src/codex/transition-state.ts +612 -0
  302. package/src/codex/upstream-host-health.ts +368 -0
  303. package/src/codex/user-identity.ts +374 -0
  304. package/src/codex/warmup.ts +192 -0
  305. package/src/codex/websocket-registry.ts +100 -0
  306. package/src/codex/write-coordination.ts +114 -0
  307. package/src/combos/failover.ts +140 -0
  308. package/src/combos/index.ts +44 -0
  309. package/src/combos/request.ts +64 -0
  310. package/src/combos/resolve.ts +232 -0
  311. package/src/combos/types.ts +392 -0
  312. package/src/config.ts +3270 -0
  313. package/src/generated/model-metadata.ts +144 -0
  314. package/src/github/star-state.ts +203 -0
  315. package/src/grok/inject.ts +540 -0
  316. package/src/grok/inspect.ts +45 -0
  317. package/src/grok/status.ts +127 -0
  318. package/src/grok/sync.ts +66 -0
  319. package/src/images/artifacts.ts +516 -0
  320. package/src/images/fulfill-video.ts +163 -0
  321. package/src/images/fulfill.ts +149 -0
  322. package/src/images/index.ts +4 -0
  323. package/src/images/loop.ts +922 -0
  324. package/src/images/plan.ts +133 -0
  325. package/src/images/synthetic-tool.ts +133 -0
  326. package/src/images/types.ts +41 -0
  327. package/src/images/xai-client.ts +141 -0
  328. package/src/images/xai-video-client.ts +163 -0
  329. package/src/index.ts +22 -0
  330. package/src/integrations/config-io.ts +151 -0
  331. package/src/integrations/journal.ts +315 -0
  332. package/src/integrations/merge.ts +135 -0
  333. package/src/integrations/native/ownership-preflight.ts +202 -0
  334. package/src/integrations/ownership.ts +111 -0
  335. package/src/integrations/registry.ts +108 -0
  336. package/src/integrations/serialize.ts +235 -0
  337. package/src/integrations/state.ts +290 -0
  338. package/src/integrations/store.ts +103 -0
  339. package/src/integrations/writer.ts +492 -0
  340. package/src/lib/abort.ts +146 -0
  341. package/src/lib/admin-secrets.ts +25 -0
  342. package/src/lib/admission.ts +83 -0
  343. package/src/lib/app-owned-memory-stores.ts +173 -0
  344. package/src/lib/app-owned-memory.ts +265 -0
  345. package/src/lib/bounded-body.ts +242 -0
  346. package/src/lib/bun-binary-validator.d.mts +3 -0
  347. package/src/lib/bun-binary-validator.mjs +18 -0
  348. package/src/lib/bun-runtime.ts +184 -0
  349. package/src/lib/bun-stream-caps.ts +127 -0
  350. package/src/lib/config-ownership.ts +438 -0
  351. package/src/lib/crash-guard.ts +344 -0
  352. package/src/lib/debug-log-buffer.ts +83 -0
  353. package/src/lib/debug-settings.ts +108 -0
  354. package/src/lib/debug.ts +31 -0
  355. package/src/lib/destination-policy.ts +316 -0
  356. package/src/lib/errors.ts +364 -0
  357. package/src/lib/eventstream-decoder.ts +253 -0
  358. package/src/lib/gcp-adc.ts +341 -0
  359. package/src/lib/injection-debug-log.ts +58 -0
  360. package/src/lib/local-management-attestation.ts +51 -0
  361. package/src/lib/open-url.ts +25 -0
  362. package/src/lib/pinned-http.ts +182 -0
  363. package/src/lib/privacy.ts +20 -0
  364. package/src/lib/process-control.ts +168 -0
  365. package/src/lib/provider-environment.ts +470 -0
  366. package/src/lib/provider-outbound.ts +203 -0
  367. package/src/lib/provider-url.ts +14 -0
  368. package/src/lib/proxy-env.ts +18 -0
  369. package/src/lib/redact.ts +510 -0
  370. package/src/lib/remodex-home.ts +616 -0
  371. package/src/lib/retry-after.ts +55 -0
  372. package/src/lib/service-secrets.ts +178 -0
  373. package/src/lib/shadow-call.ts +54 -0
  374. package/src/lib/sidecar-tracker.ts +52 -0
  375. package/src/lib/sse-decoder.ts +364 -0
  376. package/src/lib/state-store-registrations.ts +109 -0
  377. package/src/lib/state-store-sweeper.ts +184 -0
  378. package/src/lib/system-restart-contract.ts +73 -0
  379. package/src/lib/test-home-guard.ts +98 -0
  380. package/src/lib/token-estimate.ts +69 -0
  381. package/src/lib/translator-budget.ts +366 -0
  382. package/src/lib/upstream-reachability.ts +91 -0
  383. package/src/lib/upstream-retry.ts +508 -0
  384. package/src/lib/win-exec.ts +115 -0
  385. package/src/lib/win-paths.ts +68 -0
  386. package/src/lib/windows-elevation.ts +705 -0
  387. package/src/lib/windows-secret-acl.ts +817 -0
  388. package/src/lib/windows-user-principal.ts +283 -0
  389. package/src/lib/winsw.ts +402 -0
  390. package/src/model-sources.ts +73 -0
  391. package/src/oauth/anthropic-routing.ts +594 -0
  392. package/src/oauth/anthropic.ts +177 -0
  393. package/src/oauth/callback-server.ts +294 -0
  394. package/src/oauth/chatgpt.ts +150 -0
  395. package/src/oauth/command-code.ts +239 -0
  396. package/src/oauth/cursor.ts +231 -0
  397. package/src/oauth/github-copilot.ts +428 -0
  398. package/src/oauth/google-antigravity.ts +230 -0
  399. package/src/oauth/health.ts +443 -0
  400. package/src/oauth/index.ts +1280 -0
  401. package/src/oauth/key-providers.ts +128 -0
  402. package/src/oauth/kimi.ts +213 -0
  403. package/src/oauth/kiro-credentials.ts +726 -0
  404. package/src/oauth/kiro.ts +621 -0
  405. package/src/oauth/local-token-detect.ts +121 -0
  406. package/src/oauth/log.ts +48 -0
  407. package/src/oauth/login-cli.ts +163 -0
  408. package/src/oauth/pkce.ts +15 -0
  409. package/src/oauth/store.ts +655 -0
  410. package/src/oauth/token-guardian.ts +309 -0
  411. package/src/oauth/types.ts +62 -0
  412. package/src/oauth/xai.ts +241 -0
  413. package/src/providers/alibaba-region-backup.ts +75 -0
  414. package/src/providers/alibaba-region-migration.ts +156 -0
  415. package/src/providers/alibaba-region-startup.ts +36 -0
  416. package/src/providers/antigravity-models.ts +317 -0
  417. package/src/providers/api-keys.ts +140 -0
  418. package/src/providers/base-url-choices.ts +64 -0
  419. package/src/providers/codex-capacity.ts +288 -0
  420. package/src/providers/command-code-efforts.ts +85 -0
  421. package/src/providers/context-cap.ts +73 -0
  422. package/src/providers/derive.ts +451 -0
  423. package/src/providers/free-directory.ts +187 -0
  424. package/src/providers/github-copilot-transport.ts +56 -0
  425. package/src/providers/google-vertex-location.ts +14 -0
  426. package/src/providers/key-failover.ts +271 -0
  427. package/src/providers/kiro-models.ts +67 -0
  428. package/src/providers/label.ts +19 -0
  429. package/src/providers/model-discovery-limits.ts +16 -0
  430. package/src/providers/model-discovery.ts +361 -0
  431. package/src/providers/openai-sidecar.ts +235 -0
  432. package/src/providers/openai-tier-startup.ts +27 -0
  433. package/src/providers/openai-tiers.ts +301 -0
  434. package/src/providers/openai-virtual-models.ts +83 -0
  435. package/src/providers/openrouter-routing.ts +102 -0
  436. package/src/providers/provider-id-rewrite.ts +179 -0
  437. package/src/providers/quota.ts +1942 -0
  438. package/src/providers/reasoning-capabilities.ts +336 -0
  439. package/src/providers/registry.ts +2375 -0
  440. package/src/providers/slug-codec.ts +74 -0
  441. package/src/providers/xai-transport.ts +149 -0
  442. package/src/reasoning-effort.ts +243 -0
  443. package/src/responses/compaction.ts +124 -0
  444. package/src/responses/hosted-tool-policy.ts +9 -0
  445. package/src/responses/parser.ts +714 -0
  446. package/src/responses/reasoning-envelope.ts +60 -0
  447. package/src/responses/reasoning-replay-cache.ts +106 -0
  448. package/src/responses/schema.ts +159 -0
  449. package/src/responses/spill-store.ts +431 -0
  450. package/src/responses/state.ts +1039 -0
  451. package/src/responses/tool-groups.ts +19 -0
  452. package/src/router.ts +802 -0
  453. package/src/routing/analytics.ts +377 -0
  454. package/src/routing/capability.ts +205 -0
  455. package/src/routing/cost.ts +77 -0
  456. package/src/routing/evaluator.ts +444 -0
  457. package/src/routing/health.ts +401 -0
  458. package/src/routing/history/cursor.ts +43 -0
  459. package/src/routing/history/indexer.ts +605 -0
  460. package/src/routing/history/schema.ts +72 -0
  461. package/src/routing/profile-namespace.ts +15 -0
  462. package/src/routing/profile.ts +424 -0
  463. package/src/routing/quota.ts +145 -0
  464. package/src/routing/request-evidence.ts +45 -0
  465. package/src/routing/trace.ts +686 -0
  466. package/src/server/adapter-resolve.ts +83 -0
  467. package/src/server/auth-cors.ts +606 -0
  468. package/src/server/chat-completions.ts +379 -0
  469. package/src/server/claude-messages.ts +980 -0
  470. package/src/server/effort-policy.ts +251 -0
  471. package/src/server/github-copilot-responses-repair.ts +338 -0
  472. package/src/server/gui-static.ts +152 -0
  473. package/src/server/image-retry.ts +42 -0
  474. package/src/server/images.ts +485 -0
  475. package/src/server/index.ts +1633 -0
  476. package/src/server/lifecycle.ts +482 -0
  477. package/src/server/live.ts +609 -0
  478. package/src/server/management/agent-settings-routes.ts +1180 -0
  479. package/src/server/management/android-remote-routes.ts +390 -0
  480. package/src/server/management/api-access.ts +141 -0
  481. package/src/server/management/api-key-usage.ts +167 -0
  482. package/src/server/management/body.ts +35 -0
  483. package/src/server/management/combo-routes.ts +244 -0
  484. package/src/server/management/config-routes.ts +602 -0
  485. package/src/server/management/context.ts +88 -0
  486. package/src/server/management/integration-routes.ts +538 -0
  487. package/src/server/management/logs-usage-routes.ts +516 -0
  488. package/src/server/management/model-routes.ts +519 -0
  489. package/src/server/management/model-rows.ts +143 -0
  490. package/src/server/management/native-integration-routes.ts +781 -0
  491. package/src/server/management/oauth-account-routes.ts +573 -0
  492. package/src/server/management/provider-routes.ts +781 -0
  493. package/src/server/management/request-history-routes.ts +191 -0
  494. package/src/server/management/routing-analytics-routes.ts +74 -0
  495. package/src/server/management/routing-profile-routes.ts +384 -0
  496. package/src/server/management/shared.ts +277 -0
  497. package/src/server/management/sidebar-routes.ts +106 -0
  498. package/src/server/management/sync-response.ts +69 -0
  499. package/src/server/management/system-restart.ts +433 -0
  500. package/src/server/management/system-routes.ts +141 -0
  501. package/src/server/management/usage-summary-cache.ts +86 -0
  502. package/src/server/management-api.ts +269 -0
  503. package/src/server/management-auth.ts +353 -0
  504. package/src/server/memory-watchdog.ts +156 -0
  505. package/src/server/port-reclaim.ts +307 -0
  506. package/src/server/ports.ts +156 -0
  507. package/src/server/proxy-liveness.ts +326 -0
  508. package/src/server/proxy-stop.ts +92 -0
  509. package/src/server/readiness.ts +99 -0
  510. package/src/server/relay-eager.ts +353 -0
  511. package/src/server/relay.ts +1179 -0
  512. package/src/server/request-decompress.ts +132 -0
  513. package/src/server/request-log-conversation.ts +168 -0
  514. package/src/server/request-log.ts +1072 -0
  515. package/src/server/responses/collaboration.ts +409 -0
  516. package/src/server/responses/compact.ts +710 -0
  517. package/src/server/responses/core.ts +3561 -0
  518. package/src/server/responses/encrypted-payload.ts +308 -0
  519. package/src/server/responses/fetch-helpers.ts +171 -0
  520. package/src/server/responses/passthrough-error.ts +78 -0
  521. package/src/server/responses/policy-fallback.ts +152 -0
  522. package/src/server/responses/terminal-guard.ts +230 -0
  523. package/src/server/responses/upstream-error.ts +48 -0
  524. package/src/server/responses-image-gen-repair.ts +132 -0
  525. package/src/server/responses-item-id-repair.ts +272 -0
  526. package/src/server/responses-json-events.ts +52 -0
  527. package/src/server/responses-model-rewrite.ts +29 -0
  528. package/src/server/responses-snapshot-repair.ts +621 -0
  529. package/src/server/responses.ts +10 -0
  530. package/src/server/search.ts +181 -0
  531. package/src/server/sse-frame-buffer.ts +292 -0
  532. package/src/server/sse-payload-rewrite.ts +263 -0
  533. package/src/server/startup-action-control.ts +308 -0
  534. package/src/server/startup-health-cache.ts +119 -0
  535. package/src/server/system-env.ts +418 -0
  536. package/src/server/windows-tcp-drop.ts +184 -0
  537. package/src/server/windows-tray-control.ts +41 -0
  538. package/src/server/ws-bridge.ts +470 -0
  539. package/src/service-manager-probe.ts +824 -0
  540. package/src/service.ts +3011 -0
  541. package/src/stall-timeout.ts +20 -0
  542. package/src/storage/cleanup-job.ts +57 -0
  543. package/src/storage/cleanup.ts +3085 -0
  544. package/src/storage/policy-job.ts +457 -0
  545. package/src/storage/policy-scheduler.ts +40 -0
  546. package/src/storage/policy-worker.ts +59 -0
  547. package/src/storage/policy.ts +527 -0
  548. package/src/storage/restore-job.ts +299 -0
  549. package/src/storage/restore-worker.ts +58 -0
  550. package/src/storage/scanner.ts +238 -0
  551. package/src/storage/storage-mutation-coordinator.ts +139 -0
  552. package/src/storage/worker-lifecycle.ts +215 -0
  553. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  554. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  555. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  556. package/src/tray/assets/opencodex-tray.png +0 -0
  557. package/src/tray/windows-tray.ps1 +364 -0
  558. package/src/tray/windows.ts +738 -0
  559. package/src/types.ts +1531 -0
  560. package/src/update/badge.ts +72 -0
  561. package/src/update/desktop-release.ts +1620 -0
  562. package/src/update/index.ts +402 -0
  563. package/src/update/job.ts +1906 -0
  564. package/src/update/notify.ts +261 -0
  565. package/src/update/npm-cache-preflight.d.mts +47 -0
  566. package/src/update/npm-cache-preflight.mjs +201 -0
  567. package/src/update/npm-invocation.d.mts +23 -0
  568. package/src/update/npm-invocation.mjs +94 -0
  569. package/src/update/tray-update-plan.d.mts +18 -0
  570. package/src/update/tray-update-plan.mjs +38 -0
  571. package/src/usage/cost.ts +0 -0
  572. package/src/usage/debug.ts +97 -0
  573. package/src/usage/expected-prices.ts +283 -0
  574. package/src/usage/log.ts +695 -0
  575. package/src/usage/summary.ts +585 -0
  576. package/src/usage/totals.ts +14 -0
  577. package/src/vision/anthropic-describe.ts +185 -0
  578. package/src/vision/describe.ts +127 -0
  579. package/src/vision/index.ts +558 -0
  580. package/src/vision/reasoning.ts +55 -0
  581. package/src/web-search/anthropic-executor.ts +189 -0
  582. package/src/web-search/executor.ts +105 -0
  583. package/src/web-search/format-result.ts +89 -0
  584. package/src/web-search/index.ts +196 -0
  585. package/src/web-search/loop.ts +791 -0
  586. package/src/web-search/parse.ts +235 -0
  587. package/src/web-search/progress-stream.ts +342 -0
  588. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,1109 @@
1
+ /**
2
+ * Client-neutral config export core.
3
+ *
4
+ * One pure function per client, one shared input type. Every export surface (CLI,
5
+ * management API, GUI) consumes this module so the bytes a user copies, downloads, or
6
+ * curls can never drift between surfaces.
7
+ *
8
+ * Two invariants carried over from `rmx opencode` (src/cli/opencode.ts), which owned the
9
+ * OpenCode serializer before it moved here:
10
+ *
11
+ * - **No secret is ever serialized.** Configs carry only the client's documented env
12
+ * reference (`{env:VAR}` for OpenCode, `$VAR` for Pi); the real admission key travels
13
+ * through the environment. AGENTS.md treats token serialization as a release blocker.
14
+ * - **No metadata is guessed.** A model with no authoritative context window ships
15
+ * without context/output fields, and the client applies its own defaults. Pi's `cost`
16
+ * is omitted entirely rather than zero-filled, because zeros would assert "free",
17
+ * which is false for routed providers.
18
+ *
19
+ * This module never writes a file. `destination` names the canonical path for a human;
20
+ * targeting it is the caller's explicit act.
21
+ */
22
+ import { homedir } from "node:os";
23
+ import { existsSync } from "node:fs";
24
+ import { isAbsolute, join } from "node:path";
25
+ import { shouldInjectApiAuthHeader } from "../codex/inject";
26
+ import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
27
+ import { probeHostname } from "../server/proxy-liveness";
28
+ import type { OcxConfig } from "../types";
29
+
30
+ export type { ConfigFormat };
31
+
32
+ /**
33
+ * One entry Remodex owns inside a client's config: the JSON path to it and
34
+ * the value we put there.
35
+ *
36
+ * A path list rather than a single provider key because ownership is not
37
+ * always one entry — Kimi owns its provider block AND one model entry per
38
+ * model, and a writer that only knew about the provider would strand the rest
39
+ * (devlog 260802 006 §2).
40
+ */
41
+ export interface ManagedFragment {
42
+ path: readonly string[];
43
+ value: unknown;
44
+ }
45
+
46
+ /** Everything Remodex contributes to one client's config, as one unit. */
47
+ export interface ManagedContribution {
48
+ clientId: ExportClientId;
49
+ fragments: readonly ManagedFragment[];
50
+ }
51
+
52
+ export type BuildContribution = (ctx: ExportContext) => ManagedContribution;
53
+
54
+ export interface OpencodeLaunchEnv {
55
+ [key: string]: string | undefined;
56
+ }
57
+
58
+ /** Visible catalog entry keyed by the proxy's canonical namespaced selector. */
59
+ export interface OpencodeCatalogModel {
60
+ namespaced: string;
61
+ native?: boolean;
62
+ provider?: string;
63
+ id?: string;
64
+ contextWindow?: number;
65
+ displayName?: string;
66
+ }
67
+
68
+ export interface OpencodeModelEntry {
69
+ name: string;
70
+ limit?: { context: number; output: number };
71
+ }
72
+
73
+ export interface OpencodeProviderBlock {
74
+ npm: string;
75
+ name: string;
76
+ options: {
77
+ baseURL: string;
78
+ apiKey?: string;
79
+ headers?: Record<string, string>;
80
+ };
81
+ models: Record<string, OpencodeModelEntry>;
82
+ }
83
+
84
+ export interface OpencodeGeneratedConfig {
85
+ $schema: string;
86
+ provider: Record<string, OpencodeProviderBlock>;
87
+ }
88
+
89
+ /** Provider key owned by this project; the only key any exporter ever emits. */
90
+ export const OPENCODE_PROVIDER_ID = "opencodex";
91
+
92
+ export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json";
93
+
94
+ /**
95
+ * The proxy speaks the OpenAI-compatible shape at /v1, which opencode reaches through
96
+ * the AI SDK's openai-compatible package (the same wiring users hand-write today).
97
+ */
98
+ const OPENCODE_PROVIDER_NPM = "@ai-sdk/openai-compatible";
99
+
100
+ /**
101
+ * Env var carrying the proxy admission key to opencode. The config only ever holds the
102
+ * `{env:...}` reference, so the secret never lands on disk. opencode substitutes it at
103
+ * load time.
104
+ */
105
+ export const OPENCODE_API_KEY_ENV = "OPENCODEX_OPENCODE_API_KEY";
106
+
107
+ /** Env reference shared by apiKey and the dedicated proxy admission header. */
108
+ export const OPENCODE_API_KEY_ENV_REF = `{env:${OPENCODE_API_KEY_ENV}}`;
109
+
110
+ /**
111
+ * Hermes interpolates `${VAR}` anywhere in config.yaml, so the credential stays
112
+ * in the environment exactly as it does for OpenCode.
113
+ */
114
+ export const HERMES_API_KEY_ENV = "OPENCODEX_HERMES_API_KEY";
115
+ export const HERMES_API_KEY_ENV_REF = `\${${HERMES_API_KEY_ENV}}`;
116
+
117
+ /** OpenClaw interpolates `${UPPERCASE_VAR}` and fails closed when it is unset. */
118
+ export const OPENCLAW_API_KEY_ENV = "OPENCODEX_OPENCLAW_API_KEY";
119
+ export const OPENCLAW_API_KEY_ENV_REF = `\${${OPENCLAW_API_KEY_ENV}}`;
120
+
121
+ /**
122
+ * Placeholder credential for loopback-only clients (Kimi, Pi). A loopback
123
+ * bind needs no real admission key, so we emit the same placeholder the Grok
124
+ * managed block uses rather than a user secret. Pi resolves `apiKey` before
125
+ * building its model list and hides the provider when an env reference is unset.
126
+ */
127
+ export const LOOPBACK_API_KEY_PLACEHOLDER = "opencodex-loopback";
128
+
129
+ /**
130
+ * Gajae's `apiKeyEnv` is env-name-only and fail-closed. Its sibling `apiKey`
131
+ * falls back to treating the literal text as the token when the variable is
132
+ * unset, which would silently ship a bogus credential — so we never emit it.
133
+ */
134
+ export const GAJAE_API_KEY_ENV = "OPENCODEX_GAJAE_API_KEY";
135
+
136
+ /** Pi's wire-dialect selector for an OpenAI-compatible endpoint. */
137
+ const PI_API_DIALECT = "openai-completions";
138
+
139
+ /**
140
+ * opencode's config schema rejects a `limit` block that carries `context` without
141
+ * `output`, but CatalogModel has no authoritative per-model output field. Dropping
142
+ * `limit` entirely would also throw away the authoritative context window we DO have,
143
+ * so the block is emitted with this budget standing in for the missing half.
144
+ *
145
+ * The value matches REASONING_MAX_TOKENS_CEILING in src/adapters/anthropic.ts — the
146
+ * project's existing "safe ceiling across current models" figure. It is a ceiling for
147
+ * schema validity, NOT a claim about any specific model's true maximum, and it is
148
+ * clamped to the context window so a small-context model can never be emitted with
149
+ * output > context. Pi's `maxTokens` uses the same stand-in and the same clamp.
150
+ */
151
+ export const SCHEMA_REQUIRED_OUTPUT_BUDGET = 32_000;
152
+
153
+ /** Deterministic loopback default for exported provider-block helpers in tests. */
154
+ export const OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG: OcxConfig = {
155
+ port: 10100,
156
+ hostname: "127.0.0.1",
157
+ defaultProvider: "mock",
158
+ providers: { mock: { adapter: "openai-chat", baseUrl: "http://127.0.0.1/v1" } },
159
+ } as OcxConfig;
160
+
161
+ /**
162
+ * Resolve the user's global opencode config path. opencode uses the XDG layout on every
163
+ * platform (including Windows, where it is %USERPROFILE%\.config\opencode).
164
+ */
165
+ export function opencodeGlobalConfigPath(
166
+ env: OpencodeLaunchEnv = process.env,
167
+ home: string = homedir(),
168
+ ): string {
169
+ const xdg = env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.length > 0 ? env.XDG_CONFIG_HOME : join(home, ".config");
170
+ return join(xdg, "opencode", "opencode.json");
171
+ }
172
+
173
+ const OMP_PROFILE_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
174
+ const OMP_WINDOWS_RESERVED_PROFILE_RE = /^(?:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?$/i;
175
+
176
+ function ompProfileName(env: OpencodeLaunchEnv): string | undefined {
177
+ // OMP_PROFILE wins by presence, even when explicitly empty; PI_PROFILE is
178
+ // only the legacy fallback when the canonical variable is undefined.
179
+ const raw = env.OMP_PROFILE !== undefined ? env.OMP_PROFILE : env.PI_PROFILE;
180
+ const profile = raw?.trim();
181
+ if (!profile || profile === "default") return undefined;
182
+ if (
183
+ profile === "."
184
+ || profile === ".."
185
+ || profile.endsWith(".")
186
+ || !OMP_PROFILE_NAME_RE.test(profile)
187
+ || OMP_WINDOWS_RESERVED_PROFILE_RE.test(profile)
188
+ ) {
189
+ throw new ClientPathError(`Invalid OMP profile "${raw}"`);
190
+ }
191
+ return profile;
192
+ }
193
+
194
+ /** Resolve the global Oh My Pi agent directory using OMP's own env precedence. */
195
+ export function ompAgentDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
196
+ const profile = ompProfileName(env);
197
+ if (!profile) {
198
+ const override = env.PI_CODING_AGENT_DIR?.trim();
199
+ if (override) return absoluteClientPath(override, home, "PI_CODING_AGENT_DIR");
200
+ }
201
+ // OMP treats PI_CONFIG_DIR as a directory name relative to the user's home,
202
+ // even when the value starts with `/` or `~`. Mirror that path.join contract
203
+ // exactly instead of assigning those values different filesystem semantics.
204
+ const root = join(home, env.PI_CONFIG_DIR || ".omp");
205
+ return profile ? join(root, "profiles", profile, "agent") : join(root, "agent");
206
+ }
207
+
208
+ /** OMP's canonical custom-provider catalog. */
209
+ export function ompModelsConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
210
+ const agentDir = ompAgentDir(env, home);
211
+ const yamlFallback = join(agentDir, "models.yaml");
212
+ const canonical = join(agentDir, "models.yml");
213
+ return !existsSync(canonical) && existsSync(yamlFallback) ? yamlFallback : canonical;
214
+ }
215
+
216
+ /** Compose the OpenAI-compatible proxy base URL from a live probe result. */
217
+ export function opencodeProxyBaseUrl(port: number, hostname?: string): string {
218
+ return `http://${probeHostname(hostname)}:${port}/v1`;
219
+ }
220
+
221
+ /**
222
+ * Hermes resolves its home the way cc-switch's writer does: an explicit
223
+ * `HERMES_HOME`, then Windows `%LOCALAPPDATA%\hermes`, then `~/.hermes`.
224
+ */
225
+ export function hermesHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
226
+ const override = env.HERMES_HOME?.trim();
227
+ if (override) return override;
228
+ if (process.platform === "win32") {
229
+ const local = env.LOCALAPPDATA?.trim();
230
+ return join(local && local.length > 0 ? local : join(home, "AppData", "Local"), "hermes");
231
+ }
232
+ return join(home, ".hermes");
233
+ }
234
+
235
+ export function hermesConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
236
+ return join(hermesHomeDir(env, home), "config.yaml");
237
+ }
238
+
239
+ /**
240
+ * Expand `~` against the effective home; REFUSE anything still relative.
241
+ *
242
+ * A first attempt at this called `resolve()` and claimed the path would "mean
243
+ * the same thing next time". It does not: `resolve()` only anchors the current
244
+ * invocation, so applying from one directory and disabling from another still
245
+ * resolved two different files — the second reported "not applied" and left
246
+ * the managed block behind with nothing claiming it.
247
+ *
248
+ * There is no honest way to recover the other process's cwd, so a relative
249
+ * selector is refused at the boundary instead of being silently anchored to
250
+ * whichever directory we happened to start in. A `~` path IS stable, because
251
+ * it anchors to the effective home rather than the cwd.
252
+ */
253
+ export class ClientPathError extends Error {}
254
+
255
+ function absoluteClientPath(raw: string, home: string, variable: string): string {
256
+ const trimmed = raw.trim();
257
+ if (trimmed === "~") return home;
258
+ if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) return join(home, trimmed.slice(2));
259
+ if (!isAbsolute(trimmed)) {
260
+ throw new ClientPathError(
261
+ `${variable} must be an absolute path or start with ~; "${trimmed}" depends on the working directory, `
262
+ + "so Remodex and the client would disagree about which file it names.",
263
+ );
264
+ }
265
+ return trimmed;
266
+ }
267
+
268
+ /**
269
+ * OpenClaw's EFFECTIVE home: `OPENCLAW_HOME` outranks the OS home.
270
+ *
271
+ * Everything below derives from this, which is why it is separate: a profile
272
+ * directory and the default state directory both hang off the effective home,
273
+ * not off `homedir()`.
274
+ */
275
+ function openclawEffectiveHome(env: OpencodeLaunchEnv, home: string): string {
276
+ const override = env.OPENCLAW_HOME?.trim();
277
+ return override ? absoluteClientPath(override, home, "OPENCLAW_HOME") : home;
278
+ }
279
+
280
+ /**
281
+ * OpenClaw's state directory, in the gateway's own precedence order:
282
+ *
283
+ * 1. `OPENCLAW_STATE_DIR` — an explicit relocation wins outright.
284
+ * 2. `OPENCLAW_PROFILE` — a named profile is `.openclaw-<profile>` under the
285
+ * effective home. `default` is the unnamed profile, so it stays
286
+ * `.openclaw`.
287
+ * 3. `.openclaw` under the effective home (`OPENCLAW_HOME` or the OS home).
288
+ *
289
+ * This is also what "is it installed?" detection looks at, so it has to follow
290
+ * the same selectors the gateway does — otherwise an operator running a
291
+ * profile reads as not installed while their gateway runs fine. We honor the
292
+ * ENVIRONMENT selectors only: a `--profile` flag passed to some other process
293
+ * is not something we can observe, and guessing it would be worse than
294
+ * following the same environment the user gave us.
295
+ */
296
+ export function openclawHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
297
+ const stateDir = env.OPENCLAW_STATE_DIR?.trim();
298
+ const effectiveHome = openclawEffectiveHome(env, home);
299
+ if (stateDir) return absoluteClientPath(stateDir, effectiveHome, "OPENCLAW_STATE_DIR");
300
+ const profile = env.OPENCLAW_PROFILE?.trim();
301
+ // OpenClaw compares the profile name case-insensitively, so `DEFAULT` is
302
+ // still the unnamed profile rather than a `.openclaw-DEFAULT` directory.
303
+ if (profile && profile.toLowerCase() !== "default") return join(effectiveHome, `.openclaw-${profile}`);
304
+ /*
305
+ * `.clawdbot` is not migration debris — OpenClaw still treats it as an
306
+ * active runtime candidate, preferring the modern directory when it exists
307
+ * and otherwise selecting the legacy one. Mirroring that order is not
308
+ * guessing: an install that has not migrated is one this integration would
309
+ * otherwise report as "not installed" while its gateway runs fine.
310
+ */
311
+ const modern = join(effectiveHome, ".openclaw");
312
+ if (existsSync(modern)) return modern;
313
+ const legacy = join(effectiveHome, ".clawdbot");
314
+ if (existsSync(legacy)) return legacy;
315
+ return modern;
316
+ }
317
+
318
+ /**
319
+ * The config file OpenClaw actually reads.
320
+ *
321
+ * An explicit `OPENCLAW_CONFIG_PATH` wins outright — it is a file selector, so
322
+ * it does NOT relocate the state directory that detection looks at. Otherwise
323
+ * `openclaw.json` under the resolved state directory.
324
+ *
325
+ * Ignoring these selectors meant the toggle could report success after writing
326
+ * `~/.openclaw/openclaw.json` while the running gateway read somewhere else —
327
+ * and snapshot the wrong file, so the rollback promise pointed at a file
328
+ * nobody loads.
329
+ */
330
+ export function openclawConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
331
+ const explicit = env.OPENCLAW_CONFIG_PATH?.trim();
332
+ if (explicit) return absoluteClientPath(explicit, openclawEffectiveHome(env, home), "OPENCLAW_CONFIG_PATH");
333
+ /*
334
+ * OpenClaw searches FILE candidates, not directories: a `.openclaw`
335
+ * directory that exists but holds no config does not beat an actual
336
+ * `.clawdbot/clawdbot.json`. Checking the directory first picked an
337
+ * absent modern file over a real legacy one and wrote where nothing reads.
338
+ *
339
+ * An explicit state dir still scopes the search to that directory, because
340
+ * the operator named it.
341
+ */
342
+ const stateOverride = env.OPENCLAW_STATE_DIR?.trim();
343
+ const effectiveHome = openclawEffectiveHome(env, home);
344
+ const profile = env.OPENCLAW_PROFILE?.trim();
345
+ const scoped = stateOverride !== undefined && stateOverride !== ""
346
+ || (profile !== undefined && profile !== "" && profile.toLowerCase() !== "default");
347
+ const stateDir = openclawHomeDir(env, home);
348
+ const candidates = scoped
349
+ ? [join(stateDir, "openclaw.json"), join(stateDir, "clawdbot.json")]
350
+ : [
351
+ join(effectiveHome, ".openclaw", "openclaw.json"),
352
+ join(effectiveHome, ".openclaw", "clawdbot.json"),
353
+ join(effectiveHome, ".clawdbot", "openclaw.json"),
354
+ join(effectiveHome, ".clawdbot", "clawdbot.json"),
355
+ ];
356
+ for (const candidate of candidates) {
357
+ if (existsSync(candidate)) return candidate;
358
+ }
359
+ return candidates[0]!;
360
+ }
361
+
362
+ export function kimiHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
363
+ const override = env.KIMI_CODE_HOME?.trim();
364
+ return override && override.length > 0 ? override : join(home, ".kimi-code");
365
+ }
366
+
367
+ export function kimiConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
368
+ return join(kimiHomeDir(env, home), "config.toml");
369
+ }
370
+
371
+ export function gajaeHomeDir(_env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
372
+ return join(home, ".gjc");
373
+ }
374
+
375
+ export function gajaeConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
376
+ return join(gajaeHomeDir(env, home), "agent", "models.yml");
377
+ }
378
+
379
+ /**
380
+ * One proxy-routed model destined for a client config. Deliberately narrower than
381
+ * `CatalogModel` so a serializer cannot reach for a field that does not survive the
382
+ * `/api/models` boundary.
383
+ */
384
+ export interface ExportModel {
385
+ /** Canonical proxy selector: `provider/id`, or bare slug for native. */
386
+ namespaced: string;
387
+ provider: string;
388
+ id: string;
389
+ /** Native OpenAI entry. Read by the shared label rule. */
390
+ native?: boolean;
391
+ displayName?: string;
392
+ contextWindow?: number;
393
+ inputModalities?: string[];
394
+ }
395
+
396
+ export interface ExportContext {
397
+ /** `http://host:port/v1` — the OpenAI-compatible surface the client dials. */
398
+ baseUrl: string;
399
+ models: readonly ExportModel[];
400
+ /**
401
+ * Live proxy config. Only the OpenCode path reads it: a non-loopback bind moves
402
+ * admission from `apiKey` to the `x-opencodex-api-key` header.
403
+ */
404
+ config?: OcxConfig;
405
+ }
406
+
407
+ export type ExportClientId =
408
+ | "opencode"
409
+ | "pi"
410
+ | "omp"
411
+ | "hermes"
412
+ | "openclaw"
413
+ | "kimi"
414
+ | "gajae";
415
+
416
+ export interface ExportClientSpec {
417
+ id: ExportClientId;
418
+ /** Download filename; matches the destination file's own name (003 §5). */
419
+ filename: string;
420
+ /** Canonical destination for humans. Never written to. */
421
+ destination: (env: NodeJS.ProcessEnv) => string;
422
+ /** Env var the config references; the value is never serialized. */
423
+ apiKeyEnv: string;
424
+ /** Shell line the user runs before launching the client. */
425
+ exportHint: string;
426
+ build: (ctx: ExportContext) => unknown;
427
+ /**
428
+ * Text format of the client's config file. `filename` already carries the
429
+ * extension; this drives serialization and the download media type so no
430
+ * consumer has to infer either from the name.
431
+ */
432
+ format: ConfigFormat;
433
+ /**
434
+ * Count models in THIS client's document shape. Required so a new client
435
+ * cannot be added without teaching the summarizer about it — the old
436
+ * "anything that is not OpenCode must be Pi" branch was a latent bug.
437
+ */
438
+ summarize: (document: unknown) => { modelCount: number; modelsWithoutLimits: number };
439
+ /**
440
+ * The fragments Remodex owns inside this client's config. Only the builder
441
+ * knows where a client keeps our entries, so ownership paths originate here
442
+ * rather than being re-derived by the writer.
443
+ */
444
+ buildContribution: BuildContribution;
445
+ /**
446
+ * True when the generated integration deliberately supports loopback only.
447
+ *
448
+ * `/v1/chat/completions` rejects bearer credentials and requires the
449
+ * dedicated `x-opencodex-api-key` header (AUTH_MATRIX in
450
+ * src/server/auth-cors.ts). If this exporter cannot safely emit that header,
451
+ * it refuses a remote bind rather than generating a config that 401s. Same
452
+ * reasoning as the Grok managed block's non-loopback refusal.
453
+ */
454
+ loopbackOnly: boolean;
455
+ }
456
+
457
+ /**
458
+ * Authoritative context window, or undefined. Never guesses: a missing, non-finite, or
459
+ * non-positive value means the serializer omits every context-derived field.
460
+ */
461
+ function authoritativeContextWindow(contextWindow: number | undefined): number | undefined {
462
+ if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {
463
+ return Math.floor(contextWindow);
464
+ }
465
+ return undefined;
466
+ }
467
+
468
+ /** Schema-required output budget for a known context window. */
469
+ function outputBudgetFor(context: number): number {
470
+ return Math.min(SCHEMA_REQUIRED_OUTPUT_BUDGET, context);
471
+ }
472
+
473
+ /**
474
+ * Modalities a given client's schema will actually accept.
475
+ *
476
+ * Our internal vocabulary is `text | image | audio` (ALLOWED_INPUT_MODALITIES in
477
+ * src/server/management/model-routes.ts). Pi and Gajae accept only
478
+ * `text | image`, and both reject the WHOLE config file over one out-of-enum
479
+ * value — Gajae reports `/providers/opencodex/models/N/input/2: Invalid option`
480
+ * and falls back to its built-in list, Pi returns an empty model config. So a
481
+ * single `audio` model takes every routed model down with it. That is not
482
+ * hypothetical: zenmux/meta-muse-spark-1.1 advertises audio and did exactly
483
+ * this. It is also the same defect the Codex catalog had with `video`, where
484
+ * the app showed zero apps (tests/catalog-input-modality-enum.test.ts).
485
+ *
486
+ * UNKNOWN and INCOMPATIBLE are different inputs, and the Codex fix could
487
+ * conflate them safely only because its enum is wider. A model with nothing
488
+ * declared is unknown, and `text` is the honest floor — every routed model takes
489
+ * prompts. A model declaring `["audio"]` and nothing else is incompatible with a
490
+ * text|image client, and rewriting it to `["text"]` would advertise a capability
491
+ * it does not have. That input is reachable three ways: `rmx models add
492
+ * --modalities audio`, `/api/custom-models`, and provider discovery.
493
+ *
494
+ * So unknown falls back to text and incompatible returns null, which drops the
495
+ * row. Omitting a model costs the user a line in a picker; fabricating `text`
496
+ * costs them a model that fails at call time with no explanation.
497
+ *
498
+ * Deliberately NOT applied in `ExportModel` construction: the management and CLI
499
+ * boundaries carry catalog modalities verbatim on purpose, and stripping `audio`
500
+ * globally would destroy valid metadata before the destination is known.
501
+ */
502
+ const CLIENT_INPUT_MODALITIES: Record<"pi" | "gajae", ReadonlySet<string>> = {
503
+ pi: new Set(["text", "image"]),
504
+ gajae: new Set(["text", "image"]),
505
+ };
506
+
507
+ /** `null` means the model cannot be represented for this client — drop the row. */
508
+ function inputModalitiesForClient(
509
+ client: "pi" | "gajae",
510
+ modalities: readonly string[] | undefined,
511
+ ): string[] | null {
512
+ const declared = modalities ?? [];
513
+ if (declared.length === 0) return ["text"];
514
+ const accepted = CLIENT_INPUT_MODALITIES[client];
515
+ const kept: string[] = [];
516
+ for (const value of declared) {
517
+ if (accepted.has(value) && !kept.includes(value)) kept.push(value);
518
+ }
519
+ return kept.length > 0 ? kept : null;
520
+ }
521
+
522
+ /**
523
+ * Label shared by every client: `"<displayName|id> (<native|provider|routed>)"`. The
524
+ * provider suffix is what makes two same-named models from different upstreams
525
+ * distinguishable in a client's model picker.
526
+ */
527
+ function exportModelLabel(model: OpencodeCatalogModel): string {
528
+ const providerLabel = model.native ? "native" : (model.provider ?? "routed");
529
+ const id = model.id ?? model.namespaced;
530
+ if (model.displayName && model.displayName.length > 0) {
531
+ return `${model.displayName} (${providerLabel})`;
532
+ }
533
+ return `${id} (${providerLabel})`;
534
+ }
535
+
536
+ function opencodeProviderOptions(baseURL: string, config: OcxConfig): OpencodeProviderBlock["options"] {
537
+ const options: OpencodeProviderBlock["options"] = { baseURL };
538
+ // Non-loopback binds accept proxy admission only via x-opencodex-api-key so Authorization
539
+ // stays free for Codex Direct upstream credentials when applicable.
540
+ if (shouldInjectApiAuthHeader(config)) {
541
+ options.headers = { "x-opencodex-api-key": OPENCODE_API_KEY_ENV_REF };
542
+ return options;
543
+ }
544
+ options.apiKey = OPENCODE_API_KEY_ENV_REF;
545
+ return options;
546
+ }
547
+
548
+ /**
549
+ * `opencodex` provider block for a resolved base URL.
550
+ *
551
+ * `limit.context` is emitted ONLY from an authoritative context window — never guessed.
552
+ * When none is available the whole `limit` block is dropped and opencode keeps its own
553
+ * defaults; when one is present, `limit.output` rides along (opencode's schema requires
554
+ * the pair) clamped to the context window.
555
+ */
556
+ function opencodeProviderBlock(
557
+ baseURL: string,
558
+ catalogModels: readonly OpencodeCatalogModel[],
559
+ config: OcxConfig,
560
+ ): OpencodeProviderBlock {
561
+ const models: Record<string, OpencodeModelEntry> = {};
562
+ for (const model of catalogModels) {
563
+ const key = model.namespaced;
564
+ if (models[key]) continue; // first entry wins; native rows lead /api/models
565
+ const entry: OpencodeModelEntry = { name: exportModelLabel(model) };
566
+ const context = authoritativeContextWindow(model.contextWindow);
567
+ if (context !== undefined) {
568
+ entry.limit = { context, output: outputBudgetFor(context) };
569
+ }
570
+ models[key] = entry;
571
+ }
572
+ return {
573
+ npm: OPENCODE_PROVIDER_NPM,
574
+ name: "Remodex",
575
+ options: opencodeProviderOptions(baseURL, config),
576
+ models,
577
+ };
578
+ }
579
+
580
+ /**
581
+ * Build the `opencodex` provider block from proxy catalog rows keyed by each row's
582
+ * canonical `namespaced` selector. Used by the `rmx opencode` launcher, which injects
583
+ * the block through OpenCode's inline runtime layer rather than any file.
584
+ */
585
+ export function buildOpencodeProviderBlockFromCatalog(
586
+ port: number,
587
+ catalogModels: readonly OpencodeCatalogModel[],
588
+ hostname?: string,
589
+ config: OcxConfig = OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG,
590
+ ): OpencodeProviderBlock {
591
+ return opencodeProviderBlock(opencodeProxyBaseUrl(port, hostname), catalogModels, config);
592
+ }
593
+
594
+ /**
595
+ * Shared precondition for every serializer: drop duplicate `namespaced` (first wins,
596
+ * native rows lead `/api/models`) and sort by `namespaced` so two calls with the same
597
+ * models produce identical bytes. Stability matters because the GUI shows a diffable
598
+ * preview and agents may checksum the payload.
599
+ */
600
+ export function normalizeExportModels(models: readonly ExportModel[]): ExportModel[] {
601
+ const seen = new Set<string>();
602
+ const unique: ExportModel[] = [];
603
+ for (const model of models) {
604
+ if (seen.has(model.namespaced)) continue;
605
+ seen.add(model.namespaced);
606
+ unique.push(model);
607
+ }
608
+ return unique.sort((a, b) => (a.namespaced < b.namespaced ? -1 : a.namespaced > b.namespaced ? 1 : 0));
609
+ }
610
+
611
+ /** OpenCode V1 document: our provider block plus `$schema`, and nothing else. */
612
+ function buildOpencodeClientConfig(ctx: ExportContext): OpencodeGeneratedConfig {
613
+ const block = opencodeProviderBlock(
614
+ ctx.baseUrl,
615
+ normalizeExportModels(ctx.models),
616
+ ctx.config ?? OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG,
617
+ );
618
+ return { $schema: OPENCODE_CONFIG_SCHEMA, provider: { [OPENCODE_PROVIDER_ID]: block } };
619
+ }
620
+
621
+ export interface PiModelEntry {
622
+ id: string;
623
+ name: string;
624
+ input: string[];
625
+ contextWindow?: number;
626
+ maxTokens?: number;
627
+ }
628
+
629
+ export interface PiProviderBlock {
630
+ baseUrl: string;
631
+ api: string;
632
+ apiKey: string;
633
+ models: PiModelEntry[];
634
+ }
635
+
636
+ export interface PiGeneratedConfig {
637
+ providers: Record<string, PiProviderBlock>;
638
+ }
639
+
640
+ /**
641
+ * Hermes `~/.hermes/config.yaml`. We emit ONLY the provider entry — never
642
+ * `model.default` — because hijacking the user's main model is not what a
643
+ * connect action asks for.
644
+ */
645
+ export interface HermesProviderBlock {
646
+ api: string;
647
+ api_key: string;
648
+ api_mode: "chat_completions";
649
+ /** We supply the list, so skip their live `/models` probe. */
650
+ discover_models: false;
651
+ models: string[];
652
+ extra_headers?: Record<string, string>;
653
+ }
654
+
655
+ export interface HermesGeneratedConfig {
656
+ providers: Record<string, HermesProviderBlock>;
657
+ }
658
+
659
+ export interface OpenclawModelEntry {
660
+ id: string;
661
+ name: string;
662
+ contextWindow?: number;
663
+ }
664
+
665
+ export interface OpenclawProviderBlock {
666
+ baseUrl: string;
667
+ apiKey: string;
668
+ api: "openai-completions";
669
+ models: OpenclawModelEntry[];
670
+ headers?: Record<string, string>;
671
+ }
672
+
673
+ /** `mode: "merge"` keeps OpenClaw's bundled catalog alongside ours. */
674
+ export interface OpenclawGeneratedConfig {
675
+ models: {
676
+ mode: "merge";
677
+ providers: Record<string, OpenclawProviderBlock>;
678
+ };
679
+ }
680
+
681
+ export interface KimiProviderBlock {
682
+ type: "openai";
683
+ base_url: string;
684
+ api_key: string;
685
+ }
686
+
687
+ /**
688
+ * `max_context_size` is mandatory and must be positive, so a model with no
689
+ * authoritative context window is omitted from the document entirely rather
690
+ * than guessed at. `capabilities` is never emitted: our catalog does not
691
+ * assert them, and Kimi's own inference works off OpenAI-style name prefixes
692
+ * that a routed selector will not match.
693
+ */
694
+ export interface KimiModelBlock {
695
+ provider: string;
696
+ model: string;
697
+ max_context_size: number;
698
+ display_name?: string;
699
+ }
700
+
701
+ export interface KimiGeneratedConfig {
702
+ providers: Record<string, KimiProviderBlock>;
703
+ models: Record<string, KimiModelBlock>;
704
+ }
705
+
706
+ export interface GajaeModelEntry {
707
+ id: string;
708
+ name: string;
709
+ input: string[];
710
+ contextWindow?: number;
711
+ maxTokens?: number;
712
+ }
713
+
714
+ /** Gajae validates strictly: an unknown field fails the whole config. */
715
+ export interface GajaeProviderBlock {
716
+ baseUrl: string;
717
+ apiKeyEnv: string;
718
+ api: "openai-completions";
719
+ models: GajaeModelEntry[];
720
+ }
721
+
722
+ export interface GajaeGeneratedConfig {
723
+ providers: Record<string, GajaeProviderBlock>;
724
+ }
725
+
726
+ /**
727
+ * Pi's `~/.pi/agent/models.json` shape. `models` is an ARRAY (identity lives in `id`),
728
+ * unlike OpenCode's keyed object.
729
+ *
730
+ * Two fields are deliberately absent. `cost` requires all four price fields and we have
731
+ * no price data at all, so emitting zeros would assert every routed model is free.
732
+ * `reasoning` is a boolean in Pi while our catalog carries an effort list — mapping one
733
+ * to the other would be a guess.
734
+ *
735
+ * Pi's input enum IS verified: its documented model configuration accepts only
736
+ * `text` and `image`, and a validation failure yields an EMPTY model config
737
+ * rather than dropping the offending entry — one bad value costs every routed
738
+ * model. The rest of this contract (omitting `cost` and `reasoning`) is still
739
+ * ours rather than a claim about Pi's acceptance.
740
+ */
741
+ function buildPiClientConfig(ctx: ExportContext): PiGeneratedConfig {
742
+ const models: PiModelEntry[] = [];
743
+ for (const model of normalizeExportModels(ctx.models)) {
744
+ // Text is the one modality every routed model supports; anything richer must come
745
+ // from the catalog rather than an assumption — and must still be inside the enum
746
+ // Pi accepts, because one rejected value empties the whole config.
747
+ const input = inputModalitiesForClient("pi", model.inputModalities);
748
+ // An audio-only model has no honest representation here; claiming `text`
749
+ // would fail at call time instead, so the row is dropped.
750
+ if (input === null) continue;
751
+ const entry: PiModelEntry = {
752
+ id: model.namespaced,
753
+ name: exportModelLabel(model),
754
+ input,
755
+ };
756
+ const context = authoritativeContextWindow(model.contextWindow);
757
+ if (context !== undefined) {
758
+ entry.contextWindow = context;
759
+ entry.maxTokens = outputBudgetFor(context);
760
+ }
761
+ models.push(entry);
762
+ }
763
+ return {
764
+ providers: {
765
+ [OPENCODE_PROVIDER_ID]: {
766
+ baseUrl: ctx.baseUrl,
767
+ api: PI_API_DIALECT,
768
+ apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
769
+ models,
770
+ },
771
+ },
772
+ };
773
+ }
774
+
775
+ /** Extra headers a non-loopback bind needs, or nothing on loopback. */
776
+ function proxyAdmissionHeaders(config: OcxConfig | undefined, envRef: string): Record<string, string> | undefined {
777
+ return shouldInjectApiAuthHeader(config) ? { "x-opencodex-api-key": envRef } : undefined;
778
+ }
779
+
780
+ function buildHermesClientConfig(ctx: ExportContext): HermesGeneratedConfig {
781
+ const models = normalizeExportModels(ctx.models).map(model => model.namespaced);
782
+ const headers = proxyAdmissionHeaders(ctx.config, HERMES_API_KEY_ENV_REF);
783
+ return {
784
+ providers: {
785
+ [OPENCODE_PROVIDER_ID]: {
786
+ api: ctx.baseUrl,
787
+ api_key: HERMES_API_KEY_ENV_REF,
788
+ api_mode: "chat_completions",
789
+ discover_models: false,
790
+ models,
791
+ ...(headers ? { extra_headers: headers } : {}),
792
+ },
793
+ },
794
+ };
795
+ }
796
+
797
+ function buildOpenclawClientConfig(ctx: ExportContext): OpenclawGeneratedConfig {
798
+ const models: OpenclawModelEntry[] = normalizeExportModels(ctx.models).map(model => {
799
+ const context = authoritativeContextWindow(model.contextWindow);
800
+ return {
801
+ id: model.namespaced,
802
+ name: exportModelLabel(model),
803
+ ...(context !== undefined ? { contextWindow: context } : {}),
804
+ };
805
+ });
806
+ const headers = proxyAdmissionHeaders(ctx.config, OPENCLAW_API_KEY_ENV_REF);
807
+ return {
808
+ models: {
809
+ mode: "merge",
810
+ providers: {
811
+ [OPENCODE_PROVIDER_ID]: {
812
+ baseUrl: ctx.baseUrl,
813
+ apiKey: OPENCLAW_API_KEY_ENV_REF,
814
+ api: "openai-completions",
815
+ models,
816
+ ...(headers ? { headers } : {}),
817
+ },
818
+ },
819
+ },
820
+ };
821
+ }
822
+
823
+ /** Kimi's model alias: one key per model, namespaced under our provider id. */
824
+ export function kimiModelAlias(namespaced: string): string {
825
+ return `${OPENCODE_PROVIDER_ID}/${namespaced}`;
826
+ }
827
+
828
+ function buildKimiClientConfig(ctx: ExportContext): KimiGeneratedConfig {
829
+ const models: Record<string, KimiModelBlock> = {};
830
+ for (const model of normalizeExportModels(ctx.models)) {
831
+ const context = authoritativeContextWindow(model.contextWindow);
832
+ // `max_context_size` is mandatory and must be positive. We do not guess it,
833
+ // so a model without an authoritative window is left out rather than
834
+ // shipped with a number we invented.
835
+ if (context === undefined) continue;
836
+ models[kimiModelAlias(model.namespaced)] = {
837
+ provider: OPENCODE_PROVIDER_ID,
838
+ model: model.namespaced,
839
+ max_context_size: context,
840
+ ...(model.displayName ? { display_name: model.displayName } : {}),
841
+ };
842
+ }
843
+ return {
844
+ providers: {
845
+ [OPENCODE_PROVIDER_ID]: {
846
+ type: "openai",
847
+ base_url: ctx.baseUrl,
848
+ api_key: LOOPBACK_API_KEY_PLACEHOLDER,
849
+ },
850
+ },
851
+ models,
852
+ };
853
+ }
854
+
855
+ function buildGajaeClientConfig(ctx: ExportContext): GajaeGeneratedConfig {
856
+ const models: GajaeModelEntry[] = [];
857
+ for (const model of normalizeExportModels(ctx.models)) {
858
+ // Gajae's enum is text|image and it rejects the whole file over one bad
859
+ // value, naming the offending index in the error.
860
+ const input = inputModalitiesForClient("gajae", model.inputModalities);
861
+ if (input === null) continue;
862
+ const entry: GajaeModelEntry = {
863
+ id: model.namespaced,
864
+ name: exportModelLabel(model),
865
+ input,
866
+ };
867
+ const context = authoritativeContextWindow(model.contextWindow);
868
+ if (context !== undefined) {
869
+ entry.contextWindow = context;
870
+ entry.maxTokens = outputBudgetFor(context);
871
+ }
872
+ models.push(entry);
873
+ }
874
+ return {
875
+ providers: {
876
+ [OPENCODE_PROVIDER_ID]: {
877
+ baseUrl: ctx.baseUrl,
878
+ apiKeyEnv: GAJAE_API_KEY_ENV,
879
+ api: "openai-completions",
880
+ models,
881
+ },
882
+ },
883
+ };
884
+ }
885
+
886
+ /**
887
+ * Per-client model counts, read back off the SERIALIZED document rather than
888
+ * recomputed from the input rows: `modelsWithoutLimits` drives a GUI line about
889
+ * the bytes the user actually receives, so a parallel reimplementation of the
890
+ * "authoritative context window" rule would be free to drift from it.
891
+ */
892
+ function summarizeOpencode(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
893
+ const models = Object.values((document as OpencodeGeneratedConfig | undefined)?.provider?.[OPENCODE_PROVIDER_ID]?.models ?? {});
894
+ return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length };
895
+ }
896
+
897
+ function summarizePi(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
898
+ const models = (document as PiGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
899
+ return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
900
+ }
901
+
902
+ function summarizeHermes(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
903
+ const models = (document as HermesGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
904
+ // Hermes carries selectors only; it has no per-model limit to be missing.
905
+ return { modelCount: models.length, modelsWithoutLimits: 0 };
906
+ }
907
+
908
+ function summarizeOpenclaw(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
909
+ const models = (document as OpenclawGeneratedConfig | undefined)?.models?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
910
+ return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
911
+ }
912
+
913
+ function summarizeKimi(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
914
+ const models = Object.values((document as KimiGeneratedConfig | undefined)?.models ?? {});
915
+ // A model with no authoritative window is omitted entirely, so every model
916
+ // present carries max_context_size by construction.
917
+ return { modelCount: models.length, modelsWithoutLimits: 0 };
918
+ }
919
+
920
+ function summarizeGajae(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
921
+ const models = (document as GajaeGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
922
+ return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
923
+ }
924
+
925
+ /** One fragment at `path`, built from this client's own document. */
926
+ function singleFragment(clientId: ExportClientId, path: readonly string[], value: unknown): ManagedContribution {
927
+ return { clientId, fragments: [{ path, value }] };
928
+ }
929
+
930
+ function buildOpencodeContribution(ctx: ExportContext): ManagedContribution {
931
+ const doc = buildOpencodeClientConfig(ctx);
932
+ return singleFragment("opencode", ["provider", OPENCODE_PROVIDER_ID], doc.provider[OPENCODE_PROVIDER_ID]);
933
+ }
934
+
935
+ function buildPiContribution(ctx: ExportContext): ManagedContribution {
936
+ const doc = buildPiClientConfig(ctx);
937
+ return singleFragment("pi", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
938
+ }
939
+
940
+ function buildOmpContribution(ctx: ExportContext): ManagedContribution {
941
+ const doc = buildPiClientConfig(ctx);
942
+ return singleFragment("omp", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
943
+ }
944
+
945
+ function buildHermesContribution(ctx: ExportContext): ManagedContribution {
946
+ const doc = buildHermesClientConfig(ctx);
947
+ return singleFragment("hermes", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
948
+ }
949
+
950
+ function buildOpenclawContribution(ctx: ExportContext): ManagedContribution {
951
+ const doc = buildOpenclawClientConfig(ctx);
952
+ return singleFragment("openclaw", ["models", "providers", OPENCODE_PROVIDER_ID], doc.models.providers[OPENCODE_PROVIDER_ID]);
953
+ }
954
+
955
+ /**
956
+ * Kimi is why a contribution is a LIST: it owns the provider block AND one
957
+ * `models` entry per model. A writer that only knew about the provider would
958
+ * strand every model entry on disable.
959
+ */
960
+ function buildKimiContribution(ctx: ExportContext): ManagedContribution {
961
+ const doc = buildKimiClientConfig(ctx);
962
+ const fragments: ManagedFragment[] = [
963
+ { path: ["providers", OPENCODE_PROVIDER_ID], value: doc.providers[OPENCODE_PROVIDER_ID] },
964
+ ];
965
+ for (const [alias, block] of Object.entries(doc.models)) {
966
+ fragments.push({ path: ["models", alias], value: block });
967
+ }
968
+ return { clientId: "kimi", fragments };
969
+ }
970
+
971
+ function buildGajaeContribution(ctx: ExportContext): ManagedContribution {
972
+ const doc = buildGajaeClientConfig(ctx);
973
+ return singleFragment("gajae", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
974
+ }
975
+
976
+ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
977
+ opencode: {
978
+ id: "opencode",
979
+ filename: "opencode.json",
980
+ destination: env => opencodeGlobalConfigPath(env),
981
+ apiKeyEnv: OPENCODE_API_KEY_ENV,
982
+ exportHint: `export ${OPENCODE_API_KEY_ENV}=<your key>`,
983
+ build: buildOpencodeClientConfig,
984
+ format: "json",
985
+ summarize: summarizeOpencode,
986
+ buildContribution: buildOpencodeContribution,
987
+ // carries the dedicated header in provider options
988
+ loopbackOnly: false,
989
+ },
990
+ pi: {
991
+ id: "pi",
992
+ filename: "pi-models.json",
993
+ destination: () => join(homedir(), ".pi", "agent", "models.json"),
994
+ apiKeyEnv: "",
995
+ exportHint: "Pi reads a non-secret placeholder from models.json; loopback needs no key.",
996
+ build: buildPiClientConfig,
997
+ format: "json",
998
+ summarize: summarizePi,
999
+ buildContribution: buildPiContribution,
1000
+ // No header field in Pi's provider block, so there is nowhere to put the
1001
+ // dedicated admission header a remote bind requires.
1002
+ loopbackOnly: true,
1003
+ },
1004
+ omp: {
1005
+ id: "omp",
1006
+ filename: "omp-models.yaml",
1007
+ destination: env => ompModelsConfigPath(env),
1008
+ apiKeyEnv: "",
1009
+ exportHint: "OMP reads a non-secret placeholder from models.yml; loopback needs no key.",
1010
+ build: buildPiClientConfig,
1011
+ format: "yaml",
1012
+ summarize: summarizePi,
1013
+ buildContribution: buildOmpContribution,
1014
+ // OMP supports provider-level headers, but remote credential wiring is
1015
+ // intentionally deferred from this initial loopback-only integration.
1016
+ loopbackOnly: true,
1017
+ },
1018
+ hermes: {
1019
+ id: "hermes",
1020
+ filename: "hermes-config.yaml",
1021
+ destination: env => hermesConfigPath(env),
1022
+ apiKeyEnv: HERMES_API_KEY_ENV,
1023
+ exportHint: `export ${HERMES_API_KEY_ENV}=<your key>`,
1024
+ build: buildHermesClientConfig,
1025
+ format: "yaml",
1026
+ summarize: summarizeHermes,
1027
+ buildContribution: buildHermesContribution,
1028
+ // extra_headers carries the dedicated header
1029
+ loopbackOnly: false,
1030
+ },
1031
+ openclaw: {
1032
+ id: "openclaw",
1033
+ filename: "openclaw.json5",
1034
+ destination: env => openclawConfigPath(env),
1035
+ apiKeyEnv: OPENCLAW_API_KEY_ENV,
1036
+ exportHint: `export ${OPENCLAW_API_KEY_ENV}=<your key>`,
1037
+ build: buildOpenclawClientConfig,
1038
+ format: "json5",
1039
+ summarize: summarizeOpenclaw,
1040
+ buildContribution: buildOpenclawContribution,
1041
+ // headers carries the dedicated header
1042
+ loopbackOnly: false,
1043
+ },
1044
+ kimi: {
1045
+ id: "kimi",
1046
+ filename: "kimi-config.toml",
1047
+ destination: env => kimiConfigPath(env),
1048
+ // Kimi reads credentials only from its own file, so there is no env var to
1049
+ // export: a loopback bind uses the placeholder, and a remote bind is
1050
+ // refused rather than handed the user's real key.
1051
+ apiKeyEnv: "",
1052
+ exportHint: "Kimi Code reads credentials from its config file; loopback needs no key.",
1053
+ build: buildKimiClientConfig,
1054
+ format: "toml",
1055
+ summarize: summarizeKimi,
1056
+ buildContribution: buildKimiContribution,
1057
+ // no header field, and credentials come only from this file
1058
+ loopbackOnly: true,
1059
+ },
1060
+ gajae: {
1061
+ id: "gajae",
1062
+ filename: "gajae-models.yaml",
1063
+ destination: env => gajaeConfigPath(env),
1064
+ apiKeyEnv: GAJAE_API_KEY_ENV,
1065
+ exportHint: `export ${GAJAE_API_KEY_ENV}=<your key>`,
1066
+ build: buildGajaeClientConfig,
1067
+ format: "yaml",
1068
+ summarize: summarizeGajae,
1069
+ buildContribution: buildGajaeContribution,
1070
+ // strict schema with no header field, so the dedicated header has nowhere to go
1071
+ loopbackOnly: true,
1072
+ },
1073
+ };
1074
+
1075
+ export const EXPORT_CLIENT_IDS: readonly ExportClientId[] = Object.keys(EXPORT_CLIENTS) as ExportClientId[];
1076
+
1077
+ export function isExportClientId(value: string): value is ExportClientId {
1078
+ return Object.prototype.hasOwnProperty.call(EXPORT_CLIENTS, value);
1079
+ }
1080
+
1081
+ /** Single entry point every export surface calls. */
1082
+ export function buildClientConfig(client: ExportClientId, ctx: ExportContext): unknown {
1083
+ return EXPORT_CLIENTS[client].build(ctx);
1084
+ }
1085
+
1086
+ /**
1087
+ * The bytes a user actually receives, plus what they are. One place turns a
1088
+ * client id into text so the CLI, the API and the GUI cannot disagree about
1089
+ * format or media type — and so no consumer has to infer either from a
1090
+ * filename.
1091
+ */
1092
+ export function buildClientConfigText(
1093
+ client: ExportClientId,
1094
+ ctx: ExportContext,
1095
+ ): { document: unknown; text: string; format: ConfigFormat; mediaType: string } {
1096
+ const spec = EXPORT_CLIENTS[client];
1097
+ const document = spec.build(ctx);
1098
+ return {
1099
+ document,
1100
+ text: serializeDocument(document, spec.format),
1101
+ format: spec.format,
1102
+ mediaType: FORMAT_MEDIA_TYPE[spec.format],
1103
+ };
1104
+ }
1105
+
1106
+ /** The fragments Remodex owns in a client's config (writer-side). */
1107
+ export function buildClientContribution(client: ExportClientId, ctx: ExportContext): ManagedContribution {
1108
+ return EXPORT_CLIENTS[client].buildContribution(ctx);
1109
+ }