@code-yeongyu/senpi 2026.8.26 → 2026.8.28

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 (297) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +3 -3
  3. package/dist/cli/args.d.ts +3 -1
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +14 -1
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +10 -1
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/bash-executor.d.ts.map +1 -1
  11. package/dist/core/bash-executor.js +67 -29
  12. package/dist/core/bash-executor.js.map +1 -1
  13. package/dist/core/credential-accounts.d.ts +31 -0
  14. package/dist/core/credential-accounts.d.ts.map +1 -0
  15. package/dist/core/credential-accounts.js +125 -0
  16. package/dist/core/credential-accounts.js.map +1 -0
  17. package/dist/core/credential-pool/classify.d.ts +42 -0
  18. package/dist/core/credential-pool/classify.d.ts.map +1 -0
  19. package/dist/core/credential-pool/classify.js +75 -0
  20. package/dist/core/credential-pool/classify.js.map +1 -0
  21. package/dist/core/credential-pool/env-slots.d.ts +22 -0
  22. package/dist/core/credential-pool/env-slots.d.ts.map +1 -0
  23. package/dist/core/credential-pool/env-slots.js +37 -0
  24. package/dist/core/credential-pool/env-slots.js.map +1 -0
  25. package/dist/core/credential-pool/failover.d.ts +54 -0
  26. package/dist/core/credential-pool/failover.d.ts.map +1 -0
  27. package/dist/core/credential-pool/failover.js +106 -0
  28. package/dist/core/credential-pool/failover.js.map +1 -0
  29. package/dist/core/credential-pool/rotation-stream.d.ts +43 -0
  30. package/dist/core/credential-pool/rotation-stream.d.ts.map +1 -0
  31. package/dist/core/credential-pool/rotation-stream.js +96 -0
  32. package/dist/core/credential-pool/rotation-stream.js.map +1 -0
  33. package/dist/core/credential-pool/state-store.d.ts +89 -0
  34. package/dist/core/credential-pool/state-store.d.ts.map +1 -0
  35. package/dist/core/credential-pool/state-store.js +166 -0
  36. package/dist/core/credential-pool/state-store.js.map +1 -0
  37. package/dist/core/extensions/builtin/account/index.d.ts +9 -0
  38. package/dist/core/extensions/builtin/account/index.d.ts.map +1 -0
  39. package/dist/core/extensions/builtin/account/index.js +69 -0
  40. package/dist/core/extensions/builtin/account/index.js.map +1 -0
  41. package/dist/core/extensions/builtin/compaction/index.d.ts.map +1 -1
  42. package/dist/core/extensions/builtin/compaction/index.js +67 -0
  43. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  44. package/dist/core/extensions/builtin/compaction/log.d.ts +1 -1
  45. package/dist/core/extensions/builtin/compaction/log.d.ts.map +1 -1
  46. package/dist/core/extensions/builtin/compaction/log.js +1 -0
  47. package/dist/core/extensions/builtin/compaction/log.js.map +1 -1
  48. package/dist/core/extensions/builtin/config-reload/index.js +14 -22
  49. package/dist/core/extensions/builtin/config-reload/index.js.map +1 -1
  50. package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts +1 -0
  51. package/dist/core/extensions/builtin/goal/continuation-recovery.d.ts.map +1 -1
  52. package/dist/core/extensions/builtin/goal/continuation-recovery.js +2 -0
  53. package/dist/core/extensions/builtin/goal/continuation-recovery.js.map +1 -1
  54. package/dist/core/extensions/builtin/goal/continuation.d.ts +8 -1
  55. package/dist/core/extensions/builtin/goal/continuation.d.ts.map +1 -1
  56. package/dist/core/extensions/builtin/goal/continuation.js +15 -0
  57. package/dist/core/extensions/builtin/goal/continuation.js.map +1 -1
  58. package/dist/core/extensions/builtin/goal/direct-input-lifecycle.js +2 -2
  59. package/dist/core/extensions/builtin/goal/direct-input-lifecycle.js.map +1 -1
  60. package/dist/core/extensions/builtin/goal/format.d.ts +6 -1
  61. package/dist/core/extensions/builtin/goal/format.d.ts.map +1 -1
  62. package/dist/core/extensions/builtin/goal/format.js +6 -0
  63. package/dist/core/extensions/builtin/goal/format.js.map +1 -1
  64. package/dist/core/extensions/builtin/goal/lifecycle-helpers.d.ts.map +1 -1
  65. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js +5 -2
  66. package/dist/core/extensions/builtin/goal/lifecycle-helpers.js.map +1 -1
  67. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
  68. package/dist/core/extensions/builtin/goal/monitor-continuation.js +31 -7
  69. package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
  70. package/dist/core/extensions/builtin/goal/persistence.js +2 -0
  71. package/dist/core/extensions/builtin/goal/persistence.js.map +1 -1
  72. package/dist/core/extensions/builtin/goal/renderers.d.ts +14 -0
  73. package/dist/core/extensions/builtin/goal/renderers.d.ts.map +1 -0
  74. package/dist/core/extensions/builtin/goal/renderers.js +153 -0
  75. package/dist/core/extensions/builtin/goal/renderers.js.map +1 -0
  76. package/dist/core/extensions/builtin/goal/store.d.ts +6 -2
  77. package/dist/core/extensions/builtin/goal/store.d.ts.map +1 -1
  78. package/dist/core/extensions/builtin/goal/store.js +8 -2
  79. package/dist/core/extensions/builtin/goal/store.js.map +1 -1
  80. package/dist/core/extensions/builtin/goal/tool-registration.d.ts.map +1 -1
  81. package/dist/core/extensions/builtin/goal/tool-registration.js +16 -6
  82. package/dist/core/extensions/builtin/goal/tool-registration.js.map +1 -1
  83. package/dist/core/extensions/builtin/goal/types.d.ts +1 -0
  84. package/dist/core/extensions/builtin/goal/types.d.ts.map +1 -1
  85. package/dist/core/extensions/builtin/goal/types.js.map +1 -1
  86. package/dist/core/extensions/builtin/imagegen/index.d.ts.map +1 -1
  87. package/dist/core/extensions/builtin/imagegen/index.js +10 -3
  88. package/dist/core/extensions/builtin/imagegen/index.js.map +1 -1
  89. package/dist/core/extensions/builtin/index.d.ts.map +1 -1
  90. package/dist/core/extensions/builtin/index.js +4 -0
  91. package/dist/core/extensions/builtin/index.js.map +1 -1
  92. package/dist/core/extensions/builtin/todotools/index.js +1 -1
  93. package/dist/core/extensions/builtin/todotools/index.js.map +1 -1
  94. package/dist/core/extensions/builtin/todotools/native-todo-mirror.d.ts +1 -1
  95. package/dist/core/extensions/builtin/todotools/native-todo-mirror.d.ts.map +1 -1
  96. package/dist/core/extensions/builtin/todotools/native-todo-mirror.js +1 -1
  97. package/dist/core/extensions/builtin/todotools/native-todo-mirror.js.map +1 -1
  98. package/dist/core/hidden-stdout-log.d.ts +10 -0
  99. package/dist/core/hidden-stdout-log.d.ts.map +1 -1
  100. package/dist/core/hidden-stdout-log.js +36 -5
  101. package/dist/core/hidden-stdout-log.js.map +1 -1
  102. package/dist/core/model-config-schema.d.ts +85 -12
  103. package/dist/core/model-config-schema.d.ts.map +1 -1
  104. package/dist/core/model-config-schema.js +25 -0
  105. package/dist/core/model-config-schema.js.map +1 -1
  106. package/dist/core/model-runtime.d.ts +34 -1
  107. package/dist/core/model-runtime.d.ts.map +1 -1
  108. package/dist/core/model-runtime.js +98 -5
  109. package/dist/core/model-runtime.js.map +1 -1
  110. package/dist/core/tools/output-accumulator.d.ts +2 -0
  111. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  112. package/dist/core/tools/output-accumulator.js +32 -7
  113. package/dist/core/tools/output-accumulator.js.map +1 -1
  114. package/dist/index.d.ts +1 -1
  115. package/dist/index.d.ts.map +1 -1
  116. package/dist/index.js +1 -1
  117. package/dist/index.js.map +1 -1
  118. package/dist/main.d.ts.map +1 -1
  119. package/dist/main.js +38 -3
  120. package/dist/main.js.map +1 -1
  121. package/dist/modes/app-server/server/account.d.ts.map +1 -1
  122. package/dist/modes/app-server/server/account.js +5 -5
  123. package/dist/modes/app-server/server/account.js.map +1 -1
  124. package/dist/modes/index.d.ts +1 -0
  125. package/dist/modes/index.d.ts.map +1 -1
  126. package/dist/modes/index.js +1 -0
  127. package/dist/modes/index.js.map +1 -1
  128. package/dist/modes/interactive/components/footer.d.ts +11 -3
  129. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/footer.js +31 -1
  131. package/dist/modes/interactive/components/footer.js.map +1 -1
  132. package/dist/modes/interactive/components/tool-execution-fallback.d.ts.map +1 -1
  133. package/dist/modes/interactive/components/tool-execution-fallback.js +82 -1
  134. package/dist/modes/interactive/components/tool-execution-fallback.js.map +1 -1
  135. package/dist/modes/interactive/grok/chrome.d.ts +4 -4
  136. package/dist/modes/interactive/grok/chrome.d.ts.map +1 -1
  137. package/dist/modes/interactive/grok/chrome.js.map +1 -1
  138. package/dist/modes/interactive/grok/footer.d.ts +3 -3
  139. package/dist/modes/interactive/grok/footer.d.ts.map +1 -1
  140. package/dist/modes/interactive/grok/footer.js.map +1 -1
  141. package/dist/modes/interactive/interactive-host-runtime.d.ts +44 -0
  142. package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -0
  143. package/dist/modes/interactive/interactive-host-runtime.js +271 -0
  144. package/dist/modes/interactive/interactive-host-runtime.js.map +1 -0
  145. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  146. package/dist/modes/interactive/interactive-mode.js +65 -21
  147. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  148. package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
  149. package/dist/modes/rpc/connection-handler.js +19 -6
  150. package/dist/modes/rpc/connection-handler.js.map +1 -1
  151. package/dist/modes/rpc/host-ensure.d.ts +41 -0
  152. package/dist/modes/rpc/host-ensure.d.ts.map +1 -0
  153. package/dist/modes/rpc/host-ensure.js +245 -0
  154. package/dist/modes/rpc/host-ensure.js.map +1 -0
  155. package/dist/modes/rpc/host-lifecycle.d.ts +57 -0
  156. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -0
  157. package/dist/modes/rpc/host-lifecycle.js +491 -0
  158. package/dist/modes/rpc/host-lifecycle.js.map +1 -0
  159. package/dist/modes/rpc/host-watchdog.d.ts +33 -0
  160. package/dist/modes/rpc/host-watchdog.d.ts.map +1 -0
  161. package/dist/modes/rpc/host-watchdog.js +142 -0
  162. package/dist/modes/rpc/host-watchdog.js.map +1 -0
  163. package/dist/modes/rpc/multi-session-host.d.ts +7 -1
  164. package/dist/modes/rpc/multi-session-host.d.ts.map +1 -1
  165. package/dist/modes/rpc/multi-session-host.js +206 -32
  166. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  167. package/dist/modes/rpc/rpc-client.d.ts +41 -3
  168. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  169. package/dist/modes/rpc/rpc-client.js +125 -14
  170. package/dist/modes/rpc/rpc-client.js.map +1 -1
  171. package/dist/modes/rpc/rpc-mode.d.ts +1 -1
  172. package/dist/modes/rpc/rpc-mode.js +1 -1
  173. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  174. package/dist/modes/rpc/rpc-types.d.ts +6 -2
  175. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  176. package/dist/modes/rpc/rpc-types.js.map +1 -1
  177. package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
  178. package/dist/modes/rpc/session-command-router.js +24 -6
  179. package/dist/modes/rpc/session-command-router.js.map +1 -1
  180. package/dist/modes/rpc/session-event-writer.d.ts +13 -2
  181. package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
  182. package/dist/modes/rpc/session-event-writer.js +65 -25
  183. package/dist/modes/rpc/session-event-writer.js.map +1 -1
  184. package/dist/modes/rpc/session-registry.d.ts +9 -0
  185. package/dist/modes/rpc/session-registry.d.ts.map +1 -1
  186. package/dist/modes/rpc/session-registry.js +31 -3
  187. package/dist/modes/rpc/session-registry.js.map +1 -1
  188. package/docs/providers.md +48 -0
  189. package/docs/rpc.md +59 -5
  190. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +44 -0
  191. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  192. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +13 -1
  193. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/inline-worker.ts +15 -1
  194. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-indirect-eval.js +711 -1
  195. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/transport.ts +14 -1
  196. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +10 -10
  197. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-eval-result.ts +9 -3
  198. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +6 -1
  199. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.d.ts.map +1 -1
  200. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.js +20 -4
  201. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/edit.js.map +1 -1
  202. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/index.d.ts +1 -1
  203. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/index.d.ts.map +1 -1
  204. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/index.js.map +1 -1
  205. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.d.ts +21 -0
  206. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.d.ts.map +1 -0
  207. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.js +27 -0
  208. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/post-mutate.js.map +1 -0
  209. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/tool-context.d.ts +24 -0
  210. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/tool-context.d.ts.map +1 -1
  211. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/tool-context.js.map +1 -1
  212. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/write.d.ts.map +1 -1
  213. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/write.js +7 -2
  214. package/node_modules/@earendil-works/pi-agent-core/dist/harness/tools/write.js.map +1 -1
  215. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  216. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.d.ts +1 -1
  217. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.d.ts.map +1 -1
  218. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.js +46 -14
  219. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-tool-pairs.js.map +1 -1
  220. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +11 -0
  221. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
  222. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +30 -16
  223. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
  224. package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.d.ts +2 -1
  225. package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.d.ts.map +1 -1
  226. package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.js +2 -1
  227. package/node_modules/@earendil-works/pi-ai/dist/auth/credential-store.js.map +1 -1
  228. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.d.ts +16 -0
  229. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.d.ts.map +1 -0
  230. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.js +75 -0
  231. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/classify.js.map +1 -0
  232. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.d.ts +45 -0
  233. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.d.ts.map +1 -0
  234. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.js +81 -0
  235. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/failover.js.map +1 -0
  236. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.d.ts +31 -0
  237. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.d.ts.map +1 -0
  238. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.js +65 -0
  239. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/select.js.map +1 -0
  240. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.d.ts +12 -0
  241. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.d.ts.map +1 -1
  242. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.js +37 -0
  243. package/node_modules/@earendil-works/pi-ai/dist/auth/pool/slots.js.map +1 -1
  244. package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.d.ts +6 -0
  245. package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.d.ts.map +1 -1
  246. package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.js +32 -6
  247. package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.js.map +1 -1
  248. package/node_modules/@earendil-works/pi-ai/dist/auth/types.d.ts +3 -2
  249. package/node_modules/@earendil-works/pi-ai/dist/auth/types.d.ts.map +1 -1
  250. package/node_modules/@earendil-works/pi-ai/dist/auth/types.js.map +1 -1
  251. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.d.ts +6 -0
  252. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.d.ts.map +1 -1
  253. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js +6 -1
  254. package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js.map +1 -1
  255. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts +75 -0
  256. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.d.ts.map +1 -1
  257. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +75 -0
  258. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js.map +1 -1
  259. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  260. package/node_modules/@earendil-works/pi-ai/dist/providers/data/alibaba-token-plan.json +1 -1
  261. package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
  262. package/node_modules/@earendil-works/pi-ai/dist/providers/data/baseten.json +1 -1
  263. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  264. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
  265. package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
  266. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  267. package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
  268. package/node_modules/@earendil-works/pi-ai/dist/providers/data/minimax-cn.json +1 -1
  269. package/node_modules/@earendil-works/pi-ai/dist/providers/data/minimax.json +1 -1
  270. package/node_modules/@earendil-works/pi-ai/dist/providers/data/mistral.json +1 -1
  271. package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
  272. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json +1 -1
  273. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
  274. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  275. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  276. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opengateway.json +1 -1
  277. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  278. package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan-cn.json +1 -1
  279. package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan.json +1 -1
  280. package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
  281. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  282. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai-coding-cn.json +1 -1
  283. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai.json +1 -1
  284. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.d.ts.map +1 -1
  285. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +4 -0
  286. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js.map +1 -1
  287. package/node_modules/@earendil-works/pi-ai/package.json +5 -5
  288. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  289. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  290. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +5 -0
  291. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
  292. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +90 -1
  293. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
  294. package/node_modules/@earendil-works/pi-tui/dist/tui.js +1 -1
  295. package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +1 -1
  296. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  297. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAgJrB"}
