catherd-cli 1.4.0 → 2.0.0-beta.20261022113309

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 (320) hide show
  1. package/README.md +188 -169
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -239
  5. package/package.json +6 -8
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/bin/catherd-mcp +1 -1
  9. package/plugin/commands/catherd.md +2 -2
  10. package/plugin/skills/catherd/SKILL.md +97 -356
  11. package/plugin/skills/catherd-setup/SKILL.md +21 -20
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +19 -4
  14. package/src/adapters/claude-code/index.ts +126 -14
  15. package/src/adapters/codex/events.ts +10 -0
  16. package/src/adapters/codex/index.ts +17 -5
  17. package/src/adapters/cursor/home.ts +1 -1
  18. package/src/adapters/cursor/index.ts +10 -4
  19. package/src/adapters/opencode/index.ts +38 -1
  20. package/src/cli.ts +27 -27
  21. package/src/domain/agents.ts +3 -1
  22. package/src/domain/bars.ts +33 -107
  23. package/src/domain/blocked.ts +72 -0
  24. package/src/domain/branch.ts +51 -0
  25. package/src/domain/brief.ts +106 -0
  26. package/src/domain/catalog.ts +69 -313
  27. package/src/domain/changes.ts +23 -1
  28. package/src/domain/cost.ts +19 -29
  29. package/src/domain/errors.ts +6 -2
  30. package/src/domain/fail-lines.ts +26 -0
  31. package/src/domain/failover.ts +37 -75
  32. package/src/domain/findings.ts +47 -0
  33. package/src/domain/gate-brief.ts +53 -0
  34. package/src/domain/hints.ts +13 -2
  35. package/src/domain/ids.ts +13 -2
  36. package/src/domain/jev.ts +47 -2
  37. package/src/domain/judge.ts +124 -0
  38. package/src/domain/lane.ts +118 -3
  39. package/src/domain/milestone-memory.ts +53 -0
  40. package/src/domain/notice.ts +32 -8
  41. package/src/domain/notify.ts +32 -0
  42. package/src/domain/plan-doc.ts +198 -0
  43. package/src/domain/plan-lint.ts +141 -0
  44. package/src/domain/profile-rules.ts +175 -138
  45. package/src/domain/profile.ts +140 -63
  46. package/src/domain/record.ts +68 -3
  47. package/src/domain/reply.ts +29 -0
  48. package/src/domain/role-prompts.ts +182 -56
  49. package/src/domain/role-scope.ts +76 -0
  50. package/src/domain/role-tools.ts +34 -0
  51. package/src/domain/roles.ts +2 -0
  52. package/src/domain/route.ts +90 -0
  53. package/src/domain/rules.ts +33 -0
  54. package/src/domain/run-events.ts +298 -0
  55. package/src/domain/run-machine.ts +668 -0
  56. package/src/domain/run-state.ts +1310 -0
  57. package/src/domain/scorecard.ts +197 -0
  58. package/src/domain/select.ts +229 -72
  59. package/src/domain/slopalytics.ts +72 -0
  60. package/src/domain/state.ts +5 -0
  61. package/src/domain/util.ts +22 -0
  62. package/src/domain/workspace.ts +130 -0
  63. package/src/entry/answer-command.ts +39 -0
  64. package/src/entry/catalog-command.ts +24 -165
  65. package/src/entry/check-bin.ts +17 -0
  66. package/src/entry/cli-kit.ts +18 -2
  67. package/src/entry/daemon-bin.ts +22 -0
  68. package/src/entry/deps.ts +2 -0
  69. package/src/entry/doctor-command.ts +20 -2
  70. package/src/entry/glyphs.ts +15 -2
  71. package/src/entry/host-arg.ts +28 -0
  72. package/src/entry/init-command.ts +20 -69
  73. package/src/entry/knowledge-command.ts +121 -5
  74. package/src/entry/lock-command.ts +103 -8
  75. package/src/entry/mcp/handshake.ts +6 -2
  76. package/src/entry/mcp/role-bin.ts +20 -0
  77. package/src/entry/mcp/role-server.ts +134 -0
  78. package/src/entry/mcp/server.ts +85 -21
  79. package/src/entry/mcp/session-tools.ts +149 -0
  80. package/src/entry/panel/openapi.ts +189 -0
  81. package/src/entry/panel/server.ts +410 -0
  82. package/src/entry/panel-command.ts +68 -0
  83. package/src/entry/pause-command.ts +67 -0
  84. package/src/entry/profile-command.ts +33 -18
  85. package/src/entry/role-cli-command.ts +159 -0
  86. package/src/entry/runs-command.ts +81 -27
  87. package/src/entry/scorecard-command.ts +187 -0
  88. package/src/entry/supervise-bin.ts +1 -1
  89. package/src/entry/supervise-command.ts +30 -3
  90. package/src/entry/workspace-command.ts +44 -0
  91. package/src/infra/boot-lock.ts +25 -0
  92. package/src/infra/delivery.ts +3 -0
  93. package/src/infra/dispatch-dir.ts +2 -0
  94. package/src/infra/env.ts +11 -3
  95. package/src/infra/git.ts +70 -0
  96. package/src/infra/heavy-lock.ts +133 -6
  97. package/src/infra/host-probe.ts +16 -0
  98. package/src/infra/launch.ts +63 -7
  99. package/src/infra/lock-activity.ts +88 -0
  100. package/src/infra/login-env.ts +48 -0
  101. package/src/infra/memory.ts +45 -0
  102. package/src/infra/ntfy.ts +40 -0
  103. package/src/infra/open-url.ts +42 -0
  104. package/src/infra/paths.ts +37 -2
  105. package/src/infra/role-mcp.ts +69 -0
  106. package/src/infra/slopalytics.ts +30 -0
  107. package/src/infra/store.ts +6 -1
  108. package/src/infra/supervisor.ts +107 -16
  109. package/src/services/admission.ts +133 -31
  110. package/src/services/backends.ts +4 -6
  111. package/src/services/budget.ts +26 -6
  112. package/src/services/capture.ts +1 -22
  113. package/src/services/catalog-service.ts +155 -254
  114. package/src/services/credentials.ts +15 -26
  115. package/src/services/daemon.ts +181 -0
  116. package/src/services/dispatch-service.ts +209 -17
  117. package/src/services/dispatches.ts +30 -2
  118. package/src/services/doctor-access.ts +8 -3
  119. package/src/services/doctor-backends.ts +1 -1
  120. package/src/services/doctor-checks.ts +47 -0
  121. package/src/services/doctor-docker.ts +221 -0
  122. package/src/services/doctor-role-mcp.ts +65 -0
  123. package/src/services/doctor.ts +72 -15
  124. package/src/services/end-push.ts +157 -0
  125. package/src/services/failover-rungs.ts +109 -0
  126. package/src/services/finalize.ts +24 -2
  127. package/src/services/gate-env.ts +217 -0
  128. package/src/services/gate-service.ts +231 -45
  129. package/src/services/global-install.ts +46 -13
  130. package/src/services/jev-service.ts +2 -4
  131. package/src/services/lane-edit.ts +154 -0
  132. package/src/services/lane-service.ts +291 -55
  133. package/src/services/milestone-check.ts +433 -0
  134. package/src/services/milestone-tree.ts +170 -0
  135. package/src/services/milestones.ts +167 -29
  136. package/src/services/notifier.ts +124 -4
  137. package/src/services/notify-service.ts +26 -0
  138. package/src/services/orchestrator-wait.ts +82 -0
  139. package/src/services/panel-setup.ts +116 -0
  140. package/src/services/pause.ts +161 -0
  141. package/src/services/peek.ts +79 -17
  142. package/src/services/plan-input.ts +151 -0
  143. package/src/services/ports.ts +60 -5
  144. package/src/services/preflight.ts +232 -24
  145. package/src/services/profile-service.ts +31 -3
  146. package/src/services/profile-store.ts +18 -6
  147. package/src/services/protocol.ts +123 -25
  148. package/src/services/provenance.ts +34 -89
  149. package/src/services/questions.ts +24 -0
  150. package/src/services/reconcile.ts +33 -1
  151. package/src/services/reentry.ts +7 -6
  152. package/src/services/role-access.ts +27 -0
  153. package/src/services/routing-service.ts +161 -42
  154. package/src/services/run-briefs.ts +477 -0
  155. package/src/services/run-client.ts +217 -0
  156. package/src/services/run-effects.ts +837 -0
  157. package/src/services/run-events-store.ts +174 -0
  158. package/src/services/run-evidence.ts +1 -1
  159. package/src/services/run-pin.ts +125 -0
  160. package/src/services/run-read.ts +221 -0
  161. package/src/services/run-report.ts +176 -0
  162. package/src/services/run-service.ts +204 -23
  163. package/src/services/run-store.ts +159 -18
  164. package/src/services/scorecard-metrics.ts +86 -0
  165. package/src/services/scorecard.ts +382 -0
  166. package/src/services/scratch.ts +60 -0
  167. package/src/services/sessions.ts +3 -1
  168. package/src/services/state.ts +21 -0
  169. package/src/services/summary.ts +76 -12
  170. package/src/services/tree-path.ts +27 -0
  171. package/src/services/verifier-brief.ts +23 -0
  172. package/src/services/verifier-step.ts +48 -0
  173. package/src/services/workspace-admission.ts +259 -0
  174. package/src/services/workspace-service.ts +259 -0
  175. package/src/services/workspace-store.ts +95 -0
  176. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  177. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  178. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  179. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  180. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  181. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  182. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  183. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  184. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  185. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  186. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  187. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  188. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  189. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  190. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  191. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  192. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  193. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  194. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  195. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  196. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  197. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  198. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  199. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  200. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  201. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  202. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  203. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  204. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  205. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  206. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  207. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  208. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  209. package/web/dist/assets/index-C5IbylvM.css +2 -0
  210. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  211. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  212. package/web/dist/assets/label-DFSYBc62.js +1 -0
  213. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  214. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  215. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  216. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  217. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  218. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  219. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  220. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  221. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  222. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  223. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  224. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  225. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  226. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  227. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  228. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  229. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  230. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  231. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  232. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  233. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  234. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  235. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  236. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  237. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  238. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  239. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  240. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  241. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  242. package/web/dist/assets/time--9kSQbbp.js +1 -0
  243. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  244. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  245. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  246. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  247. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  248. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  249. package/web/dist/index.html +15 -0
  250. package/catalog/ATTRIBUTION.md +0 -36
  251. package/catalog/scores.json +0 -3202
  252. package/catalog/sources.json +0 -97
  253. package/src/adapters/antigravity/events.ts +0 -131
  254. package/src/adapters/antigravity/home.ts +0 -46
  255. package/src/adapters/antigravity/index.ts +0 -320
  256. package/src/adapters/antigravity/models.ts +0 -37
  257. package/src/adapters/grok/events.ts +0 -112
  258. package/src/adapters/grok/home.ts +0 -132
  259. package/src/adapters/grok/index.ts +0 -339
  260. package/src/adapters/grok/models.ts +0 -48
  261. package/src/domain/calibration.ts +0 -125
  262. package/src/domain/sources.ts +0 -144
  263. package/src/entry/mcp/dispatch-tools.ts +0 -52
  264. package/src/entry/mcp/lane-tools.ts +0 -84
  265. package/src/entry/mcp/protocol-tools.ts +0 -57
  266. package/src/entry/mcp/run-tools.ts +0 -148
  267. package/src/entry/mcp/setup-tools.ts +0 -108
  268. package/src/entry/tui/commands.ts +0 -563
  269. package/src/entry/tui/effects.ts +0 -381
  270. package/src/entry/tui/fixtures.ts +0 -637
  271. package/src/entry/tui/profile-edits.ts +0 -146
  272. package/src/entry/tui/profile-tree.ts +0 -429
  273. package/src/entry/tui/providers/app.tsx +0 -162
  274. package/src/entry/tui/providers/boundary.tsx +0 -57
  275. package/src/entry/tui/providers/data.tsx +0 -160
  276. package/src/entry/tui/providers/keymap.tsx +0 -166
  277. package/src/entry/tui/providers/root.tsx +0 -45
  278. package/src/entry/tui/providers/theme.tsx +0 -17
  279. package/src/entry/tui/providers/toast.tsx +0 -77
  280. package/src/entry/tui/run.tsx +0 -114
  281. package/src/entry/tui/state.ts +0 -373
  282. package/src/entry/tui/stories.ts +0 -114
  283. package/src/entry/tui/text.ts +0 -130
  284. package/src/entry/tui/theme.ts +0 -140
  285. package/src/entry/tui/views/app.tsx +0 -208
  286. package/src/entry/tui/views/dialogs.tsx +0 -41
  287. package/src/entry/tui/views/profile-actions.ts +0 -331
  288. package/src/entry/tui/views/profiles.tsx +0 -520
  289. package/src/entry/tui/views/runs.tsx +0 -573
  290. package/src/entry/tui/views/save-dialog.tsx +0 -340
  291. package/src/entry/tui/views/status.tsx +0 -209
  292. package/src/entry/tui/widgets/chrome.tsx +0 -114
  293. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  294. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  295. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  296. package/src/entry/tui/widgets/dialog.tsx +0 -68
  297. package/src/entry/tui/widgets/line.tsx +0 -92
  298. package/src/entry/tui/widgets/list.tsx +0 -197
  299. package/src/entry/tui/widgets/spinner.tsx +0 -49
  300. package/src/entry/tui/widgets/toast.tsx +0 -62
  301. package/src/infra/sources/arena.ts +0 -87
  302. package/src/infra/sources/artificial-analysis.ts +0 -112
  303. package/src/infra/sources/cache.ts +0 -124
  304. package/src/infra/sources/csv.ts +0 -38
  305. package/src/infra/sources/epoch.ts +0 -82
  306. package/src/infra/sources/http.ts +0 -75
  307. package/src/infra/sources/litellm.ts +0 -58
  308. package/src/infra/sources/models-dev.ts +0 -59
  309. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  310. package/src/infra/sources/openrouter-models.ts +0 -60
  311. package/src/infra/sources/rows.ts +0 -38
  312. package/src/infra/sources/vectara.ts +0 -69
  313. package/src/infra/sources/zip.ts +0 -48
  314. package/src/services/catalog-refresh.ts +0 -201
  315. package/src/services/doctor-sources.ts +0 -76
  316. package/src/services/runs-page.ts +0 -255
  317. package/src/services/source-derive.ts +0 -374
  318. package/src/services/source-sync.ts +0 -305
  319. package/src/services/standins.ts +0 -204
  320. package/src/services/treat-likes.ts +0 -97
