cartethyia 2.7.5 → 2.7.6

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 (263) hide show
  1. package/.env.example +4 -1
  2. package/README.md +1 -1
  3. package/dist/dashboard/assets/BannedPage-B1N603cM.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js +3 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
  7. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js +1 -0
  8. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-D762MIkp.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-D5rol2j2.js → ConsoleLogPage-KRYjHZsX.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-B5nZvmf0.js → InjectSystemPromptPanel-d5v3K_Yc.js} +245 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
  19. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js +1 -0
  20. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-DnQPSIA8.js → ModelPicker-C6WlpU5E.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +5 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-5SnUSy3j.js → ProxyPage-DxjvUzSD.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
  35. package/dist/dashboard/assets/{SettingsPage-BJtRZ4qD.js → SettingsPage-BW4AKj2H.js} +1 -1
  36. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
  37. package/dist/dashboard/assets/SetupPage-44BZrxFC.js +1 -0
  38. package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-BfAb5-o_.js → StudioPage-DRPavCdU.js} +4 -4
  40. package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
  41. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js +1 -0
  42. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js.gz +0 -0
  43. package/dist/dashboard/assets/{UsagePage-BNzeZ2Vk.js → UsagePage-B7M7LvMM.js} +13 -13
  44. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-KvX83mRe.js +1 -0
  46. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  47. package/dist/dashboard/assets/app-DRnQLhsc.js +9 -0
  48. package/dist/dashboard/assets/app-DRnQLhsc.js.gz +0 -0
  49. package/dist/dashboard/assets/app-mweRcsKp.css +2 -0
  50. package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
  51. package/dist/dashboard/assets/{assert-C0K-c4eo.js → assert-CidQVoNo.js} +1 -1
  52. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  53. package/dist/dashboard/assets/{badge-DfbhQU4k.js → badge-BaX2GTrn.js} +1 -1
  54. package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
  55. package/dist/dashboard/assets/{banner-BllT4LcF.js → banner-DFH4Bkte.js} +1 -1
  56. package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
  57. package/dist/dashboard/assets/{card-CtkX66Wd.js → card-CxDw0lK6.js} +1 -1
  58. package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
  59. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js +1 -0
  60. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js.gz +0 -0
  61. package/dist/dashboard/assets/{cli-tools-CQXVMn4P.js → cli-tools-Bwv5-zGZ.js} +1 -1
  62. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  63. package/dist/dashboard/assets/{clipboard-button-BQqBQd2n.js → clipboard-button-DfS1ntiq.js} +1 -1
  64. package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
  65. package/dist/dashboard/assets/dialog-BK-NRr5u.js +1 -0
  66. package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
  67. package/dist/dashboard/assets/{drawer-BrTEZ8zu.js → drawer-QqPul6sC.js} +1 -1
  68. package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-BMlYJVFj.js +1 -0
  70. package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
  71. package/dist/dashboard/assets/entry-D6WvojeD.js +4 -0
  72. package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-Dmx7gBiN.js +1 -0
  74. package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
  75. package/dist/dashboard/assets/{formatters-BAACA727.js → formatters-u7JPB5BX.js} +1 -1
  76. package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
  77. package/dist/dashboard/assets/house-h9AZlJuT.js +1 -0
  78. package/dist/dashboard/assets/house-h9AZlJuT.js.gz +0 -0
  79. package/dist/dashboard/assets/{index-FQlj0HUT.js → index-Ko4dLRnV.js} +2 -2
  80. package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
  81. package/dist/dashboard/assets/{inline-C4gU5iOL.js → inline-CHJQwxLa.js} +1 -1
  82. package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
  83. package/dist/dashboard/assets/{input-BHHX_Tmm.js → input-BlmlizVV.js} +1 -1
  84. package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
  85. package/dist/dashboard/assets/{layout-CajrYUnD.js → layout-Bz93I-yE.js} +1 -1
  86. package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
  87. package/dist/dashboard/assets/{list-row-Bx2I7XUe.js → list-row-Ml16KCfu.js} +1 -1
  88. package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
  89. package/dist/dashboard/assets/{live-DNNqRbj2.js → live-7_QpnYGu.js} +1 -1
  90. package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
  91. package/dist/dashboard/assets/{mutation-Rr0TIvcj.js → mutation-GU7XdEjz.js} +1 -1
  92. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  93. package/dist/dashboard/assets/{network-C5UBACrJ.js → network-CuMDaTzT.js} +1 -1
  94. package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
  95. package/dist/dashboard/assets/numbers-CoqpaDMx.js +2 -0
  96. package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
  97. package/dist/dashboard/assets/{pagination-Xsi9Qx04.js → pagination-B5aOoweZ.js} +1 -1
  98. package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
  99. package/dist/dashboard/assets/providers/zai.webp +0 -0
  100. package/dist/dashboard/assets/providers/zcode.webp +0 -0
  101. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js → query-keys-YSoQOUh6.js} +1 -1
  102. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js.gz → query-keys-YSoQOUh6.js.gz} +0 -0
  103. package/dist/dashboard/assets/quota-DJ1QlAOq.js +1 -0
  104. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  105. package/dist/dashboard/assets/{routing-in0f_IfK.js → routing-fnCsxgNO.js} +1 -1
  106. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  107. package/dist/dashboard/assets/{select-BLULNqTp.js → select-BHqpd-bg.js} +2 -2
  108. package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
  109. package/dist/dashboard/assets/{settings-CcGczj6I.js → settings-BxslsErn.js} +1 -1
  110. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  111. package/dist/dashboard/assets/{stack-D2lIR0tg.js → stack-j7cXjgYX.js} +1 -1
  112. package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
  113. package/dist/dashboard/assets/{state-DTARjfiW.js → state-DHmEnLa2.js} +1 -1
  114. package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
  115. package/dist/dashboard/assets/{switch-c0oPSnqW.js → switch-DmZcyTIC.js} +1 -1
  116. package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
  117. package/dist/dashboard/assets/{useMutation-m8WGNPrz.js → useMutation-CbCP4w1o.js} +1 -1
  118. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  119. package/dist/dashboard/index.html +2 -2
  120. package/dist/dashboard/index.html.gz +0 -0
  121. package/dist/dashboard/precompressed-assets.json +57 -57
  122. package/migrations/full-postgres/0042_add_telemetry_degenerate_stop.sql +14 -0
  123. package/migrations/lite-sqlite3/0042_add_telemetry_degenerate_stop.sql +12 -0
  124. package/package.json +1 -1
  125. package/src/boot/dependencies.ts +10 -2
  126. package/src/config/providers.ts +13 -7
  127. package/src/config/spec.ts +18 -11
  128. package/src/config/usage.ts +27 -0
  129. package/src/console/api-keys/routes.ts +19 -23
  130. package/src/console/cli-tools/contracts.ts +39 -295
  131. package/src/console/cli-tools/fs-ops.ts +29 -0
  132. package/src/console/cli-tools/injectors/claude.ts +3 -3
  133. package/src/console/cli-tools/injectors/codex.ts +6 -6
  134. package/src/console/cli-tools/injectors/driver.ts +8 -105
  135. package/src/console/cli-tools/injectors/kilo.ts +146 -21
  136. package/src/console/cli-tools/injectors/opencode.ts +28 -5
  137. package/src/console/cli-tools/routes.ts +6 -0
  138. package/src/console/cli-tools/service.ts +7 -6
  139. package/src/console/observability/contracts.ts +6 -0
  140. package/src/console/observability/store.ts +3 -0
  141. package/src/console/share/share-stats.ts +7 -9
  142. package/src/observability/event-log.ts +7 -0
  143. package/src/observability/event-query.ts +2 -0
  144. package/src/observability/telemetry-buffer.ts +7 -0
  145. package/src/persistence/schema.ts +8 -0
  146. package/src/persistence/stores/api-key-store.ts +19 -1
  147. package/src/protocol/dsml.ts +5 -76
  148. package/src/providers/adapters/compatible-adapter.ts +29 -1
  149. package/src/providers/default-registry.ts +12 -2
  150. package/src/providers/integrations/antigravity/antigravity-protocol.ts +88 -14
  151. package/src/providers/integrations/antigravity/antigravity-quota.ts +18 -6
  152. package/src/providers/integrations/antigravity/antigravity.ts +20 -11
  153. package/src/providers/integrations/cursor/cursor.ts +2 -3
  154. package/src/providers/integrations/github/github-discovery.ts +15 -2
  155. package/src/providers/integrations/github/github-oauth.ts +178 -9
  156. package/src/providers/integrations/github/github.ts +95 -14
  157. package/src/providers/integrations/kiro/kiro.ts +1 -2
  158. package/src/providers/integrations/qoder/qoder-catalog.ts +9 -3
  159. package/src/providers/integrations/qoder/qoder.ts +147 -88
  160. package/src/providers/integrations/trae/trae.ts +2 -2
  161. package/src/providers/operations/account-error-classification.ts +67 -17
  162. package/src/providers/operations/byok-wire-profile.ts +19 -15
  163. package/src/providers/provider-usage.ts +15 -49
  164. package/src/security/admission/service.ts +11 -4
  165. package/src/security/api-key-auth.ts +5 -3
  166. package/src/transport/dispatch/attempt-finalize.ts +69 -21
  167. package/src/transport/dispatch/attempt-loop.ts +22 -0
  168. package/src/transport/failure-policy.ts +15 -1
  169. package/src/transport/guards/error-lifecycle.ts +4 -0
  170. package/src/transport/request/state.ts +6 -0
  171. package/src/transport/routing/router.ts +7 -0
  172. package/src/transport/surface/reasoning-summary.ts +9 -14
  173. package/src/transport/vision-handoff.ts +83 -28
  174. package/dist/dashboard/assets/BannedPage-Cj15GsMp.js +0 -1
  175. package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
  176. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +0 -1
  177. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
  178. package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js +0 -1
  179. package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
  180. package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js +0 -1
  181. package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
  182. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +0 -1
  183. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
  184. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
  185. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js +0 -1
  186. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
  187. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
  188. package/dist/dashboard/assets/LoginPage-DkXb6dx-.js +0 -1
  189. package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
  190. package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
  191. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +0 -1
  192. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
  193. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +0 -5
  194. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
  195. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +0 -1
  196. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
  197. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +0 -1
  198. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
  199. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
  200. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +0 -1
  201. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
  202. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
  203. package/dist/dashboard/assets/SetupPage-WTa0sktC.js +0 -1
  204. package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
  205. package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
  206. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js +0 -1
  207. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  208. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
  209. package/dist/dashboard/assets/api-keys-l29JROPv.js +0 -1
  210. package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
  211. package/dist/dashboard/assets/app--PJWq8_C.css +0 -2
  212. package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
  213. package/dist/dashboard/assets/app-Bgd3zANQ.js +0 -9
  214. package/dist/dashboard/assets/app-Bgd3zANQ.js.gz +0 -0
  215. package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
  216. package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
  217. package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
  218. package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
  219. package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
  220. package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
  221. package/dist/dashboard/assets/dialog-BEK7KP0H.js +0 -1
  222. package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
  223. package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
  224. package/dist/dashboard/assets/entry-CHWxbS56.js +0 -1
  225. package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
  226. package/dist/dashboard/assets/entry-EBHJ3DrE.js +0 -1
  227. package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
  228. package/dist/dashboard/assets/entry-LCS85hiq.js +0 -4
  229. package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
  230. package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
  231. package/dist/dashboard/assets/github-badge-YSCE4UC2.js +0 -1
  232. package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
  233. package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
  234. package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
  235. package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
  236. package/dist/dashboard/assets/layout-CajrYUnD.js.gz +0 -0
  237. package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
  238. package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
  239. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  240. package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
  241. package/dist/dashboard/assets/numbers-CUWAZK4J.js +0 -2
  242. package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
  243. package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
  244. package/dist/dashboard/assets/providers/zai-coding-plan.svg +0 -6
  245. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  246. package/dist/dashboard/assets/quota-DvQbD_sr.js +0 -1
  247. package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
  248. package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
  249. package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
  250. package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
  251. package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
  252. package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
  253. package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
  254. package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
  255. package/src/console/cli-tools/injectors/cline.ts +0 -110
  256. package/src/console/cli-tools/injectors/copilot.ts +0 -108
  257. package/src/console/cli-tools/injectors/cowork.ts +0 -64
  258. package/src/console/cli-tools/injectors/deepseek-tui.ts +0 -85
  259. package/src/console/cli-tools/injectors/droid.ts +0 -96
  260. package/src/console/cli-tools/injectors/grok-build.ts +0 -70
  261. package/src/console/cli-tools/injectors/hermes.ts +0 -101
  262. package/src/console/cli-tools/injectors/jcode.ts +0 -115
  263. package/src/console/cli-tools/injectors/openclaw.ts +0 -161
