@hiai-gg/hiai-opencode 0.1.4 → 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.
@@ -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
  }>;
@@ -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;
@@ -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,170 +1,69 @@
1
1
  {
2
- "_description": "Canonical hiai-opencode runtime config. Copy this file to .opencode/hiai-opencode.json and edit it there. This file is the user-facing source of truth for agent models, category models, MCP switches, LSP defaults, skill discovery, service auth placeholders, and local helper defaults. Model provider credentials are configured in OpenCode Connect; this file stores model IDs only.",
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": {
4
+ "models": {
5
5
  "bob": {
6
- "model": "openrouter/anthropic/claude-3.5-opus",
7
- "description": "Primary orchestrator for end-to-end task execution, delegation, and high-level workflow control."
6
+ "model": "openrouter/moonshotai/kimi-k2.6",
7
+ "recommended": "xhigh"
8
8
  },
9
9
  "coder": {
10
- "model": "openrouter/anthropic/claude-3.5-sonnet",
11
- "description": "High-depth executor for multi-file implementation, substantial refactors, and technical delivery."
10
+ "model": "openrouter/minimax/minimax-m2.7",
11
+ "recommended": "high"
12
12
  },
13
13
  "strategist": {
14
- "model": "openrouter/z-ai/glm-5.1",
15
- "description": "Planning and architecture agent for decomposition, sequencing, and decision framing before execution."
14
+ "model": "openrouter/anthropic/claude-opus-latest",
15
+ "recommended": "high"
16
16
  },
17
17
  "guard": {
18
- "model": "openrouter/openai/gpt-4o",
19
- "description": "Execution supervisor that routes work, enforces discipline, and keeps delegated flows on track."
18
+ "model": "openrouter/qwen/qwen3.6-plus",
19
+ "recommended": "middle"
20
20
  },
21
21
  "critic": {
22
- "model": "openrouter/qwen/qwen2.5-72b-instruct",
23
- "description": "High-accuracy review gate for implementation quality, correctness, and plan validation."
22
+ "model": "openrouter/xiaomi/mimo-v2.5-pro",
23
+ "recommended": "high"
24
24
  },
25
25
  "designer": {
26
- "model": "openrouter/google/gemini-3.1-pro",
27
- "description": "Creative visual problem-solver for high-touch UI, interaction, and brand-level interface direction."
26
+ "model": "openrouter/google/gemini-3.1-pro-preview",
27
+ "recommended": "design"
28
28
  },
29
29
  "researcher": {
30
- "model": "openrouter/google/gemini-2.0-flash",
31
- "description": "Codebase exploration and external documentation research."
30
+ "model": "openrouter/deepseek/deepseek-v4-flash",
31
+ "recommended": "fast"
32
32
  },
33
- "platform-manager": {
34
- "model": "openrouter/google/gemini-2.0-flash",
35
- "description": "Manager. Unified platform management agent for session continuity, project initialization, and orchestration."
33
+ "manager": {
34
+ "model": "openrouter/qwen/qwen3.5-9b",
35
+ "recommended": "fast"
36
36
  },
37
37
  "brainstormer": {
38
- "model": "openrouter/google/gemini-2.0-flash",
39
- "description": "Idea exploration agent for divergent thinking, option generation, and concept shaping before execution."
38
+ "model": "openrouter/mistralai/mistral-small-2603",
39
+ "recommended": "writing"
40
40
  },
41
- "multimodal": {
42
- "model": "openrouter/google/gemini-3.1-pro",
43
- "description": "Vision. Multimodal analysis agent for images, PDFs, diagrams, and other non-text inputs."
44
- },
45
- "sub": {
46
- "model": "openrouter/google/gemini-2.0-flash",
47
- "description": "Hidden compatibility executor. Primary subtask execution is handled by Coder."
48
- },
49
- "quality-guardian": {
50
- "model": "openrouter/anthropic/claude-3.5-sonnet",
51
- "description": "Hidden compatibility review agent. Primary review work is handled by Critic."
52
- },
53
- "agent-skills": {
54
- "model": "openrouter/google/gemini-2.0-flash",
55
- "description": "Hidden system agent for skill registry, discovery, and capability orchestration."
41
+ "vision": {
42
+ "model": "openrouter/google/gemma-4-26b-a4b-it",
43
+ "recommended": "vision"
56
44
  }
57
45
  },
58
- "agentRequirements": {},
59
- "categories": {
60
- "visual-engineering": {
61
- "model": "openrouter/google/gemini-2.0-pro-exp-02-05",
62
- "variant": "high",
63
- "description": "Frontend and UI systems work with design-system discipline. Uses Coder deep execution contour."
64
- },
65
- "artistry": {
66
- "model": "openrouter/google/gemini-2.0-pro-exp-02-05",
67
- "variant": "high",
68
- "description": "High-effort creative problem solving beyond standard patterns. Uses Coder deep execution contour."
69
- },
70
- "ultrabrain": {
71
- "model": "openrouter/openai/gpt-4o",
72
- "variant": "xhigh",
73
- "description": "Hard logic and architecture tasks. Uses Coder deep execution contour."
74
- },
75
- "deep": {
76
- "model": "openrouter/openai/o1",
77
- "variant": "medium",
78
- "description": "Deep autonomous implementation with full context buildup. Uses Coder deep execution contour."
79
- },
80
- "quick": {
81
- "model": "openrouter/google/gemini-2.0-flash",
82
- "description": "Fast bounded execution: single-file fixes, typos, and simple modifications. Uses Coder fast execution contour."
83
- },
84
- "writing": {
85
- "model": "openrouter/kimi/kimi-latest",
86
- "description": "Documentation and prose tasks with bounded scope. Uses Coder fast execution contour."
87
- },
88
- "git": {
89
- "model": "openrouter/google/gemini-2.0-flash",
90
- "description": "Git, commit, diff, and repository hygiene tasks."
91
- },
92
- "unspecified-low": {
93
- "model": "openrouter/anthropic/claude-3.5-sonnet",
94
- "description": "Unclassifiable moderate tasks with bounded scope. Uses Coder fast execution contour."
95
- },
96
- "unspecified-high": {
97
- "model": "openrouter/anthropic/claude-3.5-opus",
98
- "variant": "max",
99
- "description": "Unclassifiable substantial tasks across modules. Uses Coder deep execution contour."
100
- }
101
- },
102
- "categoryRequirements": {},
103
- "modelFamilies": [],
104
46
  "auth": {
105
47
  "googleSearch": "{env:GOOGLE_SEARCH_API_KEY}",
106
48
  "stitch": "{env:STITCH_AI_API_KEY}",
107
49
  "firecrawl": "{env:FIRECRAWL_API_KEY}",
108
50
  "context7": "{env:CONTEXT7_API_KEY}"
109
51
  },
110
- "ollama": {
111
- "enabled": false,
112
- "model": "{env:OLLAMA_MODEL:-qwen3.5:4b}",
113
- "baseUrl": "{env:OLLAMA_BASE_URL:-http://localhost:11434}",
114
- "purpose": "helper"
115
- },
116
- "fast_apply": {
117
- "enabled": false,
118
- "ollama_url": "{env:OLLAMA_BASE_URL:-http://localhost:11434}",
119
- "model": "{env:OLLAMA_MODEL:-qwen3.5:4b}",
120
- "timeout": 30000
121
- },
122
52
  "mcp": {
123
- "playwright": {
124
- "enabled": true,
125
- "command": ["node", "{pluginRoot}/assets/mcp/playwright.mjs"],
126
- "timeout": 600000
127
- },
128
- "stitch": {
129
- "enabled": true,
130
- "type": "remote",
131
- "url": "https://stitch.googleapis.com/mcp",
132
- "headers": { "X-Goog-Api-Key": "{env:STITCH_AI_API_KEY}" },
133
- "timeout": 600000
134
- },
135
- "sequential-thinking": {
136
- "enabled": true,
137
- "command": ["node", "{pluginRoot}/assets/runtime/npm-package-runner.mjs", "@modelcontextprotocol/server-sequential-thinking"],
138
- "timeout": 600000
139
- },
140
- "firecrawl": {
141
- "enabled": true,
142
- "command": ["node", "{pluginRoot}/assets/runtime/npm-package-runner.mjs", "firecrawl-mcp"],
143
- "environment": { "FIRECRAWL_API_KEY": "{env:FIRECRAWL_API_KEY}" },
144
- "timeout": 600000
145
- },
146
- "rag": {
147
- "enabled": true,
148
- "type": "local",
149
- "command": ["node", "{pluginRoot}/assets/mcp/rag.mjs"],
150
- "environment": {
151
- "OPENCODE_RAG_URL": "{env:OPENCODE_RAG_URL:-http://localhost:9002/tools/search}"
152
- },
153
- "timeout": 600000
154
- },
155
- "mempalace": {
156
- "enabled": true,
157
- "type": "local",
158
- "command": ["node", "{pluginRoot}/assets/mcp/mempalace.mjs", "--palace", "./.opencode/palace"],
159
- "timeout": 600000
160
- },
161
- "context7": {
162
- "enabled": true,
163
- "type": "remote",
164
- "url": "https://mcp.context7.com/mcp",
165
- "headers": { "X-API-KEY": "{env:CONTEXT7_API_KEY}" },
166
- "timeout": 600000
167
- }
53
+ "playwright": { "enabled": true },
54
+ "stitch": { "enabled": true },
55
+ "sequential-thinking": { "enabled": true },
56
+ "firecrawl": { "enabled": true },
57
+ "rag": { "enabled": true },
58
+ "mempalace": { "enabled": true },
59
+ "context7": { "enabled": true }
60
+ },
61
+ "lsp": {
62
+ "typescript": { "enabled": true },
63
+ "svelte": { "enabled": true },
64
+ "eslint": { "enabled": true },
65
+ "bash": { "enabled": true },
66
+ "pyright": { "enabled": true }
168
67
  },
169
68
  "skill_discovery": {
170
69
  "config_sources": true,
@@ -175,40 +74,7 @@
175
74
  "project_agents": false,
176
75
  "global_agents": false
177
76
  },
178
- "skills": {
179
- "enabled": true,
180
- "disabled": []
181
- },
182
- "lsp": {
183
- "typescript": {
184
- "command": ["typescript-language-server", "--stdio"],
185
- "extensions": [".ts", ".tsx", ".mts", ".cts"]
186
- },
187
- "svelte": {
188
- "command": ["svelteserver", "--stdio"],
189
- "extensions": [".svelte"]
190
- },
191
- "eslint": {
192
- "command": ["node", "{pluginRoot}/assets/runtime/npm-package-runner.mjs", "eslint-lsp", "--stdio"],
193
- "extensions": [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".svelte"]
194
- },
195
- "bash": {
196
- "command": ["node", "{pluginRoot}/assets/runtime/npm-package-runner.mjs", "bash-language-server", "start"],
197
- "extensions": [".sh", ".bash"]
198
- },
199
- "pyright": {
200
- "command": ["pyright-langserver", "--stdio"],
201
- "extensions": [".py"]
202
- }
203
- },
204
77
  "subtask2": {
205
- "replace_generic": true,
206
- "generic_return": null
207
- },
208
- "permissions": {
209
- "read": { "*": "allow", "*.env": "deny", "*.env.*": "deny", "*.env.example": "allow" },
210
- "edit": { "*": "allow" },
211
- "bash": { "*": "allow" },
212
- "deny_paths": ["**/backup/**", "**/secrets.*", "**/.env", "**/.env.*"]
78
+ "replace_generic": true
213
79
  }
214
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hiai-gg/hiai-opencode",
3
- "version": "0.1.4",
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",
@@ -1,23 +1,61 @@
1
1
  /**
2
2
  * Runtime defaults for hiai-opencode.
3
3
  *
4
- * Model defaults are intentionally not defined in TypeScript.
5
- * The bundled hiai-opencode.json file is the single source of truth for:
6
- * - agent models
7
- * - category models
8
- * - user-editable runtime defaults
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.
9
6
  */
10
7
  import { existsSync, readFileSync } from "node:fs"
11
8
  import { dirname, join, normalize } from "node:path"
12
- import type { HiaiOpencodeConfig } from "./types.js"
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"],
37
+ },
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"],
42
+ },
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
+ }
13
54
 
