create-walle 0.9.21 → 0.9.23

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 (500) hide show
  1. package/README.md +27 -5
  2. package/package.json +2 -2
  3. package/template/CLAUDE.md +2 -2
  4. package/template/LICENSE +1 -1
  5. package/template/bin/ctm-dev-cleanup.js +24 -3
  6. package/template/bin/ctm-launch.sh +13 -0
  7. package/template/bin/dev.sh +156 -18
  8. package/template/bin/node-bin.sh +84 -0
  9. package/template/bin/pin-node.sh +51 -0
  10. package/template/claude-task-manager/api-prompts.js +1203 -182
  11. package/template/claude-task-manager/api-reviews.js +109 -15
  12. package/template/claude-task-manager/approval-agent.js +1360 -280
  13. package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
  14. package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
  15. package/template/claude-task-manager/db.js +4417 -295
  16. package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
  17. package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
  18. package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
  19. package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
  20. package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
  21. package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
  22. package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
  23. package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
  24. package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
  25. package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
  26. package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
  27. package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
  28. package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
  29. package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
  30. package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
  31. package/template/claude-task-manager/docs/phone-access-design.md +53 -15
  32. package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
  33. package/template/claude-task-manager/docs/phone-setup.md +3 -0
  34. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
  35. package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
  36. package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
  37. package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
  38. package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
  39. package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
  40. package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
  41. package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
  42. package/template/claude-task-manager/docs/session-title-authority.md +32 -0
  43. package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
  44. package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
  45. package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
  46. package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
  47. package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
  48. package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
  49. package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
  50. package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
  51. package/template/claude-task-manager/git-utils.js +897 -27
  52. package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
  53. package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
  54. package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
  55. package/template/claude-task-manager/lib/agent-presets.js +17 -1
  56. package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
  57. package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
  58. package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
  59. package/template/claude-task-manager/lib/async-semaphore.js +44 -0
  60. package/template/claude-task-manager/lib/auth-context.js +5 -0
  61. package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
  62. package/template/claude-task-manager/lib/auth-rules.js +29 -2
  63. package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
  64. package/template/claude-task-manager/lib/background-llm.js +144 -17
  65. package/template/claude-task-manager/lib/branch-inventory.js +212 -0
  66. package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
  67. package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
  68. package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
  69. package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
  70. package/template/claude-task-manager/lib/codex-zst.js +124 -0
  71. package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
  72. package/template/claude-task-manager/lib/connection-health.js +232 -0
  73. package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
  74. package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
  75. package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
  76. package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
  77. package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
  78. package/template/claude-task-manager/lib/document-review.js +141 -6
  79. package/template/claude-task-manager/lib/escalation-review.js +152 -0
  80. package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
  81. package/template/claude-task-manager/lib/headless-term-service.js +678 -0
  82. package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
  83. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
  84. package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
  85. package/template/claude-task-manager/lib/main-db-census.js +216 -0
  86. package/template/claude-task-manager/lib/message-pagination.js +106 -4
  87. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
  88. package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
  89. package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
  90. package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
  91. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
  92. package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
  93. package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
  94. package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
  95. package/template/claude-task-manager/lib/perf-tracker.js +242 -6
  96. package/template/claude-task-manager/lib/permission-match.js +76 -0
  97. package/template/claude-task-manager/lib/permission-sync.js +133 -20
  98. package/template/claude-task-manager/lib/process-title.js +35 -0
  99. package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
  100. package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
  101. package/template/claude-task-manager/lib/prompt-intent.js +132 -0
  102. package/template/claude-task-manager/lib/provider-user-context.js +34 -0
  103. package/template/claude-task-manager/lib/read-pool-client.js +313 -0
  104. package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
  105. package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
  106. package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
  107. package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
  108. package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
  109. package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
  110. package/template/claude-task-manager/lib/restart-guard.js +109 -0
  111. package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
  112. package/template/claude-task-manager/lib/restore-policy.js +13 -0
  113. package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
  114. package/template/claude-task-manager/lib/restore-runtime.js +68 -0
  115. package/template/claude-task-manager/lib/restore-storm.js +34 -0
  116. package/template/claude-task-manager/lib/resume-cwd.js +36 -0
  117. package/template/claude-task-manager/lib/resume-preflight.js +313 -0
  118. package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
  119. package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
  120. package/template/claude-task-manager/lib/scheduler.js +21 -1
  121. package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
  122. package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
  123. package/template/claude-task-manager/lib/server-listeners.js +239 -0
  124. package/template/claude-task-manager/lib/session-capture.js +42 -7
  125. package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
  126. package/template/claude-task-manager/lib/session-history.js +388 -43
  127. package/template/claude-task-manager/lib/session-host-manager.js +287 -0
  128. package/template/claude-task-manager/lib/session-image-refs.js +209 -0
  129. package/template/claude-task-manager/lib/session-jobs.js +399 -59
  130. package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
  131. package/template/claude-task-manager/lib/session-restore.js +53 -0
  132. package/template/claude-task-manager/lib/session-standup.js +123 -23
  133. package/template/claude-task-manager/lib/session-state-bus.js +14 -0
  134. package/template/claude-task-manager/lib/session-stream.js +64 -16
  135. package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
  136. package/template/claude-task-manager/lib/session-token-usage.js +494 -0
  137. package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
  138. package/template/claude-task-manager/lib/setup-network-config.js +9 -0
  139. package/template/claude-task-manager/lib/size-cap.js +45 -0
  140. package/template/claude-task-manager/lib/size-cap.test.js +62 -0
  141. package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
  142. package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
  143. package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
  144. package/template/claude-task-manager/lib/standup-attention.js +7 -3
  145. package/template/claude-task-manager/lib/status-authority.js +39 -0
  146. package/template/claude-task-manager/lib/status-hooks.js +4 -0
  147. package/template/claude-task-manager/lib/storage-migration.js +235 -0
  148. package/template/claude-task-manager/lib/structured-capture.js +298 -0
  149. package/template/claude-task-manager/lib/sync-io-census.js +163 -0
  150. package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
  151. package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
  152. package/template/claude-task-manager/lib/terminal-choice.js +364 -0
  153. package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
  154. package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
  155. package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
  156. package/template/claude-task-manager/lib/timeline-order.js +122 -0
  157. package/template/claude-task-manager/lib/transcript-store.js +348 -43
  158. package/template/claude-task-manager/lib/transport-security.js +84 -1
  159. package/template/claude-task-manager/lib/wait-state.js +184 -0
  160. package/template/claude-task-manager/lib/walle-client.js +47 -5
  161. package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
  162. package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
  163. package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
  164. package/template/claude-task-manager/lib/walle-native-health.js +403 -0
  165. package/template/claude-task-manager/lib/walle-repair.js +701 -0
  166. package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
  167. package/template/claude-task-manager/lib/walle-session-context.js +57 -21
  168. package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
  169. package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
  170. package/template/claude-task-manager/lib/walle-transcript.js +52 -0
  171. package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
  172. package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
  173. package/template/claude-task-manager/package.json +1 -1
  174. package/template/claude-task-manager/prompt-harvest.js +89 -66
  175. package/template/claude-task-manager/providers/claude-code.js +51 -3
  176. package/template/claude-task-manager/providers/cursor.js +140 -45
  177. package/template/claude-task-manager/public/css/reviews.css +551 -61
  178. package/template/claude-task-manager/public/css/setup.css +191 -0
  179. package/template/claude-task-manager/public/css/walle-session.css +865 -10
  180. package/template/claude-task-manager/public/css/walle.css +154 -0
  181. package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
  182. package/template/claude-task-manager/public/index.html +18516 -2058
  183. package/template/claude-task-manager/public/ipad.html +363 -0
  184. package/template/claude-task-manager/public/js/document-review-links.js +301 -0
  185. package/template/claude-task-manager/public/js/image-normalize.js +69 -36
  186. package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
  187. package/template/claude-task-manager/public/js/prompts.js +66 -29
  188. package/template/claude-task-manager/public/js/reviews.js +901 -133
  189. package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
  190. package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
  191. package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
  192. package/template/claude-task-manager/public/js/setup.js +1273 -176
  193. package/template/claude-task-manager/public/js/stream-view.js +691 -73
  194. package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
  195. package/template/claude-task-manager/public/js/walle-session.js +2455 -158
  196. package/template/claude-task-manager/public/js/walle.js +455 -28
  197. package/template/claude-task-manager/public/m/app.css +2909 -262
  198. package/template/claude-task-manager/public/m/app.js +6601 -398
  199. package/template/claude-task-manager/public/m/claim.html +224 -17
  200. package/template/claude-task-manager/public/m/index.html +117 -21
  201. package/template/claude-task-manager/public/m/sw.js +3 -1
  202. package/template/claude-task-manager/public/manifest.json +2 -2
  203. package/template/claude-task-manager/public/prompts.html +30 -14
  204. package/template/claude-task-manager/queue-engine.js +507 -28
  205. package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
  206. package/template/claude-task-manager/server.js +14341 -2197
  207. package/template/claude-task-manager/session-integrity.js +160 -18
  208. package/template/claude-task-manager/session-search-ranking.js +1 -0
  209. package/template/claude-task-manager/session-utils.js +25 -5
  210. package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
  211. package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
  212. package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
  213. package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
  214. package/template/claude-task-manager/workers/harvest-worker.js +9 -55
  215. package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
  216. package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
  217. package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
  218. package/template/claude-task-manager/workers/session-host-process.js +146 -0
  219. package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
  220. package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
  221. package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
  222. package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
  223. package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
  224. package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
  225. package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
  226. package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
  227. package/template/docs/design/markdown-review-pane.md +206 -0
  228. package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
  229. package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
  230. package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
  231. package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
  232. package/template/docs/private-memory-and-pii-policy.md +69 -0
  233. package/template/package.json +2 -1
  234. package/template/scripts/check-private-data.js +201 -0
  235. package/template/shared/sqlite-owner-guard.js +30 -0
  236. package/template/shared/sqlite-owner-write-queue.js +225 -0
  237. package/template/shared/sqlite-storage-policy.js +111 -0
  238. package/template/shared/sqlite-write-lock.js +428 -0
  239. package/template/wall-e/agent-runners/claude-code.js +5 -0
  240. package/template/wall-e/agent.js +166 -22
  241. package/template/wall-e/api-walle.js +524 -70
  242. package/template/wall-e/auth/provider-flows.js +11 -1
  243. package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
  244. package/template/wall-e/brain.js +1614 -141
  245. package/template/wall-e/chat/attachment-blocks.js +96 -0
  246. package/template/wall-e/chat/attachments.js +2 -1
  247. package/template/wall-e/chat/capability-resolver.js +7 -7
  248. package/template/wall-e/chat/context-messages.js +28 -0
  249. package/template/wall-e/chat/conversation-frame.js +630 -0
  250. package/template/wall-e/chat/provider-messages.js +125 -0
  251. package/template/wall-e/chat.js +1002 -233
  252. package/template/wall-e/coding/acceptance-contract.js +170 -0
  253. package/template/wall-e/coding/acp-adapter.js +1 -1
  254. package/template/wall-e/coding/agent-catalog.js +3 -0
  255. package/template/wall-e/coding/artifact-store.js +93 -0
  256. package/template/wall-e/coding/capability-router.js +120 -0
  257. package/template/wall-e/coding/coding-run-controller.js +423 -0
  258. package/template/wall-e/coding/compaction-service.js +157 -12
  259. package/template/wall-e/coding/frontend-verification.js +258 -0
  260. package/template/wall-e/coding/lifecycle-hooks.js +75 -0
  261. package/template/wall-e/coding/local-preview-contract.js +157 -0
  262. package/template/wall-e/coding/permission-service.js +57 -13
  263. package/template/wall-e/coding/prompt-bundle.js +19 -1
  264. package/template/wall-e/coding/prompt-section-registry.js +227 -0
  265. package/template/wall-e/coding/provider-compat.js +15 -0
  266. package/template/wall-e/coding/runtime-events.js +224 -0
  267. package/template/wall-e/coding/runtime-mode.js +3 -0
  268. package/template/wall-e/coding/side-git-snapshot.js +160 -4
  269. package/template/wall-e/coding/snapshot-service.js +143 -1
  270. package/template/wall-e/coding/stream-processor.js +388 -34
  271. package/template/wall-e/coding/task-tool.js +141 -4
  272. package/template/wall-e/coding/tool-execution-controller.js +365 -0
  273. package/template/wall-e/coding/tool-registry.js +43 -5
  274. package/template/wall-e/coding/user-hooks.js +217 -0
  275. package/template/wall-e/coding-orchestrator.js +1330 -221
  276. package/template/wall-e/coding-prompts.js +20 -4
  277. package/template/wall-e/context/context-builder.js +15 -2
  278. package/template/wall-e/decision/confidence.js +1 -1
  279. package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
  280. package/template/wall-e/docs/external-action-controller.md +26 -6
  281. package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
  282. package/template/wall-e/embeddings.js +591 -53
  283. package/template/wall-e/external-action-controller.js +12 -0
  284. package/template/wall-e/http/auth.js +1 -0
  285. package/template/wall-e/http/chat-api.js +46 -11
  286. package/template/wall-e/http/model-admin.js +836 -34
  287. package/template/wall-e/lib/boot-profile.js +88 -0
  288. package/template/wall-e/lib/event-loop-monitor.js +93 -0
  289. package/template/wall-e/lib/service-health.js +194 -0
  290. package/template/wall-e/llm/anthropic.js +130 -5
  291. package/template/wall-e/llm/client.js +266 -63
  292. package/template/wall-e/llm/default-fallback.js +382 -0
  293. package/template/wall-e/llm/health.js +19 -0
  294. package/template/wall-e/llm/message-guard.js +78 -0
  295. package/template/wall-e/llm/model-catalog.js +252 -1
  296. package/template/wall-e/llm/openai.js +26 -4
  297. package/template/wall-e/llm/portkey-sync.js +654 -0
  298. package/template/wall-e/llm/provider-error.js +30 -2
  299. package/template/wall-e/llm/registry.js +5 -1
  300. package/template/wall-e/llm/request-compat.js +67 -0
  301. package/template/wall-e/loops/backfill.js +79 -23
  302. package/template/wall-e/loops/brain-optimize.js +67 -0
  303. package/template/wall-e/loops/ingest.js +25 -10
  304. package/template/wall-e/loops/question-digest.js +160 -0
  305. package/template/wall-e/loops/reflect.js +6 -4
  306. package/template/wall-e/loops/think.js +39 -12
  307. package/template/wall-e/mcp-server.js +318 -36
  308. package/template/wall-e/memory/ctm-context-client.js +52 -14
  309. package/template/wall-e/memory/ctm-operational-context.js +237 -0
  310. package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
  311. package/template/wall-e/memory/ctm-session-context.js +111 -63
  312. package/template/wall-e/prompts/coding/deepseek.txt +3 -0
  313. package/template/wall-e/prompts/coding/gemini.txt +6 -0
  314. package/template/wall-e/prompts/coding/gpt.txt +6 -0
  315. package/template/wall-e/prompts/coding/local.txt +7 -0
  316. package/template/wall-e/runtime/decision-hooks.js +115 -0
  317. package/template/wall-e/runtime/devbox-gateway.js +82 -8
  318. package/template/wall-e/runtime/prompt-manifest.js +86 -0
  319. package/template/wall-e/runtime/tool-executor.js +269 -0
  320. package/template/wall-e/runtime/tool-result-envelope.js +138 -0
  321. package/template/wall-e/runtime/transcript-projection.js +60 -0
  322. package/template/wall-e/runtime/walle-runtime.js +224 -0
  323. package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
  324. package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
  325. package/template/wall-e/server.js +15 -0
  326. package/template/wall-e/session-files.js +9 -0
  327. package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
  328. package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
  329. package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
  330. package/template/wall-e/skills/claude-code-reader.js +7 -3
  331. package/template/wall-e/skills/script-skill-runner.js +10 -0
  332. package/template/wall-e/skills/skill-planner.js +38 -0
  333. package/template/wall-e/tools/builtin-middleware.js +19 -9
  334. package/template/wall-e/tools/local-tools.js +1428 -16
  335. package/template/wall-e/tools/permission-checker.js +73 -5
  336. package/template/wall-e/tools/question-manager.js +117 -7
  337. package/template/wall-e/training/harvester.js +12 -28
  338. package/template/wall-e/training/replay.js +25 -80
  339. package/template/website/index.html +10 -10
  340. package/template/wall-e/eval/ab-test.js +0 -203
  341. package/template/wall-e/eval/agent-runner.js +0 -772
  342. package/template/wall-e/eval/agent-scorer.js +0 -461
  343. package/template/wall-e/eval/aggregator.js +0 -414
  344. package/template/wall-e/eval/allowed-test-commands.js +0 -34
  345. package/template/wall-e/eval/benchmark-generator.js +0 -113
  346. package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
  347. package/template/wall-e/eval/benchmarks/chat.json +0 -82
  348. package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
  349. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
  350. package/template/wall-e/eval/benchmarks/coding.json +0 -122
  351. package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
  352. package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
  353. package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
  354. package/template/wall-e/eval/benchmarks.js +0 -669
  355. package/template/wall-e/eval/cc-replay.js +0 -719
  356. package/template/wall-e/eval/chat-eval.js +0 -525
  357. package/template/wall-e/eval/check-keys.js +0 -15
  358. package/template/wall-e/eval/check-providers.js +0 -42
  359. package/template/wall-e/eval/codex-cli-baseline.js +0 -669
  360. package/template/wall-e/eval/coding-agent-real.js +0 -570
  361. package/template/wall-e/eval/context-compactor.js +0 -251
  362. package/template/wall-e/eval/debug-agent003.js +0 -68
  363. package/template/wall-e/eval/diagnostics.js +0 -216
  364. package/template/wall-e/eval/eval-orchestrator.js +0 -642
  365. package/template/wall-e/eval/evaluate.js +0 -202
  366. package/template/wall-e/eval/evaluator.js +0 -373
  367. package/template/wall-e/eval/exporter.js +0 -212
  368. package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
  369. package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
  370. package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
  371. package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
  372. package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
  373. package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
  374. package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
  375. package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
  376. package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
  377. package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
  378. package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
  379. package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
  380. package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
  381. package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
  382. package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
  383. package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
  384. package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
  385. package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
  386. package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
  387. package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
  388. package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
  389. package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
  390. package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
  391. package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
  392. package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
  393. package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
  394. package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
  395. package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
  396. package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
  397. package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
  398. package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
  399. package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
  400. package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
  401. package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
  402. package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
  403. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
  404. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
  405. package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
  406. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
  407. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
  408. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
  409. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
  410. package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
  411. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
  412. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
  413. package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
  414. package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
  415. package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
  416. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
  417. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
  418. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
  419. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
  420. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
  421. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
  422. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
  423. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
  424. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
  425. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
  426. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
  427. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
  428. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
  429. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
  430. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
  431. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
  432. package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
  433. package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
  434. package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
  435. package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
  436. package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
  437. package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
  438. package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
  439. package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
  440. package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
  441. package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
  442. package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
  443. package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
  444. package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
  445. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
  446. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
  447. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
  448. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
  449. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
  450. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
  451. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
  452. package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
  453. package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
  454. package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
  455. package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
  456. package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
  457. package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
  458. package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
  459. package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
  460. package/template/wall-e/eval/harvester.js +0 -685
  461. package/template/wall-e/eval/head-to-head.js +0 -388
  462. package/template/wall-e/eval/humaneval-adapter.js +0 -321
  463. package/template/wall-e/eval/list-models.js +0 -31
  464. package/template/wall-e/eval/livecodebench-adapter.js +0 -291
  465. package/template/wall-e/eval/mail-integration.js +0 -443
  466. package/template/wall-e/eval/manifest.js +0 -186
  467. package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
  468. package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
  469. package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
  470. package/template/wall-e/eval/meta-harness/cli.js +0 -86
  471. package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
  472. package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
  473. package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
  474. package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
  475. package/template/wall-e/eval/meta-harness/frontier.js +0 -96
  476. package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
  477. package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
  478. package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
  479. package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
  480. package/template/wall-e/eval/meta-harness/reporting.js +0 -58
  481. package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
  482. package/template/wall-e/eval/meta-harness/validation.js +0 -81
  483. package/template/wall-e/eval/promoter.js +0 -228
  484. package/template/wall-e/eval/provider-normalizer.js +0 -33
  485. package/template/wall-e/eval/replay.js +0 -395
  486. package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
  487. package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
  488. package/template/wall-e/eval/run-coding-agent-real.js +0 -187
  489. package/template/wall-e/eval/run-eval.js +0 -435
  490. package/template/wall-e/eval/run-model-comparison.js +0 -142
  491. package/template/wall-e/eval/session-evaluator.js +0 -187
  492. package/template/wall-e/eval/session-miner.js +0 -207
  493. package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
  494. package/template/wall-e/eval/session-transcripts.js +0 -509
  495. package/template/wall-e/eval/shadow.js +0 -161
  496. package/template/wall-e/eval/swebench-adapter.js +0 -345
  497. package/template/wall-e/eval/swebench-docker.js +0 -192
  498. package/template/wall-e/eval/train.py +0 -320
  499. package/template/wall-e/eval/trainer.js +0 -232
  500. package/template/wall-e/eval/weekly-eval-loop.js +0 -241