@@ -6,25 +6,15 @@ import type {
6
6
  ApplyResult,
7
7
  DownloadResult,
8
8
  InjectorSpec,
9
- ToolDef,
10
9
  ToolId,
11
10
  ToolInjector,
12
11
  ToolStatus,
13
12
  } from "../contracts";
14
- import { TOOL_IDS, TOOL_REGISTRY } from "../contracts";
15
- import { checkBinaryInstalled, ensureDir, ensureV1Suffix, fileExists, keyPrefix } from "../fs-ops";
13
+ import { TOOL_IDS } from "../contracts";
14
+ import { checkBinaryInstalled, ensureDir, fileExists, keyPrefix } from "../fs-ops";
16
15
  import { claudeSpec } from './claude';
17
- import { clineSpec } from './cline';
18
16
  import { codexSpec } from './codex';
19
- import { copilotSpec } from './copilot';
20
- import { coworkSpec } from './cowork';
21
- import { deepseekTuiSpec } from './deepseek-tui';
22
- import { droidSpec } from './droid';
23
- import { grokBuildSpec } from './grok-build';
24
- import { hermesSpec } from './hermes';
25
- import { jcodeSpec } from './jcode';
26
17
  import { kiloSpec } from './kilo';
27
- import { openclawSpec } from './openclaw';
28
18
  import { opencodeSpec } from './opencode';
