@hiai-gg/hiai-opencode 0.1.4 → 0.1.6

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 (81) hide show
  1. package/.env.example +14 -8
  2. package/AGENTS.md +19 -8
  3. package/ARCHITECTURE.md +7 -6
  4. package/LICENSE.md +0 -1
  5. package/README.md +48 -17
  6. package/assets/cli/hiai-opencode.mjs +590 -7
  7. package/assets/mcp/mempalace.mjs +159 -25
  8. package/config/hiai-opencode.schema.json +82 -148
  9. package/dist/agents/dynamic-agent-core-sections.d.ts +4 -1
  10. package/dist/agents/dynamic-agent-prompt-builder.d.ts +1 -1
  11. package/dist/config/defaults.d.ts +1 -0
  12. package/dist/config/platform-schema.d.ts +275 -10
  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/oh-my-opencode-config.d.ts +1 -3
  16. package/dist/config/types.d.ts +22 -5
  17. package/dist/create-tools.d.ts +2 -0
  18. package/dist/features/builtin-commands/templates/doctor.d.ts +1 -0
  19. package/dist/features/builtin-commands/types.d.ts +1 -1
  20. package/dist/features/builtin-skills/skills/hiai-opencode-setup.d.ts +2 -0
  21. package/dist/features/builtin-skills/skills/index.d.ts +1 -0
  22. package/dist/index.js +870 -1711
  23. package/dist/mcp/types.d.ts +1 -1
  24. package/dist/plugin/tool-registry.d.ts +2 -0
  25. package/dist/shared/mcp-static-export.d.ts +22 -0
  26. package/dist/tools/ast-grep/constants.d.ts +1 -1
  27. package/dist/tools/ast-grep/environment-check.d.ts +1 -5
  28. package/dist/tools/ast-grep/language-support.d.ts +0 -1
  29. package/dist/tools/ast-grep/types.d.ts +1 -2
  30. package/dist/tools/skill-mcp/tools.d.ts +2 -0
  31. package/hiai-opencode.json +39 -171
  32. package/package.json +6 -4
  33. package/src/agents/bob/default.ts +6 -1
  34. package/src/agents/bob/gpt-pro.ts +1 -0
  35. package/src/agents/bob.ts +1 -0
  36. package/src/agents/coder/gpt-codex.ts +1 -0
  37. package/src/agents/coder/gpt-pro.ts +1 -0
  38. package/src/agents/coder/gpt.ts +1 -0
  39. package/src/agents/dynamic-agent-core-sections.ts +36 -0
  40. package/src/agents/dynamic-agent-prompt-builder.ts +1 -0
  41. package/src/config/defaults.ts +171 -28
  42. package/src/config/loader.test.ts +16 -1
  43. package/src/config/loader.ts +4 -2
  44. package/src/config/model-slots-and-export.test.ts +55 -0
  45. package/src/config/platform-schema.ts +37 -5
  46. package/src/config/schema/commands.ts +1 -0
  47. package/src/config/schema/oh-my-opencode-config.ts +0 -3
  48. package/src/config/types.ts +34 -5
  49. package/src/create-tools.ts +4 -1
  50. package/src/features/builtin-commands/commands.ts +7 -0
  51. package/src/features/builtin-commands/templates/doctor.ts +43 -0
  52. package/src/features/builtin-commands/types.ts +1 -1
  53. package/src/features/builtin-skills/skills/hiai-opencode-setup.ts +69 -0
  54. package/src/features/builtin-skills/skills/index.ts +1 -0
  55. package/src/features/builtin-skills/skills.ts +10 -1
  56. package/src/index.ts +4 -38
  57. package/src/lsp/index.ts +1 -0
  58. package/src/mcp/registry.ts +6 -1
  59. package/src/plugin/tool-registry.ts +4 -0
  60. package/src/shared/mcp-static-export.ts +121 -0
  61. package/src/tools/ast-grep/constants.ts +1 -1
  62. package/src/tools/ast-grep/environment-check.ts +2 -32
  63. package/src/tools/ast-grep/language-support.ts +0 -3
  64. package/src/tools/ast-grep/types.ts +1 -2
  65. package/src/tools/skill-mcp/tools.test.ts +44 -0
  66. package/src/tools/skill-mcp/tools.ts +45 -7
  67. package/dist/ast-grep-napi.win32-x64-msvc-67c0y8nc.node +0 -0
  68. package/dist/config/loader.test.d.ts +0 -1
  69. package/dist/config/models.d.ts +0 -13
  70. package/dist/internals/plugins/websearch-cited/google.d.ts +0 -38
  71. package/dist/internals/plugins/websearch-cited/index.d.ts +0 -11
  72. package/dist/internals/plugins/websearch-cited/openai.d.ts +0 -9
  73. package/dist/internals/plugins/websearch-cited/openrouter.d.ts +0 -2
  74. package/dist/internals/plugins/websearch-cited/types.d.ts +0 -5
  75. package/src/internals/plugins/websearch-cited/LICENSE +0 -214
  76. package/src/internals/plugins/websearch-cited/codex_prompt.txt +0 -79
  77. package/src/internals/plugins/websearch-cited/google.ts +0 -749
  78. package/src/internals/plugins/websearch-cited/index.ts +0 -301
  79. package/src/internals/plugins/websearch-cited/openai.ts +0 -407
  80. package/src/internals/plugins/websearch-cited/openrouter.ts +0 -190
  81. package/src/internals/plugins/websearch-cited/types.ts +0 -7
