@hiai-gg/hiai-opencode 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.env.example +14 -18
  2. package/AGENTS.md +77 -23
  3. package/ARCHITECTURE.md +15 -17
  4. package/LICENSE.md +1 -0
  5. package/README.md +189 -94
  6. package/assets/cli/hiai-opencode.mjs +276 -0
  7. package/assets/mcp/playwright.mjs +7 -0
  8. package/config/hiai-opencode.schema.json +85 -50
  9. package/dist/config/defaults.d.ts +1 -3
  10. package/dist/config/index.d.ts +0 -1
  11. package/dist/config/platform-schema.d.ts +343 -4
  12. package/dist/config/schema/agent-overrides.d.ts +256 -0
  13. package/dist/config/schema/categories.d.ts +1 -1
  14. package/dist/config/schema/commands.d.ts +1 -0
  15. package/dist/config/schema/index.d.ts +2 -0
  16. package/dist/config/schema/oh-my-opencode-config.d.ts +267 -0
  17. package/dist/config/schema/skill-discovery.d.ts +11 -0
  18. package/dist/config/types.d.ts +33 -3
  19. package/dist/create-tools.d.ts +2 -0
  20. package/dist/features/builtin-commands/templates/mcp-status.d.ts +1 -0
  21. package/dist/features/builtin-commands/types.d.ts +1 -1
  22. package/dist/features/opencode-skill-loader/loader.d.ts +2 -0
  23. package/dist/index.js +744 -358
  24. package/dist/internals/plugins/websearch-cited/index.d.ts +7 -1
  25. package/dist/mcp/types.d.ts +1 -1
  26. package/dist/plugin/skill-discovery-config.d.ts +4 -0
  27. package/dist/plugin/tool-registry.d.ts +2 -0
  28. package/dist/shared/startup-diagnostics.d.ts +6 -0
  29. package/dist/tools/skill-mcp/tools.d.ts +2 -0
  30. package/hiai-opencode.json +55 -33
  31. package/package.json +4 -1
  32. package/src/agents/AGENTS.md +3 -4
  33. package/src/config/defaults.ts +186 -77
  34. package/src/config/index.ts +0 -1
  35. package/src/config/loader.test.ts +16 -1
  36. package/src/config/loader.ts +4 -2
  37. package/src/config/platform-schema.ts +53 -4
  38. package/src/config/schema/agent-overrides.ts +2 -0
  39. package/src/config/schema/commands.ts +1 -0
  40. package/src/config/schema/fast-apply.ts +4 -4
  41. package/src/config/schema/index.ts +2 -0
  42. package/src/config/schema/oh-my-opencode-config.ts +3 -0
  43. package/src/config/schema/skill-discovery.ts +25 -0
  44. package/src/config/types.ts +49 -2
  45. package/src/create-tools.ts +4 -1
  46. package/src/features/builtin-commands/commands.ts +7 -0
  47. package/src/features/builtin-commands/templates/mcp-status.ts +36 -0
  48. package/src/features/builtin-commands/types.ts +1 -1
  49. package/src/features/builtin-skills/skills/playwright.ts +24 -2
  50. package/src/features/opencode-skill-loader/loader.ts +11 -0
  51. package/src/index.ts +53 -14
  52. package/src/internals/plugins/websearch-cited/index.ts +10 -5
  53. package/src/lsp/index.ts +1 -0
  54. package/src/mcp/registry.ts +6 -1
  55. package/src/plugin/hooks/create-tool-guard-hooks.ts +1 -1
  56. package/src/plugin/skill-context.ts +31 -13
  57. package/src/plugin/skill-discovery-config.ts +32 -0
  58. package/src/plugin/tool-registry.ts +4 -0
  59. package/src/plugin-handlers/agent-config-handler.ts +20 -13
  60. package/src/plugin-handlers/command-config-handler.ts +22 -12
  61. package/src/shared/migration/agent-names.ts +5 -5
  62. package/src/shared/startup-diagnostics.ts +77 -0
  63. package/src/tools/skill-mcp/tools.ts +45 -7
  64. package/src/config/models.ts +0 -32
@@ -1,4 +1,5 @@
1
1
  import { join } from "node:path"
