Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/opencode/src/cli/cmd/serve.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Server } from "../../server/server"
import { cmd } from "./cmd"
import { Wildcard } from "@/util/wildcard"

export const ServeCommand = cmd({
command: "serve",
Expand All @@ -15,14 +16,21 @@ export const ServeCommand = cmd({
type: "string",
describe: "hostname to listen on",
default: "127.0.0.1",
})
.option("tools", {
type: "string",
describe:
"comma-separated tool patterns to enable/disable (e.g., '-*,read,write,webfetch' to only enable those three)",
}),
describe: "starts a headless opencode server",
handler: async (args) => {
const hostname = args.hostname
const port = args.port
const tools = Wildcard.parseToolsPattern(args.tools)
const server = Server.listen({
port,
hostname,
tools,
})
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
await new Promise(() => {})
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { createColors, createFrames } from "../../ui/spinner.ts"
import { useDialog } from "@tui/ui/dialog"
import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
import { useToast } from "../../ui/toast"
import { useArgs } from "../../context/args"
import { Wildcard } from "@/util/wildcard"

export type PromptProps = {
sessionID?: string
Expand Down Expand Up @@ -58,6 +60,7 @@ export function Prompt(props: PromptProps) {
const sync = useSync()
const dialog = useDialog()
const toast = useToast()
const args = useArgs()
const status = createMemo(() => sync.data.session_status[props.sessionID ?? ""] ?? { type: "idle" })
const history = usePromptHistory()
const command = useCommandDialog()
Expand Down Expand Up @@ -477,12 +480,14 @@ export function Prompt(props: PromptProps) {
messageID,
})
} else {
const tools = Wildcard.parseToolsPattern(args.tools)
sdk.client.session.prompt({
sessionID,
...selectedModel,
messageID,
agent: local.agent.current().name,
model: selectedModel,
...(tools && { tools }),
parts: [
{
id: Identifier.ascending("part"),
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/cli/cmd/tui/context/args.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface Args {
prompt?: string
continue?: boolean
sessionID?: string
tools?: string
}

export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/src/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export const TuiThreadCommand = cmd({
type: "string",
describe: "agent to use",
})
.option("tools", {
type: "string",
describe:
"comma-separated tool patterns to enable/disable (e.g., '-*,read,write,webfetch' to only enable those three)",
})
.option("port", {
type: "number",
describe: "port to listen on",
Expand Down Expand Up @@ -105,6 +110,7 @@ export const TuiThreadCommand = cmd({
agent: args.agent,
model: args.model,
prompt,
tools: args.tools,
},
onExit: async () => {
await client.call("shutdown", undefined)
Expand Down
8 changes: 8 additions & 0 deletions packages/opencode/src/cli/cmd/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { UI } from "../ui"
import { cmd } from "./cmd"
import open from "open"
import { networkInterfaces } from "os"
import { Wildcard } from "@/util/wildcard"

function getNetworkIPs() {
const nets = networkInterfaces()
Expand Down Expand Up @@ -40,14 +41,21 @@ export const WebCommand = cmd({
type: "string",
describe: "hostname to listen on",
default: "127.0.0.1",
})
.option("tools", {
type: "string",
describe:
"comma-separated tool patterns to enable/disable (e.g., '-*,read,write,webfetch' to only enable those three)",
}),
describe: "starts a headless opencode server",
handler: async (args) => {
const hostname = args.hostname
const port = args.port
const tools = Wildcard.parseToolsPattern(args.tools)
const server = Server.listen({
port,
hostname,
tools,
})
UI.empty()
UI.println(UI.logo(" "))
Expand Down
13 changes: 11 additions & 2 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export namespace Server {
Connected: BusEvent.define("server.connected", z.object({})),
}

let defaultTools: Record<string, boolean> | undefined

const app = new Hono()
export const App = lazy(() =>
app
Expand Down Expand Up @@ -1163,7 +1165,11 @@ export namespace Server {
return stream(c, async (stream) => {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
const msg = await SessionPrompt.prompt({ ...body, sessionID })
const msg = await SessionPrompt.prompt({
...body,
sessionID,
tools: { ...defaultTools, ...body.tools },
})
stream.write(JSON.stringify(msg))
})
},
Expand Down Expand Up @@ -2454,7 +2460,10 @@ export namespace Server {
return result
}

export function listen(opts: { port: number; hostname: string }) {
export function listen(opts: { port: number; hostname: string; tools?: Record<string, boolean> }) {
if (opts.tools) {
defaultTools = opts.tools
}
const server = Bun.serve({
port: opts.port,
hostname: opts.hostname,
Expand Down
14 changes: 14 additions & 0 deletions packages/opencode/src/util/wildcard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ export namespace Wildcard {
return result
}

export function parseToolsPattern(pattern: string | undefined): Record<string, boolean> | undefined {
if (!pattern?.trim()) return undefined
const result: Record<string, boolean> = {}
const parts = pattern.split(",").map((x) => x.trim())
for (const part of parts) {
if (part.startsWith("-")) {
result[part.slice(1)] = false
} else {
result[part] = true
}
}
return result
}

function matchSequence(items: string[], patterns: string[]): boolean {
if (patterns.length === 0) return true
const [pattern, ...rest] = patterns
Expand Down
39 changes: 39 additions & 0 deletions packages/opencode/test/util/wildcard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,42 @@ test("allStructured handles sed flags", () => {
expect(Wildcard.allStructured({ head: "sed", tail: ["-n", "1p", "file"] }, rules)).toBe("allow")
expect(Wildcard.allStructured({ head: "sed", tail: ["-i", "-n", "/./p", "myfile.txt"] }, rules)).toBe("ask")
})

test("parseToolsPattern handles empty and undefined", () => {
expect(Wildcard.parseToolsPattern(undefined)).toBeUndefined()
expect(Wildcard.parseToolsPattern("")).toBeUndefined()
expect(Wildcard.parseToolsPattern(" ")).toBeUndefined()
})

test("parseToolsPattern disables all tools", () => {
expect(Wildcard.parseToolsPattern("-*")).toEqual({ "*": false })
})

test("parseToolsPattern enables all tools", () => {
expect(Wildcard.parseToolsPattern("*")).toEqual({ "*": true })
})

test("parseToolsPattern disables all and enables specific tools", () => {
expect(Wildcard.parseToolsPattern("-*,read,write,webfetch")).toEqual({
"*": false,
read: true,
write: true,
webfetch: true,
})
})

test("parseToolsPattern enables only specific tools", () => {
expect(Wildcard.parseToolsPattern("read,write,webfetch")).toEqual({
read: true,
write: true,
webfetch: true,
})
})

test("parseToolsPattern handles whitespace", () => {
expect(Wildcard.parseToolsPattern(" -* , read , write ")).toEqual({
"*": false,
read: true,
write: true,
})
})