@@ -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,135 @@ 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 },
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" },
54
129
  }
130
+ }
55
131
 
56
- return value
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
+ const merged = { ...entry, ...override }
139
+
140
+ if (name === "mempalace" && typeof merged.pythonPath === "string" && merged.pythonPath.trim()) {
141
+ merged.environment = {
142
+ ...(entry.environment ?? {}),
143
+ ...(merged.environment ?? {}),
144
+ MEMPALACE_PYTHON: merged.pythonPath.trim(),
145
+ }
146
+ }
147
+
148
+ // Internal launcher hint; do not leak unknown keys into final OpenCode MCP server config.
149
+ delete merged.pythonPath
150
+
151
+ return [name, merged]
152
+ }),
153
+ )
154
+ }
155
+
156
+ function deriveLsp(config: HiaiOpencodeConfig): HiaiOpencodeConfig["lsp"] {
157
+ const userLsp = config.lsp ?? {}
158
+ return Object.fromEntries(
159
+ Object.entries(DEFAULT_LSP).map(([name, entry]) => {
160
+ const override = userLsp[name] ?? {}
161
+ return [name, { ...entry, ...override }]
162
+ }),
163
+ )
164
+ }
165
+
166
+ function materializeConfig(rawConfig: HiaiOpencodeConfig): HiaiOpencodeConfig {
167
+ const models = requireModelSlots(rawConfig)
168
+ return {
169
+ ...rawConfig,
170
+ agents: deriveAgents(models),
171
+ agentRequirements: {},
172
+ categories: deriveCategories(models),
173
+ categoryRequirements: {},
174
+ modelFamilies: [],
175
+ mcp: deriveMcp(rawConfig),
176
+ lsp: deriveLsp(rawConfig),
177
+ subtask2: {
178
+ replace_generic: true,
179
+ generic_return: null,
180
+ ...(rawConfig.subtask2 ?? {}),
181
+ },
182
+ skills: {
183
+ enabled: true,
184
+ disabled: [],
185
+ ...(rawConfig.skills ?? {}),
186
+ },
187
+ permissions: {
188
+ read: { "*": "allow", "*.env": "deny", "*.env.*": "deny", "*.env.example": "allow" },
189
+ edit: { "*": "allow" },
190
+ bash: { "*": "allow" },
191
+ deny_paths: ["**/backup/**", "**/secrets.*", "**/.env", "**/.env.*"],
192
+ ...(rawConfig.permissions ?? {}),
193
+ },
194
+ }
57
195
  }