2
+ import { existsSync } from "node:fs"
2
3
  import type { McpServerConfig } from "../config/types.js"
3
4
 
4
5
  export type HiaiMcpName =
@@ -22,7 +23,11 @@ export interface HiaiMcpRegistryEntry {
22
23
  }
23
24
 
24
25
  function resolveAssetScript(...segments: string[]): string {
25
- return join(import.meta.dirname, "..", "assets", ...segments)
26
+ const candidates = [
27
+ join(import.meta.dirname, "..", "assets", ...segments),
28
+ join(import.meta.dirname, "..", "..", "assets", ...segments),
29
+ ]
30
+ return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0]
26
31
  }
27
32
 
28
33
  function createNpmPackageCommand(pkg: string, ...args: string[]): string[] {
@@ -136,7 +136,7 @@ export function createToolGuardHooks(args: {
136
136
  : null
137
137
 
138
138
  const fastApply = isHookEnabled("fast-apply")
139
- ? safeHook("fast-apply", () => createFastApplyHook(pluginConfig.fast_apply ?? { enabled: false, ollama_url: "http://localhost:11434", model: "qwen3.5:9b", timeout: 30000 }))
139
+ ? safeHook("fast-apply", () => createFastApplyHook(pluginConfig.fast_apply ?? { enabled: false, ollama_url: "", model: "", timeout: 30000 }))
140
140
  : null
141
141
 
142
142
  return {
@@ -8,6 +8,7 @@ import type {
8
8
 
9
9
  import {
10
10
  discoverConfigSourceSkills,
11
+ discoverManagedPluginSkills,
11
12
  discoverUserClaudeSkills,
12
13
  discoverProjectClaudeSkills,
13
14
  discoverOpencodeGlobalSkills,
@@ -18,6 +19,7 @@ import {
18
19
  } from "../features/opencode-skill-loader"
19
20
  import { createBuiltinSkills } from "../features/builtin-skills"
20
21
  import { getSystemMcpServerNames } from "../features/claude-code-mcp-loader"
22
+ import { resolveSkillDiscoveryConfig } from "./skill-discovery-config"
21
23
 
22
24
  export type SkillContext = {
23
25
  mergedSkills: LoadedSkill[]
@@ -71,21 +73,37 @@ export async function createSkillContext(args: {
71
73
  return true
72
74
  })
73
75
 
74
- const includeClaudeSkills = pluginConfig.claude_code?.skills !== false
75
- const [configSourceSkills, userSkills, globalSkills, projectSkills, opencodeProjectSkills, agentsProjectSkills, agentsGlobalSkills] =
76
+ const discovery = resolveSkillDiscoveryConfig(pluginConfig)
77
+ const [
78
+ managedPluginSkills,
79
+ configSourceSkills,
80
+ userSkills,
81
+ globalSkills,
82
+ projectSkills,
83
+ opencodeProjectSkills,
84
+ agentsProjectSkills,
85
+ agentsGlobalSkills,
86
+ ] =
76
87
  await Promise.all([
77
- discoverConfigSourceSkills({
78
- config: pluginConfig.skills,
79
- configDir: directory,
80
- }),
81
- includeClaudeSkills ? discoverUserClaudeSkills() : Promise.resolve([]),
82
- discoverOpencodeGlobalSkills(),
83
- includeClaudeSkills ? discoverProjectClaudeSkills(directory) : Promise.resolve([]),
84
- discoverOpencodeProjectSkills(directory),
85
- discoverProjectAgentsSkills(directory),
86
- discoverGlobalAgentsSkills(),
88
+ discoverManagedPluginSkills(),
89
+ discovery.config_sources
90
+ ? discoverConfigSourceSkills({
91
+ config: pluginConfig.skills,
92
+ configDir: directory,
93
+ })
94
+ : Promise.resolve([]),
95
+ discovery.global_claude ? discoverUserClaudeSkills() : Promise.resolve([]),
96
+ discovery.global_opencode ? discoverOpencodeGlobalSkills() : Promise.resolve([]),
97
+ discovery.project_claude ? discoverProjectClaudeSkills(directory) : Promise.resolve([]),
98
+ discovery.project_opencode ? discoverOpencodeProjectSkills(directory) : Promise.resolve([]),
99
+ discovery.project_agents ? discoverProjectAgentsSkills(directory) : Promise.resolve([]),
100
+ discovery.global_agents ? discoverGlobalAgentsSkills() : Promise.resolve([]),
87
101
  ])
88
102
 
103
+ const filteredManagedPluginSkills = filterProviderGatedSkills(
104
+ managedPluginSkills,
105
+ browserProvider,
106
+ )
89
107
  const filteredConfigSourceSkills = filterProviderGatedSkills(
90
108
  configSourceSkills,
91
109
  browserProvider,
@@ -109,7 +127,7 @@ export async function createSkillContext(args: {
109
127
  const mergedSkills = mergeSkills(
110
128
  builtinSkills,
111
129
  pluginConfig.skills,
112
- filteredConfigSourceSkills,
130
+ [...filteredManagedPluginSkills, ...filteredConfigSourceSkills],
113
131
  [...filteredUserSkills, ...filteredAgentsGlobalSkills],
114
132
  filteredGlobalSkills,
115
133
  [...filteredProjectSkills, ...filteredAgentsProjectSkills],
@@ -0,0 +1,32 @@
1
+ import type { HiaiOpenCodeConfig } from "../config"
2
+ import type { SkillDiscoveryConfig } from "../config/schema"
3
+
4
+ export type ResolvedSkillDiscoveryConfig = Required<SkillDiscoveryConfig>
5
+
6
+ const DEFAULT_SKILL_DISCOVERY: ResolvedSkillDiscoveryConfig = {
7
+ config_sources: true,
8
+ project_opencode: true,
9
+ global_opencode: false,
10
+ project_claude: false,
11
+ global_claude: false,
12
+ project_agents: false,
13
+ global_agents: false,
14
+ }
15
+
16
+ export function resolveSkillDiscoveryConfig(
17
+ pluginConfig: HiaiOpenCodeConfig,
18
+ ): ResolvedSkillDiscoveryConfig {
19
+ const resolved: ResolvedSkillDiscoveryConfig = {
20
+ ...DEFAULT_SKILL_DISCOVERY,
21
+ ...(pluginConfig.skill_discovery ?? {}),
22
+ }
23
+
24
+ // Compatibility switch: historically this only controlled Claude-style skills.
25
+ // Keep that meaning, but make it stronger for those two sources.
26
+ if (pluginConfig.claude_code?.skills === false) {
27
+ resolved.project_claude = false
28
+ resolved.global_claude = false
29
+ }
30
+
31
+ return resolved
32
+ }
@@ -5,6 +5,7 @@ import type {
5
5
  AvailableCategory,
6
6
  } from "../agents/dynamic-agent-prompt-builder"
7
7
  import type { HiaiOpenCodeConfig } from "../config"
8
+ import type { McpServerConfig } from "../config/types"
8
9
  import { isInteractiveBashEnabled } from "../create-runtime-tmux-config"
9
10
  import type { PluginContext, ToolsRecord } from "./types"
10
11
 
@@ -146,6 +147,7 @@ export function createToolRegistry(args: {
146
147
  managers: Pick<Managers, "backgroundManager" | "tmuxSessionManager" | "skillMcpManager">
147
148
  skillContext: SkillContext
148
149
  availableCategories: AvailableCategory[]
150
+ builtinMcp?: Record<string, McpServerConfig>
149
151
  interactiveBashEnabled?: boolean
150
152
  toolFactories?: Partial<ToolRegistryFactories>
151
153
  }): ToolRegistryResult {
@@ -155,6 +157,7 @@ export function createToolRegistry(args: {
155
157
  managers,
156
158
  skillContext,
157
159
  availableCategories,
160
+ builtinMcp,
158
161
  interactiveBashEnabled = isInteractiveBashEnabled(),
159
162
  toolFactories,
160
163
  } = args
@@ -216,6 +219,7 @@ export function createToolRegistry(args: {
216
219
  manager: managers.skillMcpManager,
217
220
  getLoadedSkills: () => skillContext.mergedSkills,
218
221
  getSessionID: getSessionIDForMcp,
222
+ builtinMcp,
219
223
  })
220
224
 
221
225
  const commands = factories.discoverCommandsSync(ctx.directory, {
@@ -7,6 +7,7 @@ import { AGENT_NAME_MAP } from "../shared/migration";
7
7
  import { registerAgentName } from "../features/claude-code-session-state";
8
8
  import {
9
9
  discoverConfigSourceSkills,
10
+ discoverManagedPluginSkills,
10
11
  deduplicateSkillsByName,
11
12
  discoverGlobalAgentsSkills,
12
13
  discoverOpencodeGlobalSkills,
@@ -31,6 +32,7 @@ import {
31
32
  filterProtectedAgentOverrides,
32
33
  } from "./agent-override-protection";
33
34
  import { buildStrategistAgentConfig } from "./strategist-agent-config-builder";
35
+ import { resolveSkillDiscoveryConfig } from "../plugin/skill-discovery-config";
34
36
 
35
37
  type AgentConfigRecord = Record<string, Record<string, unknown> | undefined> & {
36
38
  build?: Record<string, unknown>;
@@ -130,8 +132,9 @@ export async function applyAgentConfig(params: {
130
132
  },
131
133
  ) as typeof params.pluginConfig.disabled_agents;
132
134
 
133
- const includeClaudeSkillsForAwareness = params.pluginConfig.claude_code?.skills ?? true;
135
+ const discovery = resolveSkillDiscoveryConfig(params.pluginConfig);
134
136
  const [
137
+ discoveredManagedPluginSkills,
135
138
  discoveredConfigSourceSkills,
136
139
  discoveredUserSkills,
137
140
  discoveredProjectSkills,
@@ -140,23 +143,27 @@ export async function applyAgentConfig(params: {
140
143
  discoveredOpencodeProjectSkills,
141
144
  discoveredGlobalAgentsSkills,
142
145
  ] = await Promise.all([
143
- discoverConfigSourceSkills({
144
- config: params.pluginConfig.skills,
145
- configDir: params.ctx.directory,
146
- }),
147
- includeClaudeSkillsForAwareness ? discoverUserClaudeSkills() : Promise.resolve([]),
148
- includeClaudeSkillsForAwareness
149
- ? discoverProjectClaudeSkills(params.ctx.directory)
150
- : Promise.resolve([]),
151
- includeClaudeSkillsForAwareness
146
+ discoverManagedPluginSkills(),
147
+ discovery.config_sources
148
+ ? discoverConfigSourceSkills({
149
+ config: params.pluginConfig.skills,
150
+ configDir: params.ctx.directory,
151
+ })
152
+ : Promise.resolve([]),
153
+ discovery.global_claude ? discoverUserClaudeSkills() : Promise.resolve([]),
154
+ discovery.project_claude
155
+ ? discoverProjectClaudeSkills(params.ctx.directory)
156
+ : Promise.resolve([]),
157
+ discovery.project_agents
152
158
  ? discoverProjectAgentsSkills(params.ctx.directory)
153
159
  : Promise.resolve([]),
154
- discoverOpencodeGlobalSkills(),
155
- discoverOpencodeProjectSkills(params.ctx.directory),
156
- includeClaudeSkillsForAwareness ? discoverGlobalAgentsSkills() : Promise.resolve([]),
160
+ discovery.global_opencode ? discoverOpencodeGlobalSkills() : Promise.resolve([]),
161
+ discovery.project_opencode ? discoverOpencodeProjectSkills(params.ctx.directory) : Promise.resolve([]),
162
+ discovery.global_agents ? discoverGlobalAgentsSkills() : Promise.resolve([]),
157
163
  ]);
158
164
 
159
165
  const allDiscoveredSkills = [
166
+ ...discoveredManagedPluginSkills,
160
167
  ...discoveredConfigSourceSkills,
161
168
  ...discoveredOpencodeProjectSkills,
162
169
  ...discoveredProjectSkills,
@@ -12,6 +12,7 @@ import {
12
12
  import { loadBuiltinCommands } from "../features/builtin-commands";
13
13
  import {
14
14
  discoverConfigSourceSkills,
15
+ loadManagedPluginSkills,
15
16
  loadGlobalAgentsSkills,
16
17
  loadProjectAgentsSkills,
17
18
  loadUserSkills,
@@ -26,6 +27,7 @@ import {
26
27
  log,
27
28
  } from "../shared";
28
29
  import type { PluginComponents } from "./plugin-components-loader";
30
+ import { resolveSkillDiscoveryConfig } from "../plugin/skill-discovery-config";
29
31
 
30
32
  export async function applyCommandConfig(params: {
31
33
  config: Record<string, unknown>;
@@ -39,10 +41,13 @@ export async function applyCommandConfig(params: {
39
41
  const systemCommands = (params.config.command as Record<string, unknown>) ?? {};
40
42
 
41
43
  const includeClaudeCommands = params.pluginConfig.claude_code?.commands ?? true;
42
- const includeClaudeSkills = params.pluginConfig.claude_code?.skills ?? true;
44
+ const discovery = resolveSkillDiscoveryConfig(params.pluginConfig);
43
45
 
44
46
  const externalSkillPlugin = detectExternalSkillPlugin(params.ctx.directory);
45
- if (includeClaudeSkills && externalSkillPlugin.detected) {
47
+ if (
48
+ (discovery.project_claude || discovery.global_claude || discovery.global_opencode) &&
49
+ externalSkillPlugin.detected
50
+ ) {
46
51
  log(getSkillPluginConflictWarning(externalSkillPlugin.pluginName!));
47
52
  }
48
53
 
@@ -52,6 +57,7 @@ export async function applyCommandConfig(params: {
52
57
  projectCommands,
53
58
  opencodeGlobalCommands,
54
59
  opencodeProjectCommands,
60
+ managedPluginSkills,
55
61
  userSkills,
56
62
  globalAgentsSkills,
57
63
  projectSkills,
@@ -59,24 +65,28 @@ export async function applyCommandConfig(params: {
59
65
  opencodeGlobalSkills,
60
66
  opencodeProjectSkills,
61
67
  ] = await Promise.all([
62
- discoverConfigSourceSkills({
63
- config: params.pluginConfig.skills,
64
- configDir: params.ctx.directory,
65
- }),
68
+ discovery.config_sources
69
+ ? discoverConfigSourceSkills({
70
+ config: params.pluginConfig.skills,
71
+ configDir: params.ctx.directory,
72
+ })
73
+ : Promise.resolve([]),
66
74
  includeClaudeCommands ? loadUserCommands() : Promise.resolve({}),
67
75
  includeClaudeCommands ? loadProjectCommands(params.ctx.directory) : Promise.resolve({}),
68
76
  loadOpencodeGlobalCommands(),
69
77
  loadOpencodeProjectCommands(params.ctx.directory),
70
- includeClaudeSkills ? loadUserSkills() : Promise.resolve({}),
71
- includeClaudeSkills ? loadGlobalAgentsSkills() : Promise.resolve({}),
72
- includeClaudeSkills ? loadProjectSkills(params.ctx.directory) : Promise.resolve({}),
73
- includeClaudeSkills ? loadProjectAgentsSkills(params.ctx.directory) : Promise.resolve({}),
74
- loadOpencodeGlobalSkills(),
75
- loadOpencodeProjectSkills(params.ctx.directory),
78
+ loadManagedPluginSkills(),
79
+ discovery.global_claude ? loadUserSkills() : Promise.resolve({}),
80
+ discovery.global_agents ? loadGlobalAgentsSkills() : Promise.resolve({}),
81
+ discovery.project_claude ? loadProjectSkills(params.ctx.directory) : Promise.resolve({}),
82
+ discovery.project_agents ? loadProjectAgentsSkills(params.ctx.directory) : Promise.resolve({}),
83
+ discovery.global_opencode ? loadOpencodeGlobalSkills() : Promise.resolve({}),
84
+ discovery.project_opencode ? loadOpencodeProjectSkills(params.ctx.directory) : Promise.resolve({}),
76
85
  ]);
77
86
 
78
87
  params.config.command = {
79
88
  ...builtinCommands,
89
+ ...managedPluginSkills,
80
90
  ...skillsToCommandDefinitionRecord(configSourceSkills),
81
91
  ...userCommands,
82
92
  ...userSkills,
@@ -64,10 +64,10 @@ export const AGENT_NAME_MAP: Record<string, string> = {
64
64
  // Designer
65
65
  designer: "designer",
66
66
 
67
- // Multimodal (runtime key remains "ui" for compatibility)
68
- ui: "ui",
69
- vision: "ui",
70
- multimodal: "ui",
67
+ // Multimodal / Vision
68
+ ui: "multimodal",
69
+ vision: "multimodal",
70
+ multimodal: "multimodal",
71
71
  }
72
72
 
73
73
  export const BUILTIN_AGENT_NAMES = new Set([
@@ -77,7 +77,7 @@ export const BUILTIN_AGENT_NAMES = new Set([
77
77
  "critic",
78
78
  "designer",
79
79
  "researcher",
80
- "ui",
80
+ "multimodal",
81
81
  "platform-manager",
82
82
  "guard",
83
83
  ])
@@ -0,0 +1,77 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+
4
+ import type { HiaiOpenCodeConfig, HiaiOpencodeConfig } from "../config"
5
+ import { HIAI_MCP_REGISTRY } from "../mcp/registry"
6
+ import { parseJsoncSafe } from "./jsonc-parser"
7
+ import { getOpenCodeConfigPaths } from "./opencode-config-dir"
8
+ import { PLUGIN_NAME } from "./plugin-identity"
9
+
10
+ interface OpenCodeConfig {
11
+ plugin?: Array<string | [string, ...unknown[]]>
12
+ }
13
+
14
+ function readPlugins(configPath: string): string[] {
15
+ if (!existsSync(configPath)) return []
16
+
17
+ try {
18
+ const content = readFileSync(configPath, "utf-8")
19
+ const parsed = parseJsoncSafe<OpenCodeConfig>(content)
20
+ return (parsed.data?.plugin ?? [])
21
+ .map((entry) => typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : "")
22
+ .filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
23
+ } catch {
24
+ return []
25
+ }
26
+ }
27
+
28
+ export function warnIfListPluginEntry(directory: string): void {
29
+ const globalPaths = getOpenCodeConfigPaths({ binary: "opencode", version: null })
30
+ const candidates = [
31
+ join(directory, ".opencode", "opencode.json"),
32
+ join(directory, ".opencode", "opencode.jsonc"),
33
+ globalPaths.configJson,
34
+ globalPaths.configJsonc,
35
+ ]
36
+
37
+ for (const configPath of candidates) {
38
+ const plugins = readPlugins(configPath)
39
+ if (!plugins.includes("list")) continue
40
+
41
+ console.warn(`[hiai-opencode] WARNING: ${configPath} contains plugin: ["list"].`)
42
+ console.warn("[hiai-opencode] This can prevent hiai-opencode MCP servers from loading from that config scope.")
43
+ console.warn(`[hiai-opencode] Update it to: plugin: ["${PLUGIN_NAME}"]`)
44
+ }
45
+ }
46
+
47
+ function hasConfigAuthFallback(pluginConfig: HiaiOpenCodeConfig, envName: string): boolean {
48
+ if (envName === "FIRECRAWL_API_KEY") return !!pluginConfig.auth?.firecrawl?.trim()
49
+ if (envName === "STITCH_AI_API_KEY") return !!pluginConfig.auth?.stitch?.trim()
50
+ if (envName === "CONTEXT7_API_KEY") return !!pluginConfig.auth?.context7?.trim()
51
+ return false
52
+ }
53
+
54
+ export function warnMissingRequiredMcpEnv(args: {
55
+ pluginConfig: HiaiOpenCodeConfig
56
+ platformConfig: HiaiOpencodeConfig
57
+ }): void {
58
+ const disabled = new Set(args.pluginConfig.disabled_mcps ?? [])
59
+ const mcpConfig = args.platformConfig.mcp ?? {}
60
+
61
+ for (const [name, entry] of Object.entries(HIAI_MCP_REGISTRY)) {
62
+ if (disabled.has(name)) continue
63
+ if (mcpConfig[name]?.enabled === false) continue
64
+ if (!entry.requiredEnv || entry.requiredEnv.length === 0) continue
65
+
66
+ const missing = entry.requiredEnv.filter((envName) =>
67
+ !process.env[envName]?.trim() && !hasConfigAuthFallback(args.pluginConfig, envName)
68
+ )
69
+
70
+ if (missing.length === 0) continue
71
+
72
+ console.warn(
73
+ `[hiai-opencode] MCP "${name}" is enabled but missing required env: ${missing.join(", ")}.`
74
+ + " The plugin will continue to load; set the key or disable this MCP in hiai-opencode.json.",
75
+ )
76
+ }
77
+ }
@@ -1,14 +1,17 @@
1
1
  import { tool, type ToolDefinition } from "@opencode-ai/plugin"
2
2
  import type { ToolContext } from "@opencode-ai/plugin/tool"
3
3
  import { BUILTIN_MCP_TOOL_HINTS, SKILL_MCP_DESCRIPTION } from "./constants"
4
+ import type { McpServerConfig } from "../../config/types"
4
5
  import type { SkillMcpArgs } from "./types"
5
6
  import type { SkillMcpManager, SkillMcpClientInfo, SkillMcpServerContext } from "../../features/skill-mcp-manager"
7
+ import type { ClaudeCodeMcpServer } from "../../features/claude-code-mcp-loader/types"
6
8
  import type { LoadedSkill } from "../../features/opencode-skill-loader/types"
7
9
 
8
10
  interface SkillMcpToolOptions {
9
11
  manager: SkillMcpManager
10
12
  getLoadedSkills: () => LoadedSkill[]
11
13
  getSessionID?: () => string | undefined
14
+ builtinMcp?: Record<string, McpServerConfig>
12
15
  }
13
16
 
14
17
  type OperationType = { type: "tool" | "resource" | "prompt"; name: string }
@@ -60,6 +63,28 @@ function findMcpServer(
60
63
  return null
61
64
  }
62
65
 
66
+ function convertBuiltinMcpConfig(config: McpServerConfig): ClaudeCodeMcpServer | null {
67
+ if (config.enabled === false) return null
68
+
69
+ if (config.type === "remote") {
70
+ return {
71
+ type: "http",
72
+ url: config.url,
73
+ headers: config.headers,
74
+ }
75
+ }
76
+
77
+ const [command, ...args] = config.command ?? []
78
+ if (!command) return null
79
+
80
+ return {
81
+ type: "stdio",
82
+ command,
83
+ args,
84
+ env: config.environment,
85
+ }
86
+ }
87
+
63
88
  function formatAvailableMcps(skills: LoadedSkill[]): string {
64
89
  const mcps: string[] = []
65
90
  for (const skill of skills) {
@@ -72,6 +97,14 @@ function formatAvailableMcps(skills: LoadedSkill[]): string {
72
97
  return mcps.length > 0 ? mcps.join("\n") : " (none found)"
73
98
  }
74
99
 
100
+ function formatAvailableBuiltinMcps(builtinMcp: Record<string, McpServerConfig> | undefined): string {
101
+ const names = Object.entries(builtinMcp ?? {})
102
+ .filter(([, config]) => config.enabled !== false)
103
+ .map(([name]) => ` - "${name}" from hiai-opencode config`)
104
+
105
+ return names.length > 0 ? names.join("\n") : " (none found)"
106
+ }
107
+
75
108
  function formatBuiltinMcpHint(mcpName: string): string | null {
76
109
  const nativeTools = BUILTIN_MCP_TOOL_HINTS[mcpName]
77
110
  if (!nativeTools) return null
@@ -119,7 +152,7 @@ export function applyGrepFilter(output: string, pattern: string | undefined): st
119
152
  }
120
153
 
121
154
  export function createSkillMcpTool(options: SkillMcpToolOptions): ToolDefinition {
122
- const { manager, getLoadedSkills, getSessionID } = options
155
+ const { manager, getLoadedSkills, getSessionID, builtinMcp } = options
123
156
 
124
157
  return tool({
125
158
  description: SKILL_MCP_DESCRIPTION,
@@ -141,8 +174,10 @@ export function createSkillMcpTool(options: SkillMcpToolOptions): ToolDefinition
141
174
  const operation = validateOperationParams(args)
142
175
  const skills = getLoadedSkills()
143
176
  const found = findMcpServer(args.mcp_name, skills)
177
+ const builtinConfig = builtinMcp?.[args.mcp_name]
178
+ const convertedBuiltinConfig = builtinConfig ? convertBuiltinMcpConfig(builtinConfig) : null
144
179
 
145
- if (!found) {
180
+ if (!found && !convertedBuiltinConfig) {
146
181
  const builtinHint = formatBuiltinMcpHint(args.mcp_name)
147
182
  if (builtinHint) {
148
183
  throw new Error(builtinHint)
@@ -153,7 +188,10 @@ export function createSkillMcpTool(options: SkillMcpToolOptions): ToolDefinition
153
188
  `Available MCP servers in loaded skills:\n` +
154
189
  formatAvailableMcps(skills) +
155
190
  `\n\n` +
156
- `Hint: Load the skill first using the 'skill' tool, then call skill_mcp.`,
191
+ `Available MCP servers in hiai-opencode config:\n` +
192
+ formatAvailableBuiltinMcps(builtinMcp) +
193
+ `\n\n` +
194
+ `Hint: Load the skill first for skill-embedded MCPs. Builtin hiai-opencode MCPs can be called directly when enabled in hiai-opencode.json.`,
157
195
  )
158
196
  }
159
197
 
@@ -164,14 +202,14 @@ export function createSkillMcpTool(options: SkillMcpToolOptions): ToolDefinition
164
202
 
165
203
  const info: SkillMcpClientInfo = {
166
204
  serverName: args.mcp_name,
167
- skillName: found.skill.name,
205
+ skillName: found?.skill.name ?? "hiai-opencode",
168
206
  sessionID,
169
- scope: found.skill.scope,
207
+ scope: found?.skill.scope ?? "user",
170
208
  }
171
209
 
172
210
  const context: SkillMcpServerContext = {
173
- config: found.config,
174
- skillName: found.skill.name,
211
+ config: found?.config ?? convertedBuiltinConfig!,
212
+ skillName: found?.skill.name ?? "hiai-opencode",
175
213
  }
176
214
 
177
215
  const parsedArgs = parseArguments(args.arguments)
@@ -1,32 +0,0 @@
1
- export const MODEL_ROLE_GUIDE = [
2
- "fast: cheap/default for bounded helpers, researcher-style scans, platform chores",
3
- "mid: balanced default for steady execution/review work",
4
- "high: stronger general-purpose model for primary implementation and planning",
5
- "ultrahigh: highest-cost/high-accuracy slot for hard architecture or critical decisions",
6
- "vision: preferred for UI/media/multimodal interpretation and visual work",
7
- "reasoning: preferred for deeper multi-step reasoning when latency/cost are acceptable",
8
- ] as const;
9
-
10
- export const PROVIDER_MODEL_RULES = [
11
- "openai: use `openai/<model>` for direct OpenAI calls, e.g. `openai/gpt-5` or `openai/o1`",
12
- "anthropic: use `anthropic/<model>`, e.g. `anthropic/claude-3.5-sonnet`",
13
- "deepseek: use `deepseek/<model>` when connected directly",
14
- "glm: use `z-ai/<model>` or the provider id exposed by your gateway/client",
15
- "minimax: use `minimax/<model>`",
16
- "qwen: use `qwen/<model>`",
17
- "ollama: use the native local model id in Ollama config, e.g. `qwen3.5:4b`",
18
- "openrouter: use `openrouter/<vendor>/<model>`, e.g. `openrouter/anthropic/claude-3.5-sonnet`",
19
- "rule: store fully qualified model ids in config; avoid local aliases like `fast`, `sonnet`, or provider-less ids",
20
- ] as const;
21
-
22
- export const MODEL_PRESETS = {
23
- fast: "openrouter/google/gemini-2.0-flash",
24
- mid: "openrouter/anthropic/claude-3.5-sonnet",
25
- high: "openrouter/anthropic/claude-3.5-opus",
26
- ultrahigh: "openrouter/openai/gpt-4o",
27
- vision: "openrouter/google/gemini-2.0-pro-exp-02-05",
28
- reasoning: "openrouter/openai/o1",
29
- strategist: "openrouter/z-ai/glm-5.1",
30
- critic: "openrouter/qwen/qwen2.5-72b-instruct",
31
- writing: "openrouter/kimi/kimi-latest",
32
- } as const;