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.
- package/AGENTS.md +27 -0
- package/Dockerfile +10 -0
- package/README.md +15 -0
- package/bin/opencode +84 -0
- package/bunfig.toml +4 -0
- package/package.json +128 -0
- package/parsers-config.ts +239 -0
- package/script/build.ts +151 -0
- package/script/postinstall.mjs +122 -0
- package/script/publish.ts +256 -0
- package/script/schema.ts +47 -0
- package/src/acp/README.md +164 -0
- package/src/acp/agent.ts +812 -0
- package/src/acp/session.ts +70 -0
- package/src/acp/types.ts +22 -0
- package/src/agent/agent.ts +310 -0
- package/src/agent/generate.txt +75 -0
- package/src/auth/index.ts +70 -0
- package/src/bun/index.ts +152 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +142 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/acp.ts +88 -0
- package/src/cli/cmd/agent.ts +165 -0
- package/src/cli/cmd/auth.ts +369 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/debug/config.ts +15 -0
- package/src/cli/cmd/debug/file.ts +91 -0
- package/src/cli/cmd/debug/index.ts +41 -0
- package/src/cli/cmd/debug/lsp.ts +47 -0
- package/src/cli/cmd/debug/ripgrep.ts +83 -0
- package/src/cli/cmd/debug/scrap.ts +15 -0
- package/src/cli/cmd/debug/snapshot.ts +48 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/generate.ts +38 -0
- package/src/cli/cmd/github.ts +1200 -0
- package/src/cli/cmd/import.ts +98 -0
- package/src/cli/cmd/mcp.ts +400 -0
- package/src/cli/cmd/models.ts +77 -0
- package/src/cli/cmd/pr.ts +112 -0
- package/src/cli/cmd/run.ts +342 -0
- package/src/cli/cmd/serve.ts +31 -0
- package/src/cli/cmd/session.ts +106 -0
- package/src/cli/cmd/stats.ts +298 -0
- package/src/cli/cmd/tui/app.tsx +732 -0
- package/src/cli/cmd/tui/attach.ts +25 -0
- package/src/cli/cmd/tui/component/border.tsx +21 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-command.tsx +124 -0
- package/src/cli/cmd/tui/component/dialog-feedback.tsx +160 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +223 -0
- package/src/cli/cmd/tui/component/dialog-notification.tsx +78 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +222 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +97 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +114 -0
- package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/logo.tsx +37 -0
- package/src/cli/cmd/tui/component/notification-banner.tsx +58 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +530 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +107 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +931 -0
- package/src/cli/cmd/tui/context/args.tsx +14 -0
- package/src/cli/cmd/tui/context/directory.ts +12 -0
- package/src/cli/cmd/tui/context/exit.tsx +23 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +111 -0
- package/src/cli/cmd/tui/context/kv.tsx +49 -0
- package/src/cli/cmd/tui/context/local.tsx +339 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +45 -0
- package/src/cli/cmd/tui/context/sdk.tsx +75 -0
- package/src/cli/cmd/tui/context/sync.tsx +374 -0
- package/src/cli/cmd/tui/context/theme/aura.json +69 -0
- package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
- package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
- package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
- package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
- package/src/cli/cmd/tui/context/theme/github.json +233 -0
- package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
- package/src/cli/cmd/tui/context/theme/material.json +235 -0
- package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
- package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
- package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
- package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
- package/src/cli/cmd/tui/context/theme/nord.json +223 -0
- package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
- package/src/cli/cmd/tui/context/theme/orng.json +245 -0
- package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
- package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
- package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
- package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
- package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
- package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
- package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
- package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
- package/src/cli/cmd/tui/context/theme.tsx +1077 -0
- package/src/cli/cmd/tui/event.ts +39 -0
- package/src/cli/cmd/tui/routes/home.tsx +104 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +93 -0
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +37 -0
- package/src/cli/cmd/tui/routes/session/footer.tsx +76 -0
- package/src/cli/cmd/tui/routes/session/header.tsx +183 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +1703 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +586 -0
- package/src/cli/cmd/tui/spawn.ts +60 -0
- package/src/cli/cmd/tui/thread.ts +120 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +55 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +81 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +36 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +75 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +317 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
- package/src/cli/cmd/tui/ui/spinner.ts +368 -0
- package/src/cli/cmd/tui/ui/toast.tsx +100 -0
- package/src/cli/cmd/tui/util/clipboard.ts +127 -0
- package/src/cli/cmd/tui/util/editor.ts +32 -0
- package/src/cli/cmd/tui/util/terminal.ts +114 -0
- package/src/cli/cmd/tui/worker.ts +63 -0
- package/src/cli/cmd/uninstall.ts +344 -0
- package/src/cli/cmd/upgrade.ts +67 -0
- package/src/cli/cmd/web.ts +84 -0
- package/src/cli/error.ts +55 -0
- package/src/cli/ui.ts +84 -0
- package/src/cli/upgrade.ts +25 -0
- package/src/command/index.ts +79 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/command/template/review.txt +73 -0
- package/src/config/config.ts +886 -0
- package/src/config/markdown.ts +41 -0
- package/src/env/index.ts +26 -0
- package/src/file/fzf.ts +124 -0
- package/src/file/ignore.ts +83 -0
- package/src/file/index.ts +326 -0
- package/src/file/ripgrep.ts +391 -0
- package/src/file/time.ts +38 -0
- package/src/file/watcher.ts +89 -0
- package/src/flag/flag.ts +28 -0
- package/src/format/formatter.ts +277 -0
- package/src/format/index.ts +137 -0
- package/src/global/index.ts +52 -0
- package/src/id/id.ts +73 -0
- package/src/ide/index.ts +75 -0
- package/src/index.ts +158 -0
- package/src/installation/index.ts +194 -0
- package/src/lsp/client.ts +215 -0
- package/src/lsp/index.ts +370 -0
- package/src/lsp/language.ts +111 -0
- package/src/lsp/server.ts +1327 -0
- package/src/mcp/auth.ts +82 -0
- package/src/mcp/index.ts +576 -0
- package/src/mcp/oauth-callback.ts +203 -0
- package/src/mcp/oauth-provider.ts +132 -0
- package/src/notification/index.ts +101 -0
- package/src/patch/index.ts +622 -0
- package/src/permission/index.ts +198 -0
- package/src/plugin/index.ts +95 -0
- package/src/project/bootstrap.ts +31 -0
- package/src/project/instance.ts +68 -0
- package/src/project/project.ts +133 -0
- package/src/project/state.ts +65 -0
- package/src/project/vcs.ts +77 -0
- package/src/provider/auth.ts +143 -0
- package/src/provider/models-macro.ts +11 -0
- package/src/provider/models.ts +93 -0
- package/src/provider/provider.ts +996 -0
- package/src/provider/sdk/openai-compatible/src/README.md +5 -0
- package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
- package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
- package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
- package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +27 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
- package/src/provider/transform.ts +406 -0
- package/src/pty/index.ts +226 -0
- package/src/ratelimit/index.ts +185 -0
- package/src/server/error.ts +36 -0
- package/src/server/project.ts +50 -0
- package/src/server/server.ts +2463 -0
- package/src/server/tui.ts +71 -0
- package/src/session/compaction.ts +257 -0
- package/src/session/index.ts +470 -0
- package/src/session/message-v2.ts +641 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +443 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +105 -0
- package/src/session/prompt/anthropic_spoof.txt +1 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex.txt +318 -0
- package/src/session/prompt/compaction.txt +12 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/src/session/prompt/plan.txt +26 -0
- package/src/session/prompt/polaris.txt +107 -0
- package/src/session/prompt/qwen.txt +109 -0
- package/src/session/prompt/summarize.txt +4 -0
- package/src/session/prompt/title.txt +36 -0
- package/src/session/prompt.ts +1541 -0
- package/src/session/retry.ts +82 -0
- package/src/session/revert.ts +108 -0
- package/src/session/status.ts +75 -0
- package/src/session/summary.ts +203 -0
- package/src/session/system.ts +148 -0
- package/src/session/todo.ts +36 -0
- package/src/share/share-next.ts +195 -0
- package/src/share/share.ts +87 -0
- package/src/snapshot/index.ts +197 -0
- package/src/storage/storage.ts +226 -0
- package/src/telemetry/index.ts +232 -0
- package/src/tool/bash.ts +365 -0
- package/src/tool/bash.txt +128 -0
- package/src/tool/batch.ts +173 -0
- package/src/tool/batch.txt +28 -0
- package/src/tool/codesearch.ts +138 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +674 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/glob.ts +65 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +120 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +110 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/lsp-diagnostics.ts +26 -0
- package/src/tool/lsp-diagnostics.txt +1 -0
- package/src/tool/lsp-hover.ts +31 -0
- package/src/tool/lsp-hover.txt +1 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/patch.ts +233 -0
- package/src/tool/patch.txt +1 -0
- package/src/tool/read.ts +217 -0
- package/src/tool/read.txt +12 -0
- package/src/tool/registry.ts +148 -0
- package/src/tool/task.ts +135 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +39 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +66 -0
- package/src/tool/webfetch.ts +187 -0
- package/src/tool/webfetch.txt +14 -0
- package/src/tool/websearch.ts +150 -0
- package/src/tool/websearch.txt +11 -0
- package/src/tool/write.ts +99 -0
- package/src/tool/write.txt +8 -0
- package/src/types/shims.d.ts +3 -0
- package/src/util/color.ts +19 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +69 -0
- package/src/util/fn.ts +11 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +79 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/locale.ts +81 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +177 -0
- package/src/util/queue.ts +32 -0
- package/src/util/rpc.ts +42 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +54 -0
- package/sst-env.d.ts +9 -0
- package/test/bun.test.ts +53 -0
- package/test/config/agent-color.test.ts +66 -0
- package/test/config/config.test.ts +503 -0
- package/test/config/markdown.test.ts +89 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/fixture/fixture.ts +28 -0
- package/test/fixture/lsp/fake-lsp-server.js +77 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/keybind.test.ts +317 -0
- package/test/lsp/client.test.ts +95 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/preload.ts +38 -0
- package/test/project/project.test.ts +42 -0
- package/test/provider/provider.test.ts +1809 -0
- package/test/provider/transform.test.ts +305 -0
- package/test/session/retry.test.ts +61 -0
- package/test/session/session.test.ts +71 -0
- package/test/snapshot/snapshot.test.ts +939 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/bash.test.ts +55 -0
- package/test/tool/patch.test.ts +259 -0
- package/test/util/iife.test.ts +36 -0
- package/test/util/lazy.test.ts +50 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/wildcard.test.ts +55 -0
- 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
|
+
|
package/src/tool/bash.ts
ADDED
|
@@ -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
|
+
})
|