@@ -1,112 +0,0 @@
1
- import { type Tokens, ZERO_TOKENS } from "../../domain/record.ts";
2
-
3
- export interface GrokEnd {
4
- stopReason: string;
5
- thread: string | null;
6
- tokens: Tokens;
7
- /** only when the server reported the whole cost (API-key traffic); absent is unknown, never free */
8
- costUsd: number | null;
9
- }
10
-
11
- export interface GrokFold {
12
- /** the text after the last tool call: the worker's final message (spec 1.3 §5.5) */
13
- reply: string;
14
- end: GrokEnd | null;
15
- /** a terminal `error` event's message: a run that never started a turn prints one instead of `end` */
16
- error: string | null;
17
- /** the sum of the per-response `usage` events, for a stream with no `end` */
18
- used: Tokens;
19
- lastEvent: string | null;
20
- }
21
-
22
- /**
23
- * Research §3.10: the limit texts of the 1.0.44 binary ([bin] "rate limit for your plan", "free Grok Build
24
- * usage limit", "temporarily overloaded") and of the 2026-09-25 research's source read. Their apostrophes are
25
- * U+2019, so no pattern spells one.
26
- */
27
- export const GROK_LIMIT = [
28
- /rate limit/i,
29
- /usage limit/i,
30
- /too many requests/i,
31
- /resource.exhausted/i,
32
- /at capacity/i,
33
- /temporarily overloaded/i,
34
- ];
35
- /** A plan without Grok Build: no wait fixes it. */
36
- export const GROK_NOT_LIMIT = [/requires a Grok subscription/i];
37
- /** clap's error for a flag this grok does not know, exit 2 (research §3.3 [run]). */
38
- export const GROK_TOO_OLD = [/unexpected argument '/i];
39
- /** research §3.3 [run]: a logged-out run's one error event */
40
- const GROK_AUTH = [/Not signed in/i];
41
-
42
- type Event = Record<string, any>;
43
-
44
- export function parseGrokLine(line: string): Event | null {
45
- if (!line.trim().startsWith("{")) return null;
46
- try {
47
- const e = JSON.parse(line) as unknown;
48
- return e && typeof e === "object" && !Array.isArray(e) ? (e as Event) : null;
49
- } catch {
50
- return null;
51
- }
52
- }
53
-
54
- /** Spec 1.3 §5.5: `input_tokens` is uncached only (research §3.3), so input adds the cache reads and writes. */
55
- export function grokTokens(u: Event | undefined): Tokens {
56
- if (!u) return { ...ZERO_TOKENS };
57
- const n = (k: string) => (typeof u[k] === "number" ? (u[k] as number) : 0);
58
- const cached = n("cache_read_input_tokens");
59
- return {
60
- input: n("input_tokens") + cached + n("cache_creation_input_tokens"),
61
- cached,
62
- output: n("output_tokens"),
63
- };
64
- }
65
-
66
- export const isLimit = (text: string): boolean =>
67
- GROK_LIMIT.some((r) => r.test(text)) && !GROK_NOT_LIMIT.some((r) => r.test(text));
68
- export const isTooOld = (text: string): boolean => GROK_TOO_OLD.some((r) => r.test(text));
69
- export const isAuthFailure = (text: string): boolean => GROK_AUTH.some((r) => r.test(text));
70
-
71
- export function eventName(e: Event): string {
72
- if (e.type === "tool_call") return `tool_call/${e.toolName ?? e.kind ?? "?"}`;
73
- if (e.type === "tool_call_update") return `tool_call_update/${e.status ?? "?"}`;
74
- return String(e.type ?? "?");
75
- }
76
-
77
- /** Spec §3.7: a command the worker runs, a file it reads or edits, or its message. */
78
- export function grokActivity(e: Event): string | undefined {
79
- if (e.type === "text") return typeof e.data === "string" && e.data.trim() ? e.data : undefined;
80
- if (e.type !== "tool_call") return undefined;
81
- const input = (e.rawInput ?? {}) as Event;
82
- if (typeof input.command === "string") return `$ ${input.command}`;
83
- if (typeof input.path === "string") return `${e.kind === "read" ? "read" : "edit"} ${input.path}`;
84
- return String(e.title ?? e.toolName ?? "tool");
85
- }
86
-
87
- export function foldGrokEvents(lines: string[]): GrokFold {
88
- const f: GrokFold = { reply: "", end: null, error: null, used: { ...ZERO_TOKENS }, lastEvent: null };
89
- for (const line of lines) {
90
- const e = parseGrokLine(line);
91
- if (!e) continue;
92
- f.lastEvent = eventName(e);
93
- if (e.type === "tool_call" || e.type === "tool_call_update") f.reply = "";
94
- else if (e.type === "text" && typeof e.data === "string") f.reply += e.data;
95
- else if (e.type === "usage") {
96
- const t = grokTokens(e.usage);
97
- f.used = {
98
- input: f.used.input + t.input,
99
- cached: f.used.cached + t.cached,
100
- output: f.used.output + t.output,
101
- };
102
- } else if (e.type === "end")
103
- f.end = {
104
- stopReason: String(e.stopReason ?? "?"),
105
- thread: typeof e.sessionId === "string" && e.sessionId ? e.sessionId : null,
106
- tokens: e.usage ? grokTokens(e.usage) : f.used,
107
- costUsd: typeof e.total_cost_usd === "number" ? e.total_cost_usd : null,
108
- };
109
- else if (e.type === "error") f.error = String(e.message ?? "error");
110
- }
111
- return f;
112
- }
@@ -1,132 +0,0 @@
1
- import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { CatherdError } from "../../domain/errors.ts";
5
- import type { Access } from "../../domain/record.ts";
6
- import { dataDir } from "../../infra/paths.ts";
7
- import { writeTextAtomic } from "../../infra/store.ts";
8
- import { movedHomeEnv, writableRoots } from "../access.ts";
9
-
10
- /** The user's GROK_HOME: grok's config, login, sessions and sandbox profiles. */
11
- export const userGrokHome = (): string => process.env.GROK_HOME || join(homedir(), ".grok");
12
-
13
- /** Where isolated grok runs keep their HOME; computing it creates nothing. */
14
- export const isolatedGrokRoot = (): string => join(dataDir(), "grok-home");
15
-
16
- /** The Claude Code and Cursor compatibility toggles of grok's config reference (research §3.8). */
17
- const COMPAT = ["CLAUDE", "CURSOR"].flatMap((tool) =>
18
- ["AGENTS", "RULES", "SKILLS", "MCPS", "HOOKS"].map((what) => `GROK_${tool}_${what}_ENABLED`),
19
- );
20
-
21
- /**
22
- * Spec 1.3 §5.4: an isolated run's env. GROK_HOME and the toggles alone still load the user's Claude Code
23
- * plugins, agents and permission rules (research §3.8 [run]), so HOME moves too, and GROK_HOME lives in it.
24
- */
25
- export function grokHomeEnv(): Record<string, string> {
26
- const home = isolatedGrokRoot();
27
- return {
28
- ...movedHomeEnv(home),
29
- GROK_HOME: join(home, ".grok"),
30
- GROK_MEMORY: "0",
31
- ...Object.fromEntries(COMPAT.map((k) => [k, "0"])),
32
- };
33
- }
34
-
35
- /** What stops grok's kernel read-only profile here (research §3.6); tests point it elsewhere. */
36
- export const grokHost = { platform: process.platform as string, dockerSocket: "/var/run/docker.sock" };
37
-
38
- /** The tools a read-only role keeps where grok's read-only profile refuses to start (spec 1.3 §5.3). */
39
- export const READ_TOOLS = "read_file,grep,list_dir,web_search,web_fetch";
40
-
41
- /**
42
- * Spec 1.3 §5.3: grok refuses `read-only` (and every profile extending it) on a Mac whose docker socket is a
43
- * symlink (Docker Desktop, OrbStack, Colima; research §3.6 [run]), so a read-only role there runs in the
44
- * `workspace` profile with only the read tools: advisory, where the kernel profile is enforced.
45
- */
46
- export function readOnlyRefused(): boolean {
47
- return (
48
- grokHost.platform === "darwin" &&
49
- lstatSync(grokHost.dockerSocket, { throwIfNoEntry: false })?.isSymbolicLink() === true
50
- );
51
- }
52
-
53
- /** Spec 1.3 §5.3: the sandbox profile (and, where read-only cannot apply, the tool filter) of each access. */
54
- export function grokAccess(access: Access, network = true): string[] {
55
- if (access === "full") return ["--sandbox", "off"];
56
- if (access === "workspace-write") return ["--sandbox", network ? "catherd-ws" : "catherd-ws-offline"];
57
- return readOnlyRefused() ? ["--sandbox", "workspace", "--tools", READ_TOOLS] : ["--sandbox", "read-only"];
58
- }
59
-
60
- /**
61
- * catherd's sandbox profiles: `workspace` plus catherd's writable roots (literal directories; research §3.6),
62
- * and the same without child network (Linux only; macOS has no child-network control).
63
- */
64
- export function catherdProfiles(): Record<string, Record<string, unknown>> {
65
- const read_write = writableRoots();
66
- return {
67
- "catherd-ws": { extends: "workspace", read_write },
68
- "catherd-ws-offline": { extends: "workspace", read_write, restrict_network: true },
69
- };
70
- }
71
-
72
- const BEGIN = "# >>> catherd";
73
- const END = "# <<< catherd";
74
- const HEADER = `${BEGIN}'s sandbox profiles for its grok workers: catherd rewrites this block, so edit outside it`;
75
-
76
- const tomlError = (text: string): string | null => {
77
- try {
78
- Bun.TOML.parse(text);
79
- return null;
80
- } catch (e) {
81
- return e instanceof Error ? e.message : String(e);
82
- }
83
- };
84
-
85
- /**
86
- * `text` (a sandbox.toml) with catherd's marked block set to `profiles`: the old block cut out, the new one
87
- * appended. Outside the block only trailing blank lines change. `why` when catherd must not write: the file
88
- * is not TOML, defines a `catherd-*` profile of its own, or has lost the block's end marker.
89
- */
90
- export function withCatherdProfiles(
91
- text: string,
92
- profiles = catherdProfiles(),
93
- ): { text: string } | { why: string } {
94
- const start = text.indexOf(BEGIN);
95
- const end = start === -1 ? -1 : text.indexOf(END, start);
96
- if (start !== -1 && end === -1)
97
- return { why: `catherd's block has lost its end marker (${END}): delete the block` };
98
- // the block goes with the blank line catherd put before it
99
- const rest =
100
- start === -1
101
- ? text
102
- : text.slice(0, start).replace(/\n\n$/, "\n") + text.slice(end + END.length).replace(/^\r?\n/, "");
103
- const bad = tomlError(rest);
104
- if (bad) return { why: `it is not valid TOML (${bad})` };
105
- const own = Object.keys((Bun.TOML.parse(rest) as { profiles?: object }).profiles ?? {}).find((k) =>
106
- k.startsWith("catherd-"),
107
- );
108
- if (own) return { why: `it has a [profiles.${own}] of its own: rename it, catherd's tables are catherd-*` };
109
- const head = rest.trimEnd();
110
- const out = `${head ? `${head}\n\n` : ""}${HEADER}\n${Bun.TOML.stringify({ profiles })}${END}\n`;
111
- const after = tomlError(out);
112
- return after ? { why: `it is not valid TOML with catherd's tables added (${after})` } : { text: out };
113
- }
114
-
115
- /**
116
- * Spec 1.3 §5.3, §9 Q3: puts catherd's `[profiles.catherd-*]` tables into `home`'s sandbox.toml (grok reads
117
- * custom profiles only there or in the repo's `.grok/`), keeping the file's mode; the rest of the file is the
118
- * user's. Throws E_CONFIG_INVALID, touching nothing, when catherd must not write it.
119
- */
120
- export function writeGrokProfiles(home: string): void {
121
- const path = join(home, "sandbox.toml");
122
- // a link (a dotfiles repo) stays a link: the atomic rename goes to the file it points at
123
- const file = lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink() ? realpathSync(path) : path;
124
- const was = existsSync(file) ? readFileSync(file, "utf8") : "";
125
- const next = withCatherdProfiles(was);
126
- if ("why" in next)
127
- throw new CatherdError("E_CONFIG_INVALID", `catherd will not edit ${file}: ${next.why}`, {
128
- fix: `fix ${file}, or isolate grok (catherd profile set harness.grok.isolated true)`,
129
- });
130
- if (next.text === was) return;
131
- writeTextAtomic(file, next.text, { mode: existsSync(file) ? statSync(file).mode & 0o777 : 0o600 });
132
- }
@@ -1,339 +0,0 @@
1
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { CatherdError } from "../../domain/errors.ts";
4
- import type { Rung } from "../../domain/ids.ts";
5
- import type { Access, RunStatus } from "../../domain/record.ts";
6
- import { assetPath } from "../../infra/assets.ts";
7
- import { ensurePrivateDir } from "../../infra/store.ts";
8
- import { realTmpdir } from "../access.ts";
9
- import {
10
- type BackendAdapter,
11
- compareVersions,
12
- type DiscoveredModel,
13
- type EventDelta,
14
- extractVersion,
15
- type FinishedRun,
16
- type Outcome,
17
- type Probe,
18
- type RunRequest,
19
- type SpawnPlan,
20
- } from "../backend.ts";
21
- import { runCli } from "../cli.ts";
22
- import { discovered } from "../discovery.ts";
23
- import {
24
- eventName,
25
- foldGrokEvents,
26
- GROK_LIMIT,
27
- GROK_TOO_OLD,
28
- grokActivity,
29
- grokTokens,
30
- isAuthFailure,
31
- isLimit,
32
- isTooOld,
33
- parseGrokLine,
34
- } from "./events.ts";
35
- import {
36
- grokAccess,
37
- grokHomeEnv,
38
- isolatedGrokRoot,
39
- readOnlyRefused,
40
- userGrokHome,
41
- writeGrokProfiles,
42
- } from "./home.ts";
43
- import { parseGrokModels, type ShippedGrok } from "./models.ts";
44
-
45
- export { isolatedGrokRoot } from "./home.ts";
46
-
47
- /** Spec 1.3 §5.1: the version whose help, docs and argument checks were read (research §3). */
48
- export const GROK_MIN_VERSION = "1.0.44";
49
- export const GROK_INSTALL = "curl -fsSL https://x.ai/cli/install.sh | bash";
50
- const THREAD = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
51
- const DAY_MS = 24 * 60 * 60_000;
52
- const LOGIN_FIX = "grok login, or grok login --device-code without a browser, or export XAI_API_KEY=<key>";
53
- /** research §3.1 [doc 14]: grok never updates itself under a worker or a probe */
54
- const NO_UPDATE = { GROK_DISABLE_AUTOUPDATER: "1" };
55
-
56
- /** How long a `grok` query (version, models, the sandbox check) may take before it counts as failed. */
57
- export const grokShell = { timeoutMs: 15_000 };
58
-
59
- /**
60
- * A fresh run's session id (`-s`), derived from its dispatch dir so finalize knows it without the stream: a run
61
- * killed before grok's `end` (a timeout, a cancel) still records the session grok saved (plan 16 final review).
62
- */
63
- export function sessionFor(r: Pick<RunRequest, "dispatchDir">): string {
64
- const h = new Bun.CryptoHasher("sha256").update(r.dispatchDir).digest("hex");
65
- const variant = ((Number.parseInt(h[16] as string, 16) & 0x3) | 0x8).toString(16);
66
- return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${variant}${h.slice(17, 20)}-${h.slice(20, 32)}`;
67
- }
68
-
69
- /** Whether grok got as far as a session: any event but a lone terminal error (not signed in, a bad flag). */
70
- const sessionStarted = (lines: string[]): boolean =>
71
- lines.some((l) => {
72
- try {
73
- return (JSON.parse(l) as { type?: unknown }).type !== "error";
74
- } catch {
75
- return false;
76
- }
77
- });
78
-
79
- function plan(r: RunRequest): SpawnPlan {
80
- if (r.thread !== null && !THREAD.test(r.thread))
81
- throw new CatherdError("E_ADMIT_THREAD", `"${r.thread}" is not a grok session id`, {
82
- fix: "pass the thread from the earlier RunRecord",
83
- });
84
- const access = grokAccess(r.access, r.network !== false);
85
- return {
86
- cmd: "grok",
87
- args: [
88
- // headless grok never reads stdin (research §3.2 [doc 14]): the brief goes by file
89
- "--prompt-file",
90
- r.briefPath,
91
- "--output-format",
92
- "streaming-json",
93
- "--cwd",
94
- r.repo,
95
- "-m",
96
- r.rung.model,
97
- ...(r.rung.effort === "default" ? [] : ["--effort", r.rung.effort]),
98
- // hidden (research §3.2 [run]): trust for this session, no self-update, no memory (spec 1.3 §9 Q8)
99
- "--always-approve",
100
- "--trust",
101
- "--no-auto-update",
102
- "--no-memory",
103
- // a fresh session takes catherd's id; a resumed one keeps the profile it started with and refuses
104
- // another (research §3.5), while a tool filter is no profile: a read-only role keeps its read tools
105
- ...(r.thread === null
106
- ? [...access, "-s", sessionFor(r)]
107
- : [...(access.includes("--tools") ? access.slice(-2) : []), "-r", r.thread]),
108
- ],
109
- env: { ...NO_UPDATE, ...(r.isolated ? grokHomeEnv() : {}) },
110
- cwd: r.repo,
111
- stdinPath: null,
112
- };
113
- }
114
-
115
- /** The catalog's grok models (`on.grok`): grok lists no efforts (research §3.4), the catalog does. */
116
- let shipped: ShippedGrok | null = null;
117
- function shippedOnGrok(): ShippedGrok {
118
- if (shipped) return shipped;
119
- const doc = JSON.parse(readFileSync(assetPath("catalog/models.json"), "utf8")) as {
120
- families: { on: Record<string, { id: string; efforts: string[]; context?: number } | undefined> }[];
121
- };
122
- shipped = {};
123
- for (const f of doc.families)
124
- if (f.on.grok) shipped[f.on.grok.id] = { efforts: f.on.grok.efforts, context: f.on.grok.context ?? null };
125
- return shipped;
126
- }
127
-
128
- async function listModels(): Promise<DiscoveredModel[]> {
129
- const r = await runCli("grok", ["models"], { ...grokShell, env: NO_UPDATE });
130
- return r?.ok ? parseGrokModels(r.out, shippedOnGrok()).models : [];
131
- }
132
-
133
- /**
134
- * Spec 1.3 §5.3–§5.4: an isolated run needs XAI_API_KEY (its home holds no login, and the user's auth.json is
135
- * never copied: its refresh token rotates, research §3.9); a workspace-write run needs catherd-ws in the
136
- * sandbox.toml of the GROK_HOME it runs with; the model must be one grok lists, at an effort the catalog offers.
137
- */
138
- async function prepare(req: {
139
- rung: Rung;
140
- access: Access;
141
- isolated: boolean;
142
- repo: string;
143
- network?: boolean;
144
- }): Promise<void> {
145
- if (req.isolated && !process.env.XAI_API_KEY)
146
- throw new CatherdError("E_BACKEND_NOT_LOGGED_IN", "an isolated grok run needs XAI_API_KEY", {
147
- fix: "export XAI_API_KEY=<key>, or catherd profile set harness.grok.isolated false",
148
- });
149
- const home = req.isolated ? join(isolatedGrokRoot(), ".grok") : userGrokHome();
150
- if (req.isolated) ensurePrivateDir(home);
151
- if (req.access === "workspace-write") writeGrokProfiles(home);
152
- const { model, effort } = req.rung;
153
- const models = await discovered("grok", listModels, { maxAgeMs: DAY_MS, need: model });
154
- if (models.length === 0) return; // grok listed nothing: let the run itself say what is wrong
155
- const m = models.find((x) => x.id === model);
156
- if (!m)
157
- throw new CatherdError("E_BACKEND_MODEL_UNKNOWN", `grok does not list ${model}`, {
158
- fix: "run grok models, and use a model it lists",
159
- });
160
- if (effort !== "default" && m.efforts.length > 0 && !m.efforts.includes(effort))
161
- throw new CatherdError("E_BACKEND_MODEL_UNKNOWN", `${model} has no effort "${effort}" on grok`, {
162
- fix: `use one of: ${["default", ...m.efforts].map((e) => `grok:${model}#${e}`).join(", ")}`,
163
- });
164
- }
165
-
166
- function finalize(run: FinishedRun): Outcome {
167
- const f = foldGrokEvents(run.eventLines);
168
- const stopped = run.exit.reason;
169
- const said = f.error ?? run.stderr;
170
- const status: RunStatus =
171
- stopped === "cancelled"
172
- ? "cancelled"
173
- : stopped === "idle-timeout" || stopped === "wall-timeout"
174
- ? "timeout"
175
- : f.end?.stopReason === "end_turn"
176
- ? "ok"
177
- : f.end
178
- ? "failed"
179
- : run.exit.code === 2 || isTooOld(run.stderr)
180
- ? "cli-too-old"
181
- : isLimit(said)
182
- ? "limit"
183
- : "failed";
184
- const errLines = run.stderr.trim().split("\n");
185
- // clap's usage lines follow its error: the error line says what went wrong
186
- const lastErr = errLines.find((l) => isTooOld(l)) ?? errLines.at(-1) ?? "";
187
- const message = f.end
188
- ? `stopped: ${f.end.stopReason}`
189
- : isAuthFailure(said)
190
- ? `grok is not signed in (fix: ${LOGIN_FIX})`
191
- : f.error || lastErr || `no end event (${stopped}, exit ${run.exit.code ?? run.exit.signal})`;
192
- return {
193
- status,
194
- thread:
195
- f.end?.thread ??
196
- run.request.thread ??
197
- (sessionStarted(run.eventLines) ? sessionFor(run.request) : null),
198
- tokens: f.end?.tokens ?? f.used,
199
- costUsd: f.end?.costUsd ?? null,
200
- images: [],
201
- error: status === "ok" ? null : { code: status, message },
202
- ...(status === "ok" ? { reply: f.reply } : {}),
203
- };
204
- }
205
-
206
- function parse(line: string): EventDelta {
207
- const e = parseGrokLine(line);
208
- if (!e) return {};
209
- const d: EventDelta = { lastEvent: eventName(e) };
210
- const activity = grokActivity(e);
211
- if (activity) d.activity = activity;
212
- // a tool call runs from its tool_call until an update leaves in_progress, often printing nothing: busy
213
- if ((e.type === "tool_call" || e.type === "tool_call_update") && typeof e.toolCallId === "string")
214
- d.item = { id: e.toolCallId, open: e.type === "tool_call" || e.status === "in_progress" };
215
- // one usage event per model response (research §3.3): the input that response read
216
- if (e.type === "usage") d.requestInput = grokTokens(e.usage).input;
217
- if (e.type === "end") {
218
- d.final = true;
219
- d.tokens = grokTokens(e.usage);
220
- if (typeof e.sessionId === "string" && e.sessionId) d.thread = e.sessionId;
221
- if (typeof e.total_cost_usd === "number") d.costUsd = e.total_cost_usd;
222
- if (e.stopReason !== "end_turn") d.failure = `stopped: ${e.stopReason}`;
223
- }
224
- // research §3.3 [run]: a run that cannot go on prints an error and no end
225
- if (e.type === "error") {
226
- d.final = true;
227
- d.failure = String(e.message ?? "error");
228
- if (isLimit(d.failure)) d.limit = true;
229
- }
230
- return d;
231
- }
232
-
233
- /**
234
- * Spec 1.3 §5.6: doctor's `sandbox:grok` row, from a run that spends no turn: grok applies its sandbox before it
235
- * checks the login (research §3.6 [run]), so from an empty GROK_HOME with no API key the run stops at the
236
- * refusal when read-only cannot apply here, else at "Not signed in".
237
- */
238
- async function sandboxInfo(): Promise<{ id: string; label: string; detail: string } | null> {
239
- const home = mkdtempSync(join(realTmpdir(), "catherd-grok-check-"));
240
- try {
241
- const r = await runCli(
242
- "grok",
243
- ["-p", "hi", "--output-format", "streaming-json", "--sandbox", "read-only", "--no-auto-update"],
244
- { ...grokShell, env: { ...NO_UPDATE, GROK_HOME: home, XAI_API_KEY: "" } },
245
- );
246
- if (!r) return null;
247
- const refused = /could not apply the '?read-only'? sandbox profile/.test(r.err);
248
- const why = r.err
249
- .split("\n")
250
- .find((l) => l.startsWith("warning: "))
251
- ?.slice("warning: ".length);
252
- const detail = !refused
253
- ? "grok's read-only profile applies here; catherd runs a read-only role in it (enforced)"
254
- : `grok refuses its read-only profile here${why ? ` (${why})` : ""}; ${
255
- readOnlyRefused()
256
- ? "catherd runs a read-only role in the workspace profile with the read tools only (advisory)"
257
- : "a read-only grok role fails here: give it another backend or full access"
258
- }`;
259
- return { id: "sandbox:grok", label: "grok sandbox", detail };
260
- } finally {
261
- rmSync(home, { recursive: true, force: true });
262
- }
263
- }
264
-
265
- /**
266
- * Spec 1.3 §5.1, §5.6. Logged in: the first line of `grok models`, which answers fast, with no browser, when
267
- * logged out (research §3.4 [run]); a Grok login bills the subscription, an API key per token. A grok the OS
268
- * cannot execute (a Linux build on macOS, research §3.1) throws here, and probeBackend reports it.
269
- */
270
- async function probe(): Promise<Probe> {
271
- const v = await runCli("grok", ["--version"], { ...grokShell, env: NO_UPDATE });
272
- if (!v)
273
- return {
274
- installed: false,
275
- version: null,
276
- versionOk: false,
277
- loggedIn: null,
278
- problems: [{ code: "E_BACKEND_MISSING", message: "grok is not on PATH", fix: GROK_INSTALL }],
279
- };
280
- const version = extractVersion(v.out);
281
- const versionOk = version !== null && compareVersions(version, GROK_MIN_VERSION) >= 0;
282
- const m = await runCli("grok", ["models"], { ...grokShell, env: NO_UPDATE });
283
- const { loggedIn, login } = parseGrokModels(`${m?.out ?? ""}\n${m?.err ?? ""}`);
284
- const problems: Probe["problems"] = [];
285
- if (!versionOk)
286
- problems.push({
287
- code: "E_BACKEND_TOO_OLD",
288
- message: `grok ${version ?? "?"} is older than ${GROK_MIN_VERSION}`,
289
- fix: "grok update --stable",
290
- });
291
- if (loggedIn === false)
292
- problems.push({
293
- code: "E_BACKEND_NOT_LOGGED_IN",
294
- message: "grok is not logged in (grok models says: You are not authenticated.)",
295
- fix: LOGIN_FIX,
296
- });
297
- const info = await sandboxInfo();
298
- return {
299
- installed: true,
300
- version,
301
- versionOk,
302
- loggedIn,
303
- ...(login
304
- ? { login, billing: login === "API key" ? ("metered" as const) : ("subscription" as const) }
305
- : {}),
306
- problems,
307
- ...(info ? { info: [info] } : {}),
308
- };
309
- }
310
-
311
- export const grokAdapter: BackendAdapter = {
312
- id: "grok",
313
- minVersion: GROK_MIN_VERSION,
314
- install: GROK_INSTALL,
315
- probe,
316
- listModels,
317
- prepare,
318
- plan,
319
- parse,
320
- finalize,
321
- // spec 1.3 §5.3: the kernel holds each profile, except read-only where only a tool allowlist can (a Mac with a
322
- // symlinked docker socket); on macOS no profile controls the network
323
- enforcement: {
324
- get "read-only"() {
325
- return readOnlyRefused() ? "advisory" : "enforced";
326
- },
327
- "workspace-write": "enforced",
328
- full: "enforced",
329
- },
330
- errors: { limit: GROK_LIMIT, tooOld: GROK_TOO_OLD },
331
- // a session keeps the sandbox profile it started with and refuses another (research §3.5)
332
- resume: { supported: true, sameAccessOnly: true, threadPattern: THREAD },
333
- isolationNote:
334
- "native grok loads your Claude Code plugins, agents, skills, hooks and permission rules, and your Cursor rules and MCP",
335
- isolationKey: "XAI_API_KEY",
336
- // a one-shot run may drain uploads for ~150 s after end (research §3.3); the record is complete at end
337
- graceAfterFinalMs: 30_000,
338
- reportsCost: true,
339
- };
@@ -1,48 +0,0 @@
1
- import type { DiscoveredModel } from "../backend.ts";
2
-
3
- /** A model's efforts and context as the catalog ships them for grok (`on.grok`). */
4
- export type ShippedGrok = Record<string, { efforts: string[]; context: number | null }>;
5
-
6
- export interface GrokModels {
7
- /** null when `grok models` printed none of its auth lines */
8
- loggedIn: boolean | null;
9
- /** how: a Grok login (SuperGrok, X Premium+) or `XAI_API_KEY` */
10
- login?: "Grok" | "API key";
11
- defaultModel: string | null;
12
- models: DiscoveredModel[];
13
- }
14
-
15
- /**
16
- * `grok models` (research §3.4 [run]): an auth line first ("You are logged in with …", "You are using
17
- * XAI_API_KEY.", "You are not authenticated."), `Default model: <id>`, then `Available models:` with
18
- * ` * <id> (default)` or ` - <id>` lines. It lists no efforts: they come from the catalog's `on.grok`
19
- * (spec 1.3 §5.2), and a model the catalog lacks lists none.
20
- */
21
- export function parseGrokModels(text: string, shipped: ShippedGrok = {}): GrokModels {
22
- const login = /You are using XAI_API_KEY/.test(text)
23
- ? "API key"
24
- : /You are logged in with/.test(text)
25
- ? "Grok"
26
- : undefined;
27
- const loggedIn = login ? true : /You are not authenticated/.test(text) ? false : null;
28
- const at = text.indexOf("Available models:");
29
- const ids =
30
- at === -1
31
- ? []
32
- : text
33
- .slice(at)
34
- .split("\n")
35
- .map((l) => /^\s*[*-]\s+(\S+)/.exec(l)?.[1])
36
- .filter((id): id is string => id !== undefined);
37
- return {
38
- loggedIn,
39
- ...(login ? { login } : {}),
40
- defaultModel: /Default model:\s*(\S+)/.exec(text)?.[1] ?? null,
41
- models: ids.map((id) => ({
42
- id,
43
- efforts: [...(shipped[id]?.efforts ?? [])],
44
- context: shipped[id]?.context ?? null,
45
- imageIn: false,
46
- })),
47
- };
48
- }