@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
@@ -3,9 +3,15 @@ import type { GetAuth } from "./types";
3
3
  export declare const GOOGLE_PROVIDER_ID = "google";
4
4
  export declare const OPENAI_PROVIDER_ID = "openai";
5
5
  export declare const OPENROUTER_PROVIDER_ID = "openrouter";
6
+ type SelectedProviderID = typeof GOOGLE_PROVIDER_ID | typeof OPENAI_PROVIDER_ID | typeof OPENROUTER_PROVIDER_ID;
6
7
  export declare function registerGetAuth(providerID: string, getAuth: GetAuth): void;
7
8
  export declare function resolveGetAuth(providerID: string): GetAuth | undefined;
8
- declare const WebsearchCitedPlugin: Plugin;
9
+ type SelectedWebsearchConfig = {
10
+ providerID: SelectedProviderID;
11
+ model: string;
12
+ };
13
+ export type WebsearchCitedFallback = SelectedWebsearchConfig;
14
+ declare const WebsearchCitedPlugin: (_ctx?: unknown, fallback?: WebsearchCitedFallback) => Promise<any>;
9
15
  export declare const WebsearchCitedGooglePlugin: Plugin;
10
16
  export declare const WebsearchCitedOpenAIPlugin: Plugin;
11
17
  export default WebsearchCitedPlugin;
@@ -3,10 +3,10 @@ export declare const McpNameSchema: z.ZodEnum<{
3
3
  websearch: "websearch";
4
4
  stitch: "stitch";
5
5
  firecrawl: "firecrawl";
6
+ context7: "context7";
6
7
  playwright: "playwright";
7
8
  "sequential-thinking": "sequential-thinking";
8
9
  rag: "rag";
9
- context7: "context7";
10
10
  mempalace: "mempalace";
11
11
  grep_app: "grep_app";
12
12
  }>;
@@ -0,0 +1,4 @@
1
+ import type { HiaiOpenCodeConfig } from "../config";
2
+ import type { SkillDiscoveryConfig } from "../config/schema";
3
+ export type ResolvedSkillDiscoveryConfig = Required<SkillDiscoveryConfig>;
4
+ export declare function resolveSkillDiscoveryConfig(pluginConfig: HiaiOpenCodeConfig): ResolvedSkillDiscoveryConfig;
@@ -1,5 +1,6 @@
1
1
  import type { AvailableCategory } from "../agents/dynamic-agent-prompt-builder";
2
2
  import type { HiaiOpenCodeConfig } from "../config";
3
+ import type { McpServerConfig } from "../config/types";
3
4
  import type { PluginContext, ToolsRecord } from "./types";
4
5
  import { builtinTools, createBackgroundTools, createCallOmoAgent, createLookAt, createSkillMcpTool, createSkillTool, createGrepTools, createGlobTools, createAstGrepTools, createSessionManagerTools, createDelegateTask, discoverCommandsSync, interactive_bash, createTaskCreateTool, createTaskGetTool, createTaskList, createTaskUpdateTool, createHashlineEditTool } from "../tools";
5
6
  import type { Managers } from "../create-managers";
@@ -35,6 +36,7 @@ export declare function createToolRegistry(args: {
35
36
  managers: Pick<Managers, "backgroundManager" | "tmuxSessionManager" | "skillMcpManager">;
36
37
  skillContext: SkillContext;
37
38
  availableCategories: AvailableCategory[];
39
+ builtinMcp?: Record<string, McpServerConfig>;
38
40
  interactiveBashEnabled?: boolean;
39
41
  toolFactories?: Partial<ToolRegistryFactories>;
40
42
  }): ToolRegistryResult;
@@ -0,0 +1,6 @@
1
+ import type { HiaiOpenCodeConfig, HiaiOpencodeConfig } from "../config";
2
+ export declare function warnIfListPluginEntry(directory: string): void;
3
+ export declare function warnMissingRequiredMcpEnv(args: {
4
+ pluginConfig: HiaiOpenCodeConfig;
5
+ platformConfig: HiaiOpencodeConfig;
6
+ }): void;
@@ -1,10 +1,12 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { McpServerConfig } from "../../config/types";
2
3
  import type { SkillMcpManager } from "../../features/skill-mcp-manager";
