mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
refactor: wip
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
default =
|
||||
let
|
||||
kilo-dev = pkgs.writeShellScriptBin "kilo-dev" ''
|
||||
cd "$KILO_ROOT"
|
||||
cd "$KILO_ROOT"
|
||||
exec ${pkgs.bun}/bin/bun dev "$@"
|
||||
'';
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = {
|
||||
br: () => merge(import("@/i18n/br"), import("@opencode-ai/ui/i18n/br")),
|
||||
th: () => merge(import("@/i18n/th"), import("@opencode-ai/ui/i18n/th")),
|
||||
bs: () => merge(import("@/i18n/bs"), import("@opencode-ai/ui/i18n/bs")),
|
||||
nl: () => merge(import("@/i18n/nl"), import("@opencode-ai/ui/i18n/nl")),
|
||||
tr: () => merge(import("@/i18n/tr"), import("@opencode-ai/ui/i18n/tr")),
|
||||
}
|
||||
|
||||
|
||||
@@ -10,20 +10,10 @@ import { ProviderTransform } from "../provider/transform"
|
||||
|
||||
import PROMPT_GENERATE from "./generate.txt"
|
||||
import PROMPT_COMPACTION from "./prompt/compaction.txt"
|
||||
import PROMPT_DEBUG from "./prompt/debug.txt"
|
||||
import PROMPT_EXPLORE from "./prompt/explore.txt"
|
||||
import PROMPT_ASK from "./prompt/ask.txt"
|
||||
import PROMPT_ORCHESTRATOR from "./prompt/orchestrator.txt"
|
||||
import PROMPT_SUMMARY from "./prompt/summary.txt"
|
||||
import PROMPT_TITLE from "./prompt/title.txt"
|
||||
<<<<<<< HEAD
|
||||
|
||||
import { Permission } from "@/permission"
|
||||
import { NamedError } from "@opencode-ai/util/error" // kilocode_change
|
||||
import { Glob } from "../util/glob" // kilocode_change
|
||||
=======
|
||||
import { Permission } from "@/permission"
|
||||
>>>>>>> catrielmuller/opencode-v1.3.1
|
||||
import { mergeDeep, pipe, sortBy, values } from "remeda"
|
||||
import { Global } from "@/global"
|
||||
import path from "path"
|
||||
@@ -32,8 +22,7 @@ import { Skill } from "../skill"
|
||||
import { Effect, ServiceMap, Layer } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRunPromise } from "@/effect/run-service"
|
||||
|
||||
import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change
|
||||
import * as KiloAgent from "@/kilocode/agent" // kilocode_change
|
||||
|
||||
export namespace Agent {
|
||||
export const Info = z
|
||||
@@ -41,10 +30,10 @@ export namespace Agent {
|
||||
name: z.string(),
|
||||
displayName: z.string().optional(), // kilocode_change - human-readable name for org modes
|
||||
description: z.string().optional(),
|
||||
deprecated: z.boolean().optional(), // kilocode_change
|
||||
mode: z.enum(["subagent", "primary", "all"]),
|
||||
native: z.boolean().optional(),
|
||||
hidden: z.boolean().optional(),
|
||||
deprecated: z.boolean().optional(),
|
||||
topP: z.number().optional(),
|
||||
temperature: z.number().optional(),
|
||||
color: z.string().optional(),
|
||||
@@ -79,316 +68,6 @@ export namespace Agent {
|
||||
}>
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
const skillDirs = await Skill.dirs()
|
||||
const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
|
||||
// kilocode_change start — safe bash commands that don't need user approval.
|
||||
// only commands that cannot execute arbitrary code or subprocesses.
|
||||
const bash: Record<string, "allow" | "ask" | "deny"> = {
|
||||
"*": "ask",
|
||||
// read-only / informational
|
||||
"cat *": "allow",
|
||||
"head *": "allow",
|
||||
"tail *": "allow",
|
||||
"less *": "allow",
|
||||
"ls *": "allow",
|
||||
"tree *": "allow",
|
||||
"pwd *": "allow",
|
||||
"echo *": "allow",
|
||||
"wc *": "allow",
|
||||
"which *": "allow",
|
||||
"type *": "allow",
|
||||
"file *": "allow",
|
||||
"diff *": "allow",
|
||||
"du *": "allow",
|
||||
"df *": "allow",
|
||||
"date *": "allow",
|
||||
"uname *": "allow",
|
||||
"whoami *": "allow",
|
||||
"printenv *": "allow",
|
||||
"man *": "allow",
|
||||
// text processing
|
||||
"grep *": "allow",
|
||||
"rg *": "allow",
|
||||
"ag *": "allow",
|
||||
"sort *": "allow",
|
||||
"uniq *": "allow",
|
||||
"cut *": "allow",
|
||||
"tr *": "allow",
|
||||
"jq *": "allow",
|
||||
// file operations
|
||||
"touch *": "allow",
|
||||
"mkdir *": "allow",
|
||||
"cp *": "allow",
|
||||
"mv *": "allow",
|
||||
// compilers (no script execution)
|
||||
"tsc *": "allow",
|
||||
"tsgo *": "allow",
|
||||
// archive
|
||||
"tar *": "allow",
|
||||
"unzip *": "allow",
|
||||
"gzip *": "allow",
|
||||
"gunzip *": "allow",
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start — read-only bash commands for the ask agent.
|
||||
// Unlike the default bash allowlist, unknown commands are DENIED (not "ask")
|
||||
// because the ask agent must never modify the filesystem.
|
||||
const readOnlyBash: Record<string, "allow" | "ask" | "deny"> = {
|
||||
"*": "deny",
|
||||
// read-only / informational
|
||||
"cat *": "allow",
|
||||
"head *": "allow",
|
||||
"tail *": "allow",
|
||||
"less *": "allow",
|
||||
"ls *": "allow",
|
||||
"tree *": "allow",
|
||||
"pwd *": "allow",
|
||||
"echo *": "allow",
|
||||
"wc *": "allow",
|
||||
"which *": "allow",
|
||||
"type *": "allow",
|
||||
"file *": "allow",
|
||||
"diff *": "allow",
|
||||
"du *": "allow",
|
||||
"df *": "allow",
|
||||
"date *": "allow",
|
||||
"uname *": "allow",
|
||||
"whoami *": "allow",
|
||||
"printenv *": "allow",
|
||||
"man *": "allow",
|
||||
// text processing (stdout only, no file modification)
|
||||
"grep *": "allow",
|
||||
"rg *": "allow",
|
||||
"ag *": "allow",
|
||||
"sort *": "allow",
|
||||
"uniq *": "allow",
|
||||
"cut *": "allow",
|
||||
"tr *": "allow",
|
||||
"jq *": "allow",
|
||||
// git — allowlist of read-only subcommands, deny everything else
|
||||
"git *": "deny",
|
||||
"git log *": "allow",
|
||||
"git show *": "allow",
|
||||
"git diff *": "allow",
|
||||
"git status *": "allow",
|
||||
"git blame *": "allow",
|
||||
"git rev-parse *": "allow",
|
||||
"git rev-list *": "allow",
|
||||
"git ls-files *": "allow",
|
||||
"git ls-tree *": "allow",
|
||||
"git ls-remote *": "allow",
|
||||
"git shortlog *": "allow",
|
||||
"git describe *": "allow",
|
||||
"git cat-file *": "allow",
|
||||
"git name-rev *": "allow",
|
||||
"git stash list *": "allow",
|
||||
"git tag -l *": "allow",
|
||||
"git branch --list *": "allow",
|
||||
"git branch -a *": "allow",
|
||||
"git branch -r *": "allow",
|
||||
"git remote -v *": "allow",
|
||||
// gh — require user approval since commands vary widely
|
||||
"gh *": "ask",
|
||||
}
|
||||
|
||||
// kilocode_change start — allow MCP tools in ask agent with user approval.
|
||||
// Generates per-server wildcard rules that override "*": "deny".
|
||||
const mcpRules: Record<string, "allow" | "ask" | "deny"> = {}
|
||||
for (const key of Object.keys(cfg.mcp ?? {})) {
|
||||
const sanitized = key.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
mcpRules[sanitized + "_*"] = "ask"
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
const defaults = Permission.fromConfig({
|
||||
"*": "allow",
|
||||
bash, // kilocode_change
|
||||
doom_loop: "ask",
|
||||
recall: "ask", // kilocode_change
|
||||
external_directory: {
|
||||
"*": "ask",
|
||||
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
|
||||
},
|
||||
question: "deny",
|
||||
plan_enter: "deny",
|
||||
plan_exit: "deny",
|
||||
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
|
||||
read: {
|
||||
"*": "allow",
|
||||
"*.env": "ask",
|
||||
"*.env.*": "ask",
|
||||
"*.env.example": "allow",
|
||||
},
|
||||
})
|
||||
const user = Permission.fromConfig(cfg.permission ?? {})
|
||||
|
||||
const result: Record<string, Info> = {
|
||||
// kilocode_change start
|
||||
code: {
|
||||
name: "code",
|
||||
description: "The default agent. Executes tools based on configured permissions.",
|
||||
// kilocode_change end
|
||||
options: {},
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
question: "allow",
|
||||
plan_enter: "allow",
|
||||
}),
|
||||
user,
|
||||
),
|
||||
mode: "primary",
|
||||
native: true,
|
||||
},
|
||||
plan: {
|
||||
name: "plan",
|
||||
description: "Plan mode. Only allows editing plan files; asks before editing anything else.",
|
||||
options: {},
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
question: "allow",
|
||||
plan_exit: "allow",
|
||||
bash: readOnlyBash, // kilocode_change: read-only bash for plan mode (mirrors ask agent)
|
||||
...mcpRules, // kilocode_change: MCP with user approval for plan mode
|
||||
external_directory: {
|
||||
[path.join(Global.Path.data, "plans", "*")]: "allow",
|
||||
},
|
||||
edit: {
|
||||
"*": "ask", // kilocode_change: ask (not deny) so user can approve edits outside plan files
|
||||
[path.join(".kilo", "plans", "*.md")]: "allow", // kilocode_change
|
||||
[path.join(".opencode", "plans", "*.md")]: "allow", // kilocode_change: .opencode fallback
|
||||
[path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow",
|
||||
},
|
||||
}),
|
||||
user,
|
||||
),
|
||||
mode: "primary",
|
||||
native: true,
|
||||
},
|
||||
// kilocode_change start - add debug, orchestrator, and ask agents
|
||||
debug: {
|
||||
name: "debug",
|
||||
description: "Diagnose and fix software issues with systematic debugging methodology.",
|
||||
prompt: PROMPT_DEBUG,
|
||||
options: {},
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
question: "allow",
|
||||
plan_enter: "allow",
|
||||
}),
|
||||
user,
|
||||
),
|
||||
mode: "primary",
|
||||
native: true,
|
||||
},
|
||||
orchestrator: {
|
||||
name: "orchestrator",
|
||||
description: "Coordinate complex tasks by delegating to specialized agents in parallel.",
|
||||
prompt: PROMPT_ORCHESTRATOR,
|
||||
options: {},
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
"*": "deny",
|
||||
read: "allow",
|
||||
grep: "allow",
|
||||
glob: "allow",
|
||||
list: "allow",
|
||||
// bash: "allow", // kilocode_change - disabled to prevent orchestrator from writing files via shell commands instead of delegating to sub-agents
|
||||
question: "allow",
|
||||
task: "allow",
|
||||
todoread: "allow",
|
||||
todowrite: "allow",
|
||||
webfetch: "allow",
|
||||
websearch: "allow",
|
||||
codesearch: "allow",
|
||||
codebase_search: "allow", // kilocode_change
|
||||
external_directory: {
|
||||
[Truncate.GLOB]: "allow",
|
||||
},
|
||||
}),
|
||||
user,
|
||||
// kilocode_change start - enforce bash deny after user so user config cannot re-enable shell
|
||||
Permission.fromConfig({
|
||||
bash: "deny",
|
||||
}),
|
||||
// kilocode_change end
|
||||
),
|
||||
mode: "primary",
|
||||
native: true,
|
||||
deprecated: true,
|
||||
},
|
||||
ask: {
|
||||
name: "ask",
|
||||
description: "Get answers and explanations without making changes to the codebase.",
|
||||
prompt: PROMPT_ASK,
|
||||
options: {},
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
user, // kilocode_change: user before ask-specific so ask's deny+allowlist wins
|
||||
Permission.fromConfig({
|
||||
"*": "deny",
|
||||
bash: readOnlyBash,
|
||||
read: {
|
||||
"*": "allow",
|
||||
"*.env": "ask",
|
||||
"*.env.*": "ask",
|
||||
"*.env.example": "allow",
|
||||
},
|
||||
grep: "allow",
|
||||
glob: "allow",
|
||||
list: "allow",
|
||||
question: "allow",
|
||||
webfetch: "allow",
|
||||
websearch: "allow",
|
||||
codesearch: "allow",
|
||||
codebase_search: "allow", // kilocode_change
|
||||
external_directory: {
|
||||
[Truncate.GLOB]: "allow",
|
||||
},
|
||||
...mcpRules,
|
||||
}),
|
||||
user.filter((r) => r.action === "deny"), // kilocode_change: re-apply user denies so explicit MCP blocks win over mcpRules
|
||||
),
|
||||
mode: "primary",
|
||||
native: true,
|
||||
},
|
||||
// kilocode_change end
|
||||
general: {
|
||||
name: "general",
|
||||
description: `General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel.`,
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
todoread: "deny",
|
||||
todowrite: "deny",
|
||||
}),
|
||||
user,
|
||||
),
|
||||
options: {},
|
||||
mode: "subagent",
|
||||
native: true,
|
||||
},
|
||||
explore: {
|
||||
name: "explore",
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
"*": "deny",
|
||||
grep: "allow",
|
||||
glob: "allow",
|
||||
list: "allow",
|
||||
bash: "allow",
|
||||
webfetch: "allow",
|
||||
websearch: "allow",
|
||||
codesearch: "allow",
|
||||
codebase_search: "allow", // kilocode_change
|
||||
read: "allow",
|
||||
=======
|
||||
type State = Omit<Interface, "generate">
|
||||
|
||||
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Agent") {}
|
||||
@@ -405,114 +84,14 @@ export namespace Agent {
|
||||
const skillDirs = yield* Effect.promise(() => Skill.dirs())
|
||||
const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))]
|
||||
|
||||
const defaults = Permission.fromConfig({
|
||||
const baseDefaults = Permission.fromConfig({
|
||||
// kilocode_change: renamed from defaults
|
||||
"*": "allow",
|
||||
doom_loop: "ask",
|
||||
>>>>>>> catrielmuller/opencode-v1.3.1
|
||||
external_directory: {
|
||||
"*": "ask",
|
||||
...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
}),
|
||||
user,
|
||||
),
|
||||
description: `Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.`,
|
||||
// kilocode_change - only advertise codebase_search when the experimental flag is on
|
||||
prompt: cfg.experimental?.codebase_search
|
||||
? `Prefer using the codebase_search tool for codebase searches — it performs intelligent multi-step code search and returns the most relevant code spans.\n\n${PROMPT_EXPLORE}`
|
||||
: PROMPT_EXPLORE,
|
||||
options: {},
|
||||
mode: "subagent",
|
||||
native: true,
|
||||
},
|
||||
compaction: {
|
||||
name: "compaction",
|
||||
mode: "primary",
|
||||
native: true,
|
||||
hidden: true,
|
||||
prompt: PROMPT_COMPACTION,
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
"*": "deny",
|
||||
}),
|
||||
user,
|
||||
),
|
||||
options: {},
|
||||
},
|
||||
title: {
|
||||
name: "title",
|
||||
mode: "primary",
|
||||
options: {},
|
||||
native: true,
|
||||
hidden: true,
|
||||
temperature: 0.5,
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
"*": "deny",
|
||||
}),
|
||||
user,
|
||||
),
|
||||
prompt: PROMPT_TITLE,
|
||||
},
|
||||
summary: {
|
||||
name: "summary",
|
||||
mode: "primary",
|
||||
options: {},
|
||||
native: true,
|
||||
hidden: true,
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
"*": "deny",
|
||||
}),
|
||||
user,
|
||||
),
|
||||
prompt: PROMPT_SUMMARY,
|
||||
},
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
|
||||
// kilocode_change start
|
||||
// Treat "build" config as "code" for backward compatibility
|
||||
const effectiveKey = key === "build" ? "code" : key
|
||||
if (value.disable) {
|
||||
delete result[effectiveKey]
|
||||
continue
|
||||
}
|
||||
let item = result[effectiveKey]
|
||||
if (!item)
|
||||
item = result[effectiveKey] = {
|
||||
name: effectiveKey,
|
||||
mode: "all",
|
||||
permission: Permission.merge(defaults, user),
|
||||
options: {},
|
||||
native: false,
|
||||
}
|
||||
// kilocode_change end
|
||||
if (value.model) item.model = Provider.parseModel(value.model)
|
||||
item.variant = value.variant ?? item.variant
|
||||
item.prompt = value.prompt ?? item.prompt
|
||||
item.description = value.description ?? item.description
|
||||
item.temperature = value.temperature ?? item.temperature
|
||||
item.topP = value.top_p ?? item.topP
|
||||
item.mode = value.mode ?? item.mode
|
||||
item.color = value.color ?? item.color
|
||||
item.hidden = value.hidden ?? item.hidden
|
||||
item.deprecated = value.deprecated ?? item.deprecated
|
||||
item.name = value.name ?? item.name
|
||||
item.steps = value.steps ?? item.steps
|
||||
item.options = mergeDeep(item.options, value.options ?? {})
|
||||
// kilocode_change start - populate displayName from org mode options
|
||||
if (item.options?.displayName && typeof item.options.displayName === "string") {
|
||||
item.displayName = item.options.displayName
|
||||
}
|
||||
// kilocode_change end
|
||||
item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {}))
|
||||
}
|
||||
=======
|
||||
question: "deny",
|
||||
plan_enter: "deny",
|
||||
plan_exit: "deny",
|
||||
@@ -525,8 +104,12 @@ export namespace Agent {
|
||||
},
|
||||
})
|
||||
|
||||
// kilocode_change start - patch defaults with bash allowlist and recall permission
|
||||
const kilo = KiloAgent.prepare(cfg)
|
||||
const defaults = Permission.merge(baseDefaults, kilo.defaultsPatch)
|
||||
// kilocode_change end
|
||||
|
||||
const user = Permission.fromConfig(cfg.permission ?? {})
|
||||
>>>>>>> catrielmuller/opencode-v1.3.1
|
||||
|
||||
const agents: Record<string, Info> = {
|
||||
build: {
|
||||
@@ -658,12 +241,14 @@ export namespace Agent {
|
||||
},
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
result[name].permission = Permission.merge(
|
||||
result[name].permission,
|
||||
Permission.fromConfig({ external_directory: { [Truncate.GLOB]: "allow" } }),
|
||||
=======
|
||||
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
|
||||
// kilocode_change start - rename build→code, add debug/orchestrator/ask, patch plan/explore
|
||||
KiloAgent.patchAgents(agents, defaults, user, cfg, kilo)
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start - preprocess config to remap "build" key → "code"
|
||||
const agentConfigs = KiloAgent.preprocessConfig(cfg.agent ?? {})
|
||||
for (const [key, value] of Object.entries(agentConfigs)) {
|
||||
// kilocode_change end
|
||||
if (value.disable) {
|
||||
delete agents[key]
|
||||
continue
|
||||
@@ -690,6 +275,7 @@ export namespace Agent {
|
||||
item.steps = value.steps ?? item.steps
|
||||
item.options = mergeDeep(item.options, value.options ?? {})
|
||||
item.permission = Permission.merge(item.permission, Permission.fromConfig(value.permission ?? {}))
|
||||
KiloAgent.processConfigItem(item) // kilocode_change - populate displayName from options
|
||||
}
|
||||
|
||||
// Ensure Truncate.GLOB is allowed unless explicitly configured
|
||||
@@ -709,7 +295,7 @@ export namespace Agent {
|
||||
}
|
||||
|
||||
const get = Effect.fnUntraced(function* (agent: string) {
|
||||
return agents[agent]
|
||||
return agents[KiloAgent.resolveKey(agent)] // kilocode_change - treat "build" as "code"
|
||||
})
|
||||
|
||||
const list = Effect.fnUntraced(function* () {
|
||||
@@ -718,7 +304,7 @@ export namespace Agent {
|
||||
agents,
|
||||
values(),
|
||||
sortBy(
|
||||
[(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"],
|
||||
[(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "code"), "desc"], // kilocode_change - renamed from "build" to "code"
|
||||
[(x) => x.name, "asc"],
|
||||
),
|
||||
)
|
||||
@@ -727,7 +313,8 @@ export namespace Agent {
|
||||
const defaultAgent = Effect.fnUntraced(function* () {
|
||||
const c = yield* config()
|
||||
if (c.default_agent) {
|
||||
const agent = agents[c.default_agent]
|
||||
const effective = KiloAgent.resolveKey(c.default_agent) // kilocode_change - treat "build" as "code"
|
||||
const agent = agents[effective] // kilocode_change
|
||||
if (!agent) throw new Error(`default agent "${c.default_agent}" not found`)
|
||||
if (agent.mode === "subagent") throw new Error(`default agent "${c.default_agent}" is a subagent`)
|
||||
if (agent.hidden === true) throw new Error(`default agent "${c.default_agent}" is hidden`)
|
||||
@@ -744,7 +331,6 @@ export namespace Agent {
|
||||
defaultAgent,
|
||||
} satisfies State
|
||||
}),
|
||||
>>>>>>> catrielmuller/opencode-v1.3.1
|
||||
)
|
||||
|
||||
return Service.of({
|
||||
@@ -773,12 +359,9 @@ export namespace Agent {
|
||||
const existing = yield* InstanceState.useEffect(state, (s) => s.list())
|
||||
|
||||
const params = {
|
||||
experimental_telemetry: {
|
||||
isEnabled: cfg.experimental?.openTelemetry,
|
||||
metadata: {
|
||||
userId: cfg.username ?? "unknown",
|
||||
},
|
||||
},
|
||||
// kilocode_change start - enable telemetry with custom PostHog tracer
|
||||
experimental_telemetry: KiloAgent.telemetryOptions(cfg),
|
||||
// kilocode_change end
|
||||
temperature: 0.3,
|
||||
messages: [
|
||||
...system.map(
|
||||
@@ -829,106 +412,6 @@ export namespace Agent {
|
||||
const runPromise = makeRunPromise(Service, defaultLayer)
|
||||
|
||||
export async function get(agent: string) {
|
||||
<<<<<<< HEAD
|
||||
// kilocode_change start - Treat "build" as "code" for backward compatibility
|
||||
const effectiveAgent = agent === "build" ? "code" : agent
|
||||
return state().then((x) => x[effectiveAgent])
|
||||
// kilocode_change end
|
||||
}
|
||||
|
||||
export async function list() {
|
||||
const cfg = await Config.get()
|
||||
return pipe(
|
||||
await state(),
|
||||
values(),
|
||||
sortBy(
|
||||
[(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "code"), "desc"], // kilocode_change - renamed from "build" to "code"
|
||||
[(x) => x.name, "asc"],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export async function defaultAgent() {
|
||||
const cfg = await Config.get()
|
||||
const agents = await state()
|
||||
|
||||
if (cfg.default_agent) {
|
||||
// kilocode_change start - Treat "build" as "code" for backward compatibility
|
||||
const effectiveDefault = cfg.default_agent === "build" ? "code" : cfg.default_agent
|
||||
const agent = agents[effectiveDefault]
|
||||
if (!agent) throw new Error(`default agent "${cfg.default_agent}" not found`)
|
||||
// kilocode_change end
|
||||
if (agent.mode === "subagent") throw new Error(`default agent "${cfg.default_agent}" is a subagent`)
|
||||
if (agent.hidden === true) throw new Error(`default agent "${cfg.default_agent}" is hidden`)
|
||||
return agent.name
|
||||
}
|
||||
|
||||
const primaryVisible = Object.values(agents).find((a) => a.mode !== "subagent" && a.hidden !== true)
|
||||
if (!primaryVisible) throw new Error("no primary visible agent found")
|
||||
return primaryVisible.name
|
||||
}
|
||||
|
||||
export async function generate(input: { description: string; model?: { providerID: ProviderID; modelID: ModelID } }) {
|
||||
const cfg = await Config.get()
|
||||
const defaultModel = input.model ?? (await Provider.defaultModel())
|
||||
const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID)
|
||||
const language = await Provider.getLanguage(model)
|
||||
|
||||
const system = [PROMPT_GENERATE]
|
||||
await Plugin.trigger("experimental.chat.system.transform", { model }, { system })
|
||||
const existing = await list()
|
||||
|
||||
const params = {
|
||||
// kilocode_change start - enable telemetry by default with custom PostHog tracer
|
||||
experimental_telemetry: {
|
||||
isEnabled: cfg.experimental?.openTelemetry !== false,
|
||||
recordInputs: false, // Prevent recording prompts, messages, tool args
|
||||
recordOutputs: false, // Prevent recording completions, tool results
|
||||
tracer: Telemetry.getTracer() ?? undefined,
|
||||
metadata: {
|
||||
userId: cfg.username ?? "unknown",
|
||||
},
|
||||
},
|
||||
// kilocode_change end
|
||||
temperature: 0.3,
|
||||
messages: [
|
||||
...system.map(
|
||||
(item): ModelMessage => ({
|
||||
role: "system",
|
||||
content: item,
|
||||
}),
|
||||
),
|
||||
{
|
||||
role: "user",
|
||||
content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`,
|
||||
},
|
||||
],
|
||||
model: language,
|
||||
schema: z.object({
|
||||
identifier: z.string(),
|
||||
whenToUse: z.string(),
|
||||
systemPrompt: z.string(),
|
||||
}),
|
||||
} satisfies Parameters<typeof generateObject>[0]
|
||||
|
||||
// TODO: clean this up so provider specific logic doesnt bleed over
|
||||
if (defaultModel.providerID === "openai" && (await Auth.get(defaultModel.providerID))?.type === "oauth") {
|
||||
const result = streamObject({
|
||||
...params,
|
||||
providerOptions: ProviderTransform.providerOptions(model, {
|
||||
store: false,
|
||||
}),
|
||||
onError: () => {},
|
||||
})
|
||||
for await (const part of result.fullStream) {
|
||||
if (part.type === "error") throw part.error
|
||||
}
|
||||
return result.object
|
||||
}
|
||||
|
||||
const result = await generateObject(params)
|
||||
return result.object
|
||||
=======
|
||||
return runPromise((svc) => svc.get(agent))
|
||||
}
|
||||
|
||||
@@ -942,84 +425,12 @@ export namespace Agent {
|
||||
|
||||
export async function generate(input: { description: string; model?: { providerID: ProviderID; modelID: ModelID } }) {
|
||||
return runPromise((svc) => svc.generate(input))
|
||||
>>>>>>> catrielmuller/opencode-v1.3.1
|
||||
}
|
||||
|
||||
// kilocode_change start
|
||||
export const RemoveError = NamedError.create(
|
||||
"AgentRemoveError",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
/**
|
||||
* Remove a custom agent by deleting its markdown source file and/or
|
||||
* removing it from legacy .kilocodemodes YAML files.
|
||||
* Scans all config directories for agent/mode .md files matching the name,
|
||||
* then also checks the .kilocodemodes files the ModesMigrator reads.
|
||||
*/
|
||||
// kilocode_change start - agent removal (delegated to kilocode module)
|
||||
export const RemoveError = KiloAgent.RemoveError
|
||||
export async function remove(name: string) {
|
||||
const agents = await state()
|
||||
const agent = agents[name]
|
||||
if (!agent) throw new RemoveError({ name, message: "agent not found" })
|
||||
if (agent.native) throw new RemoveError({ name, message: "cannot remove native agent" })
|
||||
// kilocode_change start - prevent removal of organization-managed agents
|
||||
if (agent.options?.source === "organization")
|
||||
throw new RemoveError({ name, message: "cannot remove organization agent — manage it from the cloud dashboard" })
|
||||
// kilocode_change end
|
||||
|
||||
const { unlink, readFile, writeFile } = await import("fs/promises")
|
||||
let found = false
|
||||
|
||||
// 1. Delete .md files from config directories
|
||||
const dirs = await Config.directories()
|
||||
const patterns = ["{agent,agents}/**/" + name + ".md", "{mode,modes}/" + name + ".md"]
|
||||
for (const dir of dirs) {
|
||||
for (const pattern of patterns) {
|
||||
const matches = await Glob.scan(pattern, { cwd: dir, absolute: true, dot: true })
|
||||
for (const file of matches) {
|
||||
if (await Bun.file(file).exists()) {
|
||||
await unlink(file)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove from legacy .kilocodemodes YAML files (read by ModesMigrator)
|
||||
const { ModesMigrator } = await import("@/kilocode/modes-migrator")
|
||||
const { KilocodePaths } = await import("@/kilocode/paths")
|
||||
const os = await import("os")
|
||||
const matter = (await import("gray-matter")).default
|
||||
const home = os.default.homedir()
|
||||
const modesFiles = [
|
||||
path.join(KilocodePaths.vscodeGlobalStorage(), "settings", "custom_modes.yaml"),
|
||||
path.join(home, ".kilocode", "cli", "global", "settings", "custom_modes.yaml"),
|
||||
path.join(home, ".kilocodemodes"),
|
||||
path.join(Instance.directory, ".kilocodemodes"),
|
||||
]
|
||||
|
||||
for (const file of modesFiles) {
|
||||
const modes = await ModesMigrator.readModesFile(file)
|
||||
if (!modes.length) continue
|
||||
|
||||
const filtered = modes.filter((m) => m.slug !== name)
|
||||
if (filtered.length === modes.length) continue
|
||||
|
||||
// Rewrite the file without the removed mode
|
||||
const yaml = matter
|
||||
.stringify("", { customModes: filtered })
|
||||
.replace(/^---\n/, "")
|
||||
.replace(/\n---\n?$/, "")
|
||||
await writeFile(file, yaml)
|
||||
found = true
|
||||
}
|
||||
|
||||
if (!found) throw new RemoveError({ name, message: "no agent file found on disk" })
|
||||
|
||||
await Instance.dispose()
|
||||
return KiloAgent.remove(name)
|
||||
}
|
||||
// kilocode_change end
|
||||
}
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
// kilocode_change - new file
|
||||
import { Permission } from "@/permission"
|
||||
import { NamedError } from "@opencode-ai/util/error"
|
||||
import { Glob } from "../../util/glob"
|
||||
import { Truncate } from "../../tool/truncate"
|
||||
import { Config } from "../../config/config"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { Global } from "@/global"
|
||||
import { Telemetry } from "@kilocode/kilo-telemetry"
|
||||
import z from "zod"
|
||||
import path from "path"
|
||||
|
||||
import PROMPT_DEBUG from "../../agent/prompt/debug.txt"
|
||||
import PROMPT_ORCHESTRATOR from "../../agent/prompt/orchestrator.txt"
|
||||
import PROMPT_ASK from "../../agent/prompt/ask.txt"
|
||||
import PROMPT_EXPLORE from "../../agent/prompt/explore.txt"
|
||||
|
||||
// Safe bash commands that don't need user approval.
|
||||
// Only commands that cannot execute arbitrary code or subprocesses.
|
||||
export const bash: Record<string, "allow" | "ask" | "deny"> = {
|
||||
"*": "ask",
|
||||
// read-only / informational
|
||||
"cat *": "allow",
|
||||
"head *": "allow",
|
||||
"tail *": "allow",
|
||||
"less *": "allow",
|
||||
"ls *": "allow",
|
||||
"tree *": "allow",
|
||||
"pwd *": "allow",
|
||||
"echo *": "allow",
|
||||
"wc *": "allow",
|
||||
"which *": "allow",
|
||||
"type *": "allow",
|
||||
"file *": "allow",
|
||||
"diff *": "allow",
|
||||
"du *": "allow",
|
||||
"df *": "allow",
|
||||
"date *": "allow",
|
||||
"uname *": "allow",
|
||||
"whoami *": "allow",
|
||||
"printenv *": "allow",
|
||||
"man *": "allow",
|
||||
// text processing
|
||||
"grep *": "allow",
|
||||
"rg *": "allow",
|
||||
"ag *": "allow",
|
||||
"sort *": "allow",
|
||||
"uniq *": "allow",
|
||||
"cut *": "allow",
|
||||
"tr *": "allow",
|
||||
"jq *": "allow",
|
||||
// file operations
|
||||
"touch *": "allow",
|
||||
"mkdir *": "allow",
|
||||
"cp *": "allow",
|
||||
"mv *": "allow",
|
||||
// compilers (no script execution)
|
||||
"tsc *": "allow",
|
||||
"tsgo *": "allow",
|
||||
// archive
|
||||
"tar *": "allow",
|
||||
"unzip *": "allow",
|
||||
"gzip *": "allow",
|
||||
"gunzip *": "allow",
|
||||
}
|
||||
|
||||
// Read-only bash commands for ask/plan agents.
|
||||
// Unknown commands are DENIED (not "ask") because these agents must never modify the filesystem.
|
||||
export const readOnlyBash: Record<string, "allow" | "ask" | "deny"> = {
|
||||
"*": "deny",
|
||||
// read-only / informational
|
||||
"cat *": "allow",
|
||||
"head *": "allow",
|
||||
"tail *": "allow",
|
||||
"less *": "allow",
|
||||
"ls *": "allow",
|
||||
"tree *": "allow",
|
||||
"pwd *": "allow",
|
||||
"echo *": "allow",
|
||||
"wc *": "allow",
|
||||
"which *": "allow",
|
||||
"type *": "allow",
|
||||
"file *": "allow",
|
||||
"diff *": "allow",
|
||||
"du *": "allow",
|
||||
"df *": "allow",
|
||||
"date *": "allow",
|
||||
"uname *": "allow",
|
||||
"whoami *": "allow",
|
||||
"printenv *": "allow",
|
||||
"man *": "allow",
|
||||
// text processing (stdout only, no file modification)
|
||||
"grep *": "allow",
|
||||
"rg *": "allow",
|
||||
"ag *": "allow",
|
||||
"sort *": "allow",
|
||||
"uniq *": "allow",
|
||||
"cut *": "allow",
|
||||
"tr *": "allow",
|
||||
"jq *": "allow",
|
||||
// git — allowlist of read-only subcommands, deny everything else
|
||||
"git *": "deny",
|
||||
"git log *": "allow",
|
||||
"git show *": "allow",
|
||||
"git diff *": "allow",
|
||||
"git status *": "allow",
|
||||
"git blame *": "allow",
|
||||
"git rev-parse *": "allow",
|
||||
"git rev-list *": "allow",
|
||||
"git ls-files *": "allow",
|
||||
"git ls-tree *": "allow",
|
||||
"git ls-remote *": "allow",
|
||||
"git shortlog *": "allow",
|
||||
"git describe *": "allow",
|
||||
"git cat-file *": "allow",
|
||||
"git name-rev *": "allow",
|
||||
"git stash list *": "allow",
|
||||
"git tag -l *": "allow",
|
||||
"git branch --list *": "allow",
|
||||
"git branch -a *": "allow",
|
||||
"git branch -r *": "allow",
|
||||
"git remote -v *": "allow",
|
||||
// gh — require user approval since commands vary widely
|
||||
"gh *": "ask",
|
||||
}
|
||||
|
||||
// Generate per-server MCP wildcard rules that allow MCP tools with user approval.
|
||||
export function getMcpRules(cfg: Config.Info): Record<string, "allow" | "ask" | "deny"> {
|
||||
const rules: Record<string, "allow" | "ask" | "deny"> = {}
|
||||
for (const key of Object.keys(cfg.mcp ?? {})) {
|
||||
const sanitized = key.replace(/[^a-zA-Z0-9_-]/g, "_")
|
||||
rules[sanitized + "_*"] = "ask"
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
export interface KiloData {
|
||||
mcpRules: Record<string, "allow" | "ask" | "deny">
|
||||
defaultsPatch: Permission.Ruleset
|
||||
}
|
||||
|
||||
// Prepare kilo-specific data derived from config. Call once per state initialization.
|
||||
export function prepare(cfg: Config.Info): KiloData {
|
||||
const mcpRules = getMcpRules(cfg)
|
||||
const defaultsPatch = Permission.fromConfig({ bash, recall: "ask" })
|
||||
return { mcpRules, defaultsPatch }
|
||||
}
|
||||
|
||||
// Map "build" config key to "code" for backward compatibility.
|
||||
export function resolveKey(name: string): string {
|
||||
return name === "build" ? "code" : name
|
||||
}
|
||||
|
||||
// Remap "build" → "code" in agent config entries for backward compat in the config loop.
|
||||
export function preprocessConfig<T>(agentConfig: Record<string, T>): Record<string, T> {
|
||||
const result: Record<string, T> = {}
|
||||
for (const [key, value] of Object.entries(agentConfig)) {
|
||||
result[key === "build" ? "code" : key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Set displayName and deprecated from options after config item is processed.
|
||||
export function processConfigItem(item: {
|
||||
options: Record<string, unknown>
|
||||
displayName?: string
|
||||
deprecated?: boolean
|
||||
}) {
|
||||
if (item.options?.displayName && typeof item.options.displayName === "string") {
|
||||
item.displayName = item.options.displayName
|
||||
}
|
||||
}
|
||||
|
||||
// Returns experimental_telemetry config for generate calls.
|
||||
export function telemetryOptions(cfg: Config.Info) {
|
||||
return {
|
||||
isEnabled: cfg.experimental?.openTelemetry !== false,
|
||||
recordInputs: false,
|
||||
recordOutputs: false,
|
||||
tracer: Telemetry.getTracer() ?? undefined,
|
||||
metadata: {
|
||||
userId: cfg.username ?? "unknown",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Patch the base agents map in-place with all kilo-specific changes:
|
||||
// - Rename build → code
|
||||
// - Patch plan with readOnlyBash, mcpRules, .kilo paths
|
||||
// - Patch explore with codebase_search and conditional prompt
|
||||
// - Add debug, orchestrator, ask agents
|
||||
export function patchAgents(
|
||||
agents: Record<
|
||||
string,
|
||||
{
|
||||
name: string
|
||||
displayName?: string
|
||||
description?: string
|
||||
deprecated?: boolean
|
||||
mode: "subagent" | "primary" | "all"
|
||||
native?: boolean
|
||||
hidden?: boolean
|
||||
topP?: number
|
||||
temperature?: number
|
||||
color?: string
|
||||
permission: Permission.Ruleset
|
||||
model?: { modelID: string; providerID: string }
|
||||
variant?: string
|
||||
prompt?: string
|
||||
options: Record<string, unknown>
|
||||
steps?: number
|
||||
}
|
||||
>,
|
||||
defaults: Permission.Ruleset,
|
||||
user: Permission.Ruleset,
|
||||
cfg: Config.Info,
|
||||
kilo: KiloData,
|
||||
) {
|
||||
// Rename "build" → "code" for backward compatibility
|
||||
if (agents.build) {
|
||||
agents.code = { ...agents.build, name: "code" }
|
||||
delete agents.build
|
||||
}
|
||||
|
||||
// Patch plan mode
|
||||
if (agents.plan) {
|
||||
agents.plan = {
|
||||
...agents.plan,
|
||||
description: "Plan mode. Only allows editing plan files; asks before editing anything else.",
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
question: "allow",
|
||||
plan_exit: "allow",
|
||||
bash: readOnlyBash,
|
||||
...kilo.mcpRules,
|
||||
external_directory: {
|
||||
[path.join(Global.Path.data, "plans", "*")]: "allow",
|
||||
},
|
||||
edit: {
|
||||
"*": "ask",
|
||||
[path.join(".kilo", "plans", "*.md")]: "allow",
|
||||
[path.join(".opencode", "plans", "*.md")]: "allow",
|
||||
[path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow",
|
||||
},
|
||||
}),
|
||||
user,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Patch explore with codebase_search and conditional prompt
|
||||
if (agents.explore) {
|
||||
agents.explore = {
|
||||
...agents.explore,
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
"*": "deny",
|
||||
grep: "allow",
|
||||
glob: "allow",
|
||||
list: "allow",
|
||||
bash: "allow",
|
||||
webfetch: "allow",
|
||||
websearch: "allow",
|
||||
codesearch: "allow",
|
||||
codebase_search: "allow",
|
||||
read: "allow",
|
||||
external_directory: {
|
||||
"*": "ask",
|
||||
[Truncate.GLOB]: "allow",
|
||||
},
|
||||
}),
|
||||
user,
|
||||
),
|
||||
prompt: cfg.experimental?.codebase_search
|
||||
? `Prefer using the codebase_search tool for codebase searches — it performs intelligent multi-step code search and returns the most relevant code spans.\n\n${PROMPT_EXPLORE}`
|
||||
: PROMPT_EXPLORE,
|
||||
}
|
||||
}
|
||||
|
||||
// Add debug agent
|
||||
agents.debug = {
|
||||
name: "debug",
|
||||
description: "Diagnose and fix software issues with systematic debugging methodology.",
|
||||
prompt: PROMPT_DEBUG,
|
||||
options: {},
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
question: "allow",
|
||||
plan_enter: "allow",
|
||||
}),
|
||||
user,
|
||||
),
|
||||
mode: "primary",
|
||||
native: true,
|
||||
}
|
||||
|
||||
// Add orchestrator agent
|
||||
agents.orchestrator = {
|
||||
name: "orchestrator",
|
||||
description: "Coordinate complex tasks by delegating to specialized agents in parallel.",
|
||||
prompt: PROMPT_ORCHESTRATOR,
|
||||
options: {},
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
Permission.fromConfig({
|
||||
"*": "deny",
|
||||
read: "allow",
|
||||
grep: "allow",
|
||||
glob: "allow",
|
||||
list: "allow",
|
||||
question: "allow",
|
||||
task: "allow",
|
||||
todoread: "allow",
|
||||
todowrite: "allow",
|
||||
webfetch: "allow",
|
||||
websearch: "allow",
|
||||
codesearch: "allow",
|
||||
codebase_search: "allow",
|
||||
external_directory: {
|
||||
[Truncate.GLOB]: "allow",
|
||||
},
|
||||
}),
|
||||
user,
|
||||
// Enforce bash deny after user so user config cannot re-enable shell
|
||||
Permission.fromConfig({
|
||||
bash: "deny",
|
||||
}),
|
||||
),
|
||||
mode: "primary",
|
||||
native: true,
|
||||
deprecated: true,
|
||||
}
|
||||
|
||||
// Add ask agent
|
||||
agents.ask = {
|
||||
name: "ask",
|
||||
description: "Get answers and explanations without making changes to the codebase.",
|
||||
prompt: PROMPT_ASK,
|
||||
options: {},
|
||||
permission: Permission.merge(
|
||||
defaults,
|
||||
user, // user before ask-specific so ask's deny+allowlist wins
|
||||
Permission.fromConfig({
|
||||
"*": "deny",
|
||||
bash: readOnlyBash,
|
||||
read: {
|
||||
"*": "allow",
|
||||
"*.env": "ask",
|
||||
"*.env.*": "ask",
|
||||
"*.env.example": "allow",
|
||||
},
|
||||
grep: "allow",
|
||||
glob: "allow",
|
||||
list: "allow",
|
||||
question: "allow",
|
||||
webfetch: "allow",
|
||||
websearch: "allow",
|
||||
codesearch: "allow",
|
||||
codebase_search: "allow",
|
||||
external_directory: {
|
||||
[Truncate.GLOB]: "allow",
|
||||
},
|
||||
...kilo.mcpRules,
|
||||
}),
|
||||
user.filter((r: Permission.Rule) => r.action === "deny"), // re-apply user denies so explicit MCP blocks win over mcpRules
|
||||
),
|
||||
mode: "primary",
|
||||
native: true,
|
||||
}
|
||||
}
|
||||
|
||||
export const RemoveError = NamedError.create(
|
||||
"AgentRemoveError",
|
||||
z.object({
|
||||
name: z.string(),
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
/**
|
||||
* Remove a custom agent by deleting its markdown source file and/or
|
||||
* removing it from legacy .kilocodemodes YAML files.
|
||||
* Scans all config directories for agent/mode .md files matching the name,
|
||||
* then also checks the .kilocodemodes files the ModesMigrator reads.
|
||||
*/
|
||||
export async function remove(name: string) {
|
||||
const { Agent } = await import("../../agent/agent")
|
||||
const agent = await Agent.get(name)
|
||||
if (!agent) throw new RemoveError({ name, message: "agent not found" })
|
||||
if (agent.native) throw new RemoveError({ name, message: "cannot remove native agent" })
|
||||
// Prevent removal of organization-managed agents
|
||||
if (agent.options?.source === "organization")
|
||||
throw new RemoveError({ name, message: "cannot remove organization agent — manage it from the cloud dashboard" })
|
||||
|
||||
const { unlink, writeFile } = await import("fs/promises")
|
||||
let found = false
|
||||
|
||||
// 1. Delete .md files from config directories
|
||||
const { Config } = await import("../../config/config")
|
||||
const dirs = await Config.directories()
|
||||
const patterns = ["{agent,agents}/**/" + name + ".md", "{mode,modes}/" + name + ".md"]
|
||||
for (const dir of dirs) {
|
||||
for (const pattern of patterns) {
|
||||
const matches = await Glob.scan(pattern, { cwd: dir, absolute: true, dot: true })
|
||||
for (const file of matches) {
|
||||
if (await Bun.file(file).exists()) {
|
||||
await unlink(file)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove from legacy .kilocodemodes YAML files (read by ModesMigrator)
|
||||
const { ModesMigrator } = await import("@/kilocode/modes-migrator")
|
||||
const { KilocodePaths } = await import("@/kilocode/paths")
|
||||
const os = await import("os")
|
||||
const matter = (await import("gray-matter")).default
|
||||
const home = os.default.homedir()
|
||||
const modesFiles = [
|
||||
path.join(KilocodePaths.vscodeGlobalStorage(), "settings", "custom_modes.yaml"),
|
||||
path.join(home, ".kilocode", "cli", "global", "settings", "custom_modes.yaml"),
|
||||
path.join(home, ".kilocodemodes"),
|
||||
path.join(Instance.directory, ".kilocodemodes"),
|
||||
]
|
||||
|
||||
for (const file of modesFiles) {
|
||||
const modes = await ModesMigrator.readModesFile(file)
|
||||
if (!modes.length) continue
|
||||
|
||||
const filtered = modes.filter((m: { slug: string }) => m.slug !== name)
|
||||
if (filtered.length === modes.length) continue
|
||||
|
||||
// Rewrite the file without the removed mode
|
||||
const yaml = matter
|
||||
.stringify("", { customModes: filtered })
|
||||
.replace(/^---\n/, "")
|
||||
.replace(/\n---\n?$/, "")
|
||||
await writeFile(file, yaml)
|
||||
found = true
|
||||
}
|
||||
|
||||
if (!found) throw new RemoveError({ name, message: "no agent file found on disk" })
|
||||
|
||||
await Instance.dispose()
|
||||
}
|
||||
@@ -32,6 +32,19 @@ import { ConfigRoutes } from "./routes/config"
|
||||
import { ExperimentalRoutes } from "./routes/experimental"
|
||||
import { ProviderRoutes } from "./routes/provider"
|
||||
import { EventRoutes } from "./routes/event"
|
||||
import { TelemetryRoutes } from "./routes/telemetry" // kilocode_change
|
||||
import { CommitMessageRoutes } from "./routes/commit-message" // kilocode_change
|
||||
import { EnhancePromptRoutes } from "./routes/enhance-prompt" // kilocode_change
|
||||
import { KilocodeRoutes } from "./routes/kilocode" // kilocode_change
|
||||
import { PermissionKilocodeRoutes } from "../kilocode/permission/routes" // kilocode_change
|
||||
import { RemoteRoutes } from "./routes/remote" // kilocode_change
|
||||
import { NetworkRoutes } from "./routes/network" // kilocode_change
|
||||
import { createKiloRoutes } from "@kilocode/kilo-gateway" // kilocode_change
|
||||
import { Database } from "../storage/db" // kilocode_change
|
||||
import { Session } from "../session" // kilocode_change
|
||||
import { Identifier } from "../id/id" // kilocode_change
|
||||
import { SessionTable, MessageTable, PartTable } from "../session/session.sql" // kilocode_change
|
||||
import { Bus } from "@/bus" // kilocode_change
|
||||
import { InstanceBootstrap } from "../project/bootstrap"
|
||||
import { NotFoundError } from "../storage/db"
|
||||
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
||||
@@ -45,24 +58,6 @@ import { GlobalRoutes } from "./routes/global"
|
||||
import { MDNS } from "./mdns"
|
||||
import { lazy } from "@/util/lazy"
|
||||
|
||||
|
||||
// kilocode_change start
|
||||
// KILO ROUTES
|
||||
import { TelemetryRoutes } from "./routes/telemetry"
|
||||
import { CommitMessageRoutes } from "./routes/commit-message"
|
||||
import { EnhancePromptRoutes } from "./routes/enhance-prompt"
|
||||
import { KilocodeRoutes } from "./routes/kilocode"
|
||||
import { PermissionKilocodeRoutes } from "../kilocode/permission/routes"
|
||||
import { RemoteRoutes } from "./routes/remote"
|
||||
import { NetworkRoutes } from "./routes/network"
|
||||
import { Database } from "../storage/db"
|
||||
import { Session } from "../session"
|
||||
import { Identifier } from "../id/id"
|
||||
import { SessionTable, MessageTable, PartTable } from "../session/session.sql"
|
||||
import { Bus } from "@/bus"
|
||||
import { createKiloRoutes } from "@kilocode/kilo-gateway"
|
||||
// kilocode_change end
|
||||
|
||||
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
|
||||
@@ -94,19 +89,14 @@ export namespace Server {
|
||||
status: 500,
|
||||
})
|
||||
})
|
||||
if (err instanceof NamedError) {
|
||||
let status: ContentfulStatusCode
|
||||
if (err instanceof NotFoundError) status = 404
|
||||
else if (err instanceof Provider.ModelNotFoundError) status = 400
|
||||
else if (err.name === "ProviderAuthValidationFailed") status = 400
|
||||
else if (err.name.startsWith("Worktree")) status = 400
|
||||
else status = 500
|
||||
return c.json(err.toObject(), { status })
|
||||
}
|
||||
if (err instanceof HTTPException) return err.getResponse()
|
||||
const message = err instanceof Error && err.stack ? err.stack : err.toString()
|
||||
return c.json(new NamedError.Unknown({ message }).toObject(), {
|
||||
status: 500,
|
||||
.use((c, next) => {
|
||||
// Allow CORS preflight requests to succeed without auth.
|
||||
// Browser clients sending Authorization headers will preflight with OPTIONS.
|
||||
if (c.req.method === "OPTIONS") return next()
|
||||
const password = Flag.KILO_SERVER_PASSWORD // kilocode_change
|
||||
if (!password) return next()
|
||||
const username = Flag.KILO_SERVER_USERNAME ?? "kilo" // kilocode_change
|
||||
return basicAuth({ username, password })(c, next)
|
||||
})
|
||||
.use(async (c, next) => {
|
||||
// kilocode_change start
|
||||
@@ -122,357 +112,43 @@ export namespace Server {
|
||||
method: c.req.method,
|
||||
path: c.req.path,
|
||||
})
|
||||
}
|
||||
const timer = log.time("request", {
|
||||
method: c.req.method,
|
||||
path: c.req.path,
|
||||
})
|
||||
await next()
|
||||
if (!skipLogging) {
|
||||
timer.stop()
|
||||
}
|
||||
})
|
||||
.use(
|
||||
cors({
|
||||
origin(input) {
|
||||
if (!input) return
|
||||
|
||||
if (input.startsWith("http://localhost:")) return input
|
||||
if (input.startsWith("http://127.0.0.1:")) return input
|
||||
if (
|
||||
input === "tauri://localhost" ||
|
||||
input === "http://tauri.localhost" ||
|
||||
input === "https://tauri.localhost"
|
||||
)
|
||||
return input
|
||||
|
||||
// *.opencode.ai (https only, adjust if needed)
|
||||
if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
|
||||
return input
|
||||
}
|
||||
if (opts?.cors?.includes(input)) {
|
||||
return input
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
}),
|
||||
)
|
||||
.route("/global", GlobalRoutes())
|
||||
.put(
|
||||
"/auth/:providerID",
|
||||
describeRoute({
|
||||
summary: "Set auth credentials",
|
||||
description: "Set authentication credentials",
|
||||
operationId: "auth.set",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully set authentication credentials",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
providerID: ProviderID.zod,
|
||||
}),
|
||||
),
|
||||
validator("json", Auth.Info.zod),
|
||||
async (c) => {
|
||||
const providerID = c.req.valid("param").providerID
|
||||
const info = c.req.valid("json")
|
||||
await Auth.set(providerID, info)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/auth/:providerID",
|
||||
describeRoute({
|
||||
summary: "Remove auth credentials",
|
||||
description: "Remove authentication credentials",
|
||||
operationId: "auth.remove",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully removed authentication credentials",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
providerID: ProviderID.zod,
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const providerID = c.req.valid("param").providerID
|
||||
await Auth.remove(providerID)
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.use(async (c, next) => {
|
||||
if (c.req.path === "/log") return next()
|
||||
const rawWorkspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
|
||||
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
|
||||
const directory = Filesystem.resolve(
|
||||
(() => {
|
||||
try {
|
||||
return decodeURIComponent(raw)
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
})(),
|
||||
)
|
||||
|
||||
return WorkspaceContext.provide({
|
||||
workspaceID: rawWorkspaceID ? WorkspaceID.make(rawWorkspaceID) : undefined,
|
||||
async fn() {
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
async fn() {
|
||||
return next()
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
.use(WorkspaceRouterMiddleware)
|
||||
.get(
|
||||
"/doc",
|
||||
openAPIRouteHandler(app, {
|
||||
documentation: {
|
||||
info: {
|
||||
title: "opencode",
|
||||
version: "0.0.3",
|
||||
description: "opencode api",
|
||||
},
|
||||
openapi: "3.1.1",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.use(
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
directory: z.string().optional(),
|
||||
workspace: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.route("/project", ProjectRoutes())
|
||||
.route("/pty", PtyRoutes())
|
||||
.route("/config", ConfigRoutes())
|
||||
.route("/experimental", ExperimentalRoutes())
|
||||
.route("/session", SessionRoutes())
|
||||
.route("/permission", PermissionRoutes())
|
||||
.route("/question", QuestionRoutes())
|
||||
.route("/provider", ProviderRoutes())
|
||||
.route("/", FileRoutes())
|
||||
.route("/", EventRoutes())
|
||||
.route("/mcp", McpRoutes())
|
||||
.route("/tui", TuiRoutes())
|
||||
.post(
|
||||
"/instance/dispose",
|
||||
describeRoute({
|
||||
summary: "Dispose instance",
|
||||
description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
|
||||
operationId: "instance.dispose",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Instance disposed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.boolean()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
await Instance.dispose()
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/path",
|
||||
describeRoute({
|
||||
summary: "Get paths",
|
||||
description: "Retrieve the current working directory and related path information for the OpenCode instance.",
|
||||
operationId: "path.get",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Path",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(
|
||||
z
|
||||
.object({
|
||||
home: z.string(),
|
||||
state: z.string(),
|
||||
config: z.string(),
|
||||
worktree: z.string(),
|
||||
directory: z.string(),
|
||||
})
|
||||
.meta({
|
||||
ref: "Path",
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json({
|
||||
home: Global.Path.home,
|
||||
state: Global.Path.state,
|
||||
config: Global.Path.config,
|
||||
worktree: Instance.worktree,
|
||||
directory: Instance.directory,
|
||||
const timer = log.time("request", {
|
||||
method: c.req.method,
|
||||
path: c.req.path,
|
||||
})
|
||||
await next()
|
||||
if (!skipLogging) {
|
||||
timer.stop()
|
||||
}
|
||||
})
|
||||
.use(
|
||||
cors({
|
||||
origin(input) {
|
||||
if (!input) return
|
||||
|
||||
return c.json(true)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/agent",
|
||||
describeRoute({
|
||||
summary: "List agents",
|
||||
description: "Get a list of all available AI agents in the OpenCode system.",
|
||||
operationId: "app.agents",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of agents",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Agent.Info.array()),
|
||||
},
|
||||
},
|
||||
if (input.startsWith("http://localhost:")) return input
|
||||
if (input.startsWith("http://127.0.0.1:")) return input
|
||||
if (
|
||||
input === "tauri://localhost" ||
|
||||
input === "http://tauri.localhost" ||
|
||||
input === "https://tauri.localhost"
|
||||
)
|
||||
return input
|
||||
|
||||
// kilocode_change start
|
||||
// *.opencode.ai (https only, adjust if needed)
|
||||
if (/^https:\/\/([a-z0-9-]+\.)*kilo\.ai$/.test(input)) {
|
||||
return input
|
||||
}
|
||||
// kilocode_change end
|
||||
if (opts?.cors?.includes(input)) {
|
||||
return input
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const modes = await Agent.list()
|
||||
return c.json(modes)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/skill",
|
||||
describeRoute({
|
||||
summary: "List skills",
|
||||
description: "Get a list of all available skills in the OpenCode system.",
|
||||
operationId: "app.skills",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of skills",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Skill.Info.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
const skills = await Skill.all()
|
||||
return c.json(skills)
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/lsp",
|
||||
describeRoute({
|
||||
summary: "Get LSP status",
|
||||
description: "Get LSP server status",
|
||||
operationId: "lsp.status",
|
||||
responses: {
|
||||
200: {
|
||||
description: "LSP server status",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(LSP.Status.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await LSP.status())
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/formatter",
|
||||
describeRoute({
|
||||
summary: "Get formatter status",
|
||||
description: "Get formatter status",
|
||||
operationId: "formatter.status",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Formatter status",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Format.Status.array()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (c) => {
|
||||
return c.json(await Format.status())
|
||||
},
|
||||
)
|
||||
// kilocode_change start - disable proxy
|
||||
// .all("/*", async (c) => {
|
||||
// const path = c.req.path
|
||||
|
||||
// const response = await proxy(`https://app.opencode.ai${path}`, {
|
||||
// ...c.req,
|
||||
// headers: {
|
||||
// ...c.req.raw.headers,
|
||||
// host: "app.opencode.ai",
|
||||
// },
|
||||
// })
|
||||
// .use(
|
||||
// cors({
|
||||
// origin(input) {
|
||||
// if (!input) return
|
||||
|
||||
// if (input.startsWith("http://localhost:")) return input
|
||||
// if (input.startsWith("http://127.0.0.1:")) return input
|
||||
// if (
|
||||
// input === "tauri://localhost" ||
|
||||
// input === "http://tauri.localhost" ||
|
||||
// input === "https://tauri.localhost"
|
||||
// )
|
||||
// return input
|
||||
|
||||
// // *.opencode.ai (https only, adjust if needed)
|
||||
// if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
|
||||
// return input
|
||||
// }
|
||||
// if (opts?.cors?.includes(input)) {
|
||||
// return input
|
||||
// }
|
||||
|
||||
// return
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
}),
|
||||
)
|
||||
.route("/global", GlobalRoutes())
|
||||
.put(
|
||||
"/auth/:providerID",
|
||||
@@ -886,7 +562,7 @@ export namespace Server {
|
||||
.all("/*", async (c) => {
|
||||
return c.notFound()
|
||||
})
|
||||
// kilocode_change end
|
||||
// kilocode_change end
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { test, expect, describe } from "bun:test"
|
||||
import { Permission } from "../../src/permission"
|
||||
|
||||
// Reconstruct the Ask agent's readOnlyBash allowlist (mirrors agent.ts)
|
||||
// Reconstruct the Ask agent's readOnlyBash allowlist (mirrors kilocode/agent/index.ts)
|
||||
// Uses an allow-list approach for git: deny by default, allow specific read-only subcommands.
|
||||
const readOnlyBash: Record<string, "allow" | "ask" | "deny"> = {
|
||||
"*": "deny",
|
||||
// read-only / informational
|
||||
"cat *": "allow",
|
||||
"head *": "allow",
|
||||
"tail *": "allow",
|
||||
@@ -24,6 +26,7 @@ const readOnlyBash: Record<string, "allow" | "ask" | "deny"> = {
|
||||
"whoami *": "allow",
|
||||
"printenv *": "allow",
|
||||
"man *": "allow",
|
||||
// text processing (stdout only, no file modification)
|
||||
"grep *": "allow",
|
||||
"rg *": "allow",
|
||||
"ag *": "allow",
|
||||
@@ -32,37 +35,29 @@ const readOnlyBash: Record<string, "allow" | "ask" | "deny"> = {
|
||||
"cut *": "allow",
|
||||
"tr *": "allow",
|
||||
"jq *": "allow",
|
||||
"git *": "allow",
|
||||
"git add *": "deny",
|
||||
"git commit *": "deny",
|
||||
"git push *": "deny",
|
||||
"git merge *": "deny",
|
||||
"git rebase *": "deny",
|
||||
"git cherry-pick *": "deny",
|
||||
"git reset *": "deny",
|
||||
"git checkout *": "deny",
|
||||
"git switch *": "deny",
|
||||
"git stash *": "deny",
|
||||
"git tag *": "deny",
|
||||
"git am *": "deny",
|
||||
"git apply *": "deny",
|
||||
"git remote set-url *": "deny",
|
||||
"git remote add *": "deny",
|
||||
"git remote remove *": "deny",
|
||||
"git clean *": "deny",
|
||||
"git mv *": "deny",
|
||||
"git rm *": "deny",
|
||||
"git config *": "deny",
|
||||
"git clone *": "deny",
|
||||
"git pull *": "deny",
|
||||
"git init *": "deny",
|
||||
"git worktree *": "deny",
|
||||
"git submodule *": "deny",
|
||||
"git revert *": "deny",
|
||||
"git bisect *": "deny",
|
||||
"git filter-branch *": "deny",
|
||||
"git fetch *": "deny",
|
||||
"git restore *": "deny",
|
||||
// git — allowlist of read-only subcommands, deny everything else
|
||||
"git *": "deny",
|
||||
"git log *": "allow",
|
||||
"git show *": "allow",
|
||||
"git diff *": "allow",
|
||||
"git status *": "allow",
|
||||
"git blame *": "allow",
|
||||
"git rev-parse *": "allow",
|
||||
"git rev-list *": "allow",
|
||||
"git ls-files *": "allow",
|
||||
"git ls-tree *": "allow",
|
||||
"git ls-remote *": "allow",
|
||||
"git shortlog *": "allow",
|
||||
"git describe *": "allow",
|
||||
"git cat-file *": "allow",
|
||||
"git name-rev *": "allow",
|
||||
"git stash list *": "allow",
|
||||
"git tag -l *": "allow",
|
||||
"git branch --list *": "allow",
|
||||
"git branch -a *": "allow",
|
||||
"git branch -r *": "allow",
|
||||
"git remote -v *": "allow",
|
||||
// gh — require user approval since commands vary widely
|
||||
"gh *": "ask",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user