cerebras-cli 1.0.0

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 (314) hide show
  1. package/AGENTS.md +27 -0
  2. package/Dockerfile +10 -0
  3. package/README.md +15 -0
  4. package/bin/opencode +84 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +128 -0
  7. package/parsers-config.ts +239 -0
  8. package/script/build.ts +151 -0
  9. package/script/postinstall.mjs +122 -0
  10. package/script/publish.ts +256 -0
  11. package/script/schema.ts +47 -0
  12. package/src/acp/README.md +164 -0
  13. package/src/acp/agent.ts +812 -0
  14. package/src/acp/session.ts +70 -0
  15. package/src/acp/types.ts +22 -0
  16. package/src/agent/agent.ts +310 -0
  17. package/src/agent/generate.txt +75 -0
  18. package/src/auth/index.ts +70 -0
  19. package/src/bun/index.ts +152 -0
  20. package/src/bus/global.ts +10 -0
  21. package/src/bus/index.ts +142 -0
  22. package/src/cli/bootstrap.ts +17 -0
  23. package/src/cli/cmd/acp.ts +88 -0
  24. package/src/cli/cmd/agent.ts +165 -0
  25. package/src/cli/cmd/auth.ts +369 -0
  26. package/src/cli/cmd/cmd.ts +7 -0
  27. package/src/cli/cmd/debug/config.ts +15 -0
  28. package/src/cli/cmd/debug/file.ts +91 -0
  29. package/src/cli/cmd/debug/index.ts +41 -0
  30. package/src/cli/cmd/debug/lsp.ts +47 -0
  31. package/src/cli/cmd/debug/ripgrep.ts +83 -0
  32. package/src/cli/cmd/debug/scrap.ts +15 -0
  33. package/src/cli/cmd/debug/snapshot.ts +48 -0
  34. package/src/cli/cmd/export.ts +88 -0
  35. package/src/cli/cmd/generate.ts +38 -0
  36. package/src/cli/cmd/github.ts +1200 -0
  37. package/src/cli/cmd/import.ts +98 -0
  38. package/src/cli/cmd/mcp.ts +400 -0
  39. package/src/cli/cmd/models.ts +77 -0
  40. package/src/cli/cmd/pr.ts +112 -0
  41. package/src/cli/cmd/run.ts +342 -0
  42. package/src/cli/cmd/serve.ts +31 -0
  43. package/src/cli/cmd/session.ts +106 -0
  44. package/src/cli/cmd/stats.ts +298 -0
  45. package/src/cli/cmd/tui/app.tsx +732 -0
  46. package/src/cli/cmd/tui/attach.ts +25 -0
  47. package/src/cli/cmd/tui/component/border.tsx +21 -0
  48. package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
  49. package/src/cli/cmd/tui/component/dialog-command.tsx +124 -0
  50. package/src/cli/cmd/tui/component/dialog-feedback.tsx +160 -0
  51. package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
  52. package/src/cli/cmd/tui/component/dialog-model.tsx +223 -0
  53. package/src/cli/cmd/tui/component/dialog-notification.tsx +78 -0
  54. package/src/cli/cmd/tui/component/dialog-provider.tsx +222 -0
  55. package/src/cli/cmd/tui/component/dialog-session-list.tsx +97 -0
  56. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
  57. package/src/cli/cmd/tui/component/dialog-status.tsx +114 -0
  58. package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
  59. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
  60. package/src/cli/cmd/tui/component/logo.tsx +37 -0
  61. package/src/cli/cmd/tui/component/notification-banner.tsx +58 -0
  62. package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +530 -0
  63. package/src/cli/cmd/tui/component/prompt/history.tsx +107 -0
  64. package/src/cli/cmd/tui/component/prompt/index.tsx +931 -0
  65. package/src/cli/cmd/tui/context/args.tsx +14 -0
  66. package/src/cli/cmd/tui/context/directory.ts +12 -0
  67. package/src/cli/cmd/tui/context/exit.tsx +23 -0
  68. package/src/cli/cmd/tui/context/helper.tsx +25 -0
  69. package/src/cli/cmd/tui/context/keybind.tsx +111 -0
  70. package/src/cli/cmd/tui/context/kv.tsx +49 -0
  71. package/src/cli/cmd/tui/context/local.tsx +339 -0
  72. package/src/cli/cmd/tui/context/prompt.tsx +18 -0
  73. package/src/cli/cmd/tui/context/route.tsx +45 -0
  74. package/src/cli/cmd/tui/context/sdk.tsx +75 -0
  75. package/src/cli/cmd/tui/context/sync.tsx +374 -0
  76. package/src/cli/cmd/tui/context/theme/aura.json +69 -0
  77. package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
  78. package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
  79. package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
  80. package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
  81. package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
  82. package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
  83. package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
  84. package/src/cli/cmd/tui/context/theme/github.json +233 -0
  85. package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
  86. package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
  87. package/src/cli/cmd/tui/context/theme/material.json +235 -0
  88. package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
  89. package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
  90. package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
  91. package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
  92. package/src/cli/cmd/tui/context/theme/nord.json +223 -0
  93. package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
  94. package/src/cli/cmd/tui/context/theme/orng.json +245 -0
  95. package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
  96. package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
  97. package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
  98. package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
  99. package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
  100. package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
  101. package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
  102. package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
  103. package/src/cli/cmd/tui/context/theme.tsx +1077 -0
  104. package/src/cli/cmd/tui/event.ts +39 -0
  105. package/src/cli/cmd/tui/routes/home.tsx +104 -0
  106. package/src/cli/cmd/tui/routes/session/dialog-message.tsx +93 -0
  107. package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +37 -0
  108. package/src/cli/cmd/tui/routes/session/footer.tsx +76 -0
  109. package/src/cli/cmd/tui/routes/session/header.tsx +183 -0
  110. package/src/cli/cmd/tui/routes/session/index.tsx +1703 -0
  111. package/src/cli/cmd/tui/routes/session/sidebar.tsx +586 -0
  112. package/src/cli/cmd/tui/spawn.ts +60 -0
  113. package/src/cli/cmd/tui/thread.ts +120 -0
  114. package/src/cli/cmd/tui/ui/dialog-alert.tsx +55 -0
  115. package/src/cli/cmd/tui/ui/dialog-confirm.tsx +81 -0
  116. package/src/cli/cmd/tui/ui/dialog-help.tsx +36 -0
  117. package/src/cli/cmd/tui/ui/dialog-prompt.tsx +75 -0
  118. package/src/cli/cmd/tui/ui/dialog-select.tsx +317 -0
  119. package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
  120. package/src/cli/cmd/tui/ui/spinner.ts +368 -0
  121. package/src/cli/cmd/tui/ui/toast.tsx +100 -0
  122. package/src/cli/cmd/tui/util/clipboard.ts +127 -0
  123. package/src/cli/cmd/tui/util/editor.ts +32 -0
  124. package/src/cli/cmd/tui/util/terminal.ts +114 -0
  125. package/src/cli/cmd/tui/worker.ts +63 -0
  126. package/src/cli/cmd/uninstall.ts +344 -0
  127. package/src/cli/cmd/upgrade.ts +67 -0
  128. package/src/cli/cmd/web.ts +84 -0
  129. package/src/cli/error.ts +55 -0
  130. package/src/cli/ui.ts +84 -0
  131. package/src/cli/upgrade.ts +25 -0
  132. package/src/command/index.ts +79 -0
  133. package/src/command/template/initialize.txt +10 -0
  134. package/src/command/template/review.txt +73 -0
  135. package/src/config/config.ts +886 -0
  136. package/src/config/markdown.ts +41 -0
  137. package/src/env/index.ts +26 -0
  138. package/src/file/fzf.ts +124 -0
  139. package/src/file/ignore.ts +83 -0
  140. package/src/file/index.ts +326 -0
  141. package/src/file/ripgrep.ts +391 -0
  142. package/src/file/time.ts +38 -0
  143. package/src/file/watcher.ts +89 -0
  144. package/src/flag/flag.ts +28 -0
  145. package/src/format/formatter.ts +277 -0
  146. package/src/format/index.ts +137 -0
  147. package/src/global/index.ts +52 -0
  148. package/src/id/id.ts +73 -0
  149. package/src/ide/index.ts +75 -0
  150. package/src/index.ts +158 -0
  151. package/src/installation/index.ts +194 -0
  152. package/src/lsp/client.ts +215 -0
  153. package/src/lsp/index.ts +370 -0
  154. package/src/lsp/language.ts +111 -0
  155. package/src/lsp/server.ts +1327 -0
  156. package/src/mcp/auth.ts +82 -0
  157. package/src/mcp/index.ts +576 -0
  158. package/src/mcp/oauth-callback.ts +203 -0
  159. package/src/mcp/oauth-provider.ts +132 -0
  160. package/src/notification/index.ts +101 -0
  161. package/src/patch/index.ts +622 -0
  162. package/src/permission/index.ts +198 -0
  163. package/src/plugin/index.ts +95 -0
  164. package/src/project/bootstrap.ts +31 -0
  165. package/src/project/instance.ts +68 -0
  166. package/src/project/project.ts +133 -0
  167. package/src/project/state.ts +65 -0
  168. package/src/project/vcs.ts +77 -0
  169. package/src/provider/auth.ts +143 -0
  170. package/src/provider/models-macro.ts +11 -0
  171. package/src/provider/models.ts +93 -0
  172. package/src/provider/provider.ts +996 -0
  173. package/src/provider/sdk/openai-compatible/src/README.md +5 -0
  174. package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
  175. package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
  176. package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
  177. package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +27 -0
  178. package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
  179. package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
  180. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
  181. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
  182. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
  183. package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
  184. package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
  185. package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
  186. package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
  187. package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
  188. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
  189. package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
  190. package/src/provider/transform.ts +406 -0
  191. package/src/pty/index.ts +226 -0
  192. package/src/ratelimit/index.ts +185 -0
  193. package/src/server/error.ts +36 -0
  194. package/src/server/project.ts +50 -0
  195. package/src/server/server.ts +2463 -0
  196. package/src/server/tui.ts +71 -0
  197. package/src/session/compaction.ts +257 -0
  198. package/src/session/index.ts +470 -0
  199. package/src/session/message-v2.ts +641 -0
  200. package/src/session/message.ts +189 -0
  201. package/src/session/processor.ts +443 -0
  202. package/src/session/prompt/anthropic-20250930.txt +166 -0
  203. package/src/session/prompt/anthropic.txt +105 -0
  204. package/src/session/prompt/anthropic_spoof.txt +1 -0
  205. package/src/session/prompt/beast.txt +147 -0
  206. package/src/session/prompt/build-switch.txt +5 -0
  207. package/src/session/prompt/codex.txt +318 -0
  208. package/src/session/prompt/compaction.txt +12 -0
  209. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  210. package/src/session/prompt/gemini.txt +155 -0
  211. package/src/session/prompt/max-steps.txt +16 -0
  212. package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
  213. package/src/session/prompt/plan.txt +26 -0
  214. package/src/session/prompt/polaris.txt +107 -0
  215. package/src/session/prompt/qwen.txt +109 -0
  216. package/src/session/prompt/summarize.txt +4 -0
  217. package/src/session/prompt/title.txt +36 -0
  218. package/src/session/prompt.ts +1541 -0
  219. package/src/session/retry.ts +82 -0
  220. package/src/session/revert.ts +108 -0
  221. package/src/session/status.ts +75 -0
  222. package/src/session/summary.ts +203 -0
  223. package/src/session/system.ts +148 -0
  224. package/src/session/todo.ts +36 -0
  225. package/src/share/share-next.ts +195 -0
  226. package/src/share/share.ts +87 -0
  227. package/src/snapshot/index.ts +197 -0
  228. package/src/storage/storage.ts +226 -0
  229. package/src/telemetry/index.ts +232 -0
  230. package/src/tool/bash.ts +365 -0
  231. package/src/tool/bash.txt +128 -0
  232. package/src/tool/batch.ts +173 -0
  233. package/src/tool/batch.txt +28 -0
  234. package/src/tool/codesearch.ts +138 -0
  235. package/src/tool/codesearch.txt +12 -0
  236. package/src/tool/edit.ts +674 -0
  237. package/src/tool/edit.txt +10 -0
  238. package/src/tool/glob.ts +65 -0
  239. package/src/tool/glob.txt +6 -0
  240. package/src/tool/grep.ts +120 -0
  241. package/src/tool/grep.txt +8 -0
  242. package/src/tool/invalid.ts +17 -0
  243. package/src/tool/ls.ts +110 -0
  244. package/src/tool/ls.txt +1 -0
  245. package/src/tool/lsp-diagnostics.ts +26 -0
  246. package/src/tool/lsp-diagnostics.txt +1 -0
  247. package/src/tool/lsp-hover.ts +31 -0
  248. package/src/tool/lsp-hover.txt +1 -0
  249. package/src/tool/multiedit.ts +46 -0
  250. package/src/tool/multiedit.txt +41 -0
  251. package/src/tool/patch.ts +233 -0
  252. package/src/tool/patch.txt +1 -0
  253. package/src/tool/read.ts +217 -0
  254. package/src/tool/read.txt +12 -0
  255. package/src/tool/registry.ts +148 -0
  256. package/src/tool/task.ts +135 -0
  257. package/src/tool/task.txt +60 -0
  258. package/src/tool/todo.ts +39 -0
  259. package/src/tool/todoread.txt +14 -0
  260. package/src/tool/todowrite.txt +167 -0
  261. package/src/tool/tool.ts +66 -0
  262. package/src/tool/webfetch.ts +187 -0
  263. package/src/tool/webfetch.txt +14 -0
  264. package/src/tool/websearch.ts +150 -0
  265. package/src/tool/websearch.txt +11 -0
  266. package/src/tool/write.ts +99 -0
  267. package/src/tool/write.txt +8 -0
  268. package/src/types/shims.d.ts +3 -0
  269. package/src/util/color.ts +19 -0
  270. package/src/util/context.ts +25 -0
  271. package/src/util/defer.ts +12 -0
  272. package/src/util/eventloop.ts +20 -0
  273. package/src/util/filesystem.ts +69 -0
  274. package/src/util/fn.ts +11 -0
  275. package/src/util/iife.ts +3 -0
  276. package/src/util/keybind.ts +79 -0
  277. package/src/util/lazy.ts +11 -0
  278. package/src/util/locale.ts +81 -0
  279. package/src/util/lock.ts +98 -0
  280. package/src/util/log.ts +177 -0
  281. package/src/util/queue.ts +32 -0
  282. package/src/util/rpc.ts +42 -0
  283. package/src/util/scrap.ts +10 -0
  284. package/src/util/signal.ts +12 -0
  285. package/src/util/timeout.ts +14 -0
  286. package/src/util/token.ts +7 -0
  287. package/src/util/wildcard.ts +54 -0
  288. package/sst-env.d.ts +9 -0
  289. package/test/bun.test.ts +53 -0
  290. package/test/config/agent-color.test.ts +66 -0
  291. package/test/config/config.test.ts +503 -0
  292. package/test/config/markdown.test.ts +89 -0
  293. package/test/file/ignore.test.ts +10 -0
  294. package/test/fixture/fixture.ts +28 -0
  295. package/test/fixture/lsp/fake-lsp-server.js +77 -0
  296. package/test/ide/ide.test.ts +82 -0
  297. package/test/keybind.test.ts +317 -0
  298. package/test/lsp/client.test.ts +95 -0
  299. package/test/patch/patch.test.ts +348 -0
  300. package/test/preload.ts +38 -0
  301. package/test/project/project.test.ts +42 -0
  302. package/test/provider/provider.test.ts +1809 -0
  303. package/test/provider/transform.test.ts +305 -0
  304. package/test/session/retry.test.ts +61 -0
  305. package/test/session/session.test.ts +71 -0
  306. package/test/snapshot/snapshot.test.ts +939 -0
  307. package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
  308. package/test/tool/bash.test.ts +55 -0
  309. package/test/tool/patch.test.ts +259 -0
  310. package/test/util/iife.test.ts +36 -0
  311. package/test/util/lazy.test.ts +50 -0
  312. package/test/util/timeout.test.ts +21 -0
  313. package/test/util/wildcard.test.ts +55 -0
  314. package/tsconfig.json +17 -0