29
19
 
30
20
  // ── injectors/driver.ts ──
@@ -92,11 +82,12 @@ export function createFileInjector(spec: InjectorSpec): ToolInjector {
92
82
  const path = spec.resolvePath();
93
83
  const dir = spec.resolveDir ? spec.resolveDir() : dirname(path);
94
84
  await ensureDir(dir);
95
- await spec.apply(input, path);
85
+ const warnings = await spec.apply(input, path);
96
86
  return {
97
87
  success: true,
98
88
  settingsPath: path,
99
89
  message: spec.messages?.applied ?? `${spec.displayName ?? spec.toolId} settings applied`,
90
+ ...(Array.isArray(warnings) && warnings.length > 0 ? { warnings } : {}),
100
91
  };
101
92
  },
102
93
 
@@ -131,114 +122,26 @@ export function createFileInjector(spec: InjectorSpec): ToolInjector {
131
122
  };
132
123
  }
133
124
 
134
- /** Create a guide injector for a specific tool definition (any `configType: "guide"` entry). */
135
- function guideInjectorFor(def: ToolDef): ToolInjector {
136
- return {
137
- toolId: def.id,
138
-
139
- async getStatus(): Promise<ToolStatus> {
140
- // Guide tools are always "not installed" from an injection standpoint —
141
- // we can't detect the binary reliably, and config is manual.
142
- return {
143
- toolId: def.id,
144
- installed: false,
145
- configured: false,
146
- settingsPath: null,
147
- currentEndpoint: null,
148
- currentApiKeyPrefix: null,
149
- currentModels: null,
150
- message: "Guide-only tool — use Download Config to get setup instructions",
151
- };
152
- },
153
-
154
- async apply(): Promise<ApplyResult> {
155
- return { success: false, message: `${def.name} is a guide-only tool — use Download Config` };
156
- },
157
-
158
- async reset(): Promise<ApplyResult> {
159
- return { success: true, message: `${def.name} has no injected config to reset` };
160
- },
161
-
162
- async download(input: ApplyInput): Promise<DownloadResult> {
163
- const baseUrl = ensureV1Suffix(input.endpoint);
164
- const model = input.activeModel ?? input.modelIds[0] ?? "";
165
- const apiKey = input.apiKey;
166
- const codeBlock = def.codeBlock;
167
-
168
- if (codeBlock) {
169
- const content = codeBlock.code
170
- .replace(/\{\{baseUrl\}\}/g, baseUrl)
171
- .replace(/\{\{apiKey\}\}/g, apiKey)
172
- .replace(/\{\{model\}\}/g, model);
173
- const ext =
174
- codeBlock.language === "json" ? "json" : codeBlock.language === "bash" ? "sh" : "txt";
175
- return {
176
- content,
177
- filename: `${def.id}-config.${ext}`,
178
- mimeType: codeBlock.language === "json" ? "application/json" : "text/plain",
179
- };
180
- }
181
-
182
- // No code block — generate a simple guide summary.
183
- const steps = def.guideSteps ?? [];
184
- const lines = [`# ${def.name} — Setup Guide`, ""];
185
- for (const step of steps) {
186
- lines.push(`## Step ${step.step}: ${step.title}`);
187
- if (step.desc) lines.push(step.desc);
188
- if (step.value) {
189
- lines.push("```");
190
- lines.push(
191
- step.value
192
- .replace(/\{\{baseUrl\}\}/g, baseUrl)
193
- .replace(/\{\{apiKey\}\}/g, apiKey)
194
- .replace(/\{\{model\}\}/g, model),
195
- );
196
- lines.push("```");
197
- }
198
- lines.push("");
199
- }
200
- lines.push(`Endpoint: ${baseUrl}`);
201
- lines.push(`API Key: ${apiKey}`);
202
- lines.push(`Model: ${model}`);
203
- return { content: lines.join("\n"), filename: `${def.id}-guide.txt`, mimeType: "text/plain" };
204
- },
205
- };
206
- }
207
-
208
125
  // ── injectors/index.ts ──