14
55
  function findPluginRoot(): string {
15
56
  const candidates = [
16
- // Source tree: src/config/defaults.ts -> repo root
17
57
  join(import.meta.dirname, "..", ".."),
18
- // Built package: dist/index.js bundle -> package root
19
58
  join(import.meta.dirname, ".."),
20
- // Non-bundled compiled output: dist/config/defaults.js -> package root
21
59
  join(import.meta.dirname, "..", ".."),
22
60
  dirname(process.argv[1] ?? ""),
23
61
  process.cwd(),
@@ -25,35 +63,122 @@ function findPluginRoot(): string {
25
63
 
26
64
  for (const candidate of candidates) {
27
65
  const root = normalize(candidate)
28
- if (existsSync(join(root, "hiai-opencode.json"))) {
29
- return root
30
- }
66
+ if (existsSync(join(root, "hiai-opencode.json"))) return root
31
67
  }
32
68
 
33
- throw new Error(
34
- "[hiai-opencode] Cannot find bundled hiai-opencode.json. The package is incomplete.",
35
- )
69
+ throw new Error("[hiai-opencode] Cannot find bundled hiai-opencode.json. The package is incomplete.")
36
70
  }
37
71
 
38
72
  function expandPluginRootPlaceholders(value: unknown, pluginRoot: string): unknown {
39
- if (typeof value === "string") {
40
- return value.replaceAll("{pluginRoot}", pluginRoot)
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
+ )
41
79
  }
80
+ return value
81
+ }
42
82
 
43
- if (Array.isArray(value)) {
44
- return value.map((item) => expandPluginRootPlaceholders(item, pluginRoot))
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(", ")}`)
45
96
  }
97
+ return resolved
98
+ }
46
99
 
47
- if (value && typeof value === "object") {
48
- return Object.fromEntries(
49
- Object.entries(value).map(([key, entry]) => [
50
- key,
51
- expandPluginRootPlaceholders(entry, pluginRoot),
52
- ]),
53
- )
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 },
54
115
  }
116
+ }
55
117
 
56
- return value
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 ?? {}),
168
+ },
169
+ skills: {
170
+ enabled: true,
171
+ disabled: [],
172
+ ...(rawConfig.skills ?? {}),
173
+ },
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 ?? {}),
180
+ },
181
+ }
57
182
  }
58
183
 
59
184
  function loadBundledDefaultConfig(): HiaiOpencodeConfig {
@@ -61,8 +186,13 @@ function loadBundledDefaultConfig(): HiaiOpencodeConfig {
61
186
  const configPath = join(pluginRoot, "hiai-opencode.json")
62
187
  const raw = readFileSync(configPath, "utf-8")
63
188
  const parsed = JSON.parse(raw) as HiaiOpencodeConfig
189
+ const materialized = materializeConfig(parsed)
190
+
191
+ return expandPluginRootPlaceholders(materialized, pluginRoot) as HiaiOpencodeConfig
192
+ }
64
193
 
65
- return expandPluginRootPlaceholders(parsed, pluginRoot) as HiaiOpencodeConfig
194
+ export function applyModelSlots(config: HiaiOpencodeConfig): HiaiOpencodeConfig {
195
+ return materializeConfig(config)
66
196
  }
67
197
 
68
198
  export const defaultConfig: HiaiOpencodeConfig = loadBundledDefaultConfig()
@@ -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 {