@@ -0,0 +1,232 @@
1
+ import { Log } from "@/util/log"
2
+ import { Instance } from "@/project/instance"
3
+ import z from "zod"
4
+
5
+ /**
6
+ * Telemetry module for logging API request metrics to Cloudflare worker
7
+ *
8
+ * Tracks cache hit rates, token usage, conversation turns, and other metrics
9
+ * for monitoring and optimization purposes.
10
+ */
11
+ export namespace Telemetry {
12
+ const log = Log.create({ service: "telemetry" })
13
+
14
+ // Telemetry endpoint
15
+ const TELEMETRY_ENDPOINT =
16
+ process.env.OPENCODE_TELEMETRY_URL || "https://opencode-telemetry.kevin-taylor-d8d.workers.dev"
17
+
18
+ // Batch settings
19
+ const BATCH_SIZE = 10
20
+ const BATCH_INTERVAL_MS = 30_000 // 30 seconds
21
+
22
+ // Schema for telemetry entry
23
+ export const Entry = z.object({
24
+ // Identifiers
25
+ sessionID: z.string().optional(),
26
+ providerID: z.string().optional(),
27
+ modelID: z.string().optional(),
28
+
29
+ // Per-step token metrics
30
+ inputTokens: z.number().default(0),
31
+ outputTokens: z.number().default(0),
32
+ reasoningTokens: z.number().default(0),
33
+ cachedTokens: z.number().default(0),
34
+
35
+ // Per-step computed metrics
36
+ cacheHitRate: z.number().default(0),
37
+
38
+ // Session-level cumulative totals (like sidebar displays)
39
+ sessionTotalCachedTokens: z.number().default(0),
40
+ sessionTotalPromptTokens: z.number().default(0),
41
+ sessionTotalOutputTokens: z.number().default(0),
42
+ sessionOverallHitRate: z.number().default(0),
43
+
44
+ // Session context
45
+ conversationTurns: z.number().default(0),
46
+
47
+ // Metadata
48
+ finishReason: z.string().optional(),
49
+ })
50
+ export type Entry = z.infer<typeof Entry>
51
+
52
+ // Internal state for batching
53
+ const state = Instance.state(
54
+ () => {
55
+ const queue: Entry[] = []
56
+ let timer: ReturnType<typeof setTimeout> | undefined
57
+
58
+ return {
59
+ queue,
60
+ timer,
61
+ enabled: true,
62
+ }
63
+ },
64
+ async (entry) => {
65
+ // Flush remaining entries on shutdown
66
+ if (entry.timer) {
67
+ clearTimeout(entry.timer)
68
+ }
69
+ if (entry.queue.length > 0) {
70
+ await flush(entry.queue)
71
+ entry.queue.length = 0
72
+ }
73
+ },
74
+ )
75
+
76
+ /**
77
+ * Check if telemetry is enabled
78
+ */
79
+ export function isEnabled(): boolean {
80
+ return state().enabled
81
+ }
82
+
83
+ /**
84
+ * Enable or disable telemetry
85
+ */
86
+ export function setEnabled(enabled: boolean): void {
87
+ state().enabled = enabled
88
+ if (!enabled) {
89
+ // Clear any pending entries
90
+ state().queue.length = 0
91
+ if (state().timer) {
92
+ clearTimeout(state().timer)
93
+ state().timer = undefined
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Log a telemetry entry
100
+ *
101
+ * Entries are batched and sent periodically to reduce network overhead.
102
+ */
103
+ export function track(entry: Entry): void {
104
+ const s = state()
105
+ if (!s.enabled) return
106
+
107
+ log.info("tracking telemetry", {
108
+ providerID: entry.providerID,
109
+ modelID: entry.modelID,
110
+ cacheHitRate: entry.cacheHitRate,
111
+ cachedTokens: entry.cachedTokens,
112
+ inputTokens: entry.inputTokens,
113
+ })
114
+
115
+ s.queue.push(entry)
116
+
117
+ // Send immediately if batch is full
118
+ if (s.queue.length >= BATCH_SIZE) {
119
+ const entries = s.queue.splice(0, BATCH_SIZE)
120
+ flush(entries).catch((e) => log.error("flush error", { error: e }))
121
+ }
122
+
123
+ // Start batch timer if not already running
124
+ if (!s.timer) {
125
+ s.timer = setTimeout(() => {
126
+ s.timer = undefined
127
+ if (s.queue.length > 0) {
128
+ const entries = s.queue.splice(0)
129
+ flush(entries).catch((e) => log.error("flush error", { error: e }))
130
+ }
131
+ }, BATCH_INTERVAL_MS)
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Convenience method to track from session processor data
137
+ */
138
+ export function trackFromUsage(input: {
139
+ sessionID: string
140
+ providerID: string
141
+ modelID: string
142
+ // Per-step tokens
143
+ tokens: {
144
+ input: number
145
+ output: number
146
+ reasoning: number
147
+ cache: {
148
+ read: number
149
+ }
150
+ }
151
+ conversationTurns: number
152
+ finishReason?: string
153
+ // Session-level cumulative totals
154
+ sessionTotals?: {
155
+ cachedTokens: number
156
+ promptTokens: number
157
+ outputTokens: number
158
+ }
159
+ }): void {
160
+ // Calculate per-step cache hit rate
161
+ const totalPromptTokens = input.tokens.input + input.tokens.cache.read
162
+ const cacheHitRate = totalPromptTokens > 0 ? (input.tokens.cache.read / totalPromptTokens) * 100 : 0
163
+
164
+ // Calculate session-level overall hit rate
165
+ const sessionOverallHitRate =
166
+ input.sessionTotals && input.sessionTotals.promptTokens > 0
167
+ ? (input.sessionTotals.cachedTokens / input.sessionTotals.promptTokens) * 100
168
+ : 0
169
+
170
+ track({
171
+ sessionID: input.sessionID,
172
+ providerID: input.providerID,
173
+ modelID: input.modelID,
174
+ // Per-step metrics
175
+ inputTokens: input.tokens.input,
176
+ outputTokens: input.tokens.output,
177
+ reasoningTokens: input.tokens.reasoning,
178
+ cachedTokens: input.tokens.cache.read,
179
+ cacheHitRate,
180
+ // Session-level cumulative totals
181
+ sessionTotalCachedTokens: input.sessionTotals?.cachedTokens ?? 0,
182
+ sessionTotalPromptTokens: input.sessionTotals?.promptTokens ?? 0,
183
+ sessionTotalOutputTokens: input.sessionTotals?.outputTokens ?? 0,
184
+ sessionOverallHitRate,
185
+ // Context
186
+ conversationTurns: input.conversationTurns,
187
+ finishReason: input.finishReason,
188
+ })
189
+ }
190
+
191
+ /**
192
+ * Flush entries to the telemetry endpoint
193
+ */
194
+ async function flush(entries: Entry[]): Promise<void> {
195
+ if (entries.length === 0) return
196
+
197
+ log.info("flushing telemetry", { count: entries.length })
198
+
199
+ try {
200
+ const enrichedEntries = entries
201
+
202
+ const res = await fetch(`${TELEMETRY_ENDPOINT}/batch`, {
203
+ method: "POST",
204
+ headers: { "Content-Type": "application/json" },
205
+ body: JSON.stringify({ entries: enrichedEntries }),
206
+ })
207
+
208
+ if (!res.ok) {
209
+ log.error("telemetry flush failed", { status: res.status })
210
+ }
211
+ } catch (e) {
212
+ // Silently fail - telemetry should not disrupt normal operation
213
+ log.error("telemetry error", { error: e })
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Force flush any pending entries immediately
219
+ */
220
+ export async function forceFlush(): Promise<void> {
221
+ const s = state()
222
+ if (s.timer) {
223
+ clearTimeout(s.timer)
224
+ s.timer = undefined
225
+ }
226
+ if (s.queue.length > 0) {
227
+ const entries = s.queue.splice(0)
228
+ await flush(entries)
229
+ }
230
+ }
231
+ }
232
+
@@ -0,0 +1,365 @@
1
+ import z from "zod"
2
+ import { spawn } from "child_process"
3
+ import { Tool } from "./tool"
4
+ import DESCRIPTION from "./bash.txt"
5
+ import { Log } from "../util/log"
6
+ import { Instance } from "../project/instance"
7
+ import { lazy } from "@/util/lazy"
8
+ import { Language } from "web-tree-sitter"
9
+ import { Agent } from "@/agent/agent"
10
+ import { $ } from "bun"
11
+ import { Filesystem } from "@/util/filesystem"
12
+ import { Wildcard } from "@/util/wildcard"
13
+ import { Permission } from "@/permission"
14
+ import { fileURLToPath } from "url"
15
+ import { Flag } from "@/flag/flag.ts"
16
+ import path from "path"
17
+ import { iife } from "@/util/iife"
18
+
19
+ const DEFAULT_MAX_OUTPUT_LENGTH = 30_000
20
+ const MAX_OUTPUT_LENGTH = (() => {
21
+ const parsed = Number(Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH)
22
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_OUTPUT_LENGTH
23
+ })()
24
+ const DEFAULT_TIMEOUT = 2 * 60 * 1000
25
+ const SIGKILL_TIMEOUT_MS = 200
26
+
27
+ export const log = Log.create({ service: "bash-tool" })
28
+
29
+ const resolveWasm = (asset: string) => {
30
+ if (asset.startsWith("file://")) return fileURLToPath(asset)
31
+ if (asset.startsWith("/") || /^[a-z]:/i.test(asset)) return asset
32
+ const url = new URL(asset, import.meta.url)
33
+ return fileURLToPath(url)
34
+ }
35
+
36
+ const parser = lazy(async () => {
37
+ const { Parser } = await import("web-tree-sitter")
38
+ const { default: treeWasm } = await import("web-tree-sitter/tree-sitter.wasm" as string, {
39
+ with: { type: "wasm" },
40
+ })
41
+ const treePath = resolveWasm(treeWasm)
42
+ await Parser.init({
43
+ locateFile() {
44
+ return treePath
45
+ },
46
+ })
47
+ const { default: bashWasm } = await import("tree-sitter-bash/tree-sitter-bash.wasm" as string, {
48
+ with: { type: "wasm" },
49
+ })
50
+ const bashPath = resolveWasm(bashWasm)
51
+ const bashLanguage = await Language.load(bashPath)
52
+ const p = new Parser()
53
+ p.setLanguage(bashLanguage)
54
+ return p
55
+ })
56
+
57
+ // TODO: we may wanna rename this tool so it works better on other shells
58
+
59
+ export const BashTool = Tool.define("bash", async () => {
60
+ const shell = iife(() => {
61
+ const s = process.env.SHELL
62
+ if (s) {
63
+ const basename = path.basename(s)
64
+ if (!new Set(["fish", "nu"]).has(basename)) {
65
+ return s
66
+ }
67
+ }
68
+
69
+ if (process.platform === "darwin") {
70
+ return "/bin/zsh"
71
+ }
72
+
73
+ if (process.platform === "win32") {
74
+ // Let Bun / Node pick COMSPEC (usually cmd.exe)
75
+ // or explicitly:
76
+ return process.env.COMSPEC || true
77
+ }
78
+
79
+ const bash = Bun.which("bash")
80
+ if (bash) {
81
+ return bash
82
+ }
83
+
84
+ return true
85
+ })
86
+ log.info("bash tool using shell", { shell })
87
+
88
+ return {
89
+ description: DESCRIPTION,
90
+ parameters: z.object({
91
+ command: z.string().describe("The command to execute"),
92
+ timeout: z.number().describe("Optional timeout in milliseconds").optional(),
93
+ workdir: z
94
+ .string()
95
+ .describe(
96
+ `The working directory to run the command in. Defaults to ${Instance.directory}. Use this instead of 'cd' commands.`,
97
+ )
98
+ .optional(),
99
+ description: z
100
+ .string()
101
+ .describe(
102
+ "Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'",
103
+ ),
104
+ }),
105
+ async execute(params, ctx) {
106
+ const cwd = params.workdir || Instance.directory
107
+ if (params.timeout !== undefined && params.timeout < 0) {
108
+ throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
109
+ }
110
+ const timeout = params.timeout ?? DEFAULT_TIMEOUT
111
+ const tree = await parser().then((p) => p.parse(params.command))
112
+ if (!tree) {
113
+ throw new Error("Failed to parse command")
114
+ }
115
+ const agent = await Agent.get(ctx.agent)
116
+
117
+ const checkExternalDirectory = async (dir: string) => {
118
+ if (Filesystem.contains(Instance.directory, dir)) return
119
+ const title = `This command references paths outside of ${Instance.directory}`
120
+ if (agent.permission.external_directory === "ask") {
121
+ await Permission.ask({
122
+ type: "external_directory",
123
+ pattern: [dir, path.join(dir, "*")],
124
+ sessionID: ctx.sessionID,
125
+ messageID: ctx.messageID,
126
+ callID: ctx.callID,
127
+ title,
128
+ metadata: {
129
+ command: params.command,
130
+ },
131
+ })
132
+ } else if (agent.permission.external_directory === "deny") {
133
+ throw new Permission.RejectedError(
134
+ ctx.sessionID,
135
+ "external_directory",
136
+ ctx.callID,
137
+ {
138
+ command: params.command,
139
+ },
140
+ `${title} so this command is not allowed to be executed.`,
141
+ )
142
+ }
143
+ }
144
+
145
+ await checkExternalDirectory(cwd)
146
+
147
+ const permissions = agent.permission.bash
148
+
149
+ const askPatterns = new Set<string>()
150
+ for (const node of tree.rootNode.descendantsOfType("command")) {
151
+ if (!node) continue
152
+ const command = []
153
+ for (let i = 0; i < node.childCount; i++) {
154
+ const child = node.child(i)
155
+ if (!child) continue
156
+ if (
157
+ child.type !== "command_name" &&
158
+ child.type !== "word" &&
159
+ child.type !== "string" &&
160
+ child.type !== "raw_string" &&
161
+ child.type !== "concatenation"
162
+ ) {
163
+ continue
164
+ }
165
+ command.push(child.text)
166
+ }
167
+
168
+ // not an exhaustive list, but covers most common cases
169
+ if (["cd", "rm", "cp", "mv", "mkdir", "touch", "chmod", "chown"].includes(command[0])) {
170
+ for (const arg of command.slice(1)) {
171
+ if (arg.startsWith("-") || (command[0] === "chmod" && arg.startsWith("+"))) continue
172
+ const resolved = await $`realpath ${arg}`
173
+ .quiet()
174
+ .nothrow()
175
+ .text()
176
+ .then((x) => x.trim())
177
+ log.info("resolved path", { arg, resolved })
178
+ if (resolved) {
179
+ // Git Bash on Windows returns Unix-style paths like /c/Users/...
180
+ const normalized =
181
+ process.platform === "win32" && resolved.match(/^\/[a-z]\//)
182
+ ? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\")
183
+ : resolved
184
+
185
+ await checkExternalDirectory(normalized)
186
+ }
187
+ }
188
+ }
189
+
190
+ // always allow cd if it passes above check
191
+ if (command[0] !== "cd") {
192
+ const action = Wildcard.allStructured({ head: command[0], tail: command.slice(1) }, permissions)
193
+ if (action === "deny") {
194
+ throw new Error(
195
+ `The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${JSON.stringify(permissions)}`,
196
+ )
197
+ }
198
+ if (action === "ask") {
199
+ const pattern = (() => {
200
+ if (command.length === 0) return
201
+ const head = command[0]
202
+ // Find first non-flag argument as subcommand
203
+ const sub = command.slice(1).find((arg) => !arg.startsWith("-"))
204
+ return sub ? `${head} ${sub} *` : `${head} *`
205
+ })()
206
+ if (pattern) {
207
+ askPatterns.add(pattern)
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ if (askPatterns.size > 0) {
214
+ const patterns = Array.from(askPatterns)
215
+ await Permission.ask({
216
+ type: "bash",
217
+ pattern: patterns,
218
+ sessionID: ctx.sessionID,
219
+ messageID: ctx.messageID,
220
+ callID: ctx.callID,
221
+ title: params.command,
222
+ metadata: {
223
+ command: params.command,
224
+ patterns,
225
+ },
226
+ })
227
+ }
228
+
229
+ const proc = spawn(params.command, {
230
+ shell,
231
+ cwd,
232
+ env: {
233
+ ...process.env,
234
+ },
235
+ stdio: ["ignore", "pipe", "pipe"],
236
+ detached: process.platform !== "win32",
237
+ })
238
+
239
+ let output = ""
240
+
241
+ // Initialize metadata with empty output
242
+ ctx.metadata({
243
+ metadata: {
244
+ output: "",
245
+ description: params.description,
246
+ },
247
+ })
248
+
249
+ const append = (chunk: Buffer) => {
250
+ if (output.length <= MAX_OUTPUT_LENGTH) {
251
+ output += chunk.toString()
252
+ ctx.metadata({
253
+ metadata: {
254
+ output,
255
+ description: params.description,
256
+ },
257
+ })
258
+ }
259
+ }
260
+
261
+ proc.stdout?.on("data", append)
262
+ proc.stderr?.on("data", append)
263
+
264
+ let timedOut = false
265
+ let aborted = false
266
+ let exited = false
267
+
268
+ const killTree = async () => {
269
+ const pid = proc.pid
270
+ if (!pid || exited) {
271
+ return
272
+ }
273
+
274
+ if (process.platform === "win32") {
275
+ await new Promise<void>((resolve) => {
276
+ const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], { stdio: "ignore" })
277
+ killer.once("exit", resolve)
278
+ killer.once("error", resolve)
279
+ })
280
+ return
281
+ }
282
+
283
+ try {
284
+ process.kill(-pid, "SIGTERM")
285
+ await Bun.sleep(SIGKILL_TIMEOUT_MS)
286
+ if (!exited) {
287
+ process.kill(-pid, "SIGKILL")
288
+ }
289
+ } catch (_e) {
290
+ proc.kill("SIGTERM")
291
+ await Bun.sleep(SIGKILL_TIMEOUT_MS)
292
+ if (!exited) {
293
+ proc.kill("SIGKILL")
294
+ }
295
+ }
296
+ }
297
+
298
+ if (ctx.abort.aborted) {
299
+ aborted = true
300
+ await killTree()
301
+ }
302
+
303
+ const abortHandler = () => {
304
+ aborted = true
305
+ void killTree()
306
+ }
307
+
308
+ ctx.abort.addEventListener("abort", abortHandler, { once: true })
309
+
310
+ const timeoutTimer = setTimeout(() => {
311
+ timedOut = true
312
+ void killTree()
313
+ }, timeout + 100)
314
+
315
+ await new Promise<void>((resolve, reject) => {
316
+ const cleanup = () => {
317
+ clearTimeout(timeoutTimer)
318
+ ctx.abort.removeEventListener("abort", abortHandler)
319
+ }
320
+
321
+ proc.once("exit", () => {
322
+ exited = true
323
+ cleanup()
324
+ resolve()
325
+ })
326
+
327
+ proc.once("error", (error) => {
328
+ exited = true
329
+ cleanup()
330
+ reject(error)
331
+ })
332
+ })
333
+
334
+ let resultMetadata: String[] = ["<bash_metadata>"]
335
+
336
+ if (output.length > MAX_OUTPUT_LENGTH) {
337
+ output = output.slice(0, MAX_OUTPUT_LENGTH)
338
+ resultMetadata.push(`bash tool truncated output as it exceeded ${MAX_OUTPUT_LENGTH} char limit`)
339
+ }
340
+
341
+ if (timedOut) {
342
+ resultMetadata.push(`bash tool terminated commmand after exceeding timeout ${timeout} ms`)
343
+ }
344
+
345
+ if (aborted) {
346
+ resultMetadata.push("User aborted the command")
347
+ }
348
+
349
+ if (resultMetadata.length > 1) {
350
+ resultMetadata.push("</bash_metadata>")
351
+ output += "\n\n" + resultMetadata.join("\n")
352
+ }
353
+
354
+ return {
355
+ title: params.description,
356
+ metadata: {
357
+ output,
358
+ exit: proc.exitCode,
359
+ description: params.description,
360
+ },
361
+ output,
362
+ }
363
+ },
364
+ }
365
+ })