209
126
  /**
210
127
  * Single dispatch point for CliToolService: given a toolId, look up the
211
128
  * injector and call getStatus/apply/reset/download.
212
129
  *
213
130
  * File-based injectors are driven by a generic driver (createFileInjector)
214
- * configured via declarative specs. Guide-only tools (registry `configType:
215
- * "guide"`) are all built from `guideInjectorFor` — no per-tool constant
216
- * needed; the registry itself is the single source of the list. The map is
217
- * built exhaustively from that registry so a non-guide tool without an
218
- * injector fails loudly at import, never silently at lookup time.
131
+ * configured via declarative specs. The map is built exhaustively from that
132
+ * registry so a tool without an injector fails loudly at import, never
133
+ * silently at lookup time.
219
134
  */
220
135
  const FILE_INJECTORS: Partial<Record<ToolId, ToolInjector>> = {
221
136
  claude: createFileInjector(claudeSpec),
222
137
  codex: createFileInjector(codexSpec),
223
- cline: createFileInjector(clineSpec),
224
138
  opencode: createFileInjector(opencodeSpec),
225
- droid: createFileInjector(droidSpec),
226
- hermes: createFileInjector(hermesSpec),
227
- "grok-build": createFileInjector(grokBuildSpec),
228
- copilot: createFileInjector(copilotSpec),
229
- "deepseek-tui": createFileInjector(deepseekTuiSpec),
230
- jcode: createFileInjector(jcodeSpec),
231
139
  kilo: createFileInjector(kiloSpec),
232
- openclaw: createFileInjector(openclawSpec),
233
- cowork: createFileInjector(coworkSpec),
234
140
  };
235
141
 