58
196
 
59
197
  function loadBundledDefaultConfig(): HiaiOpencodeConfig {
@@ -61,8 +199,13 @@ function loadBundledDefaultConfig(): HiaiOpencodeConfig {
61
199
  const configPath = join(pluginRoot, "hiai-opencode.json")
62
200
  const raw = readFileSync(configPath, "utf-8")
63
201
  const parsed = JSON.parse(raw) as HiaiOpencodeConfig
202
+ const materialized = materializeConfig(parsed)
203
+
204
+ return expandPluginRootPlaceholders(materialized, pluginRoot) as HiaiOpencodeConfig
205
+ }
64
206
 
65
- return expandPluginRootPlaceholders(parsed, pluginRoot) as HiaiOpencodeConfig
207
+ export function applyModelSlots(config: HiaiOpencodeConfig): HiaiOpencodeConfig {
208
+ return materializeConfig(config)
66
209
  }
67
210
 
68
211
  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 {
@@ -0,0 +1,55 @@
1
+ import { expect, test } from "bun:test"
2
+
3
+ import { applyModelSlots } from "./defaults"
4
+ import type { HiaiOpencodeConfig } from "./types"
5
+ import { buildStaticMcpJson, MCP_EXPORT_MARKER } from "../shared/mcp-static-export"
6
+ import { buildHiaiIntegrationPrimerSection } from "../agents/dynamic-agent-core-sections"
7
+
8
+ const baseConfig: HiaiOpencodeConfig = {
9
+ models: {
10
+ bob: { model: "openrouter/test/bob", recommended: "xhigh" },
11
+ coder: { model: "openrouter/test/coder", recommended: "high" },
12
+ strategist: { model: "openrouter/test/strategist", recommended: "high" },
13
+ guard: { model: "openrouter/test/guard", recommended: "middle" },
14
+ critic: { model: "openrouter/test/critic", recommended: "high" },
15
+ designer: { model: "openrouter/test/designer", recommended: "design" },
16
+ researcher: { model: "openrouter/test/researcher", recommended: "fast" },
17
+ manager: { model: "openrouter/test/manager", recommended: "fast" },
18
+ brainstormer: { model: "openrouter/test/brainstormer", recommended: "writing" },
19
+ vision: { model: "openrouter/test/vision", recommended: "vision" },
20
+ },
21
+ mcp: {
22
+ mempalace: { enabled: true, pythonPath: "/opt/venv/bin/python" },
23
+ },
24
+ }
25
+
26
+ test("model slots derive canonical agents and categories", () => {
27
+ const resolved = applyModelSlots(baseConfig)
28
+ expect(resolved.agents?.bob?.model).toBe("openrouter/test/bob")
29
+ expect(resolved.agents?.coder?.model).toBe("openrouter/test/coder")
30
+ expect(resolved.agents?.["platform-manager"]?.model).toBe("openrouter/test/manager")
31
+ expect(resolved.agents?.sub?.model).toBe("openrouter/test/coder")
32
+ expect(resolved.categories?.artistry?.model).toBe("openrouter/test/designer")
33
+ expect(resolved.categories?.ultrabrain?.model).toBe("openrouter/test/strategist")
34
+ expect(resolved.categories?.writing?.model).toBe("openrouter/test/brainstormer")
35
+ })
36
+
37
+ test("compact MCP mempalace pythonPath is materialized into environment", () => {
38
+ const resolved = applyModelSlots(baseConfig)
39
+ expect(resolved.mcp?.mempalace?.environment?.MEMPALACE_PYTHON).toBe("/opt/venv/bin/python")
40
+ })
41
+
42
+ test("static MCP export includes marker metadata and servers", () => {
43
+ const resolved = applyModelSlots(baseConfig)
44
+ const exported = buildStaticMcpJson(resolved)
45
+
46
+ expect(exported._meta?.generatedBy).toBe(MCP_EXPORT_MARKER)
47
+ expect(exported._meta?.version).toBe(1)
48
+ expect(exported.mcpServers.playwright).toBeDefined()
49
+ expect(exported.mcpServers.mempalace).toBeDefined()
50
+ })
51
+
52
+ test("integration primer does not request model provider API keys", () => {
53
+ const primer = buildHiaiIntegrationPrimerSection()
54
+ expect(primer).toContain("Do not ask for `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, or `ANTHROPIC_API_KEY`")
55
+ })
@@ -8,6 +8,37 @@ export const AgentConfigSchema = z.object({
8
8
  description: z.string().optional(),
9
9
  });
10
10
 
11
+ export const ModelRecommendationSchema = z.enum([
12
+ "xhigh",
13
+ "high",
14
+ "middle",
15
+ "fast",
16
+ "vision",
17
+ "writing",
18
+ "design",
19
+ ]);
20
+
21
+ export const ModelSlotConfigSchema = z.union([
22
+ z.string(),
23
+ z.object({
24
+ model: z.string(),
25
+ recommended: ModelRecommendationSchema.optional(),
26
+ }),
27
+ ]);
28
+
29
+ export const ModelSlotsConfigSchema = z.object({
30
+ bob: ModelSlotConfigSchema.optional(),
31
+ coder: ModelSlotConfigSchema.optional(),
32
+ strategist: ModelSlotConfigSchema.optional(),
33
+ guard: ModelSlotConfigSchema.optional(),
34
+ critic: ModelSlotConfigSchema.optional(),
35
+ designer: ModelSlotConfigSchema.optional(),
36
+ researcher: ModelSlotConfigSchema.optional(),
37
+ manager: ModelSlotConfigSchema.optional(),
38
+ brainstormer: ModelSlotConfigSchema.optional(),
39
+ vision: ModelSlotConfigSchema.optional(),
40
+ });
41
+
11
42
  export const FallbackEntrySchema = z.object({
12
43
  providers: z.array(z.string()),
13
44
  model: z.string(),
@@ -45,11 +76,13 @@ export const McpServerConfigSchema = z.object({
45
76
  command: z.array(z.string()).optional(),
46
77
  timeout: z.number().optional(),
47
78
  environment: z.record(z.string(), z.string()).optional(),
79
+ pythonPath: z.string().optional(),
48
80
  });
49
81
 
50
82
  export const LspServerConfigSchema = z.object({
51
- command: z.array(z.string()),
52
- extensions: z.array(z.string()),
83
+ enabled: z.boolean().optional(),
84
+ command: z.array(z.string()).optional(),
85
+ extensions: z.array(z.string()).optional(),
53
86
  initialization: z.record(z.string(), z.unknown()).optional(),
54
87
  });
55
88
 
@@ -97,11 +130,9 @@ export const PermissionsConfigSchema = z.object({
97
130
  });
98
131
 
99
132
  export const AuthKeysSchema = z.object({
100
- googleSearch: z.string().optional(),
101
- openai: z.string().optional(),
102
- openrouter: z.string().optional(),
103
133
  stitch: z.string().optional(),
104
134
  firecrawl: z.string().optional(),
135
+ context7: z.string().optional(),
105
136
  });
106
137
 
107
138
  export const OllamaConfigSchema = z.object({
@@ -190,6 +221,7 @@ const AgentRequirementsConfigSchema = z.object({
190
221
 
191
222
  export const HiaiOpencodeConfigSchema = z.object({
192
223
  $schema: z.string().optional(),
224
+ models: ModelSlotsConfigSchema.optional(),
193
225
  agents: AgentsConfigSchema.optional(),
194
226
  agentRequirements: AgentRequirementsConfigSchema.optional(),
195
227
  categories: z.record(z.string(), CategoryConfigSchema).optional(),
@@ -10,6 +10,7 @@ export const BuiltinCommandNameSchema = z.enum([
10
10
  "stop-continuation",
11
11
  "remove-ai-slops",
12
12
  "mcp-status",
13
+ "doctor",
13
14
  ])
14
15
 
15
16
  export type BuiltinCommandName = z.infer<typeof BuiltinCommandNameSchema>
@@ -26,9 +26,6 @@ import { FastApplyConfigSchema } from "./fast-apply"
26
26
  import { WebsearchConfigSchema } from "./websearch"
27
27
 
28
28
  const AuthConfigSchema = z.object({
29
- googleSearch: z.string().optional(),
30
- openai: z.string().optional(),
31
- openrouter: z.string().optional(),
32
29
  stitch: z.string().optional(),
33
30
  firecrawl: z.string().optional(),
34
31
  context7: z.string().optional(),
@@ -7,6 +7,35 @@ export interface AgentConfig {
7
7
  description?: string;
8
8
  }
9
9
 
10
+ export type ModelRecommendation =
11
+ | "xhigh"
12
+ | "high"
13
+ | "middle"
14
+ | "fast"
15
+ | "vision"
16
+ | "writing"
17
+ | "design";
18
+
19
+ export type ModelSlotConfig =
20
+ | string
21
+ | {
22
+ model: string;
23
+ recommended?: ModelRecommendation;
24
+ };
25
+
26
+ export interface ModelSlotsConfig {
27
+ bob?: ModelSlotConfig;
28
+ coder?: ModelSlotConfig;
29
+ strategist?: ModelSlotConfig;
30
+ guard?: ModelSlotConfig;
31
+ critic?: ModelSlotConfig;
32
+ designer?: ModelSlotConfig;
33
+ researcher?: ModelSlotConfig;
34
+ manager?: ModelSlotConfig;
35
+ brainstormer?: ModelSlotConfig;
36
+ vision?: ModelSlotConfig;
37
+ }
38
+
10
39
  // Canonical 12-agent model exposed by schema/default config.
11
40
  export const CANONICAL_AGENT_NAMES = [
12
41
  "bob",
@@ -121,11 +150,13 @@ export interface McpServerConfig {
121
150
  command?: string[];
122
151
  timeout?: number;
123
152
  environment?: Record<string, string>;
153
+ pythonPath?: string;
124
154
  }
125
155
 
126
156
  export interface LspServerConfig {
127
- command: string[];
128
- extensions: string[];
157
+ enabled?: boolean;
158
+ command?: string[];
159
+ extensions?: string[];
129
160
  initialization?: Record<string, unknown>;
130
161
  }
131
162
 
@@ -157,9 +188,6 @@ export interface PermissionsConfig {
157
188
  }
158
189
 
159
190
  export interface AuthKeys {
160
- googleSearch?: string;
161
- openai?: string;
162
- openrouter?: string;
163
191
  stitch?: string;
164
192
  firecrawl?: string;
165
193
  context7?: string;
@@ -174,6 +202,7 @@ export interface OllamaConfig {
174
202
 
175
203
  export interface HiaiOpencodeConfig {
176
204
  $schema?: string;
205
+ models?: ModelSlotsConfig;
177
206
  agents?: AgentConfigMap;
178
207
  agentRequirements?: AgentRequirementMap;
179
208
  categories?: Record<string, CategoryConfig>;
@@ -1,5 +1,6 @@
1
1
  import type { AvailableCategory, AvailableSkill } from "./agents/dynamic-agent-prompt-builder"
2
2
  import type { HiaiOpenCodeConfig } from "./config"
3
+ import type { HiaiOpencodeConfig } from "./config/types"
3
4
  import type { BrowserAutomationProvider } from "./config/schema/browser-automation"
4
5
  import type { LoadedSkill } from "./features/opencode-skill-loader/types"
5
6
  import type { PluginContext, ToolsRecord } from "./plugin/types"
@@ -22,9 +23,10 @@ type CreateToolsResult = {
22
23
  export async function createTools(args: {
23
24
  ctx: PluginContext
24
25
  pluginConfig: HiaiOpenCodeConfig
26
+ platformConfig?: HiaiOpencodeConfig
25
27
  managers: Pick<Managers, "backgroundManager" | "tmuxSessionManager" | "skillMcpManager">
26
28
  }): Promise<CreateToolsResult> {
27
- const { ctx, pluginConfig, managers } = args
29
+ const { ctx, pluginConfig, platformConfig, managers } = args
28
30
 
29
31
  const skillContext = await createSkillContext({
30
32
  directory: ctx.directory,
@@ -39,6 +41,7 @@ export async function createTools(args: {
39
41
  managers,
40
42
  skillContext,
41
43
  availableCategories,
44
+ builtinMcp: platformConfig?.mcp,
42
45
  })
43
46
 
44
47
  return {
@@ -9,6 +9,7 @@ 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
11
  import { MCP_STATUS_TEMPLATE } from "./templates/mcp-status"
12
+ import { DOCTOR_TEMPLATE } from "./templates/doctor"
12
13
 
13
14
  interface LoadBuiltinCommandsOptions {
14
15
  useRegisteredAgents?: boolean
@@ -126,6 +127,12 @@ $ARGUMENTS
126
127
  description: "(builtin) Show hiai-opencode MCP server status, missing keys, and local runtime availability",
127
128
  template: `<command-instruction>
128
129
  ${MCP_STATUS_TEMPLATE}
130
+ </command-instruction>`,
131
+ },
132
+ doctor: {
133
+ description: "(builtin) Run hiai-opencode install/runtime diagnostics and explain setup issues",
134
+ template: `<command-instruction>
135
+ ${DOCTOR_TEMPLATE}
129
136
  </command-instruction>`,
130
137
  },
131
138
  }
@@ -0,0 +1,43 @@
1
+ export const DOCTOR_TEMPLATE = `# Hiai OpenCode Doctor Command
2
+
3
+ ## Purpose
4
+
5
+ Use /doctor to run the hiai-opencode install/runtime diagnostic and report actionable setup issues.
6
+
7
+ ## Execute
8
+
9
+ Run:
10
+
11
+ \`\`\`bash
12
+ hiai-opencode doctor
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 doctor
19
+ \`\`\`
20
+
21
+ ## Report
22
+
23
+ Summarize:
24
+
25
+ - config path
26
+ - enabled and disabled MCP servers
27
+ - missing env vars by name only
28
+ - static \`.mcp.json\` freshness and whether it is managed by hiai-opencode
29
+ - OpenCode Connect visibility for configured model providers
30
+ - OpenCode plugin registration sanity (including \`plugin: ["list"]\` misconfiguration warning)
31
+ - skill materialization status from skill registry
32
+ - agent count and naming summary
33
+ - LSP runtime availability
34
+ - MemPalace python source and selected interpreter (env/config/auto)
35
+ - MCP tool probes (real connect + tools/list) for stdio and basic endpoint probes for remote MCP
36
+
37
+ Rules:
38
+
39
+ - Do not print API key values.
40
+ - Do not ask for model provider env vars such as OPENROUTER_API_KEY or OPENAI_API_KEY; normal model auth belongs to OpenCode Connect.
41
+ - If \`opencode mcp list\` is empty but doctor/mcp-status sees servers, explain the runtime-vs-static config distinction and run \`hiai-opencode export-mcp .mcp.json\` if the user wants static visibility.
42
+ - Do not run package installs unless the user explicitly asks.
43
+ `
@@ -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" | "mcp-status"
3
+ export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work" | "stop-continuation" | "handoff" | "remove-ai-slops" | "mcp-status" | "doctor"
4
4
 
5
5
  export interface BuiltinCommandConfig {
6
6
  disabled_commands?: BuiltinCommandName[]