refactor: remove legacy tools format, use permission only
BREAKING: Requires OpenCode 1.1.1+ - Remove supportsNewPermissionSystem/usesLegacyToolsSystem checks - Simplify permission-compat.ts to permission format only - Unify explore/librarian deny lists: write, edit, task, sisyphus_task, call_omo_agent - Add sisyphus_task to oracle deny list - Update agent-tool-restrictions.ts with correct per-agent restrictions - Clean config-handler.ts conditional version checks - Update tests for simplified API
This commit is contained in:
parent
ede9abceb3
commit
83cbc56709
@ -1,5 +1,6 @@
|
||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
||||
import type { AgentPromptMetadata } from "./types"
|
||||
import { createAgentToolRestrictions } from "../shared/permission-compat"
|
||||
|
||||
const DEFAULT_MODEL = "opencode/glm-4.7-free"
|
||||
|
||||
@ -21,13 +22,21 @@ export const LIBRARIAN_PROMPT_METADATA: AgentPromptMetadata = {
|
||||
}
|
||||
|
||||
export function createLibrarianAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
const restrictions = createAgentToolRestrictions([
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"sisyphus_task",
|
||||
"call_omo_agent",
|
||||
])
|
||||
|
||||
return {
|
||||
description:
|
||||
"Specialized codebase understanding agent for multi-repository analysis, searching remote codebases, retrieving official documentation, and finding implementation examples using GitHub CLI, Context7, and Web Search. MUST BE USED when users ask to look up code in remote repositories, explain library internals, or find usage examples in open source.",
|
||||
mode: "subagent" as const,
|
||||
model,
|
||||
temperature: 0.1,
|
||||
tools: { write: false, edit: false, background_task: false },
|
||||
...restrictions,
|
||||
prompt: `# THE LIBRARIAN
|
||||
|
||||
You are **THE LIBRARIAN**, a specialized open-source codebase understanding agent.
|
||||
|
||||
@ -102,6 +102,7 @@ export function createOracleAgent(model: string = DEFAULT_MODEL): AgentConfig {
|
||||
"write",
|
||||
"edit",
|
||||
"task",
|
||||
"sisyphus_task",
|
||||
])
|
||||
|
||||
const base = {
|
||||
|
||||
@ -3,8 +3,7 @@ import { isGptModel } from "./types"
|
||||
import type { AgentOverrideConfig, CategoryConfig } from "../config/schema"
|
||||
import {
|
||||
createAgentToolRestrictions,
|
||||
migrateAgentConfig,
|
||||
supportsNewPermissionSystem,
|
||||
type PermissionValue,
|
||||
} from "../shared/permission-compat"
|
||||
|
||||
const SISYPHUS_JUNIOR_PROMPT = `<Role>
|
||||
@ -99,26 +98,14 @@ export function createSisyphusJuniorAgentWithOverrides(
|
||||
|
||||
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
|
||||
|
||||
let toolsConfig: Record<string, unknown> = {}
|
||||
if (supportsNewPermissionSystem()) {
|
||||
const userPermission = (override?.permission ?? {}) as Record<string, string>
|
||||
const basePermission = (baseRestrictions as { permission: Record<string, string> }).permission
|
||||
const merged: Record<string, string> = { ...userPermission }
|
||||
for (const tool of BLOCKED_TOOLS) {
|
||||
merged[tool] = "deny"
|
||||
}
|
||||
merged.call_omo_agent = "allow"
|
||||
toolsConfig = { permission: { ...merged, ...basePermission } }
|
||||
} else {
|
||||
const userTools = override?.tools ?? {}
|
||||
const baseTools = (baseRestrictions as { tools: Record<string, boolean> }).tools
|
||||
const merged: Record<string, boolean> = { ...userTools }
|
||||
for (const tool of BLOCKED_TOOLS) {
|
||||
merged[tool] = false
|
||||
}
|
||||
merged.call_omo_agent = true
|
||||
toolsConfig = { tools: { ...merged, ...baseTools } }
|
||||
const userPermission = (override?.permission ?? {}) as Record<string, PermissionValue>
|
||||
const basePermission = baseRestrictions.permission
|
||||
const merged: Record<string, PermissionValue> = { ...userPermission }
|
||||
for (const tool of BLOCKED_TOOLS) {
|
||||
merged[tool] = "deny"
|
||||
}
|
||||
merged.call_omo_agent = "allow"
|
||||
const toolsConfig = { permission: { ...merged, ...basePermission } }
|
||||
|
||||
const base: AgentConfig = {
|
||||
description: override?.description ??
|
||||
@ -153,10 +140,18 @@ export function createSisyphusJuniorAgent(
|
||||
const prompt = buildSisyphusJuniorPrompt(promptAppend)
|
||||
const model = categoryConfig.model
|
||||
const baseRestrictions = createAgentToolRestrictions(BLOCKED_TOOLS)
|
||||
const mergedConfig = migrateAgentConfig({
|
||||
...baseRestrictions,
|
||||
...(categoryConfig.tools ? { tools: categoryConfig.tools } : {}),
|
||||
})
|
||||
const categoryPermission = categoryConfig.tools
|
||||
? Object.fromEntries(
|
||||
Object.entries(categoryConfig.tools).map(([k, v]) => [
|
||||
k,
|
||||
v ? ("allow" as const) : ("deny" as const),
|
||||
])
|
||||
)
|
||||
: {}
|
||||
const mergedPermission = {
|
||||
...categoryPermission,
|
||||
...baseRestrictions.permission,
|
||||
}
|
||||
|
||||
|
||||
const base: AgentConfig = {
|
||||
@ -167,7 +162,7 @@ export function createSisyphusJuniorAgent(
|
||||
maxTokens: categoryConfig.maxTokens ?? 64000,
|
||||
prompt,
|
||||
color: "#20B2AA",
|
||||
...mergedConfig,
|
||||
permission: mergedPermission,
|
||||
}
|
||||
|
||||
if (categoryConfig.temperature !== undefined) {
|
||||
|
||||
@ -618,9 +618,7 @@ export function createSisyphusAgent(
|
||||
? buildDynamicSisyphusPrompt(availableAgents, tools, skills)
|
||||
: buildDynamicSisyphusPrompt([], tools, skills)
|
||||
|
||||
// Note: question permission allows agent to ask user questions via OpenCode's QuestionTool
|
||||
// SDK type doesn't include 'question' yet, but OpenCode runtime supports it
|
||||
const permission = { question: "allow" } as AgentConfig["permission"]
|
||||
const permission = { question: "allow", call_omo_agent: "deny" } as AgentConfig["permission"]
|
||||
const base = {
|
||||
description:
|
||||
"Sisyphus - Powerful AI orchestrator from OhMyOpenCode. Plans obsessively with todos, assesses search complexity before exploration, delegates strategically to specialized agents. Uses explore for internal code (parallel-friendly), librarian only for external docs, and always delegates UI work to frontend engineer.",
|
||||
@ -630,7 +628,6 @@ export function createSisyphusAgent(
|
||||
prompt,
|
||||
color: "#00CED1",
|
||||
permission,
|
||||
tools: { call_omo_agent: false },
|
||||
}
|
||||
|
||||
if (isGptModel(model)) {
|
||||
|
||||
@ -5,7 +5,7 @@ import type {
|
||||
LaunchInput,
|
||||
ResumeInput,
|
||||
} from "./types"
|
||||
import { log } from "../../shared/logger"
|
||||
import { log, getAgentToolRestrictions } from "../../shared"
|
||||
import { ConcurrencyManager } from "./concurrency"
|
||||
import type { BackgroundTaskConfig } from "../../config/schema"
|
||||
|
||||
@ -178,6 +178,7 @@ export class BackgroundManager {
|
||||
...(input.model ? { model: input.model } : {}),
|
||||
system: input.skillContent,
|
||||
tools: {
|
||||
...getAgentToolRestrictions(input.agent),
|
||||
task: false,
|
||||
sisyphus_task: false,
|
||||
call_omo_agent: true,
|
||||
@ -406,6 +407,7 @@ export class BackgroundManager {
|
||||
body: {
|
||||
agent: existingTask.agent,
|
||||
tools: {
|
||||
...getAgentToolRestrictions(existingTask.agent),
|
||||
task: false,
|
||||
sisyphus_task: false,
|
||||
call_omo_agent: true,
|
||||
|
||||
@ -291,37 +291,27 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
LspCodeActionResolve: false,
|
||||
};
|
||||
|
||||
type AgentWithPermission = { permission?: Record<string, unknown> };
|
||||
|
||||
if (agentResult.librarian) {
|
||||
agentResult.librarian.tools = {
|
||||
...agentResult.librarian.tools,
|
||||
"grep_app_*": true,
|
||||
};
|
||||
const agent = agentResult.librarian as AgentWithPermission;
|
||||
agent.permission = { ...agent.permission, "grep_app_*": "allow" };
|
||||
}
|
||||
if (agentResult["multimodal-looker"]) {
|
||||
agentResult["multimodal-looker"].tools = {
|
||||
...agentResult["multimodal-looker"].tools,
|
||||
task: false,
|
||||
look_at: false,
|
||||
};
|
||||
const agent = agentResult["multimodal-looker"] as AgentWithPermission;
|
||||
agent.permission = { ...agent.permission, task: "deny", look_at: "deny" };
|
||||
}
|
||||
if (agentResult["orchestrator-sisyphus"]) {
|
||||
agentResult["orchestrator-sisyphus"].tools = {
|
||||
...agentResult["orchestrator-sisyphus"].tools,
|
||||
task: false,
|
||||
call_omo_agent: false,
|
||||
};
|
||||
const agent = agentResult["orchestrator-sisyphus"] as AgentWithPermission;
|
||||
agent.permission = { ...agent.permission, task: "deny", call_omo_agent: "deny" };
|
||||
}
|
||||
if (agentResult.Sisyphus) {
|
||||
agentResult.Sisyphus.tools = {
|
||||
...agentResult.Sisyphus.tools,
|
||||
call_omo_agent: false,
|
||||
};
|
||||
const agent = agentResult.Sisyphus as AgentWithPermission;
|
||||
agent.permission = { ...agent.permission, call_omo_agent: "deny" };
|
||||
}
|
||||
if (agentResult["Prometheus (Planner)"]) {
|
||||
(agentResult["Prometheus (Planner)"] as { tools?: Record<string, unknown> }).tools = {
|
||||
...(agentResult["Prometheus (Planner)"] as { tools?: Record<string, unknown> }).tools,
|
||||
call_omo_agent: false,
|
||||
};
|
||||
const agent = agentResult["Prometheus (Planner)"] as AgentWithPermission;
|
||||
agent.permission = { ...agent.permission, call_omo_agent: "deny" };
|
||||
}
|
||||
|
||||
config.permission = {
|
||||
|
||||
59
src/shared/agent-tool-restrictions.ts
Normal file
59
src/shared/agent-tool-restrictions.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type { PermissionValue } from "./permission-compat"
|
||||
|
||||
/**
|
||||
* Agent tool restrictions for session.prompt calls.
|
||||
* OpenCode SDK's session.prompt `tools` parameter OVERRIDES agent-level permissions.
|
||||
* This provides complete restriction sets so session.prompt calls include all necessary restrictions.
|
||||
*/
|
||||
|
||||
const EXPLORATION_AGENT_DENYLIST: Record<string, PermissionValue> = {
|
||||
write: "deny",
|
||||
edit: "deny",
|
||||
task: "deny",
|
||||
sisyphus_task: "deny",
|
||||
call_omo_agent: "deny",
|
||||
}
|
||||
|
||||
const AGENT_RESTRICTIONS: Record<string, Record<string, PermissionValue>> = {
|
||||
explore: EXPLORATION_AGENT_DENYLIST,
|
||||
|
||||
librarian: EXPLORATION_AGENT_DENYLIST,
|
||||
|
||||
oracle: {
|
||||
write: "deny",
|
||||
edit: "deny",
|
||||
task: "deny",
|
||||
sisyphus_task: "deny",
|
||||
},
|
||||
|
||||
"multimodal-looker": {
|
||||
"*": "deny",
|
||||
read: "allow",
|
||||
},
|
||||
|
||||
"document-writer": {
|
||||
task: "deny",
|
||||
sisyphus_task: "deny",
|
||||
call_omo_agent: "deny",
|
||||
},
|
||||
|
||||
"frontend-ui-ux-engineer": {
|
||||
task: "deny",
|
||||
sisyphus_task: "deny",
|
||||
call_omo_agent: "deny",
|
||||
},
|
||||
|
||||
"Sisyphus-Junior": {
|
||||
task: "deny",
|
||||
sisyphus_task: "deny",
|
||||
},
|
||||
}
|
||||
|
||||
export function getAgentToolRestrictions(agentName: string): Record<string, PermissionValue> {
|
||||
return AGENT_RESTRICTIONS[agentName] ?? {}
|
||||
}
|
||||
|
||||
export function hasAgentToolRestrictions(agentName: string): boolean {
|
||||
const restrictions = AGENT_RESTRICTIONS[agentName]
|
||||
return restrictions !== undefined && Object.keys(restrictions).length > 0
|
||||
}
|
||||
@ -25,3 +25,4 @@ export * from "./agent-variant"
|
||||
export * from "./session-cursor"
|
||||
export * from "./shell-env"
|
||||
export * from "./system-directive"
|
||||
export * from "./agent-tool-restrictions"
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, spyOn, mock } from "bun:test"
|
||||
import * as childProcess from "child_process"
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import {
|
||||
parseVersion,
|
||||
compareVersions,
|
||||
isVersionGte,
|
||||
isVersionLt,
|
||||
getOpenCodeVersion,
|
||||
supportsNewPermissionSystem,
|
||||
usesLegacyToolsSystem,
|
||||
isOpenCodeVersionAtLeast,
|
||||
resetVersionCache,
|
||||
setVersionCache,
|
||||
PERMISSION_BREAKING_VERSION,
|
||||
MINIMUM_OPENCODE_VERSION,
|
||||
} from "./opencode-version"
|
||||
|
||||
describe("opencode-version", () => {
|
||||
@ -163,7 +161,7 @@ describe("opencode-version", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("supportsNewPermissionSystem", () => {
|
||||
describe("isOpenCodeVersionAtLeast", () => {
|
||||
beforeEach(() => {
|
||||
resetVersionCache()
|
||||
})
|
||||
@ -172,34 +170,34 @@ describe("opencode-version", () => {
|
||||
resetVersionCache()
|
||||
})
|
||||
|
||||
test("returns true for v1.1.1", () => {
|
||||
test("returns true for exact version", () => {
|
||||
// #given version is 1.1.1
|
||||
setVersionCache("1.1.1")
|
||||
|
||||
// #when checking permission system support
|
||||
const result = supportsNewPermissionSystem()
|
||||
// #when checking against 1.1.1
|
||||
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||
|
||||
// #then returns true
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test("returns true for versions above 1.1.1", () => {
|
||||
// #given version is above 1.1.1
|
||||
test("returns true for versions above target", () => {
|
||||
// #given version is above target
|
||||
setVersionCache("1.2.0")
|
||||
|
||||
// #when checking
|
||||
const result = supportsNewPermissionSystem()
|
||||
// #when checking against 1.1.1
|
||||
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||
|
||||
// #then returns true
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false for versions below 1.1.1", () => {
|
||||
// #given version is below 1.1.1
|
||||
test("returns false for versions below target", () => {
|
||||
// #given version is below target
|
||||
setVersionCache("1.1.0")
|
||||
|
||||
// #when checking
|
||||
const result = supportsNewPermissionSystem()
|
||||
// #when checking against 1.1.1
|
||||
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||
|
||||
// #then returns false
|
||||
expect(result).toBe(false)
|
||||
@ -210,48 +208,16 @@ describe("opencode-version", () => {
|
||||
setVersionCache(null)
|
||||
|
||||
// #when checking
|
||||
const result = supportsNewPermissionSystem()
|
||||
const result = isOpenCodeVersionAtLeast("1.1.1")
|
||||
|
||||
// #then returns true (assume newer version)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("usesLegacyToolsSystem", () => {
|
||||
beforeEach(() => {
|
||||
resetVersionCache()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
resetVersionCache()
|
||||
})
|
||||
|
||||
test("returns true for versions below 1.1.1", () => {
|
||||
// #given version is below 1.1.1
|
||||
setVersionCache("1.0.150")
|
||||
|
||||
// #when checking
|
||||
const result = usesLegacyToolsSystem()
|
||||
|
||||
// #then returns true
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false for v1.1.1 and above", () => {
|
||||
// #given version is 1.1.1
|
||||
setVersionCache("1.1.1")
|
||||
|
||||
// #when checking
|
||||
const result = usesLegacyToolsSystem()
|
||||
|
||||
// #then returns false
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("PERMISSION_BREAKING_VERSION", () => {
|
||||
describe("MINIMUM_OPENCODE_VERSION", () => {
|
||||
test("is set to 1.1.1", () => {
|
||||
expect(PERMISSION_BREAKING_VERSION).toBe("1.1.1")
|
||||
expect(MINIMUM_OPENCODE_VERSION).toBe("1.1.1")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { execSync } from "child_process"
|
||||
|
||||
export const PERMISSION_BREAKING_VERSION = "1.1.1"
|
||||
/**
|
||||
* Minimum OpenCode version required for this plugin.
|
||||
* This plugin only supports OpenCode 1.1.1+ which uses the permission system.
|
||||
*/
|
||||
export const MINIMUM_OPENCODE_VERSION = "1.1.1"
|
||||
|
||||
const NOT_CACHED = Symbol("NOT_CACHED")
|
||||
let cachedVersion: string | null | typeof NOT_CACHED = NOT_CACHED
|
||||
@ -53,14 +57,10 @@ export function getOpenCodeVersion(): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function supportsNewPermissionSystem(): boolean {
|
||||
const version = getOpenCodeVersion()
|
||||
if (!version) return true
|
||||
return isVersionGte(version, PERMISSION_BREAKING_VERSION)
|
||||
}
|
||||
|
||||
export function usesLegacyToolsSystem(): boolean {
|
||||
return !supportsNewPermissionSystem()
|
||||
export function isOpenCodeVersionAtLeast(version: string): boolean {
|
||||
const current = getOpenCodeVersion()
|
||||
if (!current) return true
|
||||
return isVersionGte(current, version)
|
||||
}
|
||||
|
||||
export function resetVersionCache(): void {
|
||||
|
||||
@ -1,27 +1,15 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import { describe, test, expect } from "bun:test"
|
||||
import {
|
||||
createAgentToolRestrictions,
|
||||
createAgentToolAllowlist,
|
||||
migrateToolsToPermission,
|
||||
migratePermissionToTools,
|
||||
migrateAgentConfig,
|
||||
} from "./permission-compat"
|
||||
import { setVersionCache, resetVersionCache } from "./opencode-version"
|
||||
|
||||
describe("permission-compat", () => {
|
||||
beforeEach(() => {
|
||||
resetVersionCache()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
resetVersionCache()
|
||||
})
|
||||
|
||||
describe("createAgentToolRestrictions", () => {
|
||||
test("returns permission format for v1.1.1+", () => {
|
||||
// #given version is 1.1.1
|
||||
setVersionCache("1.1.1")
|
||||
|
||||
test("returns permission format with deny values", () => {
|
||||
// #given tools to restrict
|
||||
// #when creating restrictions
|
||||
const result = createAgentToolRestrictions(["write", "edit"])
|
||||
|
||||
@ -31,38 +19,19 @@ describe("permission-compat", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("returns tools format for versions below 1.1.1", () => {
|
||||
// #given version is below 1.1.1
|
||||
setVersionCache("1.0.150")
|
||||
|
||||
test("returns empty permission for empty array", () => {
|
||||
// #given empty tools array
|
||||
// #when creating restrictions
|
||||
const result = createAgentToolRestrictions(["write", "edit"])
|
||||
const result = createAgentToolRestrictions([])
|
||||
|
||||
// #then returns tools format
|
||||
expect(result).toEqual({
|
||||
tools: { write: false, edit: false },
|
||||
})
|
||||
})
|
||||
|
||||
test("assumes new format when version unknown", () => {
|
||||
// #given version is null
|
||||
setVersionCache(null)
|
||||
|
||||
// #when creating restrictions
|
||||
const result = createAgentToolRestrictions(["write"])
|
||||
|
||||
// #then returns permission format (assumes new version)
|
||||
expect(result).toEqual({
|
||||
permission: { write: "deny" },
|
||||
})
|
||||
// #then returns empty permission
|
||||
expect(result).toEqual({ permission: {} })
|
||||
})
|
||||
})
|
||||
|
||||
describe("createAgentToolAllowlist", () => {
|
||||
test("returns wildcard deny with explicit allow for v1.1.1+", () => {
|
||||
// #given version is 1.1.1
|
||||
setVersionCache("1.1.1")
|
||||
|
||||
test("returns wildcard deny with explicit allow", () => {
|
||||
// #given tools to allow
|
||||
// #when creating allowlist
|
||||
const result = createAgentToolAllowlist(["read"])
|
||||
|
||||
@ -72,11 +41,9 @@ describe("permission-compat", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("returns wildcard deny with multiple allows for v1.1.1+", () => {
|
||||
// #given version is 1.1.1
|
||||
setVersionCache("1.1.1")
|
||||
|
||||
// #when creating allowlist with multiple tools
|
||||
test("returns wildcard deny with multiple allows", () => {
|
||||
// #given multiple tools to allow
|
||||
// #when creating allowlist
|
||||
const result = createAgentToolAllowlist(["read", "glob"])
|
||||
|
||||
// #then returns wildcard deny with both allows
|
||||
@ -84,35 +51,6 @@ describe("permission-compat", () => {
|
||||
permission: { "*": "deny", read: "allow", glob: "allow" },
|
||||
})
|
||||
})
|
||||
|
||||
test("returns explicit deny list for old versions", () => {
|
||||
// #given version is below 1.1.1
|
||||
setVersionCache("1.0.150")
|
||||
|
||||
// #when creating allowlist
|
||||
const result = createAgentToolAllowlist(["read"])
|
||||
|
||||
// #then returns tools format with common tools denied except read
|
||||
expect(result).toHaveProperty("tools")
|
||||
const tools = (result as { tools: Record<string, boolean> }).tools
|
||||
expect(tools.write).toBe(false)
|
||||
expect(tools.edit).toBe(false)
|
||||
expect(tools.bash).toBe(false)
|
||||
expect(tools.read).toBeUndefined()
|
||||
})
|
||||
|
||||
test("excludes allowed tools from legacy deny list", () => {
|
||||
// #given version is below 1.1.1
|
||||
setVersionCache("1.0.150")
|
||||
|
||||
// #when creating allowlist with glob
|
||||
const result = createAgentToolAllowlist(["read", "glob"])
|
||||
|
||||
// #then glob is not in deny list
|
||||
const tools = (result as { tools: Record<string, boolean> }).tools
|
||||
expect(tools.glob).toBeUndefined()
|
||||
expect(tools.write).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrateToolsToPermission", () => {
|
||||
@ -132,38 +70,9 @@ describe("permission-compat", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("migratePermissionToTools", () => {
|
||||
test("converts permission to boolean tools", () => {
|
||||
// #given permission config
|
||||
const permission = { write: "deny" as const, edit: "allow" as const }
|
||||
|
||||
// #when migrating
|
||||
const result = migratePermissionToTools(permission)
|
||||
|
||||
// #then converts correctly
|
||||
expect(result).toEqual({ write: false, edit: true })
|
||||
})
|
||||
|
||||
test("excludes ask values", () => {
|
||||
// #given permission with ask
|
||||
const permission = {
|
||||
write: "deny" as const,
|
||||
edit: "ask" as const,
|
||||
bash: "allow" as const,
|
||||
}
|
||||
|
||||
// #when migrating
|
||||
const result = migratePermissionToTools(permission)
|
||||
|
||||
// #then ask is excluded
|
||||
expect(result).toEqual({ write: false, bash: true })
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrateAgentConfig", () => {
|
||||
test("migrates tools to permission for v1.1.1+", () => {
|
||||
// #given v1.1.1 and config with tools
|
||||
setVersionCache("1.1.1")
|
||||
test("migrates tools to permission", () => {
|
||||
// #given config with tools
|
||||
const config = {
|
||||
model: "test",
|
||||
tools: { write: false, edit: false },
|
||||
@ -178,25 +87,8 @@ describe("permission-compat", () => {
|
||||
expect(result.model).toBe("test")
|
||||
})
|
||||
|
||||
test("migrates permission to tools for old versions", () => {
|
||||
// #given old version and config with permission
|
||||
setVersionCache("1.0.150")
|
||||
const config = {
|
||||
model: "test",
|
||||
permission: { write: "deny" as const, edit: "deny" as const },
|
||||
}
|
||||
|
||||
// #when migrating
|
||||
const result = migrateAgentConfig(config)
|
||||
|
||||
// #then converts to tools
|
||||
expect(result.permission).toBeUndefined()
|
||||
expect(result.tools).toEqual({ write: false, edit: false })
|
||||
})
|
||||
|
||||
test("preserves other config fields", () => {
|
||||
// #given config with other fields
|
||||
setVersionCache("1.1.1")
|
||||
const config = {
|
||||
model: "test",
|
||||
temperature: 0.5,
|
||||
@ -212,5 +104,31 @@ describe("permission-compat", () => {
|
||||
expect(result.temperature).toBe(0.5)
|
||||
expect(result.prompt).toBe("hello")
|
||||
})
|
||||
|
||||
test("merges existing permission with migrated tools", () => {
|
||||
// #given config with both tools and permission
|
||||
const config = {
|
||||
tools: { write: false },
|
||||
permission: { bash: "deny" as const },
|
||||
}
|
||||
|
||||
// #when migrating
|
||||
const result = migrateAgentConfig(config)
|
||||
|
||||
// #then merges permission (existing takes precedence)
|
||||
expect(result.tools).toBeUndefined()
|
||||
expect(result.permission).toEqual({ write: "deny", bash: "deny" })
|
||||
})
|
||||
|
||||
test("returns unchanged config if no tools", () => {
|
||||
// #given config without tools
|
||||
const config = { model: "test", permission: { edit: "deny" as const } }
|
||||
|
||||
// #when migrating
|
||||
const result = migrateAgentConfig(config)
|
||||
|
||||
// #then returns unchanged
|
||||
expect(result).toEqual(config)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,98 +1,48 @@
|
||||
import { supportsNewPermissionSystem } from "./opencode-version"
|
||||
|
||||
export { supportsNewPermissionSystem }
|
||||
/**
|
||||
* Permission system utilities for OpenCode 1.1.1+.
|
||||
* This module only supports the new permission format.
|
||||
*/
|
||||
|
||||
export type PermissionValue = "ask" | "allow" | "deny"
|
||||
|
||||
export interface LegacyToolsFormat {
|
||||
tools: Record<string, boolean>
|
||||
}
|
||||
|
||||
export interface NewPermissionFormat {
|
||||
export interface PermissionFormat {
|
||||
permission: Record<string, PermissionValue>
|
||||
}
|
||||
|
||||
export type VersionAwareRestrictions = LegacyToolsFormat | NewPermissionFormat
|
||||
|
||||
/**
|
||||
* Creates tool restrictions that deny specified tools.
|
||||
*/
|
||||
export function createAgentToolRestrictions(
|
||||
denyTools: string[]
|
||||
): VersionAwareRestrictions {
|
||||
if (supportsNewPermissionSystem()) {
|
||||
return {
|
||||
permission: Object.fromEntries(
|
||||
denyTools.map((tool) => [tool, "deny" as const])
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
): PermissionFormat {
|
||||
return {
|
||||
tools: Object.fromEntries(denyTools.map((tool) => [tool, false])),
|
||||
permission: Object.fromEntries(
|
||||
denyTools.map((tool) => [tool, "deny" as const])
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common tools that should be denied when using allowlist approach.
|
||||
* Used for legacy fallback when `*: deny` pattern is not supported.
|
||||
*/
|
||||
const COMMON_TOOLS_TO_DENY = [
|
||||
"write",
|
||||
"edit",
|
||||
"bash",
|
||||
"task",
|
||||
"sisyphus_task",
|
||||
"call_omo_agent",
|
||||
"webfetch",
|
||||
"glob",
|
||||
"grep",
|
||||
"lsp_diagnostics",
|
||||
"lsp_prepare_rename",
|
||||
"lsp_rename",
|
||||
"ast_grep_search",
|
||||
"ast_grep_replace",
|
||||
"session_list",
|
||||
"session_read",
|
||||
"session_search",
|
||||
"session_info",
|
||||
"background_output",
|
||||
"background_cancel",
|
||||
"skill",
|
||||
"skill_mcp",
|
||||
"look_at",
|
||||
"todowrite",
|
||||
"todoread",
|
||||
"interactive_bash",
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Creates tool restrictions that ONLY allow specified tools.
|
||||
* All other tools are denied by default.
|
||||
*
|
||||
* Uses `*: deny` pattern for new permission system,
|
||||
* falls back to explicit deny list for legacy systems.
|
||||
* All other tools are denied by default using `*: deny` pattern.
|
||||
*/
|
||||
export function createAgentToolAllowlist(
|
||||
allowTools: string[]
|
||||
): VersionAwareRestrictions {
|
||||
if (supportsNewPermissionSystem()) {
|
||||
return {
|
||||
permission: {
|
||||
"*": "deny" as const,
|
||||
...Object.fromEntries(
|
||||
allowTools.map((tool) => [tool, "allow" as const])
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy fallback: explicitly deny common tools except allowed ones
|
||||
const allowSet = new Set(allowTools)
|
||||
const denyTools = COMMON_TOOLS_TO_DENY.filter((tool) => !allowSet.has(tool))
|
||||
|
||||
): PermissionFormat {
|
||||
return {
|
||||
tools: Object.fromEntries(denyTools.map((tool) => [tool, false])),
|
||||
permission: {
|
||||
"*": "deny" as const,
|
||||
...Object.fromEntries(
|
||||
allowTools.map((tool) => [tool, "allow" as const])
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts legacy tools format to permission format.
|
||||
* For migrating user configs from older versions.
|
||||
*/
|
||||
export function migrateToolsToPermission(
|
||||
tools: Record<string, boolean>
|
||||
): Record<string, PermissionValue> {
|
||||
@ -104,40 +54,23 @@ export function migrateToolsToPermission(
|
||||
)
|
||||
}
|
||||
|
||||
export function migratePermissionToTools(
|
||||
permission: Record<string, PermissionValue>
|
||||
): Record<string, boolean> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(permission)
|
||||
.filter(([, value]) => value !== "ask")
|
||||
.map(([key, value]) => [key, value === "allow"])
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates agent config from legacy tools format to permission format.
|
||||
* If config has `tools`, converts to `permission`.
|
||||
*/
|
||||
export function migrateAgentConfig(
|
||||
config: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const result = { ...config }
|
||||
|
||||
if (supportsNewPermissionSystem()) {
|
||||
if (result.tools && typeof result.tools === "object") {
|
||||
const existingPermission =
|
||||
(result.permission as Record<string, PermissionValue>) || {}
|
||||
const migratedPermission = migrateToolsToPermission(
|
||||
result.tools as Record<string, boolean>
|
||||
)
|
||||
result.permission = { ...migratedPermission, ...existingPermission }
|
||||
delete result.tools
|
||||
}
|
||||
} else {
|
||||
if (result.permission && typeof result.permission === "object") {
|
||||
const existingTools = (result.tools as Record<string, boolean>) || {}
|
||||
const migratedTools = migratePermissionToTools(
|
||||
result.permission as Record<string, PermissionValue>
|
||||
)
|
||||
result.tools = { ...migratedTools, ...existingTools }
|
||||
delete result.permission
|
||||
}
|
||||
if (result.tools && typeof result.tools === "object") {
|
||||
const existingPermission =
|
||||
(result.permission as Record<string, PermissionValue>) || {}
|
||||
const migratedPermission = migrateToolsToPermission(
|
||||
result.tools as Record<string, boolean>
|
||||
)
|
||||
result.permission = { ...migratedPermission, ...existingPermission }
|
||||
delete result.tools
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@ -4,7 +4,7 @@ import { join } from "node:path"
|
||||
import { ALLOWED_AGENTS, CALL_OMO_AGENT_DESCRIPTION } from "./constants"
|
||||
import type { CallOmoAgentArgs } from "./types"
|
||||
import type { BackgroundManager } from "../../features/background-agent"
|
||||
import { log } from "../../shared/logger"
|
||||
import { log, getAgentToolRestrictions } from "../../shared"
|
||||
import { consumeNewMessages } from "../../shared/session-cursor"
|
||||
import { findFirstMessageWithAgent, findNearestMessageWithFields, MESSAGE_STORAGE } from "../../features/hook-message-injector"
|
||||
import { getSessionAgent } from "../../features/claude-code-session-state"
|
||||
@ -188,6 +188,7 @@ async function executeSync(
|
||||
body: {
|
||||
agent: args.subagent_type,
|
||||
tools: {
|
||||
...getAgentToolRestrictions(args.subagent_type),
|
||||
task: false,
|
||||
sisyphus_task: false,
|
||||
},
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import {
|
||||
lsp_goto_definition,
|
||||
lsp_find_references,
|
||||
lsp_symbols,
|
||||
lsp_diagnostics,
|
||||
lsp_prepare_rename,
|
||||
lsp_rename,
|
||||
@ -52,6 +55,9 @@ export function createBackgroundTools(manager: BackgroundManager, client: Openco
|
||||
}
|
||||
|
||||
export const builtinTools: Record<string, ToolDefinition> = {
|
||||
lsp_goto_definition,
|
||||
lsp_find_references,
|
||||
lsp_symbols,
|
||||
lsp_diagnostics,
|
||||
lsp_prepare_rename,
|
||||
lsp_rename,
|
||||
|
||||
@ -509,6 +509,37 @@ export class LSPClient {
|
||||
await new Promise((r) => setTimeout(r, 1000))
|
||||
}
|
||||
|
||||
async definition(filePath: string, line: number, character: number): Promise<unknown> {
|
||||
const absPath = resolve(filePath)
|
||||
await this.openFile(absPath)
|
||||
return this.send("textDocument/definition", {
|
||||
textDocument: { uri: pathToFileURL(absPath).href },
|
||||
position: { line: line - 1, character },
|
||||
})
|
||||
}
|
||||
|
||||
async references(filePath: string, line: number, character: number, includeDeclaration = true): Promise<unknown> {
|
||||
const absPath = resolve(filePath)
|
||||
await this.openFile(absPath)
|
||||
return this.send("textDocument/references", {
|
||||
textDocument: { uri: pathToFileURL(absPath).href },
|
||||
position: { line: line - 1, character },
|
||||
context: { includeDeclaration },
|
||||
})
|
||||
}
|
||||
|
||||
async documentSymbols(filePath: string): Promise<unknown> {
|
||||
const absPath = resolve(filePath)
|
||||
await this.openFile(absPath)
|
||||
return this.send("textDocument/documentSymbol", {
|
||||
textDocument: { uri: pathToFileURL(absPath).href },
|
||||
})
|
||||
}
|
||||
|
||||
async workspaceSymbols(query: string): Promise<unknown> {
|
||||
return this.send("workspace/symbol", { query })
|
||||
}
|
||||
|
||||
async diagnostics(filePath: string): Promise<{ items: Diagnostic[] }> {
|
||||
const absPath = resolve(filePath)
|
||||
const uri = pathToFileURL(absPath).href
|
||||
|
||||
@ -1,5 +1,34 @@
|
||||
import type { LSPServerConfig } from "./types"
|
||||
|
||||
export const SYMBOL_KIND_MAP: Record<number, string> = {
|
||||
1: "File",
|
||||
2: "Module",
|
||||
3: "Namespace",
|
||||
4: "Package",
|
||||
5: "Class",
|
||||
6: "Method",
|
||||
7: "Property",
|
||||
8: "Field",
|
||||
9: "Constructor",
|
||||
10: "Enum",
|
||||
11: "Interface",
|
||||
12: "Function",
|
||||
13: "Variable",
|
||||
14: "Constant",
|
||||
15: "String",
|
||||
16: "Number",
|
||||
17: "Boolean",
|
||||
18: "Array",
|
||||
19: "Object",
|
||||
20: "Key",
|
||||
21: "Null",
|
||||
22: "EnumMember",
|
||||
23: "Struct",
|
||||
24: "Event",
|
||||
25: "Operator",
|
||||
26: "TypeParameter",
|
||||
}
|
||||
|
||||
export const SEVERITY_MAP: Record<number, string> = {
|
||||
1: "error",
|
||||
2: "warning",
|
||||
@ -7,6 +36,8 @@ export const SEVERITY_MAP: Record<number, string> = {
|
||||
4: "hint",
|
||||
}
|
||||
|
||||
export const DEFAULT_MAX_REFERENCES = 200
|
||||
export const DEFAULT_MAX_SYMBOLS = 200
|
||||
export const DEFAULT_MAX_DIAGNOSTICS = 200
|
||||
|
||||
export const LSP_INSTALL_HINTS: Record<string, string> = {
|
||||
|
||||
@ -4,4 +4,4 @@ export * from "./config"
|
||||
export * from "./client"
|
||||
export * from "./utils"
|
||||
// NOTE: lsp_servers removed - duplicates OpenCode's built-in LspServers
|
||||
export { lsp_diagnostics, lsp_prepare_rename, lsp_rename } from "./tools"
|
||||
export { lsp_goto_definition, lsp_find_references, lsp_symbols, lsp_diagnostics, lsp_prepare_rename, lsp_rename } from "./tools"
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import {
|
||||
DEFAULT_MAX_REFERENCES,
|
||||
DEFAULT_MAX_SYMBOLS,
|
||||
DEFAULT_MAX_DIAGNOSTICS,
|
||||
} from "./constants"
|
||||
import {
|
||||
withLspClient,
|
||||
formatLocation,
|
||||
formatDocumentSymbol,
|
||||
formatSymbolInfo,
|
||||
formatDiagnostic,
|
||||
filterDiagnosticsBySeverity,
|
||||
formatPrepareRenameResult,
|
||||
@ -11,14 +16,155 @@ import {
|
||||
formatApplyResult,
|
||||
} from "./utils"
|
||||
import type {
|
||||
Location,
|
||||
LocationLink,
|
||||
DocumentSymbol,
|
||||
SymbolInfo,
|
||||
Diagnostic,
|
||||
PrepareRenameResult,
|
||||
PrepareRenameDefaultBehavior,
|
||||
WorkspaceEdit,
|
||||
} from "./types"
|
||||
|
||||
// NOTE: lsp_goto_definition, lsp_find_references, lsp_symbols are removed
|
||||
// as they duplicate OpenCode's built-in LSP tools (LspGotoDefinition, LspFindReferences, LspDocumentSymbols, LspWorkspaceSymbols)
|
||||
export const lsp_goto_definition: ToolDefinition = tool({
|
||||
description: "Jump to symbol definition. Find WHERE something is defined.",
|
||||
args: {
|
||||
filePath: tool.schema.string(),
|
||||
line: tool.schema.number().min(1).describe("1-based"),
|
||||
character: tool.schema.number().min(0).describe("0-based"),
|
||||
},
|
||||
execute: async (args, context) => {
|
||||
try {
|
||||
const result = await withLspClient(args.filePath, async (client) => {
|
||||
return (await client.definition(args.filePath, args.line, args.character)) as
|
||||
| Location
|
||||
| Location[]
|
||||
| LocationLink[]
|
||||
| null
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
const output = "No definition found"
|
||||
return output
|
||||
}
|
||||
|
||||
const locations = Array.isArray(result) ? result : [result]
|
||||
if (locations.length === 0) {
|
||||
const output = "No definition found"
|
||||
return output
|
||||
}
|
||||
|
||||
const output = locations.map(formatLocation).join("\n")
|
||||
return output
|
||||
} catch (e) {
|
||||
const output = `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
return output
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const lsp_find_references: ToolDefinition = tool({
|
||||
description: "Find ALL usages/references of a symbol across the entire workspace.",
|
||||
args: {
|
||||
filePath: tool.schema.string(),
|
||||
line: tool.schema.number().min(1).describe("1-based"),
|
||||
character: tool.schema.number().min(0).describe("0-based"),
|
||||
includeDeclaration: tool.schema.boolean().optional().describe("Include the declaration itself"),
|
||||
},
|
||||
execute: async (args, context) => {
|
||||
try {
|
||||
const result = await withLspClient(args.filePath, async (client) => {
|
||||
return (await client.references(args.filePath, args.line, args.character, args.includeDeclaration ?? true)) as
|
||||
| Location[]
|
||||
| null
|
||||
})
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
const output = "No references found"
|
||||
return output
|
||||
}
|
||||
|
||||
const total = result.length
|
||||
const truncated = total > DEFAULT_MAX_REFERENCES
|
||||
const limited = truncated ? result.slice(0, DEFAULT_MAX_REFERENCES) : result
|
||||
const lines = limited.map(formatLocation)
|
||||
if (truncated) {
|
||||
lines.unshift(`Found ${total} references (showing first ${DEFAULT_MAX_REFERENCES}):`)
|
||||
}
|
||||
const output = lines.join("\n")
|
||||
return output
|
||||
} catch (e) {
|
||||
const output = `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
return output
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const lsp_symbols: ToolDefinition = tool({
|
||||
description: "Get symbols from file (document) or search across workspace. Use scope='document' for file outline, scope='workspace' for project-wide symbol search.",
|
||||
args: {
|
||||
filePath: tool.schema.string().describe("File path for LSP context"),
|
||||
scope: tool.schema.enum(["document", "workspace"]).default("document").describe("'document' for file symbols, 'workspace' for project-wide search"),
|
||||
query: tool.schema.string().optional().describe("Symbol name to search (required for workspace scope)"),
|
||||
limit: tool.schema.number().optional().describe("Max results (default 50)"),
|
||||
},
|
||||
execute: async (args, context) => {
|
||||
try {
|
||||
const scope = args.scope ?? "document"
|
||||
|
||||
if (scope === "workspace") {
|
||||
if (!args.query) {
|
||||
return "Error: 'query' is required for workspace scope"
|
||||
}
|
||||
|
||||
const result = await withLspClient(args.filePath, async (client) => {
|
||||
return (await client.workspaceSymbols(args.query!)) as SymbolInfo[] | null
|
||||
})
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return "No symbols found"
|
||||
}
|
||||
|
||||
const total = result.length
|
||||
const limit = Math.min(args.limit ?? DEFAULT_MAX_SYMBOLS, DEFAULT_MAX_SYMBOLS)
|
||||
const truncated = total > limit
|
||||
const limited = result.slice(0, limit)
|
||||
const lines = limited.map(formatSymbolInfo)
|
||||
if (truncated) {
|
||||
lines.unshift(`Found ${total} symbols (showing first ${limit}):`)
|
||||
}
|
||||
return lines.join("\n")
|
||||
} else {
|
||||
const result = await withLspClient(args.filePath, async (client) => {
|
||||
return (await client.documentSymbols(args.filePath)) as DocumentSymbol[] | SymbolInfo[] | null
|
||||
})
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return "No symbols found"
|
||||
}
|
||||
|
||||
const total = result.length
|
||||
const limit = Math.min(args.limit ?? DEFAULT_MAX_SYMBOLS, DEFAULT_MAX_SYMBOLS)
|
||||
const truncated = total > limit
|
||||
const limited = truncated ? result.slice(0, limit) : result
|
||||
|
||||
const lines: string[] = []
|
||||
if (truncated) {
|
||||
lines.push(`Found ${total} symbols (showing first ${limit}):`)
|
||||
}
|
||||
|
||||
if ("range" in limited[0]) {
|
||||
lines.push(...(limited as DocumentSymbol[]).map((s) => formatDocumentSymbol(s)))
|
||||
} else {
|
||||
lines.push(...(limited as SymbolInfo[]).map(formatSymbolInfo))
|
||||
}
|
||||
return lines.join("\n")
|
||||
}
|
||||
} catch (e) {
|
||||
return `Error: ${e instanceof Error ? e.message : String(e)}`
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const lsp_diagnostics: ToolDefinition = tool({
|
||||
description: "Get errors, warnings, hints from language server BEFORE running build.",
|
||||
|
||||
@ -17,6 +17,33 @@ export interface Range {
|
||||
end: Position
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
uri: string
|
||||
range: Range
|
||||
}
|
||||
|
||||
export interface LocationLink {
|
||||
targetUri: string
|
||||
targetRange: Range
|
||||
targetSelectionRange: Range
|
||||
originSelectionRange?: Range
|
||||
}
|
||||
|
||||
export interface SymbolInfo {
|
||||
name: string
|
||||
kind: number
|
||||
location: Location
|
||||
containerName?: string
|
||||
}
|
||||
|
||||
export interface DocumentSymbol {
|
||||
name: string
|
||||
kind: number
|
||||
range: Range
|
||||
selectionRange: Range
|
||||
children?: DocumentSymbol[]
|
||||
}
|
||||
|
||||
export interface Diagnostic {
|
||||
range: Range
|
||||
severity?: number
|
||||
|
||||
@ -3,8 +3,12 @@ import { fileURLToPath } from "node:url"
|
||||
import { existsSync, readFileSync, writeFileSync } from "fs"
|
||||
import { LSPClient, lspManager } from "./client"
|
||||
import { findServerForExtension } from "./config"
|
||||
import { SEVERITY_MAP } from "./constants"
|
||||
import { SYMBOL_KIND_MAP, SEVERITY_MAP } from "./constants"
|
||||
import type {
|
||||
Location,
|
||||
LocationLink,
|
||||
DocumentSymbol,
|
||||
SymbolInfo,
|
||||
Diagnostic,
|
||||
PrepareRenameResult,
|
||||
PrepareRenameDefaultBehavior,
|
||||
@ -106,11 +110,51 @@ export async function withLspClient<T>(filePath: string, fn: (client: LSPClient)
|
||||
}
|
||||
}
|
||||
|
||||
export function formatLocation(loc: Location | LocationLink): string {
|
||||
if ("targetUri" in loc) {
|
||||
const uri = uriToPath(loc.targetUri)
|
||||
const line = loc.targetRange.start.line + 1
|
||||
const char = loc.targetRange.start.character
|
||||
return `${uri}:${line}:${char}`
|
||||
}
|
||||
|
||||
const uri = uriToPath(loc.uri)
|
||||
const line = loc.range.start.line + 1
|
||||
const char = loc.range.start.character
|
||||
return `${uri}:${line}:${char}`
|
||||
}
|
||||
|
||||
export function formatSymbolKind(kind: number): string {
|
||||
return SYMBOL_KIND_MAP[kind] || `Unknown(${kind})`
|
||||
}
|
||||
|
||||
export function formatSeverity(severity: number | undefined): string {
|
||||
if (!severity) return "unknown"
|
||||
return SEVERITY_MAP[severity] || `unknown(${severity})`
|
||||
}
|
||||
|
||||
export function formatDocumentSymbol(symbol: DocumentSymbol, indent = 0): string {
|
||||
const prefix = " ".repeat(indent)
|
||||
const kind = formatSymbolKind(symbol.kind)
|
||||
const line = symbol.range.start.line + 1
|
||||
let result = `${prefix}${symbol.name} (${kind}) - line ${line}`
|
||||
|
||||
if (symbol.children && symbol.children.length > 0) {
|
||||
for (const child of symbol.children) {
|
||||
result += "\n" + formatDocumentSymbol(child, indent + 1)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function formatSymbolInfo(symbol: SymbolInfo): string {
|
||||
const kind = formatSymbolKind(symbol.kind)
|
||||
const loc = formatLocation(symbol.location)
|
||||
const container = symbol.containerName ? ` (in ${symbol.containerName})` : ""
|
||||
return `${symbol.name} (${kind})${container} - ${loc}`
|
||||
}
|
||||
|
||||
export function formatDiagnostic(diag: Diagnostic): string {
|
||||
const severity = formatSeverity(diag.severity)
|
||||
const line = diag.range.start.line + 1
|
||||
|
||||
@ -11,7 +11,7 @@ import { discoverSkills } from "../../features/opencode-skill-loader"
|
||||
import { getTaskToastManager } from "../../features/task-toast-manager"
|
||||
import type { ModelFallbackInfo } from "../../features/task-toast-manager/types"
|
||||
import { subagentSessions, getSessionAgent } from "../../features/claude-code-session-state"
|
||||
import { log } from "../../shared/logger"
|
||||
import { log, getAgentToolRestrictions } from "../../shared"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
|
||||
@ -322,6 +322,7 @@ Use \`background_output\` with task_id="${task.id}" to check progress.`
|
||||
...(resumeAgent !== undefined ? { agent: resumeAgent } : {}),
|
||||
...(resumeModel !== undefined ? { model: resumeModel } : {}),
|
||||
tools: {
|
||||
...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}),
|
||||
task: false,
|
||||
sisyphus_task: false,
|
||||
call_omo_agent: true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user