@@ -3,12 +3,30 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const http = require('http');
6
- const { execFile, execFileSync } = require('child_process');
6
+ const { execFile } = require('child_process');
7
7
  const { jsonResponse: _jsonResponseDataFirst } = require('./api-utils');
8
+ const {
9
+ isPortkeyGatewayCredentialRow,
10
+ isPortkeyProviderRow,
11
+ syncPortkeyGatewayModels,
12
+ } = require('../llm/portkey-sync');
13
+ const {
14
+ buildPortkeyHeaders,
15
+ defaultPortkeyBaseUrl,
16
+ } = require('../llm/portkey');
17
+ const { getDevboxClaudePortkeyGateway } = require('../runtime/devbox-gateway');
8
18
 
9
19
  let brain = null;
10
20
  try { brain = require('../brain'); } catch {}
11
21
 
22
+ const SETUP_PROVIDER_TYPES = ['anthropic', 'openai', 'google', 'ollama', 'deepseek', 'moonshot'];
23
+ const PORTKEY_GATEWAY_PROVIDER_TYPES = ['anthropic', 'deepseek', 'google', 'moonshot', 'openai'];
24
+ const SHELL_ENV_CACHE_TTL_MS = 5 * 60 * 1000;
25
+ const SHELL_ENV_LOOKUP_TIMEOUT_MS = 5000;
26
+ const _shellEnvCache = new Map();
27
+ const _shellEnvInflight = new Map();
28
+ let _shellEnvExecFile = execFile;
29
+
12
30
  function getBrain() {
13
31
  if (!brain) return null;
14
32
  try { brain.getDb(); } catch {
@@ -44,6 +62,101 @@ function getShellRcSource(shell) {
44
62
  return '';
45
63
  }
46
64
 
65
+ function isSupportedEnvName(name) {
66
+ return /^[A-Z_][A-Z0-9_]*$/.test(String(name || ''));
67
+ }
68
+
69
+ function normalizeShellEnvValue(value) {
70
+ const str = String(value || '').trim();
71
+ return str && str.length > 3 ? str : null;
72
+ }
73
+
74
+ function _cachedShellEnvValue(name, nowMs = Date.now()) {
75
+ const cached = _shellEnvCache.get(name);
76
+ if (!cached) return null;
77
+ if (nowMs - cached.checkedAt > SHELL_ENV_CACHE_TTL_MS) return null;
78
+ return cached.value || null;
79
+ }
80
+
81
+ function _setShellEnvCache(name, value, nowMs = Date.now()) {
82
+ _shellEnvCache.set(name, { value: normalizeShellEnvValue(value), checkedAt: nowMs });
83
+ }
84
+
85
+ function _shellEnvLookupScript(names, shell) {
86
+ const refs = names.map((name) => `"$${name}"`).join(' ');
87
+ return `${getShellRcSource(shell)} printf '%s\\0' ${refs}`;
88
+ }
89
+
90
+ function _readShellEnvValues(names, options = {}) {
91
+ const uniqueNames = Array.from(new Set((names || []).filter(isSupportedEnvName)));
92
+ const out = new Map();
93
+ const nowMs = options.nowMs || Date.now();
94
+ const missing = [];
95
+ for (const name of uniqueNames) {
96
+ const direct = normalizeShellEnvValue(process.env[name]);
97
+ if (direct) {
98
+ out.set(name, direct);
99
+ _setShellEnvCache(name, direct, nowMs);
100
+ continue;
101
+ }
102
+ const cached = options.ignoreCache ? null : _cachedShellEnvValue(name, nowMs);
103
+ if (cached) {
104
+ out.set(name, cached);
105
+ continue;
106
+ }
107
+ missing.push(name);
108
+ }
109
+ if (!missing.length) return Promise.resolve(out);
110
+
111
+ const shell = process.env.SHELL || '/bin/zsh';
112
+ const script = _shellEnvLookupScript(missing, shell);
113
+ const timeoutMs = options.timeoutMs || SHELL_ENV_LOOKUP_TIMEOUT_MS;
114
+ const inflightKey = `${shell}\n${timeoutMs}\n${missing.join('\0')}`;
115
+ const inflight = _shellEnvInflight.get(inflightKey);
116
+ if (inflight) {
117
+ return inflight.then((missingValues) => {
118
+ for (const [name, value] of missingValues.entries()) {
119
+ if (value) out.set(name, value);
120
+ }
121
+ return out;
122
+ });
123
+ }
124
+ const lookup = new Promise((resolve) => {
125
+ _shellEnvExecFile(shell, ['-lc', script], {
126
+ encoding: 'utf8',
127
+ timeout: timeoutMs,
128
+ maxBuffer: 128 * 1024,
129
+ }, (err, stdout) => {
130
+ const missingValues = new Map();
131
+ if (err) {
132
+ for (const name of missing) _setShellEnvCache(name, null, nowMs);
133
+ resolve(missingValues);
134
+ return;
135
+ }
136
+ const values = String(stdout || '').split('\0');
137
+ missing.forEach((name, index) => {
138
+ const value = normalizeShellEnvValue(values[index] || '');
139
+ _setShellEnvCache(name, value, nowMs);
140
+ if (value) missingValues.set(name, value);
141
+ });
142
+ resolve(missingValues);
143
+ });
144
+ }).finally(() => {
145
+ _shellEnvInflight.delete(inflightKey);
146
+ });
147
+ _shellEnvInflight.set(inflightKey, lookup);
148
+ return lookup.then((missingValues) => {
149
+ for (const [name, value] of missingValues.entries()) out.set(name, value);
150
+ return out;
151
+ });
152
+ }
153
+
154
+ async function getShellEnvValue(name, options = {}) {
155
+ if (!isSupportedEnvName(name)) return null;
156
+ const values = await _readShellEnvValues([name], options);
157
+ return values.get(name) || null;
158
+ }
159
+
47
160
  function readSmallJsonBody(req, maxBytes = 65536) {
48
161
  return new Promise((resolve, reject) => {
49
162
  const chunks = [];
@@ -72,28 +185,431 @@ function parseCustomHeaders(value) {
72
185
  return undefined;
73
186
  }
74
187
 
75
- function getShellEnvValue(name) {
76
- if (!/^[A-Z_][A-Z0-9_]*$/.test(name)) return null;
77
- const val = process.env[name];
78
- if (val && val.length > 3) return val;
188
+ function isSqliteCatalogReadError(err) {
189
+ return !!err && (
190
+ err.code === 'SQLITE_CORRUPT'
191
+ || /database disk image is malformed|malformed database schema|btree/i.test(err.message || '')
192
+ );
193
+ }
194
+
195
+ function addCatalogWarning(warnings, where, err, extra = {}) {
196
+ if (!warnings) return;
197
+ const warning = {
198
+ code: 'model_registry_unavailable',
199
+ where,
200
+ message: err && err.message ? err.message : String(err || 'Model registry read failed'),
201
+ ...extra,
202
+ };
203
+ const key = JSON.stringify([warning.code, warning.where, warning.provider_id || '', warning.message]);
204
+ if (!warnings.some((item) => item._key === key)) warnings.push({ ...warning, _key: key });
205
+ }
206
+
207
+ function publicCatalogWarnings(warnings) {
208
+ return (warnings || []).map(({ _key, ...warning }) => warning);
209
+ }
210
+
211
+ function catalogStatusPayload(warnings) {
212
+ return warnings && warnings.length
213
+ ? {
214
+ catalog_status: 'partial',
215
+ catalog_unavailable: true,
216
+ catalog_warnings: publicCatalogWarnings(warnings),
217
+ }
218
+ : { catalog_status: 'ok' };
219
+ }
220
+
221
+ function safeGetProviderWithKey(brainApi, provider, warnings) {
79
222
  try {
80
- const shell = process.env.SHELL || '/bin/zsh';
81
- const result = execFileSync(shell, ['-lc', `${getShellRcSource(shell)} printf %s "$${name}"`], {
82
- encoding: 'utf8',
83
- timeout: 5000,
84
- }).trim();
85
- return result && result.length > 3 ? result : null;
86
- } catch {
87
- return null;
223
+ return brainApi.getModelProviderWithKey(provider.id);
224
+ } catch (err) {
225
+ if (!isSqliteCatalogReadError(err)) throw err;
226
+ addCatalogWarning(warnings, 'model_provider_key', err, { provider_id: provider.id });
227
+ return provider;
88
228
  }
89
229
  }
90
230
 
91
- function getProviderWithMeta(brainApi, provider) {
92
- const full = brainApi.getModelProviderWithKey(provider.id);
231
+ function safeProviderRoutePolicy(brainApi, type, warnings) {
232
+ if (typeof brainApi.getProviderRoutePolicy !== 'function') return 'auto';
233
+ try {
234
+ return brainApi.getProviderRoutePolicy(type);
235
+ } catch (err) {
236
+ if (!isSqliteCatalogReadError(err)) throw err;
237
+ addCatalogWarning(warnings, 'provider_route_policy', err, { provider_type: type });
238
+ return 'auto';
239
+ }
240
+ }
241
+
242
+ function safeListModelsByProvider(brainApi, providerId, warnings) {
243
+ try {
244
+ return { ok: true, models: brainApi.listModelsByProvider(providerId) || [] };
245
+ } catch (err) {
246
+ if (!isSqliteCatalogReadError(err)) throw err;
247
+ addCatalogWarning(warnings, 'models_by_provider', err, { provider_id: providerId });
248
+ return { ok: false, models: [], error: err };
249
+ }
250
+ }
251
+
252
+ function safeModelCountsByProvider(brainApi, warnings) {
253
+ try {
254
+ if (typeof brainApi.listModelCountsByProvider !== 'function') {
255
+ return { ok: false, counts: new Map(), error: null };
256
+ }
257
+ const rows = brainApi.listModelCountsByProvider();
258
+ const counts = new Map();
259
+ for (const row of rows || []) {
260
+ counts.set(String(row.provider_id || ''), Number(row.model_count || 0));
261
+ }
262
+ return { ok: true, counts };
263
+ } catch (err) {
264
+ if (!isSqliteCatalogReadError(err)) throw err;
265
+ addCatalogWarning(warnings, 'model_counts_by_provider', err);
266
+ return { ok: false, counts: new Map(), error: err };
267
+ }
268
+ }
269
+
270
+ function isPortkeyCatalogProviderRoute(provider = {}, models = []) {
271
+ const id = String(provider.id || provider.provider_id || '').toLowerCase();
272
+ const name = String(provider.name || '').toLowerCase();
273
+ if (id.includes('-portkey-catalog-') || /\bvia\s+portkey\s+catalog\b/.test(name)) return true;
274
+ if (!Array.isArray(models) || !models.length) return false;
275
+ return models.every((model) => {
276
+ const source = String(model.source || '').toLowerCase();
277
+ const status = String(model.verification_status || model.verificationStatus || '').toLowerCase();
278
+ return source === 'portkey_catalog' || status === 'catalog';
279
+ });
280
+ }
281
+
282
+ function providerCatalogSortRank(provider = {}) {
283
+ return provider.is_catalog === true || provider.catalog_only === true ? 1 : 0;
284
+ }
285
+
286
+ function getProviderWithMeta(brainApi, provider, options = {}) {
287
+ const warnings = options.warnings || null;
288
+ const full = Object.prototype.hasOwnProperty.call(options, 'full')
289
+ ? options.full
290
+ : safeGetProviderWithKey(brainApi, provider, warnings);
291
+ const routePolicy = safeProviderRoutePolicy(brainApi, provider.type, warnings);
292
+ const connectionKind = isPortkeyProviderRow(full || provider) ? 'portkey' : (provider.auth_method && provider.auth_method !== 'api_key' ? provider.auth_method : 'direct');
293
+ const modelRead = options.modelRead || null;
294
+ const isCatalog = isPortkeyCatalogProviderRoute(provider, modelRead && modelRead.ok ? modelRead.models : []);
295
+ const {
296
+ api_key_encrypted: _apiKeyEncrypted,
297
+ apiKeyEncrypted: _apiKeyEncryptedCamel,
298
+ custom_headers: _customHeaders,
299
+ customHeaders: _customHeadersCamel,
300
+ ...safeProvider
301
+ } = provider || {};
93
302
  return {
94
- ...provider,
303
+ ...safeProvider,
95
304
  has_key: !!(full && full.api_key_encrypted),
96
305
  is_default_instance: provider.id.endsWith('-default') || provider.id.endsWith('-auto'),
306
+ connection_kind: connectionKind,
307
+ connection_source: isCatalog ? 'portkey_catalog' : connectionKind,
308
+ is_catalog: isCatalog,
309
+ catalog_only: isCatalog,
310
+ route_policy: routePolicy,
311
+ model_catalog_status: options.modelCatalogStatus || 'ok',
312
+ ...(Number.isFinite(Number(options.modelCount)) ? { model_count: Number(options.modelCount) } : {}),
313
+ };
314
+ }
315
+
316
+ function _portkeyLastErrorMap(lastError) {
317
+ const out = new Map();
318
+ for (const part of String(lastError || '').split(/;\s*/)) {
319
+ if (!part) continue;
320
+ const idx = part.indexOf(':');
321
+ if (idx <= 0) continue;
322
+ const routeId = part.slice(0, idx).trim();
323
+ const message = part.slice(idx + 1).trim();
324
+ if (routeId && message) out.set(routeId, message);
325
+ }
326
+ return out;
327
+ }
328
+
329
+ function getVerifiedPortkeyProviderTypes(brainApi, warnings = null) {
330
+ const providers = brainApi.listModelProviders()
331
+ .map((provider) => safeGetProviderWithKey(brainApi, provider, warnings) || provider)
332
+ .filter(isPortkeyProviderRow);
333
+ const countsRead = safeModelCountsByProvider(brainApi, warnings);
334
+ const supported = new Set(
335
+ providers
336
+ .filter((provider) => {
337
+ if (countsRead.ok) return (countsRead.counts.get(String(provider.id || '')) || 0) > 0;
338
+ const modelRead = safeListModelsByProvider(brainApi, provider.id, warnings);
339
+ return modelRead.ok && modelRead.models.length > 0;
340
+ })
341
+ .map((provider) => provider.type)
342
+ );
343
+ const known = PORTKEY_GATEWAY_PROVIDER_TYPES.filter((type) => supported.has(type));
344
+ const custom = Array.from(supported).filter((type) => !PORTKEY_GATEWAY_PROVIDER_TYPES.includes(type)).sort();
345
+ return known.concat(custom);
346
+ }
347
+
348
+ function buildGatewaySummary(brainApi, warnings = null) {
349
+ const providers = brainApi.listModelProviders().map((provider) => {
350
+ const full = safeGetProviderWithKey(brainApi, provider, warnings) || provider;
351
+ return { ...provider, ...full };
352
+ });
353
+ const allPortkeyRoutes = providers.filter(isPortkeyProviderRow);
354
+ const gatewayCredentials = providers.filter(isPortkeyGatewayCredentialRow);
355
+ const credentialRows = gatewayCredentials.length ? gatewayCredentials : allPortkeyRoutes.slice(0, 1);
356
+ const routeModelReads = new Map();
357
+ const readRouteModels = (route) => {
358
+ if (!routeModelReads.has(route.id)) {
359
+ routeModelReads.set(route.id, safeListModelsByProvider(brainApi, route.id, warnings));
360
+ }
361
+ return routeModelReads.get(route.id);
362
+ };
363
+ const portkeyRoutes = allPortkeyRoutes.filter((route) => {
364
+ const modelRead = readRouteModels(route);
365
+ return modelRead.ok && modelRead.models.length > 0;
366
+ });
367
+ const configuredTypeSet = new Set(portkeyRoutes.map((route) => route.type).filter(Boolean));
368
+ const providerTypes = PORTKEY_GATEWAY_PROVIDER_TYPES
369
+ .filter((type) => configuredTypeSet.has(type))
370
+ .concat(Array.from(configuredTypeSet).filter((type) => !PORTKEY_GATEWAY_PROVIDER_TYPES.includes(type)).sort());
371
+ const lastSuccess = brainApi.getKv?.('model_gateway_sync:portkey:last_success_at') || '';
372
+ const lastRun = brainApi.getKv?.('model_gateway_sync:portkey:last_run_at') || '';
373
+ const lastError = brainApi.getKv?.('model_gateway_sync:portkey:last_error') || '';
374
+ const routeErrors = _portkeyLastErrorMap(lastError);
375
+ const routes = portkeyRoutes.map((route) => {
376
+ const models = readRouteModels(route).models;
377
+ const modelCount = models.length;
378
+ const catalogModelCount = models.filter((model) => model.source === 'portkey_catalog').length;
379
+ const gatewayModelCount = Math.max(0, modelCount - catalogModelCount);
380
+ const supported = modelCount > 0;
381
+ return {
382
+ id: route.id,
383
+ name: route.name,
384
+ type: route.type,
385
+ enabled: route.enabled,
386
+ model_count: modelCount,
387
+ catalog_model_count: catalogModelCount,
388
+ gateway_model_count: gatewayModelCount,
389
+ supported,
390
+ support_status: supported ? (gatewayModelCount > 0 ? 'supported' : 'catalog') : (routeErrors.has(route.id) ? 'error' : 'unverified'),
391
+ sync_error: routeErrors.get(route.id) || null,
392
+ route_policy: safeProviderRoutePolicy(brainApi, route.type, warnings),
393
+ };
394
+ });
395
+ const routesByType = new Map();
396
+ for (const route of routes) {
397
+ if (!routesByType.has(route.type)) routesByType.set(route.type, []);
398
+ routesByType.get(route.type).push(route);
399
+ }
400
+ const supportedProviderTypes = providerTypes
401
+ .filter((type) => (routesByType.get(type) || []).some((route) => route.supported));
402
+ const supportedProviders = providerTypes.map((type) => {
403
+ const typeRoutes = routesByType.get(type) || [];
404
+ const supportedRoute = typeRoutes.find((route) => route.supported);
405
+ const errorRoute = typeRoutes.find((route) => route.support_status === 'error');
406
+ const route = supportedRoute || errorRoute || typeRoutes[0] || null;
407
+ const supported = Boolean(supportedRoute);
408
+ const supportStatus = supported ? 'supported' : (errorRoute ? 'error' : (typeRoutes.length ? 'unverified' : 'not_configured'));
409
+ return {
410
+ type,
411
+ name: _providerDisplayName(type),
412
+ configured: typeRoutes.length > 0,
413
+ supported,
414
+ support_status: supportStatus,
415
+ route_id: route ? route.id : null,
416
+ model_count: typeRoutes.reduce((sum, item) => sum + item.model_count, 0),
417
+ catalog_model_count: typeRoutes.reduce((sum, item) => sum + (item.catalog_model_count || 0), 0),
418
+ gateway_model_count: typeRoutes.reduce((sum, item) => sum + (item.gateway_model_count || 0), 0),
419
+ sync_error: errorRoute ? errorRoute.sync_error : null,
420
+ route_policy: safeProviderRoutePolicy(brainApi, type, warnings),
421
+ };
422
+ });
423
+ const policies = {};
424
+ for (const type of providerTypes) {
425
+ policies[type] = safeProviderRoutePolicy(brainApi, type, warnings);
426
+ }
427
+ const totalModelCount = routes.reduce((sum, route) => sum + route.model_count, 0);
428
+ const totalCatalogModelCount = routes.reduce((sum, route) => sum + (route.catalog_model_count || 0), 0);
429
+ const totalGatewayModelCount = routes.reduce((sum, route) => sum + (route.gateway_model_count || 0), 0);
430
+ const visibleLastError = totalModelCount > 0 ? '' : lastError;
431
+ return [{
432
+ type: 'portkey',
433
+ name: 'Portkey Gateway',
434
+ enabled: credentialRows.some((row) => row.enabled !== 0) || portkeyRoutes.some((route) => route.enabled !== 0),
435
+ configured: credentialRows.length > 0 || allPortkeyRoutes.length > 0,
436
+ credential_count: credentialRows.length,
437
+ route_count: routes.length,
438
+ provider_count: providerTypes.length,
439
+ verified_provider_count: supportedProviderTypes.length,
440
+ model_count: totalModelCount,
441
+ catalog_model_count: totalCatalogModelCount,
442
+ gateway_model_count: totalGatewayModelCount,
443
+ provider_types: providerTypes,
444
+ configured_provider_types: providerTypes,
445
+ supported_provider_types: supportedProviderTypes,
446
+ supported_providers: supportedProviders,
447
+ routes,
448
+ policies,
449
+ last_success_at: lastSuccess,
450
+ last_run_at: lastRun,
451
+ last_error: visibleLastError,
452
+ last_warning: visibleLastError ? '' : lastError,
453
+ next_sync_hint: 'hourly',
454
+ }];
455
+ }
456
+
457
+ function _slugFromLabel(label) {
458
+ return String(label || '')
459
+ .trim()
460
+ .toLowerCase()
461
+ .replace(/[^a-z0-9]+/g, '-')
462
+ .replace(/^-|-$/g, '')
463
+ .slice(0, 40);
464
+ }
465
+
466
+ function _providerDisplayName(type) {
467
+ switch (type) {
468
+ case 'anthropic': return 'Anthropic';
469
+ case 'openai': return 'OpenAI';
470
+ case 'google': return 'Google Gemini';
471
+ case 'deepseek': return 'DeepSeek';
472
+ case 'moonshot': return 'Moonshot / Kimi';
473
+ default:
474
+ return String(type || 'Provider')
475
+ .split(/[-_]+/)
476
+ .filter(Boolean)
477
+ .map((part) => part.length <= 3 ? part.toUpperCase() : part.charAt(0).toUpperCase() + part.slice(1))
478
+ .join(' ') || 'Provider';
479
+ }
480
+ }
481
+
482
+ function _sanitizeSetupProviderType(value) {
483
+ const sanitized = typeof value === 'string'
484
+ ? value.toLowerCase().replace(/[^a-z]/g, '').slice(0, 20)
485
+ : '';
486
+ if (['kimi', 'kimik', 'kimik2', 'moonshotai', 'moonshotkimi'].includes(sanitized)) return 'moonshot';
487
+ return sanitized;
488
+ }
489
+
490
+ function _sanitizeSetupModel(value) {
491
+ return typeof value === 'string'
492
+ ? value.replace(/[\r\n\s]/g, '').slice(0, 100)
493
+ : '';
494
+ }
495
+
496
+ function handleLocalModelOwnerWrite(pathname, { method = 'POST', body = null } = {}) {
497
+ const brainApi = getBrain();
498
+ if (!brainApi) throw new Error('Wall-E brain not available');
499
+ const p = String(pathname || '');
500
+ const m = String(method || 'POST').toUpperCase();
501
+ const payload = body && typeof body === 'object' ? body : {};
502
+
503
+ if (p === '/api/wall-e/setup/provider' && m === 'POST') {
504
+ const type = _sanitizeSetupProviderType(payload.type);
505
+ if (!type) throw new Error('Invalid provider type');
506
+ if (!SETUP_PROVIDER_TYPES.includes(type)) {
507
+ throw new Error('Invalid provider type. Must be one of: ' + SETUP_PROVIDER_TYPES.join(', '));
508
+ }
509
+ const hasBaseUrl = Object.prototype.hasOwnProperty.call(payload, 'base_url')
510
+ || Object.prototype.hasOwnProperty.call(payload, 'baseUrl');
511
+ const result = brainApi.saveSetupProvider({
512
+ id: payload.id || `${type}-default`,
513
+ name: payload.name || _providerDisplayName(type),
514
+ type,
515
+ baseUrl: hasBaseUrl ? (payload.base_url || payload.baseUrl || null) : undefined,
516
+ apiKeyEncrypted: typeof payload.api_key === 'string'
517
+ ? payload.api_key.replace(/[\r\n\s]/g, '').slice(0, 200)
518
+ : (payload.apiKeyEncrypted || null),
519
+ customHeaders: payload.custom_headers || payload.customHeaders || null,
520
+ enabled: payload.enabled !== false,
521
+ model: _sanitizeSetupModel(payload.model),
522
+ setDefault: Boolean(payload.set_default),
523
+ authMethod: payload.auth_method || null,
524
+ });
525
+ return { ok: true, authority: 'wall-e-local-model-admin', owner_write: 'wall-e-local-model-admin', ...result };
526
+ }
527
+
528
+ if (p === '/api/wall-e/setup/default-provider' && m === 'POST') {
529
+ const type = _sanitizeSetupProviderType(payload.type);
530
+ if (!type) throw new Error('Invalid provider type');
531
+ if (!SETUP_PROVIDER_TYPES.includes(type)) {
532
+ throw new Error('Invalid provider type. Must be one of: ' + SETUP_PROVIDER_TYPES.join(', '));
533
+ }
534
+ const result = brainApi.setDefaultProviderSelection({
535
+ type,
536
+ requestedModel: _sanitizeSetupModel(payload.requested_model ?? payload.requestedModel ?? payload.model),
537
+ targetModel: _sanitizeSetupModel(payload.target_model ?? payload.targetModel ?? payload.model),
538
+ });
539
+ return { ok: true, authority: 'wall-e-local-model-admin', owner_write: 'wall-e-local-model-admin', ...result };
540
+ }
541
+
542
+ const providerDelete = p.match(/^\/api\/wall-e\/setup\/provider\/([a-z0-9_-]+)$/);
543
+ if (providerDelete && m === 'DELETE') {
544
+ const type = _sanitizeSetupProviderType(providerDelete[1]);
545
+ if (!type) throw new Error('Invalid provider type');
546
+ const result = brainApi.disableModelProviderByType(type);
547
+ return { ok: true, authority: 'wall-e-local-model-admin', owner_write: 'wall-e-local-model-admin', ...result };
548
+ }
549
+
550
+ throw new Error(`No local model owner write exists for ${m} ${p}`);
551
+ }
552
+
553
+ function _portkeyRuntimeBaseUrl(baseUrl, type) {
554
+ const normalized = String(baseUrl || defaultPortkeyBaseUrl('openai')).trim().replace(/\/+$/, '');
555
+ if (type === 'anthropic' && /^https:\/\/api\.portkey\.ai\/v1$/i.test(normalized)) {
556
+ return defaultPortkeyBaseUrl('anthropic');
557
+ }
558
+ return normalized || defaultPortkeyBaseUrl(type);
559
+ }
560
+
561
+ function _resolveDevboxPortkeyGateway(body = {}, apiKey = '') {
562
+ const source = String(body.source || body.credentialSource || body.detectedSource || '').toLowerCase();
563
+ const wantsDevbox = source.includes('devbox') || body.useDevboxHeaders === true;
564
+ const devboxGateway = getDevboxClaudePortkeyGateway();
565
+ if (!devboxGateway) return null;
566
+ if (wantsDevbox) return devboxGateway;
567
+ return apiKey && apiKey === devboxGateway.apiKey ? devboxGateway : null;
568
+ }
569
+
570
+ function upsertPortkeyGateway(brainApi, body = {}) {
571
+ let apiKey = String(body.apiKey || body.apiKeyEncrypted || '').trim();
572
+ const devboxGateway = _resolveDevboxPortkeyGateway(body, apiKey);
573
+ if (!apiKey && devboxGateway) apiKey = devboxGateway.apiKey;
574
+ if (!apiKey) throw new Error('Portkey API key required');
575
+ const label = String(body.label || (devboxGateway && body.source ? devboxGateway.label : '')).trim();
576
+ const slug = _slugFromLabel(label);
577
+ const suffix = slug ? `portkey-${slug}` : 'portkey';
578
+ const baseUrl = String((devboxGateway && devboxGateway.baseUrl) || body.baseUrl || body.gatewayUrl || defaultPortkeyBaseUrl('openai')).trim() || defaultPortkeyBaseUrl('openai');
579
+ const bodyHeaders = parseCustomHeaders(body.customHeaders || body.custom_headers) || {};
580
+ const seedHeaders = Object.assign({}, devboxGateway ? devboxGateway.customHeaders : {}, bodyHeaders);
581
+ const customHeaders = JSON.stringify(buildPortkeyHeaders({
582
+ apiKey,
583
+ configId: body.configId,
584
+ virtualKey: body.virtualKey,
585
+ headers: seedHeaders,
586
+ }));
587
+ let removedLegacyRoutes = 0;
588
+ for (const provider of brainApi.listModelProviders?.() || []) {
589
+ const full = brainApi.getModelProviderWithKey?.(provider.id) || provider;
590
+ if (isPortkeyProviderRow(full) && (brainApi.listModelsByProvider(full.id) || []).length === 0) {
591
+ removedLegacyRoutes += brainApi.deleteModelProvider(full.id) || 0;
592
+ }
593
+ }
594
+ const credential = {
595
+ id: `portkey-gateway-${slug || 'default'}`,
596
+ name: `Portkey Gateway${label ? ` (${label})` : ''}`,
597
+ type: 'portkey_gateway',
598
+ baseUrl: _portkeyRuntimeBaseUrl(baseUrl, 'openai'),
599
+ apiKeyEncrypted: apiKey,
600
+ customHeaders,
601
+ enabled: 1,
602
+ };
603
+ brainApi.upsertModelProvider(credential);
604
+ brainApi.setKv?.('model_gateway:portkey:label', label);
605
+ brainApi.setKv?.('model_gateway:portkey:base_url', baseUrl);
606
+ if (devboxGateway) brainApi.setKv?.('model_gateway:portkey:source', devboxGateway.source);
607
+ return {
608
+ ok: true,
609
+ gateway: 'portkey',
610
+ provider: { id: credential.id, type: credential.type, name: credential.name },
611
+ source: devboxGateway ? devboxGateway.source : (body.source || ''),
612
+ removed_legacy_routes: removedLegacyRoutes,
97
613
  };
98
614
  }
99
615
 
@@ -147,16 +663,168 @@ async function handleModelAdminApi(req, res, url) {
147
663
  if (p === '/api/models/providers' && m === 'GET') {
148
664
  if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
149
665
  try {
150
- const providers = brainApi.listModelProviders().map((provider) => getProviderWithMeta(brainApi, provider));
666
+ const warnings = [];
667
+ const modelCountsByProvider = safeModelCountsByProvider(brainApi, warnings);
668
+ const providerRows = brainApi.listModelProviders()
669
+ .map((provider, index) => {
670
+ const full = safeGetProviderWithKey(brainApi, provider, warnings) || provider;
671
+ const isPortkey = isPortkeyProviderRow(full);
672
+ const providerId = String(full.id || provider.id || '');
673
+ const modelCount = modelCountsByProvider.ok
674
+ ? (modelCountsByProvider.counts.get(providerId) || 0)
675
+ : null;
676
+ return { provider, full, isPortkey, modelCount, index };
677
+ })
678
+ .filter(({ full, isPortkey, modelCount }) => {
679
+ if (isPortkeyGatewayCredentialRow(full)) return false;
680
+ if (isPortkey && modelCountsByProvider.ok && modelCount === 0) return false;
681
+ return true;
682
+ })
683
+ .map(({ provider, full, modelCount, index }) => ({
684
+ provider: getProviderWithMeta(brainApi, provider, {
685
+ full,
686
+ modelCount: modelCountsByProvider.ok ? modelCount : undefined,
687
+ warnings,
688
+ modelCatalogStatus: warnings.length ? 'partial' : 'ok',
689
+ }),
690
+ index,
691
+ }));
692
+ providerRows.sort((a, b) => {
693
+ const rank = providerCatalogSortRank(a.provider) - providerCatalogSortRank(b.provider);
694
+ if (rank !== 0) return rank;
695
+ return a.index - b.index;
696
+ });
697
+ const providers = providerRows.map((row) => row.provider);
151
698
  const typeCounts = {};
152
699
  for (const provider of providers) typeCounts[provider.type] = (typeCounts[provider.type] || 0) + 1;
153
700
  for (const provider of providers) provider.instance_count = typeCounts[provider.type];
154
- return jsonResponse(res, 200, { providers });
701
+ return jsonResponse(res, 200, { providers, ...catalogStatusPayload(warnings) });
702
+ } catch (e) {
703
+ return jsonResponse(res, 500, { error: e.message });
704
+ }
705
+ }
706
+
707
+ if (p === '/api/models/gateways' && m === 'GET') {
708
+ if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
709
+ try {
710
+ const warnings = [];
711
+ return jsonResponse(res, 200, { gateways: buildGatewaySummary(brainApi, warnings), ...catalogStatusPayload(warnings) });
155
712
  } catch (e) {
156
713
  return jsonResponse(res, 500, { error: e.message });
157
714
  }
158
715
  }
159
716
 
717
+ if (p === '/api/models/provider-route-policy' && m === 'POST') {
718
+ if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
719
+ try {
720
+ const body = await readSmallJsonBody(req);
721
+ const type = String(body.type || '').trim().toLowerCase();
722
+ const policy = String(body.policy || 'auto').trim().toLowerCase();
723
+ const warnings = [];
724
+ if (policy === 'portkey' && !getVerifiedPortkeyProviderTypes(brainApi, warnings).includes(type)) {
725
+ if (warnings.length) {
726
+ return jsonResponse(res, 409, {
727
+ error: `${_providerDisplayName(type)} could not be verified by the Portkey gateway because the model catalog is partially unavailable. Repair or resync the model catalog, then try again.`,
728
+ code: 'model_registry_unavailable',
729
+ type,
730
+ ...catalogStatusPayload(warnings),
731
+ });
732
+ }
733
+ return jsonResponse(res, 409, {
734
+ error: `${_providerDisplayName(type)} is not verified by the Portkey gateway yet. Fix the Portkey sync error, then sync models before selecting Portkey.`,
735
+ code: 'portkey_provider_not_verified',
736
+ type,
737
+ });
738
+ }
739
+ const result = brainApi.setProviderRoutePolicy({ type: body.type, policy: body.policy });
740
+ return jsonResponse(res, 200, { ok: true, ...result });
741
+ } catch (e) {
742
+ return jsonResponse(res, 400, { error: e.message });
743
+ }
744
+ }
745
+
746
+ if (p === '/api/models/portkey/sync' && m === 'POST') {
747
+ if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
748
+ try {
749
+ const body = await readSmallJsonBody(req);
750
+ const result = await syncPortkeyGatewayModels({ brainApi, providerId: body.provider_id || body.providerId || null });
751
+ return jsonResponse(res, result.errors.length ? 207 : 200, { ok: result.errors.length === 0, ...result });
752
+ } catch (e) {
753
+ return jsonResponse(res, 400, { error: e.message });
754
+ }
755
+ }
756
+
757
+ if (p === '/api/models/gateways/portkey' && m === 'POST') {
758
+ if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
759
+ try {
760
+ const body = await readSmallJsonBody(req);
761
+ return jsonResponse(res, 200, upsertPortkeyGateway(brainApi, body));
762
+ } catch (e) {
763
+ return jsonResponse(res, 400, { error: e.message });
764
+ }
765
+ }
766
+
767
+ if (p === '/api/models/portkey/apply-all' && m === 'POST') {
768
+ if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
769
+ try {
770
+ const providers = brainApi.listModelProviders()
771
+ .map((provider) => brainApi.getModelProviderWithKey(provider.id) || provider)
772
+ .filter(isPortkeyProviderRow);
773
+ const types = Array.from(new Set(providers.map((provider) => provider.type))).sort();
774
+ const supportedTypes = getVerifiedPortkeyProviderTypes(brainApi);
775
+ const supportedSet = new Set(supportedTypes);
776
+ const skippedTypes = types.filter((type) => !supportedSet.has(type));
777
+ for (const type of supportedTypes) brainApi.setProviderRoutePolicy({ type, policy: 'portkey' });
778
+ for (const type of skippedTypes) {
779
+ if (brainApi.getProviderRoutePolicy?.(type) === 'portkey') {
780
+ brainApi.setProviderRoutePolicy({ type, policy: 'auto' });
781
+ }
782
+ }
783
+ const payload = { ok: supportedTypes.length > 0, gateway: 'portkey', provider_types: supportedTypes, skipped_provider_types: skippedTypes };
784
+ return jsonResponse(res, supportedTypes.length > 0 ? 200 : 409, supportedTypes.length > 0 ? payload : {
785
+ ...payload,
786
+ error: 'No Portkey providers are verified yet. Fix the Portkey sync error, then sync models before applying Portkey.',
787
+ code: 'no_verified_portkey_providers',
788
+ });
789
+ } catch (e) {
790
+ return jsonResponse(res, 400, { error: e.message });
791
+ }
792
+ }
793
+
794
+ if (p === '/api/models/portkey/disable-default' && m === 'POST') {
795
+ if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
796
+ try {
797
+ const providers = brainApi.listModelProviders()
798
+ .map((provider) => brainApi.getModelProviderWithKey(provider.id) || provider)
799
+ .filter(isPortkeyProviderRow);
800
+ const types = Array.from(new Set(providers.map((provider) => provider.type))).sort();
801
+ for (const type of types) brainApi.setProviderRoutePolicy({ type, policy: 'auto' });
802
+ return jsonResponse(res, 200, { ok: true, gateway: 'portkey', provider_types: types });
803
+ } catch (e) {
804
+ return jsonResponse(res, 400, { error: e.message });
805
+ }
806
+ }
807
+
808
+ if (p === '/api/models/gateways/portkey' && m === 'DELETE') {
809
+ if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
810
+ try {
811
+ const providers = brainApi.listModelProviders()
812
+ .map((provider) => brainApi.getModelProviderWithKey(provider.id) || provider)
813
+ .filter((provider) => isPortkeyGatewayCredentialRow(provider) || isPortkeyProviderRow(provider));
814
+ const types = Array.from(new Set(providers.filter(isPortkeyProviderRow).map((provider) => provider.type)));
815
+ let deletedProviders = 0;
816
+ let deletedModels = 0;
817
+ for (const provider of providers) {
818
+ deletedModels += brainApi.deleteModelRegistryByProvider(provider.id) || 0;
819
+ deletedProviders += brainApi.deleteModelProvider(provider.id) || 0;
820
+ }
821
+ for (const type of types) brainApi.setProviderRoutePolicy({ type, policy: 'auto' });
822
+ return jsonResponse(res, 200, { ok: true, gateway: 'portkey', deleted_providers: deletedProviders, deleted_models: deletedModels });
823
+ } catch (e) {
824
+ return jsonResponse(res, 400, { error: e.message });
825
+ }
826
+ }
827
+
160
828
  if (p === '/api/models/providers' && m === 'POST') {
161
829
  if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
162
830
  try {
@@ -195,8 +863,82 @@ async function handleModelAdminApi(req, res, url) {
195
863
  if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
196
864
  try {
197
865
  const providerId = url.searchParams.get('provider_id');
198
- const models = providerId ? brainApi.listModelsByProvider(providerId) : brainApi.listAllModels();
199
- return jsonResponse(res, 200, { models });
866
+ const warnings = [];
867
+ if (providerId) {
868
+ const modelRead = safeListModelsByProvider(brainApi, providerId, warnings);
869
+ return jsonResponse(res, 200, { models: modelRead.models, ...catalogStatusPayload(warnings) });
870
+ }
871
+
872
+ const providers = brainApi.listModelProviders();
873
+ const models = [];
874
+ for (const provider of providers) {
875
+ const modelRead = safeListModelsByProvider(brainApi, provider.id, warnings);
876
+ for (const model of modelRead.models) {
877
+ models.push({
878
+ ...model,
879
+ provider_name: provider.name,
880
+ provider_type: provider.type,
881
+ });
882
+ }
883
+ }
884
+ models.sort((a, b) => String(a.provider_name || '').localeCompare(String(b.provider_name || ''))
885
+ || String(a.display_name || '').localeCompare(String(b.display_name || '')));
886
+ return jsonResponse(res, 200, { models, ...catalogStatusPayload(warnings) });
887
+ } catch (e) {
888
+ return jsonResponse(res, 500, { error: e.message });
889
+ }
890
+ }
891
+
892
+ if (p === '/api/models/coding-catalog' && m === 'GET') {
893
+ if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
894
+ try {
895
+ const warnings = [];
896
+ const providerId = url.searchParams.get('provider_id');
897
+ const includeLive = url.searchParams.get('live') === '1' || url.searchParams.get('live') === 'true';
898
+ const providers = brainApi.listModelProviders()
899
+ .filter((provider) => provider.enabled !== 0 && provider.enabled !== false)
900
+ .filter((provider) => !providerId || provider.id === providerId);
901
+ const providerRows = [];
902
+ const models = [];
903
+ for (const provider of providers) {
904
+ const modelRead = safeListModelsByProvider(brainApi, provider.id, warnings);
905
+ providerRows.push(getProviderWithMeta(brainApi, provider, {
906
+ warnings,
907
+ modelRead,
908
+ modelCatalogStatus: modelRead.ok ? 'ok' : 'partial',
909
+ modelCount: modelRead.models.length,
910
+ }));
911
+ for (const model of modelRead.models) {
912
+ models.push({
913
+ ...model,
914
+ provider_name: provider.name,
915
+ provider_type: provider.type,
916
+ });
917
+ }
918
+ }
919
+ let liveAvailability = null;
920
+ if (includeLive && providers.length > 0) {
921
+ try {
922
+ const { createClient } = require('../llm/client');
923
+ const { listCodingModelAvailability } = require('../llm/coding-availability');
924
+ liveAvailability = await listCodingModelAvailability({
925
+ providers,
926
+ getProviderWithKey: (id) => brainApi.getModelProviderWithKey(id),
927
+ createClient,
928
+ });
929
+ } catch (err) {
930
+ addCatalogWarning(warnings, 'coding_catalog_live_probe', err);
931
+ }
932
+ }
933
+ const { buildSessionModelCatalog } = require('../llm/model-catalog');
934
+ return jsonResponse(res, 200, {
935
+ ...buildSessionModelCatalog({
936
+ providers: providerRows,
937
+ models,
938
+ liveAvailability,
939
+ }),
940
+ ...catalogStatusPayload(warnings),
941
+ });
200
942
  } catch (e) {
201
943
  return jsonResponse(res, 500, { error: e.message });
202
944
  }
@@ -270,6 +1012,32 @@ async function handleModelAdminApi(req, res, url) {
270
1012
  }
271
1013
  }
272
1014
 
1015
+ if (p === '/api/models/usage-ledger' && m === 'GET') {
1016
+ if (!brainApi) return jsonResponse(res, 500, { error: 'Wall-E brain not available' });
1017
+ try {
1018
+ const filters = {
1019
+ sessionId: url.searchParams.get('session_id') || url.searchParams.get('sessionId') || '',
1020
+ providerType: url.searchParams.get('provider') || url.searchParams.get('provider_type') || '',
1021
+ providerId: url.searchParams.get('provider_id') || '',
1022
+ modelId: url.searchParams.get('model') || url.searchParams.get('model_id') || '',
1023
+ modelRegistryId: url.searchParams.get('model_registry_id') || '',
1024
+ source: url.searchParams.get('source') || '',
1025
+ since: url.searchParams.get('since') || '',
1026
+ until: url.searchParams.get('until') || '',
1027
+ limit: url.searchParams.get('limit') || '',
1028
+ };
1029
+ const entries = typeof brainApi.listModelUsageLedger === 'function'
1030
+ ? brainApi.listModelUsageLedger(filters)
1031
+ : [];
1032
+ const summary = typeof brainApi.summarizeModelUsageLedger === 'function'
1033
+ ? brainApi.summarizeModelUsageLedger(filters)
1034
+ : [];
1035
+ return jsonResponse(res, 200, { entries, summary });
1036
+ } catch (e) {
1037
+ return jsonResponse(res, 500, { error: e.message });
1038
+ }
1039
+ }
1040
+
273
1041
  if (p === '/api/models/test-connection' && m === 'POST') {
274
1042
  try {
275
1043
  const body = await readSmallJsonBody(req);
@@ -512,13 +1280,21 @@ async function handleModelAdminApi(req, res, url) {
512
1280
  if (p === '/api/models/detect-keys' && m === 'GET') {
513
1281
  try {
514
1282
  const detected = [];
515
- const anthropicKey = getShellEnvValue('ANTHROPIC_API_KEY');
1283
+ const envValues = await _readShellEnvValues([
1284
+ 'ANTHROPIC_API_KEY',
1285
+ 'OPENAI_API_KEY',
1286
+ 'GOOGLE_API_KEY',
1287
+ 'GEMINI_API_KEY',
1288
+ 'MOONSHOT_API_KEY',
1289
+ 'PORTKEY_API_KEY',
1290
+ ], { ignoreCache: url.searchParams.get('refresh') === '1' });
1291
+ const anthropicKey = envValues.get('ANTHROPIC_API_KEY');
516
1292
  if (anthropicKey) detected.push({ type: 'anthropic', name: 'Anthropic', source: 'ANTHROPIC_API_KEY', apiKeyMasked: anthropicKey.slice(0, 8) + '...' + anthropicKey.slice(-4) });
517
- const openaiKey = getShellEnvValue('OPENAI_API_KEY');
1293
+ const openaiKey = envValues.get('OPENAI_API_KEY');
518
1294
  if (openaiKey) detected.push({ type: 'openai', name: 'OpenAI', source: 'OPENAI_API_KEY', apiKeyMasked: openaiKey.slice(0, 8) + '...' + openaiKey.slice(-4) });
519
- const googleKey = getShellEnvValue('GOOGLE_API_KEY') || getShellEnvValue('GEMINI_API_KEY');
1295
+ const googleKey = envValues.get('GOOGLE_API_KEY') || envValues.get('GEMINI_API_KEY');
520
1296
  if (googleKey) {
521
- const source = getShellEnvValue('GOOGLE_API_KEY') ? 'GOOGLE_API_KEY' : 'GEMINI_API_KEY';
1297
+ const source = envValues.get('GOOGLE_API_KEY') ? 'GOOGLE_API_KEY' : 'GEMINI_API_KEY';
522
1298
  detected.push({ type: 'google', name: 'Google Gemini', source, apiKeyMasked: googleKey.slice(0, 8) + '...' + googleKey.slice(-4) });
523
1299
  } else {
524
1300
  try {
@@ -531,14 +1307,32 @@ async function handleModelAdminApi(req, res, url) {
531
1307
  }
532
1308
  } catch {}
533
1309
  }
534
- const moonshotKey = getShellEnvValue('MOONSHOT_API_KEY');
1310
+ const moonshotKey = envValues.get('MOONSHOT_API_KEY');
535
1311
  if (moonshotKey) detected.push({ type: 'moonshot', name: 'Moonshot / Kimi', source: 'MOONSHOT_API_KEY', apiKeyMasked: moonshotKey.slice(0, 8) + '...' + moonshotKey.slice(-4) });
536
1312
  if (await detectOllama()) {
537
1313
  detected.push({ type: 'ollama', name: 'Ollama (Local)', source: 'localhost:11434', apiKeyMasked: 'n/a' });
538
1314
  }
539
1315
  const gateways = [];
540
- const portkeyKey = getShellEnvValue('PORTKEY_API_KEY');
541
- if (portkeyKey) gateways.push({ type: 'portkey', name: 'Portkey gateway', source: 'PORTKEY_API_KEY', apiKeyMasked: portkeyKey.slice(0, 8) + '...' + portkeyKey.slice(-4) });
1316
+ const devboxPortkey = getDevboxClaudePortkeyGateway();
1317
+ if (devboxPortkey) {
1318
+ gateways.push({
1319
+ type: 'portkey',
1320
+ name: 'Portkey gateway',
1321
+ source: devboxPortkey.source,
1322
+ label: devboxPortkey.label,
1323
+ apiKeyMasked: devboxPortkey.apiKeyMasked,
1324
+ baseUrl: devboxPortkey.baseUrl,
1325
+ canAutoConfigure: true,
1326
+ hasProvider: devboxPortkey.hasProvider,
1327
+ hasVirtualKey: devboxPortkey.hasVirtualKey,
1328
+ hasConfig: devboxPortkey.hasConfig,
1329
+ hasMetadata: devboxPortkey.hasMetadata,
1330
+ });
1331
+ }
1332
+ const portkeyKey = envValues.get('PORTKEY_API_KEY');
1333
+ if (portkeyKey && (!devboxPortkey || portkeyKey !== devboxPortkey.apiKey)) {
1334
+ gateways.push({ type: 'portkey', name: 'Portkey gateway', source: 'PORTKEY_API_KEY', apiKeyMasked: portkeyKey.slice(0, 8) + '...' + portkeyKey.slice(-4) });
1335
+ }
542
1336
  const existingTypes = new Set((brainApi ? brainApi.listModelProviders() : []).map((provider) => provider.type));
543
1337
  return jsonResponse(res, 200, { detected: detected.filter((item) => !existingTypes.has(item.type)), gateways });
544
1338
  } catch (e) {
@@ -555,10 +1349,11 @@ async function handleModelAdminApi(req, res, url) {
555
1349
  if (!type) return jsonResponse(res, 400, { error: 'Missing type' });
556
1350
 
557
1351
  const keyMap = {
558
- anthropic: () => getShellEnvValue('ANTHROPIC_API_KEY'),
559
- openai: () => getShellEnvValue('OPENAI_API_KEY'),
560
- google: () => {
561
- const envKey = getShellEnvValue('GOOGLE_API_KEY') || getShellEnvValue('GEMINI_API_KEY');
1352
+ anthropic: () => getShellEnvValue('ANTHROPIC_API_KEY', { ignoreCache: true }),
1353
+ openai: () => getShellEnvValue('OPENAI_API_KEY', { ignoreCache: true }),
1354
+ google: async () => {
1355
+ const envValues = await _readShellEnvValues(['GOOGLE_API_KEY', 'GEMINI_API_KEY'], { ignoreCache: true });
1356
+ const envKey = envValues.get('GOOGLE_API_KEY') || envValues.get('GEMINI_API_KEY');
562
1357
  if (envKey) return envKey;
563
1358
  try {
564
1359
  const creds = JSON.parse(fs.readFileSync(path.join(process.env.HOME, '.gemini', 'oauth_creds.json'), 'utf8'));
@@ -567,13 +1362,13 @@ async function handleModelAdminApi(req, res, url) {
567
1362
  return null;
568
1363
  }
569
1364
  },
570
- deepseek: () => getShellEnvValue('DEEPSEEK_API_KEY'),
571
- moonshot: () => getShellEnvValue('MOONSHOT_API_KEY'),
1365
+ deepseek: () => getShellEnvValue('DEEPSEEK_API_KEY', { ignoreCache: true }),
1366
+ moonshot: () => getShellEnvValue('MOONSHOT_API_KEY', { ignoreCache: true }),
572
1367
  ollama: () => null,
573
1368
  };
574
1369
 
575
1370
  const nameMap = { anthropic: 'Anthropic', openai: 'OpenAI', google: 'Google Gemini', deepseek: 'DeepSeek', moonshot: 'Moonshot / Kimi', ollama: 'Ollama (Local)' };
576
- const apiKey = keyMap[type] ? keyMap[type]() : null;
1371
+ const apiKey = keyMap[type] ? await keyMap[type]() : null;
577
1372
  if (!apiKey && type !== 'ollama') return jsonResponse(res, 400, { error: `No API key found in environment for ${type}` });
578
1373
 
579
1374
  const existingProviders = brainApi.listModelProviders().filter((provider) => provider.type === type);
@@ -625,4 +1420,11 @@ async function handleModelAdminApi(req, res, url) {
625
1420
 
626
1421
  module.exports = {
627
1422
  handleModelAdminApi,
1423
+ handleLocalModelOwnerWrite,
1424
+ _test: {
1425
+ _readShellEnvValues,
1426
+ _shellEnvLookupScript,
1427
+ clearShellEnvCache() { _shellEnvCache.clear(); _shellEnvInflight.clear(); },
1428
+ setShellEnvExecFile(fn) { _shellEnvExecFile = typeof fn === 'function' ? fn : execFile; },
1429
+ },
628
1430
  };