236
142
  export const INJECTORS: Record<ToolId, ToolInjector> = Object.fromEntries(
237
143
  TOOL_IDS.map((id) => {
238
- const injector =
239
- TOOL_REGISTRY[id].configType === "guide"
240
- ? guideInjectorFor(TOOL_REGISTRY[id])
241
- : FILE_INJECTORS[id];
144
+ const injector = FILE_INJECTORS[id];
242
145
  if (injector === undefined) {
243
146
  throw new Error(`No injector registered for tool: ${id}`);
244
147
  }
@@ -1,13 +1,36 @@
1
1
  import { platform } from "node:os";
2
2
  import type {} from "../contracts";
3
- import { resolveApplyTarget, jsonDownload } from "../contracts";
4
- import { homeDir, isLocalEndpoint, join, readJsonFile, writeJsonFile } from "../fs-ops";
3
+ import { jsonDownload } from "../contracts";
4
+ import { homeDir, isLocalEndpoint, join, readJsonFile, writeJsonFileWithBackup } from "../fs-ops";
5
5
  import type { InjectorSpec } from "../contracts";
6
6
 
7
7
  const IS_WIN = platform() === "win32";
8
8
  const IS_MAC = platform() === "darwin";
9
9
 
10
10
  // Kilo Code injector spec.
11
+ //
12
+ // Kilo Code is a fork of OpenCode, but it now reads its own config file —
13
+ // `~/.config/kilo/kilo.jsonc` (Windows: `%USERPROFILE%\.config\kilo\kilo.jsonc`)
14
+ // — with the same provider shape as OpenCode: `provider.<id>.npm/options/models`
15
+ // plus a top-level `model`. Unlike OpenCode, Kilo runs on the AI SDK, so the
16
+ // `npm` value must be an `@ai-sdk/*` package (default `@ai-sdk/openai-compatible`).
17
+ //
18
+ // The older VS Code extension also keeps a credential store at
19
+ // `~/.local/share/kilo/auth.json`; we still write the endpoint there for
20
+ // backward compatibility, but the canonical config is now `kilo.jsonc`.
21
+
22
+ const KILO_PROVIDER_ID = "cartethyia";
23
+
24
+ function resolveKiloConfigDirectory(): string {
25
+ if (IS_WIN) return join(process.env.USERPROFILE ?? homeDir(), ".config", "kilo");
26
+ if (IS_MAC) return join(homeDir(), ".config", "kilo");
27
+ return join(homeDir(), ".config", "kilo");
28
+ }
29
+
30
+ function kiloConfigPath(): string {
31
+ return join(resolveKiloConfigDirectory(), "kilo.jsonc");
32
+ }
33
+
11
34
  function resolveKiloDataDirectory(): string {
12
35
  if (IS_WIN) return join(process.env.LOCALAPPDATA ?? homeDir(), "kilo");
13
36
  if (IS_MAC) return join(homeDir(), "Library", "Application Support", "kilo");
@@ -21,15 +44,52 @@ function kiloVscodeSettingsPath(): string {
21
44
  return join(homeDir(), ".config", "Code", "User", "settings.json");
22
45
  }
23
46
 
47
+ function kiloModelDef(model: string): unknown {
48
+ return {
49
+ name: model,
50
+ limit: { context: 128000, output: 64000 },
51
+ };
52
+ }
53
+
54
+ function buildKiloModels(models: readonly string[]): Record<string, unknown> {
55
+ const map: Record<string, unknown> = {};
56
+ for (const m of models) {
57
+ if (!m) continue;
58
+ map[m] = kiloModelDef(m);
59
+ }
60
+ return map;
61
+ }
62
+
24
63
  export const kiloSpec: InjectorSpec = {
25
64
  toolId: "kilo",
26
65
  displayName: "Kilo Code",
27
66
  binary: "kilo",
28
- resolveDir: resolveKiloDataDirectory,
29
- resolvePath: () => join(resolveKiloDataDirectory(), "auth.json"),
67
+ resolveDir: resolveKiloConfigDirectory,
68
+ resolvePath: kiloConfigPath,
30
69
 
31
70
  async readStatus(path) {
32
- const auth = (await readJsonFile(path)) as Record<
71
+ const config = (await readJsonFile(path)) as {
72
+ provider?: Record<string, { npm?: string; options?: { baseURL?: string; apiKey?: string }; models?: Record<string, unknown> }>;
73
+ model?: string;
74
+ } | null;
75
+ const provider = config?.provider?.[KILO_PROVIDER_ID];
76
+ const endpoint = provider?.options?.baseURL ?? null;
77
+ const models = provider?.models ? Object.keys(provider.models) : null;
78
+ const active =
79
+ typeof config?.model === "string" && config.model.startsWith(`${KILO_PROVIDER_ID}/`)
80
+ ? config.model.split("/").slice(1).join("/")
81
+ : null;
82
+ if (endpoint) {
83
+ return {
84
+ configured: isLocalEndpoint(endpoint),
85
+ currentEndpoint: endpoint,
86
+ rawApiKey: provider?.options?.apiKey ?? null,
87
+ currentModels: active && models ? [active, ...models.filter((m) => m !== active)] : models,
88
+ };
89
+ }
90
+
91
+ // Fallback to the legacy auth.json store (older VS Code extension).
92
+ const auth = (await readJsonFile(join(resolveKiloDataDirectory(), "auth.json"))) as Record<
33
93
  string,
34
94
  { baseUrl?: string; baseURL?: string; apiKey?: string; model?: string }
35
95
  > | null;
@@ -47,22 +107,77 @@ export const kiloSpec: InjectorSpec = {
47
107
  },
48
108
 
49
109
  async apply(input, path) {
50
- const { model, baseUrl } = resolveApplyTarget(input);
51
- const auth = ((await readJsonFile(path)) as Record<string, unknown> | null) ?? {};
52
- auth["openai-compatible"] = { baseUrl, apiKey: input.apiKey, model };
53
- await writeJsonFile(path, auth);
110
+ // Canonical config: ~/.config/kilo/kilo.jsonc
111
+ const existing = (await readJsonFile(path)) as Record<string, unknown> | null;
112
+ const config: Record<string, unknown> = (existing ?? {}) as Record<string, unknown>;
113
+ if (!config.provider || typeof config.provider !== "object") config.provider = {};
114
+ const providers = config.provider as Record<string, unknown>;
115
+ const prior = (providers[KILO_PROVIDER_ID] as Record<string, unknown> | undefined) ?? {};
116
+ const priorModels = (prior.models as Record<string, unknown> | undefined) ?? {};
117
+ const otherProviders = Object.keys(providers).filter((id) => id !== KILO_PROVIDER_ID);
118
+ providers[KILO_PROVIDER_ID] = {
119
+ npm: input.sdk ?? (prior as { npm?: string }).npm ?? "@ai-sdk/openai-compatible",
120
+ options: {
121
+ ...((prior.options as Record<string, unknown> | undefined) ?? {}),
122
+ baseURL: input.endpoint,
123
+ apiKey: input.apiKey,
124
+ },
125
+ models: { ...priorModels, ...buildKiloModels(input.modelIds) },
126
+ };
127
+ const activeModel = input.activeModel ?? input.modelIds[0] ?? "";
128
+ config.model = activeModel === "" ? "" : `${KILO_PROVIDER_ID}/${activeModel}`;
129
+
130
+ // YOLO mode: auto-approve shell/read/write.
131
+ if (input.bypassPermissions === true) {
132
+ config.permissions = [
133
+ { action: "shell", resource: "*", effect: "allow" },
134
+ { action: "external_directory", resource: "*", effect: "allow" },
135
+ { action: "question", resource: "*", effect: "deny" },
136
+ ];
137
+ } else if (input.bypassPermissions === false) {
138
+ delete config.permissions;
139
+ }
140
+
141
+ await writeJsonFileWithBackup(path, config);
142
+
143
+ // Legacy credential store for the older VS Code extension.
144
+ const authPath = join(resolveKiloDataDirectory(), "auth.json");
145
+ const auth = ((await readJsonFile(authPath)) as Record<string, unknown> | null) ?? {};
146
+ auth["openai-compatible"] = { baseUrl: input.endpoint, apiKey: input.apiKey, model: activeModel };
147
+ await writeJsonFileWithBackup(authPath, auth);
54
148
 
55
149
  const settings =
56
150
  ((await readJsonFile(kiloVscodeSettingsPath())) as Record<string, unknown> | null) ?? {};
57
151
  settings["kilo.code.authProviderOverride"] = "openai-compatible";
58
- await writeJsonFile(kiloVscodeSettingsPath(), settings);
152
+ await writeJsonFileWithBackup(kiloVscodeSettingsPath(), settings);
153
+
154
+ return otherProviders.length > 0
155
+ ? [
156
+ `Config already has other provider(s): ${otherProviders.join(", ")}. They were kept as-is — only ${KILO_PROVIDER_ID} was written/updated.`,
157
+ ]
158
+ : undefined;
59
159
  },
60
160
 
61
161
  async reset(path) {
62
- const auth = (await readJsonFile(path)) as Record<string, unknown> | null;
63
- if (!auth) return false;
64
- delete auth["openai-compatible"];
65
- await writeJsonFile(path, auth);
162
+ const config = (await readJsonFile(path)) as Record<string, unknown> | null;
163
+ if (config && config.provider && typeof config.provider === "object") {
164
+ const providers = config.provider as Record<string, unknown>;
165
+ delete providers[KILO_PROVIDER_ID];
166
+ if (Object.keys(providers).length === 0) delete config.provider;
167
+ }
168
+ if (typeof config?.model === "string" && config.model.startsWith(`${KILO_PROVIDER_ID}/`)) {
169
+ delete config.model;
170
+ }
171
+ if (config) await writeJsonFileWithBackup(path, config);
172
+
173
+ const auth = (await readJsonFile(join(resolveKiloDataDirectory(), "auth.json"))) as Record<
174
+ string,
175
+ unknown
176
+ > | null;
177
+ if (auth) {
178
+ delete auth["openai-compatible"];
179
+ await writeJsonFileWithBackup(join(resolveKiloDataDirectory(), "auth.json"), auth);
180
+ }
66
181
 
67
182
  const settings = (await readJsonFile(kiloVscodeSettingsPath())) as Record<
68
183
  string,
@@ -70,21 +185,31 @@ export const kiloSpec: InjectorSpec = {
70
185
  > | null;
71
186
  if (settings && "kilo.code.authProviderOverride" in settings) {
72
187
  delete settings["kilo.code.authProviderOverride"];
73
- await writeJsonFile(kiloVscodeSettingsPath(), settings);
188
+ await writeJsonFileWithBackup(kiloVscodeSettingsPath(), settings);
74
189
  }
75
190
  },
76
191
 
77
192
  download(input) {
78
- const { model, baseUrl } = resolveApplyTarget(input);
79
- return jsonDownload({ "openai-compatible": { baseUrl, apiKey: input.apiKey, model } }, {
80
- filename: "auth.json",
81
- });
193
+ const activeModel = input.activeModel ?? input.modelIds[0] ?? "";
194
+ const config: Record<string, unknown> = {
195
+ provider: {
196
+ [KILO_PROVIDER_ID]: {
197
+ npm: input.sdk ?? "@ai-sdk/openai-compatible",
198
+ options: {
199
+ baseURL: input.endpoint,
200
+ apiKey: input.apiKey,
201
+ },
202
+ models: buildKiloModels(input.modelIds),
203
+ },
204
+ },
205
+ model: activeModel === "" ? "" : `${KILO_PROVIDER_ID}/${activeModel}`,
206
+ };
207
+ return jsonDownload(config, { filename: "kilo.jsonc" });
82
208
  },
83
209
 
84
210
  messages: {
85
211
  applied: "Kilo Code settings applied",
86
212
  reset: "Cartethyia settings removed from Kilo Code",
87
- resetMissing: "No auth file to reset",
213
+ resetMissing: "No config file to reset",
88
214
  },
89
215
  };
90
-
@@ -1,6 +1,7 @@
1
1
  import type {} from "../contracts";
2
- import { ensureV1Suffix, homeDir, join, readJsonFile, writeJsonFile } from "../fs-ops";
2
+ import { ensureV1Suffix, homeDir, join, readJsonFile, writeJsonFileWithBackup } from "../fs-ops";
3
3
  import type { InjectorSpec } from "../contracts";
4
+ import { DEFAULT_SDK } from "../contracts";
4
5
 
5
6
 
6
7
  // OpenCode injector spec.
@@ -71,8 +72,9 @@ export const opencodeSpec: InjectorSpec = {
71
72
 
72
73
  if (!config.provider) config.provider = {};
73
74
  const prior = config.provider.cartethyia;
75
+ const otherProviders = Object.keys(config.provider).filter((id) => id !== "cartethyia");
74
76
  config.provider.cartethyia = {
75
- npm: "@ai-sdk/openai-compatible",
77
+ npm: input.sdk ?? prior?.npm ?? DEFAULT_SDK,
76
78
  options: {
77
79
  ...(prior?.options ?? {}),
78
80
  baseURL: ensureV1Suffix(input.endpoint),
@@ -84,6 +86,22 @@ export const opencodeSpec: InjectorSpec = {
84
86
  const activeModel = input.activeModel ?? input.modelIds[0] ?? "";
85
87
  config.model = activeModel === "" ? "" : `cartethyia/${activeModel}`;
86
88
 
89
+ if (input.websearch !== undefined) {
90
+ config.websearch = { provider: input.websearch };
91
+ }
92
+
93
+ // YOLO mode: auto-approve shell/read/write. When on, write OpenCode's
94
+ // permission rules; when off, leave whatever the user had (or nothing).
95
+ if (input.bypassPermissions === true) {
96
+ config.permissions = [
97
+ { action: "shell", resource: "*", effect: "allow" },
98
+ { action: "external_directory", resource: "*", effect: "allow" },
99
+ { action: "question", resource: "*", effect: "deny" },
100
+ ];
101
+ } else if (input.bypassPermissions === false) {
102
+ delete config.permissions;
103
+ }
104
+
87
105
  if (!config.agent) config.agent = {};
88
106
  const subagentModel = input.subagentModel ?? input.modelIds[0] ?? activeModel;
89
107
  config.agent.explorer = {
@@ -92,7 +110,12 @@ export const opencodeSpec: InjectorSpec = {
92
110
  model: `cartethyia/${subagentModel}`,
93
111
  };
94
112
 
95
- await writeJsonFile(path, config);
113
+ await writeJsonFileWithBackup(path, config);
114
+ return otherProviders.length > 0
115
+ ? [
116
+ `Config already has other provider(s): ${otherProviders.join(", ")}. They were kept as-is — only cartethyia was written/updated.`,
117
+ ]
118
+ : undefined;
96
119
  },
97
120
 
98
121
  async reset(path) {
@@ -113,7 +136,7 @@ export const opencodeSpec: InjectorSpec = {
113
136
  if (Object.keys(config.agent).length === 0) delete config.agent;
114
137
  }
115
138
 
116
- await writeJsonFile(path, config);
139
+ await writeJsonFileWithBackup(path, config);
117
140
  },
118
141
 
119
142
  download(input) {
@@ -122,7 +145,7 @@ export const opencodeSpec: InjectorSpec = {
122
145
  const config: OpencodeConfig = {
123
146
  provider: {
124
147
  cartethyia: {
125
- npm: "@ai-sdk/openai-compatible",
148
+ npm: input.sdk ?? DEFAULT_SDK,
126
149
  options: {
127
150
  baseURL: ensureV1Suffix(input.endpoint),
128
151
  apiKey: input.apiKey,
@@ -102,6 +102,10 @@ function parseApplyInput(
102
102
  ...(value.mode === "file" || value.mode === "remote" || value.mode === "both"
103
103
  ? { mode: value.mode }
104
104
  : {}),
105
+ ...(typeof value.sdk === "string" && value.sdk.length > 0 ? { sdk: value.sdk } : {}),
106
+ ...(typeof value.websearch === "string" && value.websearch.length > 0
107
+ ? { websearch: value.websearch }
108
+ : {}),
105
109
  };
106
110
  }
107
111
 
@@ -128,6 +132,8 @@ const applyBody = t.Object({
128
132
  }),
129
133
  ),
130
134
  bypassPermissions: t.Optional(t.Boolean()),
135
+ sdk: t.Optional(t.String()),
136
+ websearch: t.Optional(t.String()),
131
137
  });
132
138
 
133
139
  const mappingBody = t.Object({
@@ -44,8 +44,6 @@ function toToolRegistryEntry(def: ToolDef): ToolRegistryEntry {
44
44
  ...(def.settingsFile === undefined ? {} : { settingsFile: def.settingsFile }),
45
45
  ...(def.docsUrl === undefined ? {} : { docsUrl: def.docsUrl }),
46
46
  ...(def.notes === undefined ? {} : { notes: def.notes }),
47
- ...(def.guideSteps === undefined ? {} : { guideSteps: def.guideSteps }),
48
- ...(def.codeBlock === undefined ? {} : { codeBlock: def.codeBlock }),
49
47
  };
50
48
  }
51
49
 
@@ -64,8 +62,8 @@ function applyMessage(outcome: ApplyOutcome, toolId: string, fileMessage: string
64
62
  case "remote":
65
63
  return `Saved the remote route for ${toolId}. No config file was written.`;
66
64
  case "none":
67
- // A guide tool has no config to inject; the injector's own message says
68
- // what to do instead (download the guide), so surface it verbatim.
65
+ // Nothing was written and no remote route was saved; surface the
66
+ // injector's own message verbatim.
69
67
  return fileMessage.length > 0 ? fileMessage : `Nothing to apply for ${toolId}.`;
70
68
  }
71
69
  }
@@ -274,8 +272,8 @@ export class CliToolService {
274
272
  *
275
273
  * `mode` defaults to `both` so the common single-machine case does the
276
274
  * obviously-right thing without the operator choosing. A tool that has no
277
- * injectable config (a guide tool) reports `remote` honestly instead of
278
- * claiming a file write it never performed.
275
+ * injectable config reports `remote` honestly instead of claiming a file
276
+ * write it never performed.
279
277
  */
280
278
  async applyConfig(
281
279
  tenantId: string,
@@ -292,12 +290,14 @@ export class CliToolService {
292
290
  let wroteFile = false;
293
291
  let settingsPath: string | undefined;
294
292
  let fileMessage = "";
293
+ let fileWarnings: readonly string[] | undefined;
295
294
 
296
295
  if (wantsFile) {
297
296
  const result: ApplyResult = await injector.apply(input);
298
297
  wroteFile = result.success;
299
298
  if (result.settingsPath !== undefined) settingsPath = result.settingsPath;
300
299
  fileMessage = result.message;
300
+ fileWarnings = result.warnings;
301
301
  }
302
302
 
303
303
  let savedRemoteRoute = false;
@@ -321,6 +321,7 @@ export class CliToolService {
321
321
  savedRemoteRoute,
322
322
  ...(settingsPath === undefined ? {} : { settingsPath }),
323
323
  message: applyMessage(outcome, toolId, fileMessage),
324
+ ...(fileWarnings && fileWarnings.length > 0 ? { warnings: fileWarnings } : {}),
324
325
  };
325
326
  }
326
327
 
@@ -232,6 +232,12 @@ export interface UsageRequestItem {
232
232
  * written before the column existed.
233
233
  */
234
234
  payloadCaptureState?: string;
235
+ /**
236
+ * True for a completed request whose output was far smaller than the model's
237
+ * ceiling and which ended with an ordinary `stop` (a "degenerate stop").
238
+ * Absent for everything else — including rows written before the column.
239
+ */
240
+ degenerateStop?: boolean;
235
241
  /**
236
242
  * Bitmask of the payload stages captured for this request: 1 request,
237
243
  * 2 provider request, 4 provider response, 8 client response. Read from the
@@ -165,6 +165,9 @@ function mapUsageRequestItem(
165
165
  // Why the drawer is empty for this request — see the contract field. This is
166
166
  // what makes the blank panels diagnosable without server logs.
167
167
  ...(event.payloadCaptureState ? { payloadCaptureState: event.payloadCaptureState } : {}),
168
+ // Degenerate stop (short output ended by an ordinary `stop`) — carried so
169
+ // the request detail can flag the pattern.
170
+ ...(event.degenerateStop ? { degenerateStop: true } : {}),
168
171
  };
169
172
  }
170
173
  async function resolveProxyLabel(
@@ -371,11 +371,9 @@ async function computeFamilyStatsBody(
371
371
  .where(eq(apiKeys.id, templateKeyId))
372
372
  .limit(1),
373
373
  ]);
374
- // Cumulative family spend: the template's own counter plus the telemetry
375
- // total. The counter is a manually-configured seed (an operator limit), not
376
- // traffic, and it is never reset; azmetry rows for revoked/deleted children
377
- // likewise stay counted, so the running total is monotonic and consistent
378
- // across the overview, the share dialog, and this page.
374
+ // Cumulative family spend: the template's own counter is the live
375
+ // accumulator (updated by every recipient commit), so it IS the total.
376
+ // Telemetry still drives the bounded daily/monthly windows below.
379
377
  const templateLifetime = Number(template[0]?.lifetime ?? 0);
380
378
 
381
379
  // The ranked tables need real events, bounded to the 24-hour window. The
@@ -398,10 +396,10 @@ async function computeFamilyStatsBody(
398
396
  });
399
397
  rollup.totals = {
400
398
  ...rollup.totals,
401
- // The template counter is the seed on the running total, not a distinct
402
- // bucket: daily/monthly windows are telemetry-only (the counter has no
403
- // time axis), so those keep their own sums.
404
- totalTokens: templateLifetime + kpi.tokens,
399
+ // The template counter IS the running family total (live accumulator).
400
+ // Daily/monthly windows stay telemetry-only (the counter has no time
401
+ // axis), so those keep their own sums.
402
+ totalTokens: templateLifetime,
405
403
  todayTokens,
406
404
  monthTokens,
407
405
  // Lifetime counts come from the durable rollup, not the page: a family
@@ -57,6 +57,12 @@ export interface TelemetryEventRecord {
57
57
  readonly lastEventAtMs: number | null;
58
58
  /** Why the drawer payloads are empty, or `captured`; absent when unrecorded. */
59
59
  readonly payloadCaptureState?: string | null;
60
+ /**
61
+ * True for a completed request whose output was far smaller than the model's
62
+ * ceiling and which ended with an ordinary `stop` (a "degenerate stop").
63
+ * NULL/unset for everything else.
64
+ */
65
+ readonly degenerateStop?: boolean;
60
66
  }
61
67
 
62
68
  function asString(value: unknown): string | null {
@@ -109,6 +115,7 @@ function normalize(raw: Record<string, unknown>): TelemetryEventRecord | undefin
109
115
  tokensPerSec: asNumber(raw["tokensPerSec"]),
110
116
  firstContentDeltaAtMs: asNumber(raw["firstContentDeltaAtMs"]),
111
117
  lastEventAtMs: asNumber(raw["lastEventAtMs"]),
118
+ ...(raw["degenerateStop"] === true ? { degenerateStop: true } : {}),
112
119
  };
113
120
  }
114
121
 
@@ -184,6 +184,7 @@ function eventRecordFromRow(row: {
184
184
  readonly firstContentDeltaAtMs: number | null;
185
185
  readonly lastEventAtMs: number | null;
186
186
  readonly payloadCaptureState: string | null;
187
+ readonly degenerateStop: boolean | null;
187
188
  }): TelemetryEventRecord {
188
189
  return {
189
190
  id: row.id ?? row.requestId,
@@ -219,6 +220,7 @@ function eventRecordFromRow(row: {
219
220
  firstContentDeltaAtMs: row.firstContentDeltaAtMs,
220
221
  lastEventAtMs: row.lastEventAtMs,
221
222
  payloadCaptureState: row.payloadCaptureState,
223
+ ...(row.degenerateStop === true ? { degenerateStop: true } : {}),
222
224
  };
223
225
  }
224
226
 
@@ -65,6 +65,12 @@ export interface TelemetryEventInput {
65
65
  * list instead of needing the server's logs.
66
66
  */
67
67
  readonly payloadCaptureState?: string;
68
+ /**
69
+ * True for a completed request whose output was far smaller than the model's
70
+ * ceiling and which ended with an ordinary `stop` (a "degenerate stop").
71
+ * Carried onto the telemetry row so this pattern can be measured.
72
+ */
73
+ readonly degenerateStop?: boolean;
68
74
  }
69
75
 
70
76
  function tokenCountToInt(value: number | "unavailable" | undefined): number | undefined {
@@ -155,6 +161,7 @@ function telemetryEventRow(event: TelemetryEventInput) {
155
161
  firstContentDeltaAtMs: event.firstContentDeltaAtMs ?? null,
156
162
  lastEventAtMs: event.lastEventAtMs ?? null,
157
163
  payloadCaptureState: event.payloadCaptureState ?? null,
164
+ degenerateStop: event.degenerateStop ?? null,
158
165
  };
159
166
  }
160
167
 
@@ -1483,6 +1483,14 @@ export const telemetryEvents = pgTable(
1483
1483
  * tell "not configured" from "broken".
1484
1484
  */
1485
1485
  payloadCaptureState: text("payload_capture_state"),
1486
+ /**
1487
+ * Set for a completed request whose output is far smaller than what the
1488
+ * model can produce AND which the model ended with an ordinary `stop`
1489
+ * (a "degenerate stop"). Introduced to measure how often a model
1490
+ * truncates itself on a cached prefix — the pattern that surfaced as
1491
+ * short, cut-off chat replies. NULL/unset for everything else.
1492
+ */
1493
+ degenerateStop: boolean("degenerate_stop"),
1486
1494
  },
1487
1495
  (table) => [
1488
1496
  index("telemetry_events_created_at_idx").on(table.createdAt),