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