1
+ {"version":3,"file":"bash-executor.d.ts","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAOtD,MAAM,WAAW,mBAAmB;IACnC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAMD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,cAAc,EAC1B,OAAO,CAAC,EAAE,mBAAmB,GAC3B,OAAO,CAAC,UAAU,CAAC,CAgLrB"}
@@ -26,6 +26,8 @@ export async function executeBashWithOperations(command, cwd, operations, option
26
26
  const maxOutputBytes = DEFAULT_MAX_BYTES * 2;
27
27
  let tempFilePath;
28
28
  let tempFileStream;
29
+ let tempFileError;
30
+ let tempFileErrorListener;
29
31
  let totalBytes = 0;
30
32
  const compactOutputChunks = () => {
31
33
  if (outputChunkStart < 64) {
@@ -42,6 +44,10 @@ export async function executeBashWithOperations(command, cwd, operations, option
42
44
  const id = randomBytes(8).toString("hex");
43
45
  tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
44
46
  tempFileStream = createWriteStream(tempFilePath);
47
+ tempFileErrorListener = (error) => {
48
+ tempFileError ??= error;
49
+ };
50
+ tempFileStream.on("error", tempFileErrorListener);
45
51
  for (let i = outputChunkStart; i < outputChunks.length; i++) {
46
52
  const chunk = outputChunks[i];
47
53
  if (chunk !== undefined) {
@@ -55,17 +61,38 @@ export async function executeBashWithOperations(command, cwd, operations, option
55
61
  if (!stream) {
56
62
  return;
57
63
  }
64
+ if (tempFileError) {
65
+ if (tempFileErrorListener)
66
+ stream.off("error", tempFileErrorListener);
67
+ stream.destroy();
68
+ throw tempFileError;
69
+ }
58
70
  await new Promise((resolve, reject) => {
71
+ let settled = false;
72
+ const cleanup = () => {
73
+ stream.off("error", onError);
74
+ if (tempFileErrorListener)
75
+ stream.off("error", tempFileErrorListener);
76
+ stream.off("close", onClose);
77
+ };
78
+ const settle = (error) => {
79
+ if (settled)
80
+ return;
81
+ settled = true;
82
+ cleanup();
83
+ if (error)
84
+ reject(error);
85
+ else
86
+ resolve();
87
+ };
59
88
  const onError = (error) => {
60
- stream.off("finish", onFinish);
61
- reject(error);
89
+ tempFileError ??= error;
62
90
  };
63
- const onFinish = () => {
64
- stream.off("error", onError);
65
- resolve();
91
+ const onClose = () => {
92
+ settle(tempFileError);
66
93
  };
67
- stream.once("error", onError);
68
- stream.once("finish", onFinish);
94
+ stream.on("error", onError);
95
+ stream.once("close", onClose);
69
96
  stream.end();
70
97
  });
71
98
  };
@@ -103,36 +130,37 @@ export async function executeBashWithOperations(command, cwd, operations, option
103
130
  onData(Buffer.from(finalText, "utf-8"));
104
131
  }
105
132
  };
133
+ const prepareFinalOutput = async () => {
134
+ try {
135
+ finishDecoder();
136
+ const fullOutput = outputText();
137
+ const truncationResult = truncateTail(fullOutput);
138
+ if (truncationResult.truncated) {
139
+ ensureTempFile();
140
+ }
141
+ return { fullOutput, truncationResult };
142
+ }
143
+ catch (error) {
144
+ try {
145
+ await closeTempFileStream();
146
+ }
147
+ catch (closeError) {
148
+ throw new AggregateError([error, closeError], "Bash output finalization and spill cleanup failed");
149
+ }
150
+ throw error;
151
+ }
152
+ };
153
+ let result;
106
154
  try {
107
- const result = await operations.exec(command, cwd, {
155
+ result = await operations.exec(command, cwd, {
108
156
  onData,
109
157
  signal: options?.signal,
110
158
  });
111
- finishDecoder();
112
- const fullOutput = outputText();
113
- const truncationResult = truncateTail(fullOutput);
114
- if (truncationResult.truncated) {
115
- ensureTempFile();
116
- }
117
- await closeTempFileStream();
118
- const cancelled = options?.signal?.aborted ?? false;
119
- return {
120
- output: truncationResult.truncated ? truncationResult.content : fullOutput,
121
- exitCode: cancelled ? undefined : (result.exitCode ?? undefined),
122
- cancelled,
123
- truncated: truncationResult.truncated,
124
- fullOutputPath: tempFilePath,
125
- };
126
159
  }
127
160
  catch (err) {
128
161
  // Check if it was an abort
129
162
  if (options?.signal?.aborted) {
130
- finishDecoder();
131
- const fullOutput = outputText();
132
- const truncationResult = truncateTail(fullOutput);
133
- if (truncationResult.truncated) {
134
- ensureTempFile();
135
- }
163
+ const { fullOutput, truncationResult } = await prepareFinalOutput();
136
164
  await closeTempFileStream();
137
165
  return {
138
166
  output: truncationResult.truncated ? truncationResult.content : fullOutput,
@@ -145,5 +173,15 @@ export async function executeBashWithOperations(command, cwd, operations, option
145
173
  await closeTempFileStream();
146
174
  throw err;
147
175
  }
176
+ const { fullOutput, truncationResult } = await prepareFinalOutput();
177
+ await closeTempFileStream();
178
+ const cancelled = options?.signal?.aborted ?? false;
179
+ return {
180
+ output: truncationResult.truncated ? truncationResult.content : fullOutput,
181
+ exitCode: cancelled ? undefined : (result.exitCode ?? undefined),
182
+ cancelled,
183
+ truncated: truncationResult.truncated,
184
+ fullOutputPath: tempFilePath,
185
+ };
148
186
  }
149
187
  //# sourceMappingURL=bash-executor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"bash-executor.js","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tconst outputChunks: string[] = [];\n\tlet outputChunkStart = 0;\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet totalBytes = 0;\n\n\tconst compactOutputChunks = () => {\n\t\tif (outputChunkStart < 64) {\n\t\t\treturn;\n\t\t}\n\t\toutputChunks.splice(0, outputChunkStart);\n\t\toutputChunkStart = 0;\n\t};\n\n\tconst outputText = () => outputChunks.slice(outputChunkStart).join(\"\");\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\tfor (let i = outputChunkStart; i < outputChunks.length; i++) {\n\t\t\tconst chunk = outputChunks[i];\n\t\t\tif (chunk !== undefined) {\n\t\t\t\ttempFileStream.write(chunk);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst closeTempFileStream = async () => {\n\t\tconst stream = tempFileStream;\n\t\ttempFileStream = undefined;\n\t\tif (!stream) {\n\t\t\treturn;\n\t\t}\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tconst onError = (error: Error) => {\n\t\t\t\tstream.off(\"finish\", onFinish);\n\t\t\t\treject(error);\n\t\t\t};\n\t\t\tconst onFinish = () => {\n\t\t\t\tstream.off(\"error\", onError);\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tstream.once(\"error\", onError);\n\t\t\tstream.once(\"finish\", onFinish);\n\t\t\tstream.end();\n\t\t});\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunkStart < outputChunks.length - 1) {\n\t\t\tconst removed = outputChunks[outputChunkStart];\n\t\t\tif (removed === undefined) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\toutputBytes -= removed.length;\n\t\t\toutputChunkStart++;\n\t\t}\n\t\tcompactOutputChunks();\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\tconst finishDecoder = () => {\n\t\tconst finalText = sanitizeBinaryOutput(stripAnsi(decoder.decode())).replace(/\\r/g, \"\");\n\t\tif (finalText.length > 0) {\n\t\t\tonData(Buffer.from(finalText, \"utf-8\"));\n\t\t}\n\t};\n\n\ttry {\n\t\tconst result = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\n\t\tfinishDecoder();\n\t\tconst fullOutput = outputText();\n\t\tconst truncationResult = truncateTail(fullOutput);\n\t\tif (truncationResult.truncated) {\n\t\t\tensureTempFile();\n\t\t}\n\t\tawait closeTempFileStream();\n\t\tconst cancelled = options?.signal?.aborted ?? false;\n\n\t\treturn {\n\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\t\tcancelled,\n\t\t\ttruncated: truncationResult.truncated,\n\t\t\tfullOutputPath: tempFilePath,\n\t\t};\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tfinishDecoder();\n\t\t\tconst fullOutput = outputText();\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\tawait closeTempFileStream();\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tawait closeTempFileStream();\n\n\t\tthrow err;\n\t}\n}\n"]}
1
+ {"version":3,"file":"bash-executor.js","sourceRoot":"","sources":["../../src/core/bash-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bash command execution with streaming support and cancellation.\n *\n * This module provides a unified bash execution implementation used by:\n * - AgentSession.executeBash() for interactive and RPC modes\n * - Direct calls from modes that need bash execution\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { stripAnsi } from \"../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../utils/shell.ts\";\nimport type { BashOperations } from \"./tools/bash.ts\";\nimport { DEFAULT_MAX_BYTES, truncateTail } from \"./tools/truncate.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BashExecutorOptions {\n\t/** Callback for streaming output chunks (already sanitized) */\n\tonChunk?: (chunk: string) => void;\n\t/** AbortSignal for cancellation */\n\tsignal?: AbortSignal;\n}\n\nexport interface BashResult {\n\t/** Combined stdout + stderr output (sanitized, possibly truncated) */\n\toutput: string;\n\t/** Process exit code (undefined if killed/cancelled) */\n\texitCode: number | undefined;\n\t/** Whether the command was cancelled via signal */\n\tcancelled: boolean;\n\t/** Whether the output was truncated */\n\ttruncated: boolean;\n\t/** Path to temp file containing full output (if output exceeded truncation threshold) */\n\tfullOutputPath?: string;\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Execute a bash command using custom BashOperations.\n * Used for remote execution (SSH, containers, etc.).\n */\nexport async function executeBashWithOperations(\n\tcommand: string,\n\tcwd: string,\n\toperations: BashOperations,\n\toptions?: BashExecutorOptions,\n): Promise<BashResult> {\n\tconst outputChunks: string[] = [];\n\tlet outputChunkStart = 0;\n\tlet outputBytes = 0;\n\tconst maxOutputBytes = DEFAULT_MAX_BYTES * 2;\n\n\tlet tempFilePath: string | undefined;\n\tlet tempFileStream: WriteStream | undefined;\n\tlet tempFileError: Error | undefined;\n\tlet tempFileErrorListener: ((error: Error) => void) | undefined;\n\tlet totalBytes = 0;\n\n\tconst compactOutputChunks = () => {\n\t\tif (outputChunkStart < 64) {\n\t\t\treturn;\n\t\t}\n\t\toutputChunks.splice(0, outputChunkStart);\n\t\toutputChunkStart = 0;\n\t};\n\n\tconst outputText = () => outputChunks.slice(outputChunkStart).join(\"\");\n\n\tconst ensureTempFile = () => {\n\t\tif (tempFilePath) {\n\t\t\treturn;\n\t\t}\n\t\tconst id = randomBytes(8).toString(\"hex\");\n\t\ttempFilePath = join(tmpdir(), `pi-bash-${id}.log`);\n\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\ttempFileErrorListener = (error) => {\n\t\t\ttempFileError ??= error;\n\t\t};\n\t\ttempFileStream.on(\"error\", tempFileErrorListener);\n\t\tfor (let i = outputChunkStart; i < outputChunks.length; i++) {\n\t\t\tconst chunk = outputChunks[i];\n\t\t\tif (chunk !== undefined) {\n\t\t\t\ttempFileStream.write(chunk);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst closeTempFileStream = async () => {\n\t\tconst stream = tempFileStream;\n\t\ttempFileStream = undefined;\n\t\tif (!stream) {\n\t\t\treturn;\n\t\t}\n\t\tif (tempFileError) {\n\t\t\tif (tempFileErrorListener) stream.off(\"error\", tempFileErrorListener);\n\t\t\tstream.destroy();\n\t\t\tthrow tempFileError;\n\t\t}\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tlet settled = false;\n\t\t\tconst cleanup = () => {\n\t\t\t\tstream.off(\"error\", onError);\n\t\t\t\tif (tempFileErrorListener) stream.off(\"error\", tempFileErrorListener);\n\t\t\t\tstream.off(\"close\", onClose);\n\t\t\t};\n\t\t\tconst settle = (error?: Error) => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tcleanup();\n\t\t\t\tif (error) reject(error);\n\t\t\t\telse resolve();\n\t\t\t};\n\t\t\tconst onError = (error: Error) => {\n\t\t\t\ttempFileError ??= error;\n\t\t\t};\n\t\t\tconst onClose = () => {\n\t\t\t\tsettle(tempFileError);\n\t\t\t};\n\t\t\tstream.on(\"error\", onError);\n\t\t\tstream.once(\"close\", onClose);\n\t\t\tstream.end();\n\t\t});\n\t};\n\n\tconst decoder = new TextDecoder();\n\n\tconst onData = (data: Buffer) => {\n\t\ttotalBytes += data.length;\n\n\t\t// Sanitize: strip ANSI, replace binary garbage, normalize newlines\n\t\tconst text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace(/\\r/g, \"\");\n\n\t\t// Start writing to temp file if exceeds threshold\n\t\tif (totalBytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureTempFile();\n\t\t}\n\n\t\tif (tempFileStream) {\n\t\t\ttempFileStream.write(text);\n\t\t}\n\n\t\toutputChunks.push(text);\n\t\toutputBytes += text.length;\n\t\twhile (outputBytes > maxOutputBytes && outputChunkStart < outputChunks.length - 1) {\n\t\t\tconst removed = outputChunks[outputChunkStart];\n\t\t\tif (removed === undefined) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\toutputBytes -= removed.length;\n\t\t\toutputChunkStart++;\n\t\t}\n\t\tcompactOutputChunks();\n\n\t\t// Stream to callback\n\t\tif (options?.onChunk) {\n\t\t\toptions.onChunk(text);\n\t\t}\n\t};\n\n\tconst finishDecoder = () => {\n\t\tconst finalText = sanitizeBinaryOutput(stripAnsi(decoder.decode())).replace(/\\r/g, \"\");\n\t\tif (finalText.length > 0) {\n\t\t\tonData(Buffer.from(finalText, \"utf-8\"));\n\t\t}\n\t};\n\n\tconst prepareFinalOutput = async () => {\n\t\ttry {\n\t\t\tfinishDecoder();\n\t\t\tconst fullOutput = outputText();\n\t\t\tconst truncationResult = truncateTail(fullOutput);\n\t\t\tif (truncationResult.truncated) {\n\t\t\t\tensureTempFile();\n\t\t\t}\n\t\t\treturn { fullOutput, truncationResult };\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tawait closeTempFileStream();\n\t\t\t} catch (closeError) {\n\t\t\t\tthrow new AggregateError([error, closeError], \"Bash output finalization and spill cleanup failed\");\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t};\n\n\tlet result: Awaited<ReturnType<BashOperations[\"exec\"]>>;\n\ttry {\n\t\tresult = await operations.exec(command, cwd, {\n\t\t\tonData,\n\t\t\tsignal: options?.signal,\n\t\t});\n\t} catch (err) {\n\t\t// Check if it was an abort\n\t\tif (options?.signal?.aborted) {\n\t\t\tconst { fullOutput, truncationResult } = await prepareFinalOutput();\n\t\t\tawait closeTempFileStream();\n\t\t\treturn {\n\t\t\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\t\t\texitCode: undefined,\n\t\t\t\tcancelled: true,\n\t\t\t\ttruncated: truncationResult.truncated,\n\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t};\n\t\t}\n\n\t\tawait closeTempFileStream();\n\n\t\tthrow err;\n\t}\n\n\tconst { fullOutput, truncationResult } = await prepareFinalOutput();\n\tawait closeTempFileStream();\n\tconst cancelled = options?.signal?.aborted ?? false;\n\n\treturn {\n\t\toutput: truncationResult.truncated ? truncationResult.content : fullOutput,\n\t\texitCode: cancelled ? undefined : (result.exitCode ?? undefined),\n\t\tcancelled,\n\t\ttruncated: truncationResult.truncated,\n\t\tfullOutputPath: tempFilePath,\n\t};\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import type { Credential } from "@earendil-works/pi-ai";
2
+ import type { AuthStorage } from "./auth-storage.ts";
3
+ import { CredentialSlotRepository } from "./credential-pool/state-store.ts";
4
+ export type CredentialAccountSource = "login" | "import" | "env";
5
+ /** Account metadata safe to surface: names and health only, never key material. */
6
+ export type CredentialAccountSummary = {
7
+ readonly name: string;
8
+ readonly source: CredentialAccountSource;
9
+ readonly blocked: boolean;
10
+ readonly pinned: boolean;
11
+ };
12
+ export declare function assertValidAccountName(name: string): void;
13
+ /**
14
+ * Lists a provider's credential accounts for ANY provider, not just one lane.
15
+ * Stored slots own the listing when a credential exists; env slots are listed
16
+ * only when nothing is stored, mirroring resolution precedence exactly. Health
17
+ * comes from the pool sidecar, so a cooldown persisted by a failover shows up
18
+ * here without auth.json ever carrying block state.
19
+ */
20
+ export declare function getCredentialAccounts(storage: AuthStorage, provider: string, env?: NodeJS.ProcessEnv, repository?: CredentialSlotRepository): Promise<CredentialAccountSummary[]>;
21
+ /** Storage-free variant for callers that already hold the credential (e.g. auth check). */
22
+ export declare function summarizeCredentialAccounts(provider: string, stored: Credential | undefined, env?: NodeJS.ProcessEnv, repository?: CredentialSlotRepository): Promise<CredentialAccountSummary[]>;
23
+ /** Pins one slot, or clears the pin when `name` is null. */
24
+ export declare function pinCredentialAccount(storage: AuthStorage, provider: string, name: string | null, env?: NodeJS.ProcessEnv, repository?: CredentialSlotRepository): Promise<void>;
25
+ /**
26
+ * Removes one stored slot. Env-backed accounts are refused: they are owned by
27
+ * the environment, and deleting one here would silently disagree with the
28
+ * variable that still defines it.
29
+ */
30
+ export declare function removeCredentialAccount(storage: AuthStorage, provider: string, name: string, env?: NodeJS.ProcessEnv, repository?: CredentialSlotRepository): Promise<void>;
31
+ //# sourceMappingURL=credential-accounts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-accounts.d.ts","sourceRoot":"","sources":["../../src/core/credential-accounts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,EAAE,wBAAwB,EAAwC,MAAM,kCAAkC,CAAC;AAGlH,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEjE,mFAAmF;AACnF,MAAM,MAAM,wBAAwB,GAAG;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CACzB,CAAC;AAIF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMzD;AAkCD;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CAC1C,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,UAAU,GAAE,wBAAyD,GACnE,OAAO,CAAC,wBAAwB,EAAE,CAAC,CAErC;AAED,2FAA2F;AAC3F,wBAAsB,2BAA2B,CAChD,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,UAAU,GAAG,SAAS,EAC9B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,UAAU,GAAE,wBAAyD,GACnE,OAAO,CAAC,wBAAwB,EAAE,CAAC,CA8BrC;AAED,4DAA4D;AAC5D,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,UAAU,GAAE,wBAAyD,GACnE,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC5C,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,UAAU,GAAE,wBAAyD,GACnE,OAAO,CAAC,IAAI,CAAC,CAaf"}
@@ -0,0 +1,125 @@
1
+ import { listSlots, pinSlot, removeSlot } from "@earendil-works/pi-ai/auth/pool/slots";
2
+ import { discoverEnvSlots } from "./credential-pool/env-slots.js";
3
+ import { CredentialSlotRepository, slotHealth } from "./credential-pool/state-store.js";
4
+ import { emitProviderAccountsChanged } from "./extensions/builtin/claude-sdk-oauth/account-events.js";
5
+ const ACCOUNT_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
6
+ export function assertValidAccountName(name) {
7
+ if (!ACCOUNT_NAME_PATTERN.test(name)) {
8
+ throw new Error(`Invalid account name '${name}': use letters, digits, '-' or '_', starting with a letter or digit`);
9
+ }
10
+ }
11
+ function pooledFrom(credential) {
12
+ return credential === undefined ? undefined : credential;
13
+ }
14
+ function pinnedName(credential) {
15
+ return credential?.pinned;
16
+ }
17
+ function numberField(value, key) {
18
+ const found = Object.entries(value).find(([candidate]) => candidate === key)?.[1];
19
+ return typeof found === "number" ? found : undefined;
20
+ }
21
+ function stringField(value, key) {
22
+ const found = Object.entries(value).find(([candidate]) => candidate === key)?.[1];
23
+ return typeof found === "string" ? found : undefined;
24
+ }
25
+ /**
26
+ * Block state has two sources and both are authoritative: a slot persisted by an
27
+ * existing provider lane carries its own `blockedUntil`/`blockReason` inside
28
+ * auth.json, while generic pool failover records health in the sidecar. Reading
29
+ * only one would silently downgrade a real block to "available".
30
+ */
31
+ function slotBlocked(slot, sidecar, now) {
32
+ if (slotHealth(sidecar, now) === "blocked")
33
+ return true;
34
+ const reason = stringField(slot, "blockReason");
35
+ if (reason === "auth_error" || reason === "account_disabled")
36
+ return true;
37
+ const blockedUntil = numberField(slot, "blockedUntil");
38
+ return blockedUntil !== undefined && blockedUntil > now;
39
+ }
40
+ /**
41
+ * Lists a provider's credential accounts for ANY provider, not just one lane.
42
+ * Stored slots own the listing when a credential exists; env slots are listed
43
+ * only when nothing is stored, mirroring resolution precedence exactly. Health
44
+ * comes from the pool sidecar, so a cooldown persisted by a failover shows up
45
+ * here without auth.json ever carrying block state.
46
+ */
47
+ export async function getCredentialAccounts(storage, provider, env = process.env, repository = new CredentialSlotRepository()) {
48
+ return summarizeCredentialAccounts(provider, storage.get(provider), env, repository);
49
+ }
50
+ /** Storage-free variant for callers that already hold the credential (e.g. auth check). */
51
+ export async function summarizeCredentialAccounts(provider, stored, env = process.env, repository = new CredentialSlotRepository()) {
52
+ const credential = pooledFrom(stored);
53
+ const now = Date.now();
54
+ const pinned = pinnedName(credential);
55
+ if (credential) {
56
+ const state = await repository.listSlots(provider, "stored");
57
+ return listSlots(credential).map((slot) => ({
58
+ name: slot.name,
59
+ source: slot.source ?? "login",
60
+ blocked: slotBlocked(slot, state[slot.name], now),
61
+ pinned: pinned === slot.name,
62
+ }));
63
+ }
64
+ const state = await repository.listSlots(provider, "env");
65
+ const summaries = [];
66
+ for (const slot of discoverEnvSlots(provider, (name) => env[name])) {
67
+ const persisted = state[slot.name];
68
+ const revision = await repository.envCredentialRevision(slot.envVarName, slot.key);
69
+ // A block belongs to the value that earned it; a rotated env key starts clean.
70
+ const applicable = persisted?.credentialRevision === revision ? persisted : undefined;
71
+ summaries.push({
72
+ name: slot.name,
73
+ source: "env",
74
+ blocked: slotHealth(applicable, now) === "blocked",
75
+ pinned: pinned === slot.name,
76
+ });
77
+ }
78
+ return summaries;
79
+ }
80
+ /** Pins one slot, or clears the pin when `name` is null. */
81
+ export async function pinCredentialAccount(storage, provider, name, env = process.env, repository = new CredentialSlotRepository()) {
82
+ if (name !== null) {
83
+ assertValidAccountName(name);
84
+ const accounts = await getCredentialAccounts(storage, provider, env, repository);
85
+ if (!accounts.some((account) => account.name === name)) {
86
+ throw new Error(`Provider account not found: ${name}`);
87
+ }
88
+ }
89
+ await storage.modify(provider, async (current) => {
90
+ if (current === undefined)
91
+ throw new Error(`No stored credential for provider: ${provider}`);
92
+ if (name === null) {
93
+ if (pinnedName(current) === undefined)
94
+ return current;
95
+ const { pinned: _pinned, ...unpinned } = { ...current, pinned: undefined };
96
+ return unpinned;
97
+ }
98
+ return pinSlot(current, name);
99
+ });
100
+ // Subscribed clients re-read the list after any mutation; skipping this leaves
101
+ // a desktop account picker showing a stale pin.
102
+ emitProviderAccountsChanged(provider);
103
+ }
104
+ /**
105
+ * Removes one stored slot. Env-backed accounts are refused: they are owned by
106
+ * the environment, and deleting one here would silently disagree with the
107
+ * variable that still defines it.
108
+ */
109
+ export async function removeCredentialAccount(storage, provider, name, env = process.env, repository = new CredentialSlotRepository()) {
110
+ const accounts = await getCredentialAccounts(storage, provider, env, repository);
111
+ const account = accounts.find((candidate) => candidate.name === name);
112
+ if (!account)
113
+ throw new Error(`Provider account not found: ${name}`);
114
+ if (account.source === "env") {
115
+ throw new Error(`Environment provider account cannot be removed: ${name}`);
116
+ }
117
+ await storage.modify(provider, async (current) => {
118
+ if (current === undefined)
119
+ throw new Error(`No stored credential for provider: ${provider}`);
120
+ return removeSlot(current, name);
121
+ });
122
+ await repository.mutateSlotState(provider, "stored", name, () => undefined);
123
+ emitProviderAccountsChanged(provider);
124
+ }
125
+ //# sourceMappingURL=credential-accounts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-accounts.js","sourceRoot":"","sources":["../../src/core/credential-accounts.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Credential } from \"@earendil-works/pi-ai\";\nimport { listSlots, type PooledCredential, pinSlot, removeSlot } from \"@earendil-works/pi-ai/auth/pool/slots\";\nimport type { AuthStorage } from \"./auth-storage.ts\";\nimport { discoverEnvSlots } from \"./credential-pool/env-slots.ts\";\nimport { CredentialSlotRepository, type CredentialSlotState, slotHealth } from \"./credential-pool/state-store.ts\";\nimport { emitProviderAccountsChanged } from \"./extensions/builtin/claude-sdk-oauth/account-events.ts\";\n\nexport type CredentialAccountSource = \"login\" | \"import\" | \"env\";\n\n/** Account metadata safe to surface: names and health only, never key material. */\nexport type CredentialAccountSummary = {\n\treadonly name: string;\n\treadonly source: CredentialAccountSource;\n\treadonly blocked: boolean;\n\treadonly pinned: boolean;\n};\n\nconst ACCOUNT_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;\n\nexport function assertValidAccountName(name: string): void {\n\tif (!ACCOUNT_NAME_PATTERN.test(name)) {\n\t\tthrow new Error(\n\t\t\t`Invalid account name '${name}': use letters, digits, '-' or '_', starting with a letter or digit`,\n\t\t);\n\t}\n}\n\nfunction pooledFrom(credential: Credential | undefined): PooledCredential | undefined {\n\treturn credential === undefined ? undefined : credential;\n}\n\nfunction pinnedName(credential: PooledCredential | undefined): string | undefined {\n\treturn credential?.pinned;\n}\n\nfunction numberField(value: object, key: string): number | undefined {\n\tconst found = Object.entries(value).find(([candidate]) => candidate === key)?.[1];\n\treturn typeof found === \"number\" ? found : undefined;\n}\n\nfunction stringField(value: object, key: string): string | undefined {\n\tconst found = Object.entries(value).find(([candidate]) => candidate === key)?.[1];\n\treturn typeof found === \"string\" ? found : undefined;\n}\n\n/**\n * Block state has two sources and both are authoritative: a slot persisted by an\n * existing provider lane carries its own `blockedUntil`/`blockReason` inside\n * auth.json, while generic pool failover records health in the sidecar. Reading\n * only one would silently downgrade a real block to \"available\".\n */\nfunction slotBlocked(slot: object, sidecar: CredentialSlotState | undefined, now: number): boolean {\n\tif (slotHealth(sidecar, now) === \"blocked\") return true;\n\tconst reason = stringField(slot, \"blockReason\");\n\tif (reason === \"auth_error\" || reason === \"account_disabled\") return true;\n\tconst blockedUntil = numberField(slot, \"blockedUntil\");\n\treturn blockedUntil !== undefined && blockedUntil > now;\n}\n\n/**\n * Lists a provider's credential accounts for ANY provider, not just one lane.\n * Stored slots own the listing when a credential exists; env slots are listed\n * only when nothing is stored, mirroring resolution precedence exactly. Health\n * comes from the pool sidecar, so a cooldown persisted by a failover shows up\n * here without auth.json ever carrying block state.\n */\nexport async function getCredentialAccounts(\n\tstorage: AuthStorage,\n\tprovider: string,\n\tenv: NodeJS.ProcessEnv = process.env,\n\trepository: CredentialSlotRepository = new CredentialSlotRepository(),\n): Promise<CredentialAccountSummary[]> {\n\treturn summarizeCredentialAccounts(provider, storage.get(provider), env, repository);\n}\n\n/** Storage-free variant for callers that already hold the credential (e.g. auth check). */\nexport async function summarizeCredentialAccounts(\n\tprovider: string,\n\tstored: Credential | undefined,\n\tenv: NodeJS.ProcessEnv = process.env,\n\trepository: CredentialSlotRepository = new CredentialSlotRepository(),\n): Promise<CredentialAccountSummary[]> {\n\tconst credential = pooledFrom(stored);\n\tconst now = Date.now();\n\tconst pinned = pinnedName(credential);\n\n\tif (credential) {\n\t\tconst state = await repository.listSlots(provider, \"stored\");\n\t\treturn listSlots(credential).map((slot) => ({\n\t\t\tname: slot.name,\n\t\t\tsource: slot.source ?? \"login\",\n\t\t\tblocked: slotBlocked(slot, state[slot.name], now),\n\t\t\tpinned: pinned === slot.name,\n\t\t}));\n\t}\n\n\tconst state = await repository.listSlots(provider, \"env\");\n\tconst summaries: CredentialAccountSummary[] = [];\n\tfor (const slot of discoverEnvSlots(provider, (name) => env[name])) {\n\t\tconst persisted = state[slot.name];\n\t\tconst revision = await repository.envCredentialRevision(slot.envVarName, slot.key);\n\t\t// A block belongs to the value that earned it; a rotated env key starts clean.\n\t\tconst applicable = persisted?.credentialRevision === revision ? persisted : undefined;\n\t\tsummaries.push({\n\t\t\tname: slot.name,\n\t\t\tsource: \"env\",\n\t\t\tblocked: slotHealth(applicable, now) === \"blocked\",\n\t\t\tpinned: pinned === slot.name,\n\t\t});\n\t}\n\treturn summaries;\n}\n\n/** Pins one slot, or clears the pin when `name` is null. */\nexport async function pinCredentialAccount(\n\tstorage: AuthStorage,\n\tprovider: string,\n\tname: string | null,\n\tenv: NodeJS.ProcessEnv = process.env,\n\trepository: CredentialSlotRepository = new CredentialSlotRepository(),\n): Promise<void> {\n\tif (name !== null) {\n\t\tassertValidAccountName(name);\n\t\tconst accounts = await getCredentialAccounts(storage, provider, env, repository);\n\t\tif (!accounts.some((account) => account.name === name)) {\n\t\t\tthrow new Error(`Provider account not found: ${name}`);\n\t\t}\n\t}\n\tawait storage.modify(provider, async (current) => {\n\t\tif (current === undefined) throw new Error(`No stored credential for provider: ${provider}`);\n\t\tif (name === null) {\n\t\t\tif (pinnedName(current) === undefined) return current;\n\t\t\tconst { pinned: _pinned, ...unpinned } = { ...current, pinned: undefined };\n\t\t\treturn unpinned;\n\t\t}\n\t\treturn pinSlot(current, name);\n\t});\n\t// Subscribed clients re-read the list after any mutation; skipping this leaves\n\t// a desktop account picker showing a stale pin.\n\temitProviderAccountsChanged(provider);\n}\n\n/**\n * Removes one stored slot. Env-backed accounts are refused: they are owned by\n * the environment, and deleting one here would silently disagree with the\n * variable that still defines it.\n */\nexport async function removeCredentialAccount(\n\tstorage: AuthStorage,\n\tprovider: string,\n\tname: string,\n\tenv: NodeJS.ProcessEnv = process.env,\n\trepository: CredentialSlotRepository = new CredentialSlotRepository(),\n): Promise<void> {\n\tconst accounts = await getCredentialAccounts(storage, provider, env, repository);\n\tconst account = accounts.find((candidate) => candidate.name === name);\n\tif (!account) throw new Error(`Provider account not found: ${name}`);\n\tif (account.source === \"env\") {\n\t\tthrow new Error(`Environment provider account cannot be removed: ${name}`);\n\t}\n\tawait storage.modify(provider, async (current) => {\n\t\tif (current === undefined) throw new Error(`No stored credential for provider: ${provider}`);\n\t\treturn removeSlot(current, name);\n\t});\n\tawait repository.mutateSlotState(provider, \"stored\", name, () => undefined);\n\temitProviderAccountsChanged(provider);\n}\n"]}
@@ -0,0 +1,42 @@
1
+ export declare const COOLDOWN_BASE_MS = 60000;
2
+ export declare const COOLDOWN_CAP_MS: number;
3
+ export declare const RETRY_SAME_MAX_ATTEMPTS = 2;
4
+ export type CredentialBlock = {
5
+ reason: "auth_error";
6
+ } | {
7
+ reason: "account_disabled";
8
+ } | {
9
+ reason: "rate_limit";
10
+ cooldownMs: number;
11
+ retryAfterWasCapped: boolean;
12
+ };
13
+ export type CredentialAction = {
14
+ kind: "failover";
15
+ block: CredentialBlock;
16
+ } | {
17
+ kind: "retry_same";
18
+ maxAttempts: typeof RETRY_SAME_MAX_ATTEMPTS;
19
+ } | {
20
+ kind: "fail_request";
21
+ };
22
+ /**
23
+ * Per-slot exponential cooldown with the server hint as a FLOOR, never an
24
+ * override: a hint can only lengthen the wait the failure count already earned,
25
+ * and everything caps at 48 hours with the capping recorded.
26
+ */
27
+ export declare function rateLimitCooldown(failureCount: number, serverHintMs?: number): {
28
+ cooldownMs: number;
29
+ retryAfterWasCapped: boolean;
30
+ };
31
+ /**
32
+ * Maps a provider failure onto the credential-pool action space. Credential-
33
+ * scoped failures fail over (permanently for auth/billing, cooldown for rate
34
+ * limits); provider-scoped faults (5xx/overload/network) retry the SAME slot
35
+ * without blocking it, because blocking a healthy credential for a provider
36
+ * outage only destroys prompt-cache locality; everything else fails the
37
+ * request so the model fallback chain above keeps owning it.
38
+ */
39
+ export declare function classifyCredentialFailure(error: unknown, context?: {
40
+ failureCount?: number;
41
+ }): CredentialAction;
42
+ //# sourceMappingURL=classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../../src/core/credential-pool/classify.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,gBAAgB,QAAwB,CAAC;AACtD,eAAO,MAAM,eAAe,QAAoB,CAAC;AACjD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,MAAM,MAAM,eAAe,GACxB;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,GACxB;IAAE,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC9B;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,OAAO,CAAA;CAAE,CAAC;AAE9E,MAAM,MAAM,gBAAgB,GACzB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,eAAe,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,WAAW,EAAE,OAAO,uBAAuB,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,CAAC;AAE5B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAChC,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,GACnB;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,OAAO,CAAA;CAAE,CAItD;AAuBD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,gBAAgB,CAgCnH"}
@@ -0,0 +1,75 @@
1
+ import { DEFAULT_SLOT_BLOCK_MS, MAX_SLOT_BLOCK_MS } from "@earendil-works/pi-ai/auth/pool/failover";
2
+ import { normalizeProviderError } from "@earendil-works/pi-ai/utils/error-body";
3
+ import { getOverflowPatterns } from "@earendil-works/pi-ai/utils/overflow";
4
+ import { extract429RetryAfterMs } from "@earendil-works/pi-ai/utils/retry-hint";
5
+ export const COOLDOWN_BASE_MS = DEFAULT_SLOT_BLOCK_MS;
6
+ export const COOLDOWN_CAP_MS = MAX_SLOT_BLOCK_MS;
7
+ export const RETRY_SAME_MAX_ATTEMPTS = 2;
8
+ /**
9
+ * Per-slot exponential cooldown with the server hint as a FLOOR, never an
10
+ * override: a hint can only lengthen the wait the failure count already earned,
11
+ * and everything caps at 48 hours with the capping recorded.
12
+ */
13
+ export function rateLimitCooldown(failureCount, serverHintMs) {
14
+ const backoff = Math.min(COOLDOWN_CAP_MS, COOLDOWN_BASE_MS * 2 ** Math.max(0, failureCount));
15
+ const floored = serverHintMs === undefined ? backoff : Math.max(backoff, serverHintMs);
16
+ return { cooldownMs: Math.min(COOLDOWN_CAP_MS, floored), retryAfterWasCapped: floored > COOLDOWN_CAP_MS };
17
+ }
18
+ const INVALID_KEY_TEXT = /invalid[ _-]?(?:api[ _-]?)?key|authentication[_ ]?error|invalid x-api-key|unauthorized/i;
19
+ const ACCOUNT_SCOPED_403_TEXT = /account|credential|token|api[ _-]?key|organization|subscription/i;
20
+ const RATE_LIMIT_TEXT = /rate[ _-]?limit|too many requests|resource_exhausted/i;
21
+ const BILLING_TEXT = /billing|credits?[ _-]?(?:required|exhausted|balance)|insufficient[ _-]?(?:funds|quota|credit)|payment[ _-]?required|quota[ _-]?exhausted/i;
22
+ const OVERLOAD_TEXT = /overloaded/i;
23
+ const NETWORK_TEXT = /econnreset|econnrefused|etimedout|enotfound|socket hang up|fetch failed|network error|request timed out/i;
24
+ const FAIL_FAST_TEXT = /context[ _-]?(?:length|window)|maximum context|invalid[ _-]?model|model[ _-]?not[ _-]?found|malformed[ _-]?stream|premature[ _-]?(?:close|stream)/i;
25
+ const ABORT_TEXT = /\baborted?\b/i;
26
+ function isAbort(error, message) {
27
+ if (error instanceof Error && error.name === "AbortError")
28
+ return true;
29
+ return ABORT_TEXT.test(message);
30
+ }
31
+ function isOverflowText(text) {
32
+ return getOverflowPatterns().some((pattern) => pattern.test(text));
33
+ }
34
+ /**
35
+ * Maps a provider failure onto the credential-pool action space. Credential-
36
+ * scoped failures fail over (permanently for auth/billing, cooldown for rate
37
+ * limits); provider-scoped faults (5xx/overload/network) retry the SAME slot
38
+ * without blocking it, because blocking a healthy credential for a provider
39
+ * outage only destroys prompt-cache locality; everything else fails the
40
+ * request so the model fallback chain above keeps owning it.
41
+ */
42
+ export function classifyCredentialFailure(error, context = {}) {
43
+ const normalized = normalizeProviderError(error);
44
+ const text = normalized.messageCarriesBody ? normalized.message : `${normalized.message} ${normalized.body ?? ""}`;
45
+ const status = normalized.status;
46
+ const failureCount = context.failureCount ?? 0;
47
+ if (isAbort(error, text))
48
+ return { kind: "fail_request" };
49
+ if (status === 401 || INVALID_KEY_TEXT.test(text)) {
50
+ return { kind: "failover", block: { reason: "auth_error" } };
51
+ }
52
+ if (status === 403) {
53
+ return ACCOUNT_SCOPED_403_TEXT.test(text)
54
+ ? { kind: "failover", block: { reason: "auth_error" } }
55
+ : { kind: "fail_request" };
56
+ }
57
+ if (status === 402 || BILLING_TEXT.test(text)) {
58
+ return { kind: "failover", block: { reason: "account_disabled" } };
59
+ }
60
+ if (status === 429 || RATE_LIMIT_TEXT.test(text)) {
61
+ const hint = extract429RetryAfterMs({ status: status ?? 429, bodyText: text });
62
+ return { kind: "failover", block: { reason: "rate_limit", ...rateLimitCooldown(failureCount, hint) } };
63
+ }
64
+ if (isOverflowText(text) || status === 400 || status === 404 || FAIL_FAST_TEXT.test(text)) {
65
+ return { kind: "fail_request" };
66
+ }
67
+ if (status === 529 || OVERLOAD_TEXT.test(text) || (status !== undefined && status >= 500 && status < 600)) {
68
+ return { kind: "retry_same", maxAttempts: RETRY_SAME_MAX_ATTEMPTS };
69
+ }
70
+ if (status === 408 || NETWORK_TEXT.test(text)) {
71
+ return { kind: "retry_same", maxAttempts: RETRY_SAME_MAX_ATTEMPTS };
72
+ }
73
+ return { kind: "fail_request" };
74
+ }
75
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../../src/core/credential-pool/classify.ts"],"names":[],"mappings":"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","sourcesContent":["import { DEFAULT_SLOT_BLOCK_MS, MAX_SLOT_BLOCK_MS } from \"@earendil-works/pi-ai/auth/pool/failover\";\nimport { normalizeProviderError } from \"@earendil-works/pi-ai/utils/error-body\";\nimport { getOverflowPatterns } from \"@earendil-works/pi-ai/utils/overflow\";\nimport { extract429RetryAfterMs } from \"@earendil-works/pi-ai/utils/retry-hint\";\n\nexport const COOLDOWN_BASE_MS = DEFAULT_SLOT_BLOCK_MS;\nexport const COOLDOWN_CAP_MS = MAX_SLOT_BLOCK_MS;\nexport const RETRY_SAME_MAX_ATTEMPTS = 2;\n\nexport type CredentialBlock =\n\t| { reason: \"auth_error\" }\n\t| { reason: \"account_disabled\" }\n\t| { reason: \"rate_limit\"; cooldownMs: number; retryAfterWasCapped: boolean };\n\nexport type CredentialAction =\n\t| { kind: \"failover\"; block: CredentialBlock }\n\t| { kind: \"retry_same\"; maxAttempts: typeof RETRY_SAME_MAX_ATTEMPTS }\n\t| { kind: \"fail_request\" };\n\n/**\n * Per-slot exponential cooldown with the server hint as a FLOOR, never an\n * override: a hint can only lengthen the wait the failure count already earned,\n * and everything caps at 48 hours with the capping recorded.\n */\nexport function rateLimitCooldown(\n\tfailureCount: number,\n\tserverHintMs?: number,\n): { cooldownMs: number; retryAfterWasCapped: boolean } {\n\tconst backoff = Math.min(COOLDOWN_CAP_MS, COOLDOWN_BASE_MS * 2 ** Math.max(0, failureCount));\n\tconst floored = serverHintMs === undefined ? backoff : Math.max(backoff, serverHintMs);\n\treturn { cooldownMs: Math.min(COOLDOWN_CAP_MS, floored), retryAfterWasCapped: floored > COOLDOWN_CAP_MS };\n}\n\nconst INVALID_KEY_TEXT = /invalid[ _-]?(?:api[ _-]?)?key|authentication[_ ]?error|invalid x-api-key|unauthorized/i;\nconst ACCOUNT_SCOPED_403_TEXT = /account|credential|token|api[ _-]?key|organization|subscription/i;\nconst RATE_LIMIT_TEXT = /rate[ _-]?limit|too many requests|resource_exhausted/i;\nconst BILLING_TEXT =\n\t/billing|credits?[ _-]?(?:required|exhausted|balance)|insufficient[ _-]?(?:funds|quota|credit)|payment[ _-]?required|quota[ _-]?exhausted/i;\nconst OVERLOAD_TEXT = /overloaded/i;\nconst NETWORK_TEXT =\n\t/econnreset|econnrefused|etimedout|enotfound|socket hang up|fetch failed|network error|request timed out/i;\nconst FAIL_FAST_TEXT =\n\t/context[ _-]?(?:length|window)|maximum context|invalid[ _-]?model|model[ _-]?not[ _-]?found|malformed[ _-]?stream|premature[ _-]?(?:close|stream)/i;\nconst ABORT_TEXT = /\\baborted?\\b/i;\n\nfunction isAbort(error: unknown, message: string): boolean {\n\tif (error instanceof Error && error.name === \"AbortError\") return true;\n\treturn ABORT_TEXT.test(message);\n}\n\nfunction isOverflowText(text: string): boolean {\n\treturn getOverflowPatterns().some((pattern) => pattern.test(text));\n}\n\n/**\n * Maps a provider failure onto the credential-pool action space. Credential-\n * scoped failures fail over (permanently for auth/billing, cooldown for rate\n * limits); provider-scoped faults (5xx/overload/network) retry the SAME slot\n * without blocking it, because blocking a healthy credential for a provider\n * outage only destroys prompt-cache locality; everything else fails the\n * request so the model fallback chain above keeps owning it.\n */\nexport function classifyCredentialFailure(error: unknown, context: { failureCount?: number } = {}): CredentialAction {\n\tconst normalized = normalizeProviderError(error);\n\tconst text = normalized.messageCarriesBody ? normalized.message : `${normalized.message} ${normalized.body ?? \"\"}`;\n\tconst status = normalized.status;\n\tconst failureCount = context.failureCount ?? 0;\n\n\tif (isAbort(error, text)) return { kind: \"fail_request\" };\n\tif (status === 401 || INVALID_KEY_TEXT.test(text)) {\n\t\treturn { kind: \"failover\", block: { reason: \"auth_error\" } };\n\t}\n\tif (status === 403) {\n\t\treturn ACCOUNT_SCOPED_403_TEXT.test(text)\n\t\t\t? { kind: \"failover\", block: { reason: \"auth_error\" } }\n\t\t\t: { kind: \"fail_request\" };\n\t}\n\tif (status === 402 || BILLING_TEXT.test(text)) {\n\t\treturn { kind: \"failover\", block: { reason: \"account_disabled\" } };\n\t}\n\tif (status === 429 || RATE_LIMIT_TEXT.test(text)) {\n\t\tconst hint = extract429RetryAfterMs({ status: status ?? 429, bodyText: text });\n\t\treturn { kind: \"failover\", block: { reason: \"rate_limit\", ...rateLimitCooldown(failureCount, hint) } };\n\t}\n\tif (isOverflowText(text) || status === 400 || status === 404 || FAIL_FAST_TEXT.test(text)) {\n\t\treturn { kind: \"fail_request\" };\n\t}\n\tif (status === 529 || OVERLOAD_TEXT.test(text) || (status !== undefined && status >= 500 && status < 600)) {\n\t\treturn { kind: \"retry_same\", maxAttempts: RETRY_SAME_MAX_ATTEMPTS };\n\t}\n\tif (status === 408 || NETWORK_TEXT.test(text)) {\n\t\treturn { kind: \"retry_same\", maxAttempts: RETRY_SAME_MAX_ATTEMPTS };\n\t}\n\treturn { kind: \"fail_request\" };\n}\n"]}
@@ -0,0 +1,22 @@
1
+ export declare const MAX_ENV_SLOT_INDEX = 16;
2
+ export type EnvCredentialSlot = {
3
+ name: string;
4
+ envVarName: string;
5
+ key: string;
6
+ source: "env";
7
+ };
8
+ /**
9
+ * Anthropic maps to bearer/OAuth token vars ahead of its API-key var; numbered
10
+ * slots must extend the API-key lane, so prefer the `*_API_KEY` entry when the
11
+ * canonical mapping lists several variables.
12
+ */
13
+ export declare function primaryEnvVar(providerId: string): string | undefined;
14
+ /**
15
+ * Discovers numbered env credential slots: the primary var yields slot `env`,
16
+ * and `<VAR>_2` .. `<VAR>_16` yield `env-2` .. `env-16`. Discovery is
17
+ * gap-tolerant - a configured `<VAR>_3` with no `<VAR>_2` is still found and no
18
+ * slot is invented for the gap. Stored credentials keep outranking every env
19
+ * slot because resolution consults env only when nothing is stored.
20
+ */
21
+ export declare function discoverEnvSlots(providerId: string, env: (name: string) => string | undefined): EnvCredentialSlot[];
22
+ //# sourceMappingURL=env-slots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-slots.d.ts","sourceRoot":"","sources":["../../../src/core/credential-pool/env-slots.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,MAAM,MAAM,iBAAiB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,KAAK,CAAC;CACd,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIpE;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,iBAAiB,EAAE,CAYnH"}
@@ -0,0 +1,37 @@
1
+ import { getApiKeyEnvVars } from "@earendil-works/pi-ai";
2
+ export const MAX_ENV_SLOT_INDEX = 16;
3
+ /**
4
+ * Anthropic maps to bearer/OAuth token vars ahead of its API-key var; numbered
5
+ * slots must extend the API-key lane, so prefer the `*_API_KEY` entry when the
6
+ * canonical mapping lists several variables.
7
+ */
8
+ export function primaryEnvVar(providerId) {
9
+ const envVars = getApiKeyEnvVars(providerId);
10
+ if (!envVars || envVars.length === 0)
11
+ return undefined;
12
+ return envVars.find((name) => name.endsWith("_API_KEY")) ?? envVars[0];
13
+ }
14
+ /**
15
+ * Discovers numbered env credential slots: the primary var yields slot `env`,
16
+ * and `<VAR>_2` .. `<VAR>_16` yield `env-2` .. `env-16`. Discovery is
17
+ * gap-tolerant - a configured `<VAR>_3` with no `<VAR>_2` is still found and no
18
+ * slot is invented for the gap. Stored credentials keep outranking every env
19
+ * slot because resolution consults env only when nothing is stored.
20
+ */
21
+ export function discoverEnvSlots(providerId, env) {
22
+ const primary = primaryEnvVar(providerId);
23
+ if (!primary)
24
+ return [];
25
+ const slots = [];
26
+ const base = env(primary);
27
+ if (base)
28
+ slots.push({ name: "env", envVarName: primary, key: base, source: "env" });
29
+ for (let index = 2; index <= MAX_ENV_SLOT_INDEX; index++) {
30
+ const envVarName = `${primary}_${index}`;
31
+ const value = env(envVarName);
32
+ if (value)
33
+ slots.push({ name: `env-${index}`, envVarName, key: value, source: "env" });
34
+ }
35
+ return slots;
36
+ }
37
+ //# sourceMappingURL=env-slots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-slots.js","sourceRoot":"","sources":["../../../src/core/credential-pool/env-slots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AASrC;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,UAAkB;IAC/C,MAAM,OAAO,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC7C,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACvD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;AACxE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,GAAyC;IAC7F,MAAM,OAAO,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC;IAC1C,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,MAAM,KAAK,GAAwB,EAAE,CAAC;IACtC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;IAC1B,IAAI,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACrF,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,kBAAkB,EAAE,KAAK,EAAE,EAAE,CAAC;QAC1D,MAAM,UAAU,GAAG,GAAG,OAAO,IAAI,KAAK,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC;QAC9B,IAAI,KAAK;YAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC","sourcesContent":["import { getApiKeyEnvVars } from \"@earendil-works/pi-ai\";\n\nexport const MAX_ENV_SLOT_INDEX = 16;\n\nexport type EnvCredentialSlot = {\n\tname: string;\n\tenvVarName: string;\n\tkey: string;\n\tsource: \"env\";\n};\n\n/**\n * Anthropic maps to bearer/OAuth token vars ahead of its API-key var; numbered\n * slots must extend the API-key lane, so prefer the `*_API_KEY` entry when the\n * canonical mapping lists several variables.\n */\nexport function primaryEnvVar(providerId: string): string | undefined {\n\tconst envVars = getApiKeyEnvVars(providerId);\n\tif (!envVars || envVars.length === 0) return undefined;\n\treturn envVars.find((name) => name.endsWith(\"_API_KEY\")) ?? envVars[0];\n}\n\n/**\n * Discovers numbered env credential slots: the primary var yields slot `env`,\n * and `<VAR>_2` .. `<VAR>_16` yield `env-2` .. `env-16`. Discovery is\n * gap-tolerant - a configured `<VAR>_3` with no `<VAR>_2` is still found and no\n * slot is invented for the gap. Stored credentials keep outranking every env\n * slot because resolution consults env only when nothing is stored.\n */\nexport function discoverEnvSlots(providerId: string, env: (name: string) => string | undefined): EnvCredentialSlot[] {\n\tconst primary = primaryEnvVar(providerId);\n\tif (!primary) return [];\n\tconst slots: EnvCredentialSlot[] = [];\n\tconst base = env(primary);\n\tif (base) slots.push({ name: \"env\", envVarName: primary, key: base, source: \"env\" });\n\tfor (let index = 2; index <= MAX_ENV_SLOT_INDEX; index++) {\n\t\tconst envVarName = `${primary}_${index}`;\n\t\tconst value = env(envVarName);\n\t\tif (value) slots.push({ name: `env-${index}`, envVarName, key: value, source: \"env\" });\n\t}\n\treturn slots;\n}\n"]}