Compare commits

..

15 Commits

Author SHA1 Message Date
Timo Clasen 986144b377 docs: how to disable mcp server (#543)
Co-authored-by: GitHub Action <action@github.com>
2025-06-29 21:33:30 -04:00
Dax Raad 1fdb326aa7 ignore: refactoring 2025-06-29 21:30:23 -04:00
Dax Raad 463257e7e4 add zig, python, clang, and kotlin formatters
Co-authored-by: Suhas-Koheda <Suhas-Koheda@users.noreply.github.com>
Co-authored-by: Polo123456789 <Polo123456789@users.noreply.github.com>
Co-authored-by: theodore-s-beers <theodore-s-beers@users.noreply.github.com>
Co-authored-by: TylerHillery <TylerHillery@users.noreply.github.com>
2025-06-29 21:27:35 -04:00
Dax Raad 0f41e60bd6 restructure formatters 2025-06-29 21:22:21 -04:00
Polo123456789 7df81f7b3e Formatters as plugins (#487) 2025-06-29 21:13:32 -04:00
Adam Spiers dd22cb2bb0 chore: add .editorconfig (#536)
Co-authored-by: Adam Spiers <opencode@adamspiers.org>
2025-06-29 21:12:58 -04:00
Dax Raad 248325925f fix issue with costs resetting once chat is completed 2025-06-29 19:43:03 -04:00
Dax Raad ca48a4f0fb better amazon bedrock caching with anthropic models 2025-06-29 19:27:07 -04:00
Dax 98ee5a3d87 Update STATS.md 2025-06-29 13:04:44 -04:00
GitHub Action 67480e5a1c Update download stats 2025-06-29 2025-06-29 12:23:40 +00:00
GitHub Action 2581a9b54c Update download stats 2025-06-29 2025-06-29 02:00:18 +00:00
Dax Raad 14a293e124 ci: stats 2025-06-28 21:59:14 -04:00
Dax Raad 780419ecae ci: daily stats script 2025-06-28 21:57:46 -04:00
Timo Clasen f0962e2d9c Add Option to Disable MCP Servers (#513) 2025-06-28 21:05:31 -04:00
Dax Raad 3a9584a419 fix context display 2025-06-28 21:01:53 -04:00
15 changed files with 470 additions and 128 deletions
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
insert_final_newline = true
end_of_line = lf
indent_style = space
indent_size = 2
max_line_length = 80
+32
View File
@@ -0,0 +1,32 @@
name: stats
on:
schedule:
- cron: "0 12 * * *" # Run daily at 12:00 UTC
workflow_dispatch: # Allow manual trigger
jobs:
stats:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Run stats script
run: bun scripts/stats.ts
- name: Commit stats
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add STATS.md
git diff --staged --quiet || git commit -m "Update download stats $(date -I)"
git push
+5
View File
@@ -0,0 +1,5 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | ------------- | ----------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
+8
View File
@@ -264,6 +264,10 @@
"type": "string"
},
"description": "Environment variables to set when running the MCP server"
},
"enabled": {
"type": "boolean",
"description": "Enable or disable the MCP server on startup"
}
},
"required": ["type", "command"],
@@ -280,6 +284,10 @@
"url": {
"type": "string",
"description": "URL of the remote MCP server"
},
"enabled": {
"type": "boolean",
"description": "Enable or disable the MCP server on startup"
}
},
"required": ["type", "url"],
+8
View File
@@ -37,6 +37,10 @@ export namespace Config {
.record(z.string(), z.string())
.optional()
.describe("Environment variables to set when running the MCP server"),
enabled: z
.boolean()
.optional()
.describe("Enable or disable the MCP server on startup"),
})
.strict()
.openapi({
@@ -47,6 +51,10 @@ export namespace Config {
.object({
type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"),
enabled: z
.boolean()
.optional()
.describe("Enable or disable the MCP server on startup"),
})
.strict()
.openapi({
+133
View File
@@ -0,0 +1,133 @@
import { App } from "../app/app"
import { BunProc } from "../bun"
export interface Info {
name: string
command: string[]
environment?: Record<string, string>
extensions: string[]
enabled(): Promise<boolean>
}
export const gofmt: Info = {
name: "gofmt",
command: ["gofmt", "-w", "$FILE"],
extensions: [".go"],
async enabled() {
return Bun.which("gofmt") !== null
},
}
export const mix: Info = {
name: "mix",
command: ["mix", "format", "$FILE"],
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
async enabled() {
return Bun.which("mix") !== null
},
}
export const prettier: Info = {
name: "prettier",
command: [BunProc.which(), "run", "prettier", "--write", "$FILE"],
environment: {
BUN_BE_BUN: "1",
},
extensions: [
".js",
".jsx",
".mjs",
".cjs",
".ts",
".tsx",
".mts",
".cts",
".html",
".htm",
".css",
".scss",
".sass",
".less",
".vue",
".svelte",
".json",
".jsonc",
".yaml",
".yml",
".toml",
".xml",
".md",
".mdx",
".graphql",
".gql",
],
async enabled() {
// this is more complicated because we only want to use prettier if it's
// being used with the current project
try {
const proc = Bun.spawn({
cmd: [BunProc.which(), "run", "prettier", "--version"],
cwd: App.info().path.cwd,
env: {
BUN_BE_BUN: "1",
},
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
return exit === 0
} catch {
return false
}
},
}
export const zig: Info = {
name: "zig",
command: ["zig", "fmt", "$FILE"],
extensions: [".zig", ".zon"],
async enabled() {
return Bun.which("zig") !== null
},
}
export const clang: Info = {
name: "clang-format",
command: ["clang-format", "-i", "$FILE"],
extensions: [
".c",
".cc",
".cpp",
".cxx",
".c++",
".h",
".hh",
".hpp",
".hxx",
".h++",
".ino",
".C",
".H",
],
async enabled() {
return Bun.which("clang-format") !== null
},
}
export const ktlint: Info = {
name: "ktlint",
command: ["ktlint", "-F", "$FILE"],
extensions: [".kt", ".kts"],
async enabled() {
return Bun.which("ktlint") !== null
},
}
export const ruff: Info = {
name: "ruff",
command: ["ruff", "format", "$FILE"],
extensions: [".py", ".pyi"],
async enabled() {
return Bun.which("ruff") !== null
},
}
+4 -104
View File
@@ -1,10 +1,11 @@
import { App } from "../app/app"
import { BunProc } from "../bun"
import { Bus } from "../bus"
import { File } from "../file"
import { Log } from "../util/log"
import path from "path"
import * as Formatter from "./formatter"
export namespace Format {
const log = Log.create({ service: "format" })
@@ -16,7 +17,7 @@ export namespace Format {
}
})
async function isEnabled(item: Definition) {
async function isEnabled(item: Formatter.Info) {
const s = state()
let status = s.enabled[item.name]
if (status === undefined) {
@@ -28,7 +29,7 @@ export namespace Format {
async function getFormatter(ext: string) {
const result = []
for (const item of FORMATTERS) {
for (const item of Object.values(Formatter)) {
if (!item.extensions.includes(ext)) continue
if (!isEnabled(item)) continue
result.push(item)
@@ -61,105 +62,4 @@ export namespace Format {
}
})
}
interface Definition {
name: string
command: string[]
environment?: Record<string, string>
extensions: string[]
enabled(): Promise<boolean>
}
const FORMATTERS: Definition[] = [
{
name: "prettier",
command: [BunProc.which(), "run", "prettier", "--write", "$FILE"],
environment: {
BUN_BE_BUN: "1",
},
extensions: [
".js",
".jsx",
".mjs",
".cjs",
".ts",
".tsx",
".mts",
".cts",
".html",
".htm",
".css",
".scss",
".sass",
".less",
".vue",
".svelte",
".json",
".jsonc",
".yaml",
".yml",
".toml",
".xml",
".md",
".mdx",
".graphql",
".gql",
],
async enabled() {
try {
const proc = Bun.spawn({
cmd: [BunProc.which(), "run", "prettier", "--version"],
cwd: App.info().path.cwd,
env: {
BUN_BE_BUN: "1",
},
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
return exit === 0
} catch {
return false
}
},
},
{
name: "mix",
command: ["mix", "format", "$FILE"],
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
async enabled() {
try {
const proc = Bun.spawn({
cmd: ["mix", "--version"],
cwd: App.info().path.cwd,
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
return exit === 0
} catch {
return false
}
},
},
{
name: "gofmt",
command: ["gofmt", "-w", "$FILE"],
extensions: [".go"],
async enabled() {
try {
const proc = Bun.spawn({
cmd: ["gofmt", "-h"],
cwd: App.info().path.cwd,
stdout: "ignore",
stderr: "ignore",
})
const exit = await proc.exited
return exit === 0
} catch {
return false
}
},
},
]
}
+4
View File
@@ -26,6 +26,10 @@ export namespace MCP {
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
} = {}
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
if (mcp.enabled === false) {
log.info("mcp server disabled", { key })
continue
}
log.info("found", { key, type: mcp.type })
if (mcp.type === "remote") {
const client = await experimental_createMCPClient({
@@ -185,6 +185,7 @@ export namespace Provider {
source,
info,
options,
getModel,
}
return
}
@@ -20,6 +20,19 @@ export namespace ProviderTransform {
}
}
}
if (providerID === "amazon-bedrock" || modelID.includes("anthropic")) {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
for (const msg of unique([...system, ...final])) {
msg.providerMetadata = {
...msg.providerMetadata,
bedrock: {
cachePoint: { type: "ephemeral" },
},
}
}
}
return msgs
}
}
+7 -12
View File
@@ -502,15 +502,6 @@ export namespace Session {
}
text = undefined
},
async onFinish(input) {
log.info("message finish", {
reason: input.finishReason,
})
const assistant = next.metadata!.assistant!
const usage = getUsage(model.info, input.usage, input.providerMetadata)
assistant.cost = usage.cost
await updateMessage(next)
},
onError(err) {
log.error("callback error", err)
switch (true) {
@@ -547,7 +538,7 @@ export namespace Session {
// return step
// },
toolCallStreaming: true,
maxTokens: model.info.limit.output || undefined,
maxTokens: Math.max(0, model.info.limit.output) || undefined,
abortSignal: abort.signal,
maxSteps: 1000,
providerOptions: model.info.options,
@@ -681,7 +672,7 @@ export namespace Session {
value.usage,
value.providerMetadata,
)
assistant.cost = usage.cost
assistant.cost += usage.cost
await updateMessage(next)
if (value.finishReason === "length")
throw new Message.OutputLengthError({})
@@ -830,7 +821,7 @@ export namespace Session {
async onFinish(input) {
const assistant = next.metadata!.assistant!
const usage = getUsage(model.info, input.usage, input.providerMetadata)
assistant.cost = usage.cost
assistant.cost += usage.cost
assistant.tokens = usage.tokens
next.metadata!.time.completed = Date.now()
await updateMessage(next)
@@ -882,8 +873,12 @@ export namespace Session {
reasoning: 0,
cache: {
write: (metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
// @ts-expect-error
metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
0) as number,
read: (metadata?.["anthropic"]?.["cacheReadInputTokens"] ??
// @ts-expect-error
metadata?.["bedrock"]?.["usage"]?.["cacheReadInputTokens"] ??
0) as number,
},
}
@@ -100,16 +100,18 @@ func (m statusComponent) View() string {
contextWindow := m.app.Model.Limit.Context
for _, message := range m.app.Messages {
if message.Metadata.Assistant.Cost > 0 {
cost += message.Metadata.Assistant.Cost
usage := message.Metadata.Assistant.Tokens
if usage.Output > 0 {
tokens = (usage.Input +
usage.Cache.Write +
usage.Cache.Read +
usage.Output +
usage.Reasoning)
cost += message.Metadata.Assistant.Cost
usage := message.Metadata.Assistant.Tokens
if usage.Output > 0 {
if message.Metadata.Assistant.Summary {
tokens = usage.Output
continue
}
tokens = (usage.Input +
usage.Cache.Write +
usage.Cache.Read +
usage.Output +
usage.Reasoning)
}
}
@@ -26,14 +26,16 @@ Add a local MCP servers under `mcp.localmcp`.
"localmcp": {
"type": "local",
"command": ["bun", "x", "my-mcp-command"],
"enabled": true,
"environment": {
"MY_ENV_VAR": "my_env_var_value"
}
}
}
}
```
You can also disable a server by setting `enabled` to `false`. This is useful if you want to temporarily disable a server without removing it from your config.
### Remote
Add a remote MCP servers under `mcp.remotemcp`.
@@ -44,7 +46,8 @@ Add a remote MCP servers under `mcp.remotemcp`.
"mcp": {
"remotemcp": {
"type": "remote",
"url": "https://my-mcp-server.com"
"url": "https://my-mcp-server.com",
"enabled": true
}
}
}
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env bun
interface Asset {
name: string
download_count: number
}
interface Release {
tag_name: string
name: string
assets: Asset[]
}
interface NpmDownloadsRange {
start: string
end: string
package: string
downloads: Array<{
downloads: number
day: string
}>
}
async function fetchNpmDownloads(packageName: string): Promise<number> {
try {
// Use a range from 2020 to current year + 5 years to ensure it works forever
const currentYear = new Date().getFullYear()
const endYear = currentYear + 5
const response = await fetch(
`https://api.npmjs.org/downloads/range/2020-01-01:${endYear}-12-31/${packageName}`,
)
if (!response.ok) {
console.warn(
`Failed to fetch npm downloads for ${packageName}: ${response.status}`,
)
return 0
}
const data: NpmDownloadsRange = await response.json()
return data.downloads.reduce((total, day) => total + day.downloads, 0)
} catch (error) {
console.warn(`Error fetching npm downloads for ${packageName}:`, error)
return 0
}
}
async function fetchReleases(): Promise<Release[]> {
const releases: Release[] = []
let page = 1
const per = 100
while (true) {
const url = `https://api.github.com/repos/sst/opencode/releases?page=${page}&per_page=${per}`
const response = await fetch(url)
if (!response.ok) {
throw new Error(
`GitHub API error: ${response.status} ${response.statusText}`,
)
}
const batch: Release[] = await response.json()
if (batch.length === 0) break
releases.push(...batch)
console.log(`Fetched page ${page} with ${batch.length} releases`)
if (batch.length < per) break
page++
}
return releases
}
function calculate(releases: Release[]) {
let total = 0
const stats = []
for (const release of releases) {
let downloads = 0
const assets = []
for (const asset of release.assets) {
downloads += asset.download_count
assets.push({
name: asset.name,
downloads: asset.download_count,
})
}
total += downloads
stats.push({
tag: release.tag_name,
name: release.name,
downloads,
assets,
})
}
return { total, stats }
}
async function save(githubTotal: number, npmDownloads: number) {
const file = "STATS.md"
const date = new Date().toISOString().split("T")[0]
const total = githubTotal + npmDownloads
let previousGithub = 0
let previousNpm = 0
let previousTotal = 0
let content = ""
try {
content = await Bun.file(file).text()
const lines = content.trim().split("\n")
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim()
if (
line.startsWith("|") &&
!line.includes("Date") &&
!line.includes("---")
) {
const match = line.match(
/\|\s*[\d-]+\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|/,
)
if (match) {
previousGithub = parseInt(match[1].replace(/,/g, ""))
previousNpm = parseInt(match[2].replace(/,/g, ""))
previousTotal = parseInt(match[3].replace(/,/g, ""))
break
}
}
}
} catch {
content =
"# Download Stats\n\n| Date | GitHub Downloads | npm Downloads | Total |\n|------|------------------|---------------|-------|\n"
}
const githubChange = githubTotal - previousGithub
const npmChange = npmDownloads - previousNpm
const totalChange = total - previousTotal
const githubChangeStr =
githubChange > 0
? ` (+${githubChange.toLocaleString()})`
: githubChange < 0
? ` (${githubChange.toLocaleString()})`
: " (+0)"
const npmChangeStr =
npmChange > 0
? ` (+${npmChange.toLocaleString()})`
: npmChange < 0
? ` (${npmChange.toLocaleString()})`
: " (+0)"
const totalChangeStr =
totalChange > 0
? ` (+${totalChange.toLocaleString()})`
: totalChange < 0
? ` (${totalChange.toLocaleString()})`
: " (+0)"
const line = `| ${date} | ${githubTotal.toLocaleString()}${githubChangeStr} | ${npmDownloads.toLocaleString()}${npmChangeStr} | ${total.toLocaleString()}${totalChangeStr} |\n`
if (!content.includes("# Download Stats")) {
content =
"# Download Stats\n\n| Date | GitHub Downloads | npm Downloads | Total |\n|------|------------------|---------------|-------|\n"
}
await Bun.write(file, content + line)
await Bun.spawn(["bunx", "prettier", "--write", file]).exited
console.log(
`\nAppended stats to ${file}: GitHub ${githubTotal.toLocaleString()}${githubChangeStr}, npm ${npmDownloads.toLocaleString()}${npmChangeStr}, Total ${total.toLocaleString()}${totalChangeStr}`,
)
}
console.log("Fetching GitHub releases for sst/opencode...\n")
const releases = await fetchReleases()
console.log(`\nFetched ${releases.length} releases total\n`)
const { total: githubTotal, stats } = calculate(releases)
console.log("Fetching npm all-time downloads for opencode-ai...\n")
const npmDownloads = await fetchNpmDownloads("opencode-ai")
console.log(
`Fetched npm all-time downloads: ${npmDownloads.toLocaleString()}\n`,
)
await save(githubTotal, npmDownloads)
const totalDownloads = githubTotal + npmDownloads
console.log("=".repeat(60))
console.log(`TOTAL DOWNLOADS: ${totalDownloads.toLocaleString()}`)
console.log(` GitHub: ${githubTotal.toLocaleString()}`)
console.log(` npm: ${npmDownloads.toLocaleString()}`)
console.log("=".repeat(60))
console.log("\nDownloads by release:")
console.log("-".repeat(60))
stats
.sort((a, b) => b.downloads - a.downloads)
.forEach((release) => {
console.log(
`${release.tag.padEnd(15)} ${release.downloads.toLocaleString().padStart(10)} downloads`,
)
if (release.assets.length > 1) {
release.assets
.sort((a, b) => b.downloads - a.downloads)
.forEach((asset) => {
console.log(
` └─ ${asset.name.padEnd(25)} ${asset.downloads.toLocaleString().padStart(8)}`,
)
})
}
})
console.log("-".repeat(60))
console.log(
`GitHub Total: ${githubTotal.toLocaleString()} downloads across ${releases.length} releases`,
)
console.log(`npm Total: ${npmDownloads.toLocaleString()} downloads`)
console.log(`Combined Total: ${totalDownloads.toLocaleString()} downloads`)
+5 -1
View File
@@ -1 +1,5 @@
{}
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@tsconfig/bun/tsconfig.json",
"compilerOptions": {}
}