3
4
  import type { LoadedSkill } from "../../features/opencode-skill-loader/types";
4
5
  interface SkillMcpToolOptions {
5
6
  manager: SkillMcpManager;
6
7
  getLoadedSkills: () => LoadedSkill[];
7
8
  getSessionID?: () => string | undefined;
9
+ builtinMcp?: Record<string, McpServerConfig>;
8
10
  }
9
11
  export declare function applyGrepFilter(output: string, pattern: string | undefined): string;
10
12
  export declare function createSkillMcpTool(options: SkillMcpToolOptions): ToolDefinition;
@@ -1,43 +1,54 @@
1
1
  {
2
- "_description": "hiai-opencode user configuration template. Public display names (shown to users): Bob, Coder, Strategist, Guard, Critic, Designer, Researcher, Manager, Brainstormer, Vision. Config keys (used in this file and in code) are: bob, coder, strategist, guard, critic, designer, researcher, manager, brainstormer, vision. Internal-only keys (hidden from normal users): sub, agent-skills. MCP enable/disable lives in the mcp object below; default MCP definitions live in src/mcp/registry.ts. Source model defaults: src/config/defaults.ts and src/config/models.ts.",
2
+ "_description": "hiai-opencode user config. Edit only this file in your project as .opencode/hiai-opencode.json. Model provider credentials are configured in OpenCode Connect; this file stores model IDs only.",
3
3
  "$schema": "./config/hiai-opencode.schema.json",
4
- "agents": {
5
- "bob": { "model": "openrouter/anthropic/claude-3.5-opus" },
6
- "coder": { "model": "openrouter/anthropic/claude-3.5-sonnet" },
7
- "strategist": { "model": "openrouter/anthropic/claude-3.5-haiku" },
8
- "guard": { "model": "openrouter/openai/gpt-4o" },
9
- "critic": { "model": "openrouter/qwen/qwen2.5-72b-instruct" },
10
- "designer": { "model": "openrouter/google/gemini-3.1-pro" },
11
- "researcher": { "model": "openrouter/google/gemini-2.0-flash" },
12
- "manager": { "model": "openrouter/google/gemini-2.0-flash" },
13
- "brainstormer": { "model": "openrouter/google/gemini-2.0-flash" },
14
- "vision": { "model": "openrouter/google/gemini-3.1-pro" }
15
- },
16
- "categories": {
17
- "visual-engineering": { "model": "openrouter/google/gemini-2.0-pro-exp-02-05", "variant": "high" },
18
- "artistry": { "model": "openrouter/google/gemini-2.0-pro-exp-02-05", "variant": "high" },
19
- "ultrabrain": { "model": "openrouter/openai/gpt-4o", "variant": "xhigh" },
20
- "deep": { "model": "openrouter/anthropic/claude-3.5-sonnet", "variant": "medium" },
21
- "quick": { "model": "openrouter/google/gemini-2.0-flash" },
22
- "writing": { "model": "openrouter/openai/gpt-4o-mini" },
23
- "git": { "model": "openrouter/google/gemini-2.0-flash" },
24
- "unspecified-low": { "model": "openrouter/anthropic/claude-3.5-haiku" },
25
- "unspecified-high": { "model": "openrouter/anthropic/claude-3.5-opus", "variant": "max" }
4
+ "models": {
5
+ "bob": {
6
+ "model": "openrouter/moonshotai/kimi-k2.6",
7
+ "recommended": "xhigh"
8
+ },
9
+ "coder": {
10
+ "model": "openrouter/minimax/minimax-m2.7",
11
+ "recommended": "high"
12
+ },
13
+ "strategist": {
14
+ "model": "openrouter/anthropic/claude-opus-latest",
15
+ "recommended": "high"
16
+ },
17
+ "guard": {
18
+ "model": "openrouter/qwen/qwen3.6-plus",
19
+ "recommended": "middle"
20
+ },
21
+ "critic": {
22
+ "model": "openrouter/xiaomi/mimo-v2.5-pro",
23
+ "recommended": "high"
24
+ },
25
+ "designer": {
26
+ "model": "openrouter/google/gemini-3.1-pro-preview",
27
+ "recommended": "design"
28
+ },
29
+ "researcher": {
30
+ "model": "openrouter/deepseek/deepseek-v4-flash",
31
+ "recommended": "fast"
32
+ },
33
+ "manager": {
34
+ "model": "openrouter/qwen/qwen3.5-9b",
35
+ "recommended": "fast"
36
+ },
37
+ "brainstormer": {
38
+ "model": "openrouter/mistralai/mistral-small-2603",
39
+ "recommended": "writing"
40
+ },
41
+ "vision": {
42
+ "model": "openrouter/google/gemma-4-26b-a4b-it",
43
+ "recommended": "vision"
44
+ }
26
45
  },
27
46
  "auth": {
28
- "openrouter": "{env:OPENROUTER_API_KEY}",
29
- "openai": "{env:OPENAI_API_KEY}",
30
47
  "googleSearch": "{env:GOOGLE_SEARCH_API_KEY}",
31
48
  "stitch": "{env:STITCH_AI_API_KEY}",
32
49
  "firecrawl": "{env:FIRECRAWL_API_KEY}",
33
50
  "context7": "{env:CONTEXT7_API_KEY}"
34
51
  },
35
- "ollama": {
36
- "enabled": false,
37
- "model": "qwen3.5:4b",
38
- "baseUrl": "http://localhost:11434",
39
- "purpose": "helper"
40
- },
41
52
  "mcp": {
42
53
  "playwright": { "enabled": true },
43
54
  "stitch": { "enabled": true },
@@ -49,8 +60,19 @@
49
60
  },
50
61
  "lsp": {
51
62
  "typescript": { "enabled": true },
52
- "python": { "enabled": true },
53
- "svelte": { "enabled": true }
63
+ "svelte": { "enabled": true },
64
+ "eslint": { "enabled": true },
65
+ "bash": { "enabled": true },
66
+ "pyright": { "enabled": true }
67
+ },
68
+ "skill_discovery": {
69
+ "config_sources": true,
70
+ "project_opencode": true,
71
+ "global_opencode": false,
72
+ "project_claude": false,
73
+ "global_claude": false,
74
+ "project_agents": false,
75
+ "global_agents": false
54
76
  },
55
77
  "subtask2": {
56
78
  "replace_generic": true
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@hiai-gg/hiai-opencode",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Unified OpenCode plugin — canonical 12-agent model with bundled skills, MCP integrations, LSP, and permissions in one install.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "hiai-opencode": "./assets/cli/hiai-opencode.mjs"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "import": "./dist/index.js",
@@ -56,9 +56,8 @@ These files run after the prompt library and can override what was authored. Alw
56
56
 
57
57
  | What to change | Edit |
58
58
  |---|---|
59
- | Preset definitions (`fast`, `mid`, `high`, `vision`) | `src/config/models.ts` |
60
- | Per-agent default model | `src/config/defaults.ts` |
61
- | Per-category default model | `src/config/defaults.ts` |
59
+ | Per-agent default model | `hiai-opencode.json` |
60
+ | Per-category default model | `hiai-opencode.json` |
62
61
  | Config schema defaults | `src/config/types.ts` |
63
62
 
64
63
  ## Prompt Measurement
@@ -72,4 +71,4 @@ Run `bun run prompts:measure` to generate snapshot files in `dist/prompt-snapsho
72
71
  3. Local helper launchers: `assets/mcp/*`
73
72
  4. NPM bootstrapper for local tools: `assets/runtime/npm-package-runner.mjs`
74
73
 
75
- For more detail see `AGENTS.md` (root) and `ARCHITECTURE.md` (root).
74
+ For more detail see `AGENTS.md` (root) and `ARCHITECTURE.md` (root).
@@ -1,89 +1,198 @@
1
1
  /**
2
- * Default config values for hiai-opencode
2
+ * Runtime defaults for hiai-opencode.
3
+ *
4
+ * The user-facing config owns model choice through 10 primary agent slots.
5
+ * Internal routing derives hidden agents and task categories from those slots.
3
6
  */
4
- import type { HiaiOpencodeConfig } from "./types.js";
5
- import { MODEL_PRESETS } from "./models.js";
6
- import { createDefaultMcpConfig } from "../mcp/registry.js";
7
- import { join } from "node:path";
8
-
9
- export const defaultConfig: HiaiOpencodeConfig = {
10
- agents: {
11
- bob: { model: MODEL_PRESETS.high },
12
- guard: { model: MODEL_PRESETS.ultrahigh },
13
- strategist: { model: MODEL_PRESETS.strategist },
14
- critic: { model: MODEL_PRESETS.critic },
15
- coder: { model: MODEL_PRESETS.mid },
16
- designer: {
17
- model: "openrouter/google/gemini-3.1-pro",
18
- description: "Creative visual problem-solver for high-touch UI, interaction, and brand-level interface direction. Best used when the task needs taste, composition, and design judgment rather than plain implementation. (Designer - HiaiOpenCode)",
19
- },
20
- sub: { model: MODEL_PRESETS.fast },
21
- researcher: { model: MODEL_PRESETS.fast },
22
- multimodal: { model: MODEL_PRESETS.vision },
23
- "quality-guardian": { model: MODEL_PRESETS.mid },
24
- "platform-manager": { model: MODEL_PRESETS.fast },
25
- brainstormer: { model: MODEL_PRESETS.fast },
26
- "agent-skills": { model: MODEL_PRESETS.fast },
7
+ import { existsSync, readFileSync } from "node:fs"
8
+ import { dirname, join, normalize } from "node:path"
9
+ import { createDefaultMcpConfig } from "../mcp/registry.js"
10
+ import type { HiaiOpencodeConfig, LspServerConfig } from "./types.js"
11
+
12
+ const REQUIRED_MODEL_SLOTS = [
13
+ "bob",
14
+ "coder",
15
+ "strategist",
16
+ "guard",
17
+ "critic",
18
+ "designer",
19
+ "researcher",
20
+ "manager",
21
+ "brainstormer",
22
+ "vision",
23
+ ] as const
24
+
25
+ type ModelSlot = (typeof REQUIRED_MODEL_SLOTS)[number]
26
+
27
+ const DEFAULT_LSP: Record<string, LspServerConfig> = {
28
+ typescript: {
29
+ enabled: true,
30
+ command: ["typescript-language-server", "--stdio"],
31
+ extensions: [".ts", ".tsx", ".mts", ".cts"],
32
+ },
33
+ svelte: {
34
+ enabled: true,
35
+ command: ["svelteserver", "--stdio"],
36
+ extensions: [".svelte"],
27
37
  },
28
- agentRequirements: {},
29
- categories: {
30
- "visual-engineering": { model: MODEL_PRESETS.vision, variant: "high" },
31
- artistry: { model: MODEL_PRESETS.vision, variant: "high" },
32
- ultrabrain: { model: MODEL_PRESETS.ultrahigh, variant: "xhigh" },
33
- deep: { model: MODEL_PRESETS.reasoning, variant: "medium" },
34
- quick: { model: MODEL_PRESETS.fast },
35
- writing: { model: MODEL_PRESETS.writing },
36
- git: { model: MODEL_PRESETS.fast },
37
- "unspecified-low": { model: MODEL_PRESETS.mid },
38
- "unspecified-high": { model: MODEL_PRESETS.high, variant: "max" },
38
+ eslint: {
39
+ enabled: true,
40
+ command: ["node", "{pluginRoot}/assets/runtime/npm-package-runner.mjs", "eslint-lsp", "--stdio"],
41
+ extensions: [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".svelte"],
39
42
  },
40
- categoryRequirements: {},
43
+ bash: {
44
+ enabled: true,
45
+ command: ["node", "{pluginRoot}/assets/runtime/npm-package-runner.mjs", "bash-language-server", "start"],
46
+ extensions: [".sh", ".bash"],
47
+ },
48
+ pyright: {
49
+ enabled: true,
50
+ command: ["pyright-langserver", "--stdio"],
51
+ extensions: [".py"],
52
+ },
53
+ }
41
54
 
42
- mcp: createDefaultMcpConfig(),
55
+ function findPluginRoot(): string {
56
+ const candidates = [
57
+ join(import.meta.dirname, "..", ".."),
58
+ join(import.meta.dirname, ".."),
59
+ join(import.meta.dirname, "..", ".."),
60
+ dirname(process.argv[1] ?? ""),
61
+ process.cwd(),
62
+ ]
43
63
 
44
- lsp: {
45
- typescript: {
46
- command: ["typescript-language-server", "--stdio"],
47
- extensions: [".ts", ".tsx", ".mts", ".cts"],
48
- },
49
- svelte: {
50
- command: ["svelteserver", "--stdio"],
51
- extensions: [".svelte"],
52
- },
53
- eslint: {
54
- command: ["node", join(import.meta.dirname, "..", "assets", "runtime", "npm-package-runner.mjs"), "eslint-lsp", "--stdio"],
55
- extensions: [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".svelte"],
64
+ for (const candidate of candidates) {
65
+ const root = normalize(candidate)
66
+ if (existsSync(join(root, "hiai-opencode.json"))) return root
67
+ }
68
+
69
+ throw new Error("[hiai-opencode] Cannot find bundled hiai-opencode.json. The package is incomplete.")
70
+ }
71
+
72
+ function expandPluginRootPlaceholders(value: unknown, pluginRoot: string): unknown {
73
+ if (typeof value === "string") return value.replaceAll("{pluginRoot}", pluginRoot)
74
+ if (Array.isArray(value)) return value.map((item) => expandPluginRootPlaceholders(item, pluginRoot))
75
+ if (value && typeof value === "object") {
76
+ return Object.fromEntries(
77
+ Object.entries(value).map(([key, entry]) => [key, expandPluginRootPlaceholders(entry, pluginRoot)]),
78
+ )
79
+ }
80
+ return value
81
+ }
82
+
83
+ function requireModelSlots(config: HiaiOpencodeConfig): Record<ModelSlot, string> {
84
+ const models = config.models ?? {}
85
+ const resolved = Object.fromEntries(
86
+ REQUIRED_MODEL_SLOTS.map((slot) => {
87
+ const value = models[slot]
88
+ const model = typeof value === "string" ? value : value?.model
89
+ return [slot, model?.trim() ?? ""]
90
+ }),
91
+ ) as Record<ModelSlot, string>
92
+
93
+ const missing = REQUIRED_MODEL_SLOTS.filter((slot) => !resolved[slot])
94
+ if (missing.length > 0) {
95
+ throw new Error(`[hiai-opencode] Missing required model slot(s) in hiai-opencode.json: ${missing.join(", ")}`)
96
+ }
97
+ return resolved
98
+ }
99
+
100
+ function deriveAgents(models: Record<ModelSlot, string>): HiaiOpencodeConfig["agents"] {
101
+ return {
102
+ bob: { model: models.bob },
103
+ coder: { model: models.coder },
104
+ strategist: { model: models.strategist },
105
+ guard: { model: models.guard },
106
+ critic: { model: models.critic },
107
+ designer: { model: models.designer },
108
+ researcher: { model: models.researcher },
109
+ "platform-manager": { model: models.manager },
110
+ brainstormer: { model: models.brainstormer },
111
+ multimodal: { model: models.vision },
112
+ sub: { model: models.coder },
113
+ "quality-guardian": { model: models.critic },
114
+ "agent-skills": { model: models.manager },
115
+ }
116
+ }
117
+
118
+ function deriveCategories(models: Record<ModelSlot, string>): HiaiOpencodeConfig["categories"] {
119
+ return {
120
+ "visual-engineering": { model: models.designer, variant: "high" },
121
+ artistry: { model: models.designer, variant: "high" },
122
+ ultrabrain: { model: models.strategist, variant: "xhigh" },
123
+ deep: { model: models.coder, variant: "medium" },
124
+ quick: { model: models.researcher },
125
+ writing: { model: models.brainstormer },
126
+ git: { model: models.manager },
127
+ "unspecified-low": { model: models.coder },
128
+ "unspecified-high": { model: models.bob, variant: "max" },
129
+ }
130
+ }
131
+
132
+ function deriveMcp(config: HiaiOpencodeConfig): HiaiOpencodeConfig["mcp"] {
133
+ const defaults = createDefaultMcpConfig()
134
+ const userMcp = config.mcp ?? {}
135
+ return Object.fromEntries(
136
+ Object.entries(defaults).map(([name, entry]) => {
137
+ const override = userMcp[name] ?? {}
138
+ return [name, { ...entry, ...override }]
139
+ }),
140
+ )
141
+ }
142
+
143
+ function deriveLsp(config: HiaiOpencodeConfig): HiaiOpencodeConfig["lsp"] {
144
+ const userLsp = config.lsp ?? {}
145
+ return Object.fromEntries(
146
+ Object.entries(DEFAULT_LSP).map(([name, entry]) => {
147
+ const override = userLsp[name] ?? {}
148
+ return [name, { ...entry, ...override }]
149
+ }),
150
+ )
151
+ }
152
+
153
+ function materializeConfig(rawConfig: HiaiOpencodeConfig): HiaiOpencodeConfig {
154
+ const models = requireModelSlots(rawConfig)
155
+ return {
156
+ ...rawConfig,
157
+ agents: deriveAgents(models),
158
+ agentRequirements: {},
159
+ categories: deriveCategories(models),
160
+ categoryRequirements: {},
161
+ modelFamilies: [],
162
+ mcp: deriveMcp(rawConfig),
163
+ lsp: deriveLsp(rawConfig),
164
+ subtask2: {
165
+ replace_generic: true,
166
+ generic_return: null,
167
+ ...(rawConfig.subtask2 ?? {}),
56
168
  },
57
- bash: {
58
- command: ["node", join(import.meta.dirname, "..", "assets", "runtime", "npm-package-runner.mjs"), "bash-language-server", "start"],
59
- extensions: [".sh", ".bash"],
169
+ skills: {
170
+ enabled: true,
171
+ disabled: [],
172
+ ...(rawConfig.skills ?? {}),
60
173
  },
61
- pyright: {
62
- command: ["pyright-langserver", "--stdio"],
63
- extensions: [".py"],
174
+ permissions: {
175
+ read: { "*": "allow", "*.env": "deny", "*.env.*": "deny", "*.env.example": "allow" },
176
+ edit: { "*": "allow" },
177
+ bash: { "*": "allow" },
178
+ deny_paths: ["**/backup/**", "**/secrets.*", "**/.env", "**/.env.*"],
179
+ ...(rawConfig.permissions ?? {}),
64
180
  },
65
- },
181
+ }
182
+ }
66
183
 
67
- subtask2: {
68
- replace_generic: true,
69
- generic_return: null,
70
- },
184
+ function loadBundledDefaultConfig(): HiaiOpencodeConfig {
185
+ const pluginRoot = findPluginRoot()
186
+ const configPath = join(pluginRoot, "hiai-opencode.json")
187
+ const raw = readFileSync(configPath, "utf-8")
188
+ const parsed = JSON.parse(raw) as HiaiOpencodeConfig
189
+ const materialized = materializeConfig(parsed)
71
190
 
72
- skills: {
73
- enabled: true,
74
- disabled: [],
75
- },
191
+ return expandPluginRootPlaceholders(materialized, pluginRoot) as HiaiOpencodeConfig
192
+ }
76
193
 
77
- permissions: {
78
- read: { "*": "allow", "*.env": "deny", "*.env.*": "deny", "*.env.example": "allow" },
79
- edit: { "*": "allow" },
80
- bash: { "*": "allow" },
81
- deny_paths: ["**/backup/**", "**/secrets.*", "**/.env", "**/.env.*"],
82
- },
83
- ollama: {
84
- enabled: false,
85
- model: "{env:OLLAMA_MODEL:-qwen3.5:4b}",
86
- baseUrl: "http://localhost:11434",
87
- purpose: "helper",
88
- },
89
- };
194
+ export function applyModelSlots(config: HiaiOpencodeConfig): HiaiOpencodeConfig {
195
+ return materializeConfig(config)
196
+ }
197
+
198
+ export const defaultConfig: HiaiOpencodeConfig = loadBundledDefaultConfig()
@@ -11,7 +11,6 @@
11
11
  export { loadConfig, resolveEnvVars, resolveMcpEnv } from "./loader.js";
12
12
  export { HiaiOpencodeConfigSchema } from "./platform-schema.js";
13
13
  export { defaultConfig } from "./defaults.js";
14
- export { MODEL_PRESETS, MODEL_ROLE_GUIDE, PROVIDER_MODEL_RULES } from "./models.js";
15
14
  export type {
16
15
  HiaiOpencodeConfig,
17
16
  AgentConfig,
@@ -28,9 +28,23 @@ function withTempProject(
28
28
  }
29
29
  }
30
30
 
31
+ const requiredModels = {
32
+ bob: { model: "openrouter/test/bob", recommended: "xhigh" },
33
+ coder: { model: "openrouter/test/coder", recommended: "high" },
34
+ strategist: { model: "openrouter/test/strategist", recommended: "high" },
35
+ guard: "openrouter/test/guard",
36
+ critic: "openrouter/test/critic",
37
+ designer: "openrouter/test/designer",
38
+ researcher: "openrouter/test/researcher",
39
+ manager: "openrouter/test/manager",
40
+ brainstormer: "openrouter/test/brainstormer",
41
+ vision: "openrouter/test/vision",
42
+ };
43
+
31
44
  test("loadConfig accepts compact enabled-only LSP entries for builtin servers", () => {
32
45
  withTempProject(
33
46
  {
47
+ models: requiredModels,
34
48
  lsp: {
35
49
  typescript: {
36
50
  enabled: true,
@@ -42,7 +56,7 @@ test("loadConfig accepts compact enabled-only LSP entries for builtin servers",
42
56
  const tsServer = loaded.lsp?.typescript;
43
57
 
44
58
  expect(tsServer).toBeDefined();
45
- expect(tsServer?.command.length).toBeGreaterThan(0);
59
+ expect(tsServer?.command?.length).toBeGreaterThan(0);
46
60
  expect(tsServer?.extensions).toContain(".ts");
47
61
  },
48
62
  );
@@ -51,6 +65,7 @@ test("loadConfig accepts compact enabled-only LSP entries for builtin servers",
51
65
  test("loadConfig ignores unknown compact enabled-only LSP entries", () => {
52
66
  withTempProject(
53
67
  {
68
+ models: requiredModels,
54
69
  lsp: {
55
70
  "custom-server": {
56
71
  enabled: true,
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parse } from "jsonc-parser";
4
4
  import { HiaiOpencodeConfigSchema } from "./platform-schema.js";
5
- import { defaultConfig } from "./defaults.js";
5
+ import { applyModelSlots, defaultConfig } from "./defaults.js";
6
6
  import {
7
7
  LEGACY_AGENT_ALIAS_TO_CANONICAL,
8
8
  type CanonicalAgentName,
@@ -157,10 +157,12 @@ export function loadConfig(projectDir: string): HiaiOpencodeConfig {
157
157
  const validated = HiaiOpencodeConfigSchema.parse(normalizedParsed);
158
158
  const normalized = normalizeAgentAliases(validated);
159
159
 
160
- return deepMerge(
160
+ const merged = deepMerge(
161
161
  BASE_CONFIG as unknown as Record<string, unknown>,
162
162
  normalized as unknown as Record<string, unknown>,
163
163
  ) as HiaiOpencodeConfig;
164
+
165
+ return applyModelSlots(merged);
164
166
  }
165
167
 
166
168
  export function resolveEnvVars(value: string): string {