@hiai-gg/hiai-opencode 0.1.3 → 0.1.4

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 (51) hide show
  1. package/.env.example +14 -18
  2. package/AGENTS.md +75 -23
  3. package/ARCHITECTURE.md +11 -14
  4. package/LICENSE.md +1 -0
  5. package/README.md +177 -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 +113 -1
  9. package/dist/config/defaults.d.ts +0 -3
  10. package/dist/config/index.d.ts +0 -1
  11. package/dist/config/platform-schema.d.ts +70 -0
  12. package/dist/config/schema/agent-overrides.d.ts +256 -0
  13. package/dist/config/schema/categories.d.ts +2 -2
  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 +12 -1
  19. package/dist/features/builtin-commands/templates/mcp-status.d.ts +1 -0
  20. package/dist/features/builtin-commands/types.d.ts +1 -1
  21. package/dist/features/opencode-skill-loader/loader.d.ts +2 -0
  22. package/dist/index.js +692 -541
  23. package/dist/plugin/skill-discovery-config.d.ts +4 -0
  24. package/dist/shared/startup-diagnostics.d.ts +6 -0
  25. package/hiai-opencode.json +191 -35
  26. package/package.json +4 -1
  27. package/src/agents/AGENTS.md +3 -4
  28. package/src/config/defaults.ts +60 -81
  29. package/src/config/index.ts +0 -1
  30. package/src/config/platform-schema.ts +17 -2
  31. package/src/config/schema/agent-overrides.ts +2 -0
  32. package/src/config/schema/commands.ts +1 -0
  33. package/src/config/schema/fast-apply.ts +4 -4
  34. package/src/config/schema/index.ts +2 -0
  35. package/src/config/schema/oh-my-opencode-config.ts +3 -0
  36. package/src/config/schema/skill-discovery.ts +25 -0
  37. package/src/config/types.ts +16 -0
  38. package/src/features/builtin-commands/commands.ts +7 -0
  39. package/src/features/builtin-commands/templates/mcp-status.ts +36 -0
  40. package/src/features/builtin-commands/types.ts +1 -1
  41. package/src/features/builtin-skills/skills/playwright.ts +24 -2
  42. package/src/features/opencode-skill-loader/loader.ts +11 -0
  43. package/src/index.ts +14 -13
  44. package/src/plugin/hooks/create-tool-guard-hooks.ts +1 -1
  45. package/src/plugin/skill-context.ts +31 -13
  46. package/src/plugin/skill-discovery-config.ts +32 -0
  47. package/src/plugin-handlers/agent-config-handler.ts +20 -13
  48. package/src/plugin-handlers/command-config-handler.ts +22 -12
  49. package/src/shared/migration/agent-names.ts +5 -5
  50. package/src/shared/startup-diagnostics.ts +77 -0
  51. package/src/config/models.ts +0 -32
@@ -32,6 +32,8 @@ export const LEGACY_AGENT_ALIAS_NAMES = [
32
32
  "zoe",
33
33
  "build",
34
34
  "pre-plan",
35
+ "manager",
36
+ "vision",
35
37
  "logician",
36
38
  "librarian",
37
39
  "explore",
@@ -54,6 +56,8 @@ export const LEGACY_AGENT_ALIAS_TO_CANONICAL: Record<
54
56
  zoe: "bob",
55
57
  build: "bob",
56
58
  "pre-plan": "strategist",
59
+ manager: "platform-manager",
60
+ vision: "multimodal",
57
61
  logician: "strategist",
58
62
  librarian: "researcher",
59
63
  explore: "researcher",
@@ -135,6 +139,16 @@ export interface SkillsConfig {
135
139
  disabled?: string[];
136
140
  }
137
141
 
142
+ export interface SkillDiscoveryConfig {
143
+ config_sources?: boolean;
144
+ project_opencode?: boolean;
145
+ global_opencode?: boolean;
146
+ project_claude?: boolean;
147
+ global_claude?: boolean;
148
+ project_agents?: boolean;
149
+ global_agents?: boolean;
150
+ }
151
+
138
152
  export interface PermissionsConfig {
139
153
  read?: Record<string, string>;
140
154
  edit?: Record<string, string>;
@@ -148,6 +162,7 @@ export interface AuthKeys {
148
162
  openrouter?: string;
149
163
  stitch?: string;
150
164
  firecrawl?: string;
165
+ context7?: string;
151
166
  }
152
167
 
153
168
  export interface OllamaConfig {
@@ -168,6 +183,7 @@ export interface HiaiOpencodeConfig {
168
183
  lsp?: Record<string, LspServerConfig>;
169
184
  subtask2?: Subtask2Config;
170
185
  skills?: SkillsConfig;
186
+ skill_discovery?: SkillDiscoveryConfig;
171
187
  permissions?: PermissionsConfig;
172
188
  auth?: AuthKeys;
173
189
  ollama?: OllamaConfig;
@@ -8,6 +8,7 @@ import { REFACTOR_TEMPLATE } from "./templates/refactor"
8
8
  import { START_WORK_TEMPLATE } from "./templates/start-work"
9
9
  import { HANDOFF_TEMPLATE } from "./templates/handoff"
10
10
  import { REMOVE_AI_SLOPS_TEMPLATE } from "./templates/remove-ai-slops"
11
+ import { MCP_STATUS_TEMPLATE } from "./templates/mcp-status"
11
12
 
12
13
  interface LoadBuiltinCommandsOptions {
13
14
  useRegisteredAgents?: boolean
@@ -121,6 +122,12 @@ $ARGUMENTS
121
122
  </user-request>`,
122
123
  argumentHint: "[goal]",
123
124
  },
125
+ "mcp-status": {
126
+ description: "(builtin) Show hiai-opencode MCP server status, missing keys, and local runtime availability",
127
+ template: `<command-instruction>
128
+ ${MCP_STATUS_TEMPLATE}
129
+ </command-instruction>`,
130
+ },
124
131
  }
125
132
  }
126
133
 
@@ -0,0 +1,36 @@
1
+ export const MCP_STATUS_TEMPLATE = `# MCP Status Command
2
+
3
+ ## Purpose
4
+
5
+ Use /mcp-status to show the effective hiai-opencode MCP setup without relying on OpenCode's mcp list output.
6
+
7
+ ## Execute
8
+
9
+ Run:
10
+
11
+ \`\`\`bash
12
+ hiai-opencode mcp-status
13
+ \`\`\`
14
+
15
+ If the binary is not on PATH, try the package-local fallback:
16
+
17
+ \`\`\`bash
18
+ node ./node_modules/@hiai-gg/hiai-opencode/assets/cli/hiai-opencode.mjs mcp-status
19
+ \`\`\`
20
+
21
+ ## Report
22
+
23
+ Summarize the output in a compact status table:
24
+
25
+ - MCP server name
26
+ - status: ok, warning, error, disabled
27
+ - cause or next action
28
+
29
+ Rules:
30
+
31
+ - Do not print API key values.
32
+ - If a key is missing, name the env var only.
33
+ - If a runtime is missing, give the exact install hint from the command output or the shortest safe next command.
34
+ - Do not edit config unless the user explicitly asks.
35
+ - Do not run package installs unless the user explicitly asks.
36
+ `
@@ -1,6 +1,6 @@
1
1
  import type { CommandDefinition } from "../claude-code-command-loader"
2
2
 
3
- export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work" | "stop-continuation" | "handoff" | "remove-ai-slops"
3
+ export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work" | "stop-continuation" | "handoff" | "remove-ai-slops" | "mcp-status"
4
4
 
5
5
  export interface BuiltinCommandConfig {
6
6
  disabled_commands?: BuiltinCommandName[]
@@ -5,11 +5,33 @@ export const playwrightSkill: BuiltinSkill = {
5
5
  description: "MUST USE for any browser-related tasks. Browser automation via Playwright MCP - verification, browsing, information gathering, web scraping, testing, screenshots, and all browser interactions.",
6
6
  template: `# Playwright Browser Automation
7
7
 
8
- This skill provides browser automation capabilities via the Playwright MCP server.`,
8
+ This skill provides browser automation capabilities via the Playwright MCP server.
9
+
10
+ ## Required workflow
11
+
12
+ 1. Load this skill before calling \`skill_mcp\`.
13
+ 2. Use \`skill_mcp\` with \`mcp_name="playwright"\` for browser navigation, interaction, screenshots, and visual verification.
14
+ 3. If the host says \`MCP server "playwright" not found\`, do not conclude that Playwright is impossible. First report that the skill was not loaded or the Playwright MCP server was not registered in this session.
15
+ 4. If Chromium starts but fails with missing Linux libraries such as \`libnspr4\`, \`libnss3\`, \`libatk-bridge\`, \`libgtk-3\`, or similar, distinguish browser OS dependencies from MCP availability.
16
+
17
+ ## Linux dependency fallback
18
+
19
+ Playwright has two dependency layers:
20
+
21
+ - Browser binary: installable without sudo with \`npx playwright install chromium\` or by setting \`HIAI_PLAYWRIGHT_INSTALL_BROWSERS=1\` before OpenCode starts.
22
+ - System libraries: on minimal Linux images these usually require admin rights via \`sudo npx playwright install-deps chromium\` or OS package manager equivalents.
23
+
24
+ If sudo is unavailable, try these alternatives before falling back to curl-only checks:
25
+
26
+ - Use an already installed Chrome/Chromium/Edge by adding Playwright MCP args in \`hiai-opencode.json\`, for example \`--browser chrome\` or \`--browser msedge\`.
27
+ - Use a remote/browser service or CDP-backed browser when available.
28
+ - Switch the browser automation provider to \`agent-browser\` or \`playwright-cli\` if the workspace has those tools installed.
29
+
30
+ Only use \`curl\` as a final degraded check. Clearly say that HTTP checks do not replace interactive browser verification.`,
9
31
  mcpConfig: {
10
32
  playwright: {
11
33
  command: "npx",
12
- args: ["@playwright/mcp@latest"],
34
+ args: ["-y", "@playwright/mcp@latest"],
13
35
  },
14
36
  },
15
37
  }
@@ -62,6 +62,12 @@ export async function loadGlobalAgentsSkills(): Promise<Record<string, CommandDe
62
62
  return skillsToCommandDefinitionRecord(skills)
63
63
  }
64
64
 
65
+ export async function loadManagedPluginSkills(): Promise<Record<string, CommandDefinition>> {
66
+ const skillsDir = join(getOpenCodeConfigDir({ binary: "opencode" }), ".hiai", "skills", "plugin")
67
+ const skills = await loadSkillsFromDir({ skillsDir, scope: "builtin" })
68
+ return skillsToCommandDefinitionRecord(skills)
69
+ }
70
+
65
71
  export interface DiscoverSkillsOptions {
66
72
  includeClaudeCodePaths?: boolean
67
73
  directory?: string
@@ -170,3 +176,8 @@ export async function discoverGlobalAgentsSkills(): Promise<LoadedSkill[]> {
170
176
  const agentsGlobalDir = join(getAgentsConfigDir(), "skills")
171
177
  return loadSkillsFromDir({ skillsDir: agentsGlobalDir, scope: "user" })
172
178
  }
179
+
180
+ export async function discoverManagedPluginSkills(): Promise<LoadedSkill[]> {
181
+ const skillsDir = join(getOpenCodeConfigDir({ binary: "opencode" }), ".hiai", "skills", "plugin")
182
+ return loadSkillsFromDir({ skillsDir, scope: "builtin" })
183
+ }
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ import { injectServerAuthIntoClient, log } from "./shared"
18
18
  import { hydratePluginConfigWithPlatformDefaults } from "./shared/runtime-plugin-config"
19
19
  import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shared/external-plugin-detector"
20
20
  import { PLUGIN_NAME } from "./shared/plugin-identity"
21
+ import { warnIfListPluginEntry, warnMissingRequiredMcpEnv } from "./shared/startup-diagnostics"
21
22
  import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash"
22
23
  import { lspManager } from "./tools/lsp/client"
23
24
 
@@ -69,6 +70,7 @@ const HiaiOpenCodePlugin: Plugin = async (ctx) => {
69
70
  log("[HiaiOpenCodePlugin] ENTRY - plugin loading", {
70
71
  directory: ctx.directory,
71
72
  })
73
+ warnIfListPluginEntry(ctx.directory)
72
74
 
73
75
  const skillPluginCheck = detectExternalSkillPlugin(ctx.directory)
74
76
  if (skillPluginCheck.detected && skillPluginCheck.pluginName) {
@@ -83,6 +85,10 @@ const HiaiOpenCodePlugin: Plugin = async (ctx) => {
83
85
  loadPluginConfig(ctx.directory, ctx),
84
86
  internalConfig,
85
87
  )
88
+ warnMissingRequiredMcpEnv({
89
+ pluginConfig,
90
+ platformConfig: internalConfig,
91
+ })
86
92
 
87
93
  materializeBuiltinSkills(
88
94
  createBuiltinSkills({
@@ -220,29 +226,24 @@ const HiaiOpenCodePlugin: Plugin = async (ctx) => {
220
226
  auth: {
221
227
  provider: "hiai-opencode",
222
228
  methods: [
223
- { type: "api" as const, label: "Google API key" },
224
- { type: "api" as const, label: "OpenAI API key" },
225
- { type: "api" as const, label: "OpenRouter API key" },
229
+ { type: "api" as const, label: "Google Search API key" },
226
230
  ],
227
231
  loader: async (getAuth: any) => {
228
232
  const authData = await getAuth();
229
233
  const { registerGetAuth, GOOGLE_PROVIDER_ID, OPENAI_PROVIDER_ID, OPENROUTER_PROVIDER_ID } = await import("./internals/plugins/websearch-cited/index");
230
234
 
231
- // Helper to get key from config or authData
232
- const getKey = (label: string, configKey?: string) => {
233
- const fromAuth = authData[label];
234
- if (fromAuth) return fromAuth;
235
+ const getConfiguredKey = (configKey?: string) => {
235
236
  if (configKey) return resolveEnvVars(configKey);
236
237
  return undefined;
237
238
  };
238
239
 
239
- const googleKey = getKey("Google API key", internalConfig.auth?.googleSearch);
240
- const openaiKey = getKey("OpenAI API key", internalConfig.auth?.openai);
241
- const openRouterKey = getKey("OpenRouter API key", internalConfig.auth?.openrouter);
240
+ const googleKey = authData["Google Search API key"] || getConfiguredKey(internalConfig.auth?.googleSearch);
241
+ const openaiKey = getConfiguredKey(internalConfig.auth?.openai);
242
+ const openRouterKey = getConfiguredKey(internalConfig.auth?.openrouter);
242
243
 
243
- if (googleKey) registerGetAuth(GOOGLE_PROVIDER_ID, () => Promise.resolve(googleKey));
244
- if (openaiKey) registerGetAuth(OPENAI_PROVIDER_ID, () => Promise.resolve(openaiKey));
245
- if (openRouterKey) registerGetAuth(OPENROUTER_PROVIDER_ID, () => Promise.resolve(openRouterKey));
244
+ if (googleKey) registerGetAuth(GOOGLE_PROVIDER_ID, () => Promise.resolve({ type: "api", key: googleKey }));
245
+ if (openaiKey) registerGetAuth(OPENAI_PROVIDER_ID, () => Promise.resolve({ type: "api", key: openaiKey }));
246
+ if (openRouterKey) registerGetAuth(OPENROUTER_PROVIDER_ID, () => Promise.resolve({ type: "api", key: openRouterKey }));
246
247
 
247
248
  return {};
248
249
  },
@@ -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
+ }
@@ -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,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;