@hiai-gg/hiai-opencode 0.1.5 → 0.1.7

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 (180) hide show
  1. package/.env.example +21 -8
  2. package/AGENTS.md +60 -6
  3. package/ARCHITECTURE.md +6 -3
  4. package/LICENSE.md +0 -1
  5. package/README.md +113 -33
  6. package/assets/cli/hiai-opencode.mjs +668 -7
  7. package/assets/mcp/mempalace.mjs +159 -25
  8. package/config/hiai-opencode.schema.json +29 -3
  9. package/dist/agents/agent-skills.d.ts +7 -0
  10. package/dist/agents/bob/default.d.ts +1 -0
  11. package/dist/agents/bob/gemini.d.ts +1 -0
  12. package/dist/agents/bob/gpt-pro.d.ts +1 -0
  13. package/dist/agents/brainstormer.d.ts +7 -0
  14. package/dist/agents/coder/gpt-codex.d.ts +1 -1
  15. package/dist/agents/coder/gpt-pro.d.ts +1 -0
  16. package/dist/agents/coder/gpt.d.ts +2 -1
  17. package/dist/agents/designer.d.ts +7 -0
  18. package/dist/agents/dynamic-agent-core-sections.d.ts +4 -1
  19. package/dist/agents/dynamic-agent-prompt-builder.d.ts +1 -1
  20. package/dist/agents/strategist/gemini.d.ts +1 -0
  21. package/dist/agents/strategist/gpt.d.ts +1 -0
  22. package/dist/agents/types.d.ts +3 -1
  23. package/dist/config/index.d.ts +0 -1
  24. package/dist/config/platform-schema.d.ts +34 -6
  25. package/dist/config/schema/commands.d.ts +1 -0
  26. package/dist/config/schema/hooks.d.ts +0 -2
  27. package/dist/config/schema/index.d.ts +0 -2
  28. package/dist/config/schema/oh-my-opencode-config.d.ts +1 -9
  29. package/dist/config/types.d.ts +4 -4
  30. package/dist/create-hooks.d.ts +0 -2
  31. package/dist/features/builtin-commands/templates/doctor.d.ts +1 -0
  32. package/dist/features/builtin-commands/types.d.ts +1 -1
  33. package/dist/features/builtin-skills/skills/hiai-opencode-setup.d.ts +2 -0
  34. package/dist/features/builtin-skills/skills/index.d.ts +2 -0
  35. package/dist/features/builtin-skills/skills/website-copywriting.d.ts +2 -0
  36. package/dist/hooks/agent-usage-reminder/constants.d.ts +1 -1
  37. package/dist/hooks/index.d.ts +0 -2
  38. package/dist/hooks/keyword-detector/ultrawork/default.d.ts +1 -1
  39. package/dist/hooks/keyword-detector/ultrawork/gemini.d.ts +1 -1
  40. package/dist/hooks/keyword-detector/ultrawork/gpt.d.ts +1 -1
  41. package/dist/hooks/keyword-detector/ultrawork/planner.d.ts +1 -1
  42. package/dist/index.js +7719 -153698
  43. package/dist/mcp/index.d.ts +0 -1
  44. package/dist/mcp/registry.d.ts +1 -1
  45. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -2
  46. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -3
  47. package/dist/plugin/startup-diagnostics.d.ts +1 -0
  48. package/dist/shared/logger.d.ts +2 -0
  49. package/dist/shared/mcp-static-export.d.ts +22 -0
  50. package/dist/shared/mode-routing.d.ts +6 -0
  51. package/dist/tools/ast-grep/constants.d.ts +1 -1
  52. package/dist/tools/ast-grep/environment-check.d.ts +1 -5
  53. package/dist/tools/ast-grep/language-support.d.ts +0 -1
  54. package/dist/tools/ast-grep/types.d.ts +1 -2
  55. package/dist/tools/delegate-task/git-categories.d.ts +2 -0
  56. package/dist/tools/delegate-task/sub-agent.d.ts +2 -0
  57. package/dist/tools/skill-mcp/constants.d.ts +1 -1
  58. package/hiai-opencode.json +50 -19
  59. package/package.json +10 -5
  60. package/src/agents/agent-skills.ts +70 -0
  61. package/src/agents/bob/default.ts +7 -1
  62. package/src/agents/bob/gemini.ts +1 -0
  63. package/src/agents/bob/gpt-pro.ts +3 -1
  64. package/src/agents/bob.ts +3 -0
  65. package/src/agents/brainstormer.ts +72 -0
  66. package/src/agents/builtin-agents.ts +59 -3
  67. package/src/agents/coder/gpt-codex.ts +5 -3
  68. package/src/agents/coder/gpt-pro.ts +4 -2
  69. package/src/agents/coder/gpt.ts +3 -1
  70. package/src/agents/critic/agent.ts +1 -0
  71. package/src/agents/designer.ts +70 -0
  72. package/src/agents/dynamic-agent-category-skills-guide.ts +6 -0
  73. package/src/agents/dynamic-agent-core-sections.ts +36 -0
  74. package/src/agents/dynamic-agent-prompt-builder.ts +1 -0
  75. package/src/agents/guard/default.ts +1 -0
  76. package/src/agents/guard/gemini.ts +1 -0
  77. package/src/agents/guard/gpt.ts +1 -0
  78. package/src/agents/platform-manager.ts +17 -1
  79. package/src/agents/prompt-library/platform.ts +34 -0
  80. package/src/agents/researcher.ts +1 -0
  81. package/src/agents/strategist/gemini.ts +1 -0
  82. package/src/agents/strategist/gpt.ts +1 -0
  83. package/src/agents/types.ts +4 -1
  84. package/src/agents/ui.ts +1 -0
  85. package/src/config/defaults.ts +45 -13
  86. package/src/config/index.ts +0 -1
  87. package/src/config/model-slots-and-export.test.ts +73 -0
  88. package/src/config/platform-schema.ts +3 -3
  89. package/src/config/schema/commands.ts +1 -0
  90. package/src/config/schema/hooks.ts +0 -2
  91. package/src/config/schema/index.ts +0 -2
  92. package/src/config/schema/oh-my-opencode-config.ts +0 -5
  93. package/src/config/types.ts +4 -4
  94. package/src/features/builtin-commands/commands.ts +7 -0
  95. package/src/features/builtin-commands/templates/doctor.ts +43 -0
  96. package/src/features/builtin-commands/types.ts +1 -1
  97. package/src/features/builtin-skills/skills/hiai-opencode-setup.ts +69 -0
  98. package/src/features/builtin-skills/skills/index.ts +2 -0
  99. package/src/features/builtin-skills/skills/website-copywriting.ts +41 -0
  100. package/src/features/builtin-skills/skills.test.ts +8 -0
  101. package/src/features/builtin-skills/skills.ts +12 -1
  102. package/src/features/skill-mcp-manager/AGENTS.md +1 -1
  103. package/src/hooks/agent-usage-reminder/constants.ts +4 -4
  104. package/src/hooks/index.ts +0 -2
  105. package/src/hooks/keyword-detector/ultrawork/default.ts +18 -18
  106. package/src/hooks/keyword-detector/ultrawork/gemini.ts +21 -21
  107. package/src/hooks/keyword-detector/ultrawork/gpt.ts +6 -8
  108. package/src/hooks/keyword-detector/ultrawork/planner.ts +5 -5
  109. package/src/index.ts +8 -78
  110. package/src/internals/plugins/subtask2/commands/manifest.ts +2 -6
  111. package/src/internals/plugins/subtask2/hooks/command-hooks.ts +2 -2
  112. package/src/internals/plugins/subtask2/hooks/message-hooks.ts +1 -1
  113. package/src/internals/plugins/subtask2/parsing/parallel.ts +13 -10
  114. package/src/mcp/index.ts +0 -1
  115. package/src/mcp/registry.ts +27 -0
  116. package/src/plugin/chat-message.ts +0 -2
  117. package/src/plugin/hooks/create-session-hooks.ts +0 -17
  118. package/src/plugin/startup-diagnostics.ts +27 -0
  119. package/src/plugin-handlers/agent-config-handler.ts +3 -2
  120. package/src/plugin-handlers/mcp-config-handler.test.ts +63 -0
  121. package/src/plugin-handlers/mcp-config-handler.ts +29 -14
  122. package/src/plugin-handlers/strategist-agent-config-builder.ts +1 -1
  123. package/src/shared/agent-display-names.test.ts +9 -0
  124. package/src/shared/agent-display-names.ts +5 -0
  125. package/src/shared/log-legacy-plugin-startup-warning.ts +6 -8
  126. package/src/shared/logger.ts +8 -0
  127. package/src/shared/mcp-static-export.ts +119 -0
  128. package/src/shared/migration/agent-names.ts +8 -0
  129. package/src/shared/migration/hook-names.ts +1 -1
  130. package/src/shared/mode-routing.test.ts +88 -0
  131. package/src/shared/mode-routing.ts +30 -0
  132. package/src/shared/startup-diagnostics.ts +6 -7
  133. package/src/tools/ast-grep/constants.ts +1 -1
  134. package/src/tools/ast-grep/environment-check.ts +2 -32
  135. package/src/tools/ast-grep/language-support.ts +0 -3
  136. package/src/tools/ast-grep/types.ts +1 -2
  137. package/src/tools/call-omo-agent/tools.ts +11 -4
  138. package/src/tools/delegate-task/anthropic-categories.ts +3 -3
  139. package/src/tools/delegate-task/builtin-categories.ts +2 -0
  140. package/src/tools/delegate-task/categories.test.ts +87 -0
  141. package/src/tools/delegate-task/category-resolver.ts +8 -9
  142. package/src/tools/delegate-task/git-categories.ts +30 -0
  143. package/src/tools/delegate-task/model-string-parser.test.ts +90 -0
  144. package/src/tools/delegate-task/openai-categories.ts +26 -22
  145. package/src/tools/delegate-task/sub-agent.ts +10 -0
  146. package/src/tools/delegate-task/subagent-discovery.test.ts +123 -0
  147. package/src/tools/delegate-task/subagent-resolver.ts +18 -1
  148. package/src/tools/skill-mcp/constants.ts +1 -1
  149. package/src/tools/skill-mcp/tools.test.ts +44 -0
  150. package/dist/ast-grep-napi.win32-x64-msvc-67c0y8nc.node +0 -0
  151. package/dist/config/loader.test.d.ts +0 -1
  152. package/dist/config/models.d.ts +0 -13
  153. package/dist/config/schema/websearch.d.ts +0 -13
  154. package/dist/hooks/no-bob-gpt/hook.d.ts +0 -16
  155. package/dist/hooks/no-bob-gpt/index.d.ts +0 -1
  156. package/dist/hooks/no-coder-non-gpt/hook.d.ts +0 -20
  157. package/dist/hooks/no-coder-non-gpt/index.d.ts +0 -1
  158. package/dist/internals/plugins/websearch-cited/google.d.ts +0 -38
  159. package/dist/internals/plugins/websearch-cited/index.d.ts +0 -17
  160. package/dist/internals/plugins/websearch-cited/openai.d.ts +0 -9
  161. package/dist/internals/plugins/websearch-cited/openrouter.d.ts +0 -2
  162. package/dist/internals/plugins/websearch-cited/types.d.ts +0 -5
  163. package/dist/mcp/grep-app.d.ts +0 -6
  164. package/dist/mcp/omo-mcp-index.d.ts +0 -10
  165. package/dist/mcp/websearch.d.ts +0 -11
  166. package/src/config/schema/websearch.ts +0 -15
  167. package/src/hooks/no-bob-gpt/hook.ts +0 -56
  168. package/src/hooks/no-bob-gpt/index.ts +0 -1
  169. package/src/hooks/no-coder-non-gpt/hook.ts +0 -67
  170. package/src/hooks/no-coder-non-gpt/index.ts +0 -1
  171. package/src/internals/plugins/websearch-cited/LICENSE +0 -214
  172. package/src/internals/plugins/websearch-cited/codex_prompt.txt +0 -79
  173. package/src/internals/plugins/websearch-cited/google.ts +0 -749
  174. package/src/internals/plugins/websearch-cited/index.ts +0 -306
  175. package/src/internals/plugins/websearch-cited/openai.ts +0 -407
  176. package/src/internals/plugins/websearch-cited/openrouter.ts +0 -190
  177. package/src/internals/plugins/websearch-cited/types.ts +0 -7
  178. package/src/mcp/grep-app.ts +0 -6
  179. package/src/mcp/omo-mcp-index.ts +0 -30
  180. package/src/mcp/websearch.ts +0 -44
@@ -1,12 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync } from "node:child_process"
4
- import { existsSync, readFileSync } from "node:fs"
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
5
5
  import { dirname, join } from "node:path"
6
+ import { fileURLToPath } from "node:url"
6
7
  import { homedir } from "node:os"
7
8
  import { parse } from "jsonc-parser"
9
+ import { createHash } from "node:crypto"
10
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js"
11
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
8
12
 
9
13
  const DEFAULT_RAG_URL = "http://localhost:9002/tools/search"
14
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..")
15
+ const MCP_EXPORT_MARKER = "hiai-opencode"
10
16
 
11
17
  const MCP_REGISTRY = {
12
18
  playwright: {
@@ -52,10 +58,16 @@ function usage() {
52
58
  console.log(`hiai-opencode
53
59
 
54
60
  Usage:
61
+ hiai-opencode doctor
55
62
  hiai-opencode mcp-status
63
+ hiai-opencode export-mcp [path]
64
+ hiai-opencode diagnose [path]
56
65
 
57
66
  Commands:
67
+ doctor Full install/runtime diagnostic: MCP status + static export freshness + provider/skills/agents/LSP checks + MCP tool probes.
58
68
  mcp-status Check hiai-opencode MCP configuration, keys, and local runtimes.
69
+ export-mcp Write a static .mcp.json for hosts whose mcp list ignores plugin runtime MCP.
70
+ diagnose Collect full diagnostic bundle to file (local only, no remote sending).
59
71
  `)
60
72
  }
61
73
 
@@ -106,6 +118,254 @@ function loadConfig() {
106
118
  return { path: null, config: {} }
107
119
  }
108
120
 
121
+ function candidateOpenCodeConfigPaths() {
122
+ const cwd = process.cwd()
123
+ const paths = [
124
+ join(cwd, ".opencode", "opencode.json"),
125
+ join(cwd, ".opencode", "opencode.jsonc"),
126
+ join(homedir(), ".config", "opencode", "opencode.json"),
127
+ join(homedir(), ".config", "opencode", "opencode.jsonc"),
128
+ ]
129
+
130
+ if (process.platform === "win32" && process.env.APPDATA) {
131
+ paths.push(join(process.env.APPDATA, "opencode", "opencode.json"))
132
+ paths.push(join(process.env.APPDATA, "opencode", "opencode.jsonc"))
133
+ }
134
+
135
+ return paths
136
+ }
137
+
138
+ function checkOpenCodePluginRegistration() {
139
+ for (const path of candidateOpenCodeConfigPaths()) {
140
+ if (!existsSync(path)) continue
141
+ try {
142
+ const parsed = parse(readFileSync(path, "utf-8")) ?? {}
143
+ const pluginEntries = Array.isArray(parsed?.plugin) ? parsed.plugin : []
144
+ const plugins = pluginEntries
145
+ .map((entry) => typeof entry === "string" ? entry : Array.isArray(entry) ? entry[0] : "")
146
+ .filter((entry) => typeof entry === "string" && entry.length > 0)
147
+
148
+ if (plugins.includes("list")) {
149
+ return {
150
+ level: "warn",
151
+ detail: `${path} contains plugin: [\"list\"] which can block MCP loading. Replace with [\"@hiai-gg/hiai-opencode\"].`,
152
+ }
153
+ }
154
+
155
+ if (plugins.includes("@hiai-gg/hiai-opencode")) {
156
+ return {
157
+ level: "ok",
158
+ detail: `plugin registered in ${path}`,
159
+ }
160
+ }
161
+
162
+ return {
163
+ level: "warn",
164
+ detail: `${path} found but @hiai-gg/hiai-opencode is not registered`,
165
+ }
166
+ } catch (error) {
167
+ return {
168
+ level: "warn",
169
+ detail: `failed to parse ${path}: ${error instanceof Error ? error.message : String(error)}`,
170
+ }
171
+ }
172
+ }
173
+
174
+ return {
175
+ level: "warn",
176
+ detail: "no opencode.json/opencode.jsonc found in project or global config paths",
177
+ }
178
+ }
179
+
180
+ function enabled(config, name) {
181
+ return config?.mcp?.[name]?.enabled ?? MCP_REGISTRY[name]?.defaultEnabled ?? true
182
+ }
183
+
184
+ function assetPath(...segments) {
185
+ return join(PACKAGE_ROOT, "assets", ...segments)
186
+ }
187
+
188
+ function createMcpExport(config) {
189
+ const servers = {}
190
+
191
+ if (enabled(config, "playwright")) {
192
+ servers.playwright = {
193
+ command: "node",
194
+ args: [assetPath("mcp", "playwright.mjs")],
195
+ }
196
+ }
197
+
198
+ if (enabled(config, "stitch")) {
199
+ servers.stitch = {
200
+ type: "http",
201
+ url: "https://stitch.googleapis.com/mcp",
202
+ headers: {
203
+ "X-Goog-Api-Key": config?.auth?.stitch || "${STITCH_AI_API_KEY}",
204
+ },
205
+ }
206
+ }
207
+
208
+ if (enabled(config, "sequential-thinking")) {
209
+ servers["sequential-thinking"] = {
210
+ command: "node",
211
+ args: [
212
+ assetPath("runtime", "npm-package-runner.mjs"),
213
+ "@modelcontextprotocol/server-sequential-thinking",
214
+ ],
215
+ }
216
+ }
217
+
218
+ if (enabled(config, "firecrawl")) {
219
+ servers.firecrawl = {
220
+ command: "node",
221
+ args: [assetPath("runtime", "npm-package-runner.mjs"), "firecrawl-mcp"],
222
+ env: {
223
+ FIRECRAWL_API_KEY: config?.auth?.firecrawl || "${FIRECRAWL_API_KEY}",
224
+ },
225
+ }
226
+ }
227
+
228
+ if (enabled(config, "rag")) {
229
+ servers.rag = {
230
+ command: "node",
231
+ args: [assetPath("mcp", "rag.mjs")],
232
+ env: {
233
+ OPENCODE_RAG_URL:
234
+ process.env.OPENCODE_RAG_URL
235
+ || resolveEnvTemplate(config?.mcp?.rag?.environment?.OPENCODE_RAG_URL)
236
+ || DEFAULT_RAG_URL,
237
+ },
238
+ }
239
+ }
240
+
241
+ if (enabled(config, "mempalace")) {
242
+ const mempalacePython =
243
+ process.env.MEMPALACE_PYTHON?.trim()
244
+ || resolveEnvTemplate(config?.mcp?.mempalace?.pythonPath?.trim())
245
+
246
+ servers.mempalace = {
247
+ command: "node",
248
+ args: [assetPath("mcp", "mempalace.mjs"), "--palace", "./.opencode/palace"],
249
+ env: mempalacePython
250
+ ? { MEMPALACE_PYTHON: mempalacePython }
251
+ : undefined,
252
+ }
253
+ }
254
+
255
+ if (enabled(config, "context7")) {
256
+ servers.context7 = {
257
+ type: "http",
258
+ url: "https://mcp.context7.com/mcp",
259
+ headers: config?.auth?.context7 || process.env.CONTEXT7_API_KEY
260
+ ? { "X-API-KEY": config?.auth?.context7 || "${CONTEXT7_API_KEY}" }
261
+ : undefined,
262
+ }
263
+ }
264
+
265
+ return {
266
+ _meta: {
267
+ generatedBy: MCP_EXPORT_MARKER,
268
+ version: 1,
269
+ generatedAt: new Date().toISOString(),
270
+ },
271
+ mcpServers: Object.fromEntries(
272
+ Object.entries(servers).map(([name, value]) => [
273
+ name,
274
+ Object.fromEntries(Object.entries(value).filter(([, field]) => field !== undefined)),
275
+ ]),
276
+ ),
277
+ }
278
+ }
279
+
280
+ function isManagedStaticMcpFile(path) {
281
+ if (!existsSync(path)) return false
282
+
283
+ try {
284
+ const parsed = JSON.parse(readFileSync(path, "utf-8"))
285
+ return parsed?._meta?.generatedBy === MCP_EXPORT_MARKER
286
+ } catch {
287
+ return false
288
+ }
289
+ }
290
+
291
+ function stableHash(value) {
292
+ return createHash("sha256").update(value).digest("hex")
293
+ }
294
+
295
+ function readExistingStaticMcp(path) {
296
+ if (!existsSync(path)) return null
297
+ try {
298
+ return JSON.parse(readFileSync(path, "utf-8"))
299
+ } catch {
300
+ return null
301
+ }
302
+ }
303
+
304
+ function checkStaticMcpFreshness(outputPath, config) {
305
+ const expected = createMcpExport(config)
306
+ const existing = readExistingStaticMcp(outputPath)
307
+ if (!existing) {
308
+ return { status: "missing", detail: `${outputPath} is missing` }
309
+ }
310
+
311
+ const expectedNormalized = {
312
+ ...expected,
313
+ _meta: { ...expected._meta, generatedAt: "<normalized>" },
314
+ }
315
+ const existingNormalized = {
316
+ ...existing,
317
+ _meta: existing._meta ? { ...existing._meta, generatedAt: "<normalized>" } : undefined,
318
+ }
319
+
320
+ const expectedHash = stableHash(JSON.stringify(expectedNormalized))
321
+ const existingHash = stableHash(JSON.stringify(existingNormalized))
322
+ const managed = existing?._meta?.generatedBy === MCP_EXPORT_MARKER
323
+
324
+ if (expectedHash === existingHash) {
325
+ return {
326
+ status: "fresh",
327
+ detail: `${outputPath} is up to date${managed ? " (managed)" : ""}`,
328
+ }
329
+ }
330
+
331
+ if (!managed) {
332
+ return {
333
+ status: "drift-unmanaged",
334
+ detail: `${outputPath} differs and is not managed by ${MCP_EXPORT_MARKER}`,
335
+ }
336
+ }
337
+
338
+ return {
339
+ status: "stale",
340
+ detail: `${outputPath} is stale (run: hiai-opencode export-mcp ${outputPath})`,
341
+ }
342
+ }
343
+
344
+ function exportMcp(outputPath = join(process.cwd(), ".mcp.json")) {
345
+ const { path, config, error } = loadConfig()
346
+ if (error) {
347
+ console.error(`Config parse warning: ${error}`)
348
+ }
349
+
350
+ const output = createMcpExport(config)
351
+ const mode = process.env.HIAI_OPENCODE_EXPORT_MCP_MODE?.trim().toLowerCase() || "safe"
352
+ const force = mode === "force"
353
+
354
+ if (existsSync(outputPath) && !isManagedStaticMcpFile(outputPath) && !force) {
355
+ console.error(
356
+ `Refusing to overwrite non-managed ${outputPath}. ` +
357
+ `Set HIAI_OPENCODE_EXPORT_MCP_MODE=force to override.`,
358
+ )
359
+ process.exit(1)
360
+ }
361
+
362
+ mkdirSync(dirname(outputPath), { recursive: true })
363
+ writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`)
364
+ console.log(`Wrote ${outputPath}`)
365
+ console.log(`Source config: ${path ?? "defaults"}`)
366
+ console.log(`Servers: ${Object.keys(output.mcpServers).join(", ") || "(none)"}`)
367
+ }
368
+
109
369
  function hasCommand(command, args = ["--version"]) {
110
370
  const result = spawnSync(command, args, {
111
371
  stdio: "ignore",
@@ -133,10 +393,41 @@ function checkNodeNpx() {
133
393
  }
134
394
  }
135
395
 
136
- function pythonCandidates() {
396
+ function resolveVenvPythonCandidates(basePath) {
397
+ if (!basePath) return []
398
+ if (process.platform === "win32") {
399
+ return [
400
+ join(basePath, ".venv", "Scripts", "python.exe"),
401
+ join(basePath, "venv", "Scripts", "python.exe"),
402
+ ]
403
+ }
404
+
405
+ return [
406
+ join(basePath, ".venv", "bin", "python"),
407
+ join(basePath, ".venv", "bin", "python3"),
408
+ join(basePath, "venv", "bin", "python"),
409
+ join(basePath, "venv", "bin", "python3"),
410
+ ]
411
+ }
412
+
413
+ function pythonCandidates(config) {
137
414
  const configured = process.env.MEMPALACE_PYTHON?.trim()
415
+ || resolveEnvTemplate(config?.mcp?.mempalace?.pythonPath?.trim())
138
416
  const candidates = []
139
417
  if (configured) candidates.push(configured)
418
+
419
+ for (const candidate of resolveVenvPythonCandidates(process.cwd())) {
420
+ if (existsSync(candidate)) candidates.push(candidate)
421
+ }
422
+
423
+ for (const candidate of resolveVenvPythonCandidates(PACKAGE_ROOT)) {
424
+ if (existsSync(candidate)) candidates.push(candidate)
425
+ }
426
+
427
+ for (const candidate of resolveVenvPythonCandidates(join(homedir(), ".config", "opencode", "plugins", "hiai-opencode"))) {
428
+ if (existsSync(candidate)) candidates.push(candidate)
429
+ }
430
+
140
431
  if (process.platform === "win32") {
141
432
  candidates.push("py", "python", "python3")
142
433
  } else {
@@ -158,18 +449,18 @@ function canImportMempalace(command) {
158
449
  return result.status === 0
159
450
  }
160
451
 
161
- function checkMempalace() {
452
+ function checkMempalace(config) {
162
453
  if (hasCommand(process.platform === "win32" ? "uv.exe" : "uv")) {
163
454
  return { level: "ok", detail: "uv available" }
164
455
  }
165
456
 
166
- for (const candidate of pythonCandidates()) {
457
+ for (const candidate of pythonCandidates(config)) {
167
458
  if (canImportMempalace(candidate)) {
168
459
  return { level: "ok", detail: `${candidate} with mempalace available` }
169
460
  }
170
461
  }
171
462
 
172
- const hasPython = pythonCandidates().some((candidate) =>
463
+ const hasPython = pythonCandidates(config).some((candidate) =>
173
464
  hasCommand(candidate, candidate === "py" ? ["-3", "--version"] : ["--version"]),
174
465
  )
175
466
 
@@ -211,6 +502,247 @@ function checkRemoteOptionalKey(_config, name) {
211
502
  return { level: "ok", detail: "remote endpoint configured" }
212
503
  }
213
504
 
505
+ function detectMempalacePythonSource(config) {
506
+ const envPython = process.env.MEMPALACE_PYTHON?.trim()
507
+ if (envPython) {
508
+ return { source: "env:MEMPALACE_PYTHON", value: envPython }
509
+ }
510
+
511
+ const cfgPython = resolveEnvTemplate(config?.mcp?.mempalace?.pythonPath?.trim())
512
+ if (cfgPython) {
513
+ return { source: "config:mcp.mempalace.pythonPath", value: cfgPython }
514
+ }
515
+
516
+ const candidates = pythonCandidates(config)
517
+ if (candidates.length > 0) {
518
+ const resolved = candidates.find((candidate) =>
519
+ hasCommand(candidate, candidate === "py" ? ["-3", "--version"] : ["--version"]),
520
+ )
521
+ if (resolved) {
522
+ return { source: "auto-detect", value: resolved }
523
+ }
524
+ }
525
+
526
+ return { source: "none", value: "" }
527
+ }
528
+
529
+ function parseProviderFromModelId(modelId) {
530
+ if (!modelId || typeof modelId !== "string") return null
531
+ const normalized = modelId.trim()
532
+ if (!normalized) return null
533
+ const directProvider = normalized.split("/")[0]
534
+ if (directProvider === "openrouter") {
535
+ const routedProvider = normalized.split("/")[1]
536
+ return routedProvider || "openrouter"
537
+ }
538
+ return directProvider
539
+ }
540
+
541
+ function collectConfiguredProviders(config) {
542
+ const providerSet = new Set()
543
+ const modelEntries = Object.values(config?.models ?? {})
544
+ for (const entry of modelEntries) {
545
+ const modelId = typeof entry === "string" ? entry : entry?.model
546
+ const provider = parseProviderFromModelId(modelId)
547
+ if (provider) providerSet.add(provider)
548
+ }
549
+ return [...providerSet].sort()
550
+ }
551
+
552
+ function checkOpenCodeConnectVisibility(config) {
553
+ const providers = collectConfiguredProviders(config)
554
+ const opencodeBinary = process.platform === "win32" ? "opencode.cmd" : "opencode"
555
+ const opencodeAvailable = hasCommand(opencodeBinary, ["--version"])
556
+ if (!opencodeAvailable) {
557
+ return {
558
+ level: "warn",
559
+ detail: `opencode binary not available in PATH; cannot inspect Connect providers. Configured model providers: ${providers.join(", ") || "(none)"}`,
560
+ }
561
+ }
562
+
563
+ const commands = [
564
+ [opencodeBinary, ["connect", "list", "--json"]],
565
+ [opencodeBinary, ["connect", "list"]],
566
+ ]
567
+
568
+ for (const [binary, args] of commands) {
569
+ const result = spawnSync(binary, args, { encoding: "utf-8", timeout: 15000, shell: process.platform === "win32" })
570
+ if (result.status === 0) {
571
+ const out = (result.stdout || "").trim()
572
+ const summary = out ? out.split("\n").slice(0, 3).join(" | ") : "connect list ok"
573
+ return {
574
+ level: "ok",
575
+ detail: `OpenCode Connect visible. Configured model providers: ${providers.join(", ") || "(none)"}; connect summary: ${summary}`,
576
+ }
577
+ }
578
+ }
579
+
580
+ return {
581
+ level: "warn",
582
+ detail: `Could not read OpenCode Connect state. Configured model providers: ${providers.join(", ") || "(none)"}`,
583
+ }
584
+ }
585
+
586
+ function getSkillRegistryPath() {
587
+ if (process.platform === "win32" && process.env.APPDATA) {
588
+ return join(process.env.APPDATA, "opencode", ".hiai", "skill-registry.json")
589
+ }
590
+ return join(homedir(), ".config", "opencode", ".hiai", "skill-registry.json")
591
+ }
592
+
593
+ function checkSkillMaterialization() {
594
+ const registryPath = getSkillRegistryPath()
595
+ if (!existsSync(registryPath)) {
596
+ return { level: "warn", detail: `skill registry missing: ${registryPath}` }
597
+ }
598
+
599
+ try {
600
+ const parsed = JSON.parse(readFileSync(registryPath, "utf-8"))
601
+ const summary = parsed?.summary
602
+ const total = summary?.total ?? 0
603
+ const builtin = summary?.builtin ?? 0
604
+ const plugin = summary?.plugin ?? 0
605
+ return {
606
+ level: "ok",
607
+ detail: `materialized skills: total=${total}, builtin=${builtin}, plugin=${plugin}`,
608
+ }
609
+ } catch (error) {
610
+ return {
611
+ level: "warn",
612
+ detail: `failed to parse skill registry: ${error instanceof Error ? error.message : String(error)}`,
613
+ }
614
+ }
615
+ }
616
+
617
+ function getAgentSummary(config) {
618
+ const visible = [
619
+ "Bob",
620
+ "Coder",
621
+ "Strategist",
622
+ "Guard",
623
+ "Critic",
624
+ "Designer",
625
+ "Researcher",
626
+ "Manager",
627
+ "Brainstormer",
628
+ "Vision",
629
+ ]
630
+ const hidden = ["Agent Skills", "Sub", "build", "plan"]
631
+ const modelCount = Object.keys(config?.models ?? {}).length
632
+ return {
633
+ level: modelCount >= 10 ? "ok" : "warn",
634
+ detail: `visible=${visible.length} [${visible.join(", ")}]; hidden=${hidden.length} [${hidden.join(", ")}]; model slots configured=${modelCount}/10`,
635
+ }
636
+ }
637
+
638
+ function getLspDefaults() {
639
+ return {
640
+ typescript: ["typescript-language-server", "--stdio"],
641
+ svelte: ["svelteserver", "--stdio"],
642
+ eslint: ["node", join(PACKAGE_ROOT, "assets", "runtime", "npm-package-runner.mjs"), "eslint-lsp", "--stdio"],
643
+ bash: ["node", join(PACKAGE_ROOT, "assets", "runtime", "npm-package-runner.mjs"), "bash-language-server", "start"],
644
+ pyright: ["pyright-langserver", "--stdio"],
645
+ }
646
+ }
647
+
648
+ function checkLspAvailability(config) {
649
+ const defaults = getLspDefaults()
650
+ const results = []
651
+
652
+ for (const [name, command] of Object.entries(defaults)) {
653
+ const enabled = config?.lsp?.[name]?.enabled ?? true
654
+ if (!enabled) {
655
+ results.push(`⚪ ${name}: disabled`)
656
+ continue
657
+ }
658
+ const binary = command[0]
659
+ const args = binary === "node" ? ["--version"] : ["--version"]
660
+ const ok = hasCommand(binary, args)
661
+ results.push(`${ok ? "✅" : "⚠️ "} ${name}: ${ok ? "runtime available" : `${binary} not found`}`)
662
+ }
663
+
664
+ return {
665
+ level: results.some((line) => line.startsWith("⚠️")) ? "warn" : "ok",
666
+ detail: results.join(" | "),
667
+ }
668
+ }
669
+
670
+ async function probeStdioMcp(serverName, serverConfig) {
671
+ const command = serverConfig?.command
672
+ const args = serverConfig?.args ?? []
673
+ if (!command) {
674
+ return { level: "warn", detail: `${serverName}: missing command` }
675
+ }
676
+
677
+ const env = { ...process.env, ...(serverConfig?.env ?? {}) }
678
+ const client = new Client(
679
+ { name: "hiai-opencode-doctor", version: "0.1.0" },
680
+ { capabilities: { tools: {}, prompts: {}, resources: {} } },
681
+ )
682
+
683
+ const transport = new StdioClientTransport({
684
+ command,
685
+ args,
686
+ env,
687
+ stderr: "pipe",
688
+ })
689
+
690
+ const timeoutMs = 12000
691
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), timeoutMs))
692
+
693
+ try {
694
+ await Promise.race([client.connect(transport), timeout])
695
+ const toolsResponse = await Promise.race([client.listTools(), timeout])
696
+ const count = toolsResponse?.tools?.length ?? 0
697
+ await client.close()
698
+ return { level: "ok", detail: `${serverName}: reachable, tools=${count}` }
699
+ } catch (error) {
700
+ try { await client.close() } catch {}
701
+ return {
702
+ level: "warn",
703
+ detail: `${serverName}: probe failed (${error instanceof Error ? error.message : String(error)})`,
704
+ }
705
+ }
706
+ }
707
+
708
+ async function probeRemoteMcp(serverName, serverConfig) {
709
+ const url = serverConfig?.url
710
+ if (!url) return { level: "warn", detail: `${serverName}: missing url` }
711
+
712
+ try {
713
+ const controller = new AbortController()
714
+ const timeout = setTimeout(() => controller.abort(), 4000)
715
+ const response = await fetch(url, { method: "GET", signal: controller.signal })
716
+ clearTimeout(timeout)
717
+ if (response.status < 500) {
718
+ return { level: "ok", detail: `${serverName}: endpoint reachable (${response.status})` }
719
+ }
720
+ return { level: "warn", detail: `${serverName}: endpoint returned ${response.status}` }
721
+ } catch (error) {
722
+ return {
723
+ level: "warn",
724
+ detail: `${serverName}: endpoint probe failed (${error instanceof Error ? error.message : String(error)})`,
725
+ }
726
+ }
727
+ }
728
+
729
+ async function probeMcpServers(config) {
730
+ const payload = createMcpExport(config)
731
+ const results = []
732
+ for (const [name, server] of Object.entries(payload.mcpServers ?? {})) {
733
+ if (server.command) {
734
+ results.push(await probeStdioMcp(name, server))
735
+ continue
736
+ }
737
+ if (server.type === "http") {
738
+ results.push(await probeRemoteMcp(name, server))
739
+ continue
740
+ }
741
+ results.push({ level: "warn", detail: `${name}: unsupported server shape` })
742
+ }
743
+ return results
744
+ }
745
+
214
746
  function hasEnvOrAuth(config, envName, authKey) {
215
747
  if (process.env[envName]?.trim()) return true
216
748
  if (authKey && config?.auth?.[authKey]?.trim()) return true
@@ -223,11 +755,13 @@ function statusIcon(level) {
223
755
  return "❌"
224
756
  }
225
757
 
226
- async function mcpStatus() {
758
+ async function mcpStatus(options = {}) {
227
759
  const { path, config, error } = loadConfig()
228
- console.log("hiai-opencode mcp-status")
760
+ const staticMcpPath = join(process.cwd(), ".mcp.json")
761
+ console.log(options.doctor ? "hiai-opencode doctor" : "hiai-opencode mcp-status")
229
762
  console.log(`Config: ${path ?? "not found; using defaults"}`)
230
763
  if (error) console.log(`Config parse warning: ${error}`)
764
+ console.log(`Static MCP export: ${staticMcpPath}`)
231
765
  console.log("")
232
766
  console.log("MCP Servers:")
233
767
 
@@ -251,6 +785,118 @@ async function mcpStatus() {
251
785
  const result = await entry.check(config, name)
252
786
  console.log(`${statusIcon(result.level)} ${name.padEnd(20)} - ${result.detail}`)
253
787
  }
788
+
789
+ if (options.doctor) {
790
+ console.log("")
791
+ console.log("Doctor Checks:")
792
+
793
+ const freshness = checkStaticMcpFreshness(staticMcpPath, config)
794
+ const freshIcon = freshness.status === "fresh" ? "✅" : freshness.status === "missing" ? "⚠️ " : "❌"
795
+ console.log(`${freshIcon} static .mcp.json freshness - ${freshness.detail}`)
796
+
797
+ const connect = checkOpenCodeConnectVisibility(config)
798
+ console.log(`${statusIcon(connect.level)} OpenCode Connect visibility - ${connect.detail}`)
799
+
800
+ const pluginRegistration = checkOpenCodePluginRegistration()
801
+ console.log(`${statusIcon(pluginRegistration.level)} OpenCode plugin registration - ${pluginRegistration.detail}`)
802
+
803
+ const skills = checkSkillMaterialization()
804
+ console.log(`${statusIcon(skills.level)} Skill materialization - ${skills.detail}`)
805
+
806
+ const agents = getAgentSummary(config)
807
+ console.log(`${statusIcon(agents.level)} Agent count and naming - ${agents.detail}`)
808
+
809
+ const lsp = checkLspAvailability(config)
810
+ console.log(`${statusIcon(lsp.level)} LSP runtime availability - ${lsp.detail}`)
811
+
812
+ const mempalacePython = detectMempalacePythonSource(config)
813
+ const mempalacePythonIcon = mempalacePython.value ? "✅" : "⚠️ "
814
+ console.log(`${mempalacePythonIcon} MemPalace python selection - ${mempalacePython.source}${mempalacePython.value ? ` (${mempalacePython.value})` : ""}`)
815
+
816
+ console.log("")
817
+ console.log("MCP Tool Probes:")
818
+ const probeResults = await probeMcpServers(config)
819
+ for (const probe of probeResults) {
820
+ console.log(`${statusIcon(probe.level)} ${probe.detail}`)
821
+ }
822
+
823
+ console.log("")
824
+ console.log("Recommended follow-ups:")
825
+ console.log(" - hiai-opencode export-mcp .mcp.json")
826
+ console.log(" - opencode debug config")
827
+ console.log(" - opencode mcp list --print-logs --log-level INFO")
828
+ }
829
+ }
830
+
831
+ async function runDiagnose(outputPath) {
832
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
833
+ const defaultPath = outputPath
834
+ || join(process.cwd(), `hiai-diagnose-${timestamp}.txt`)
835
+ const { path: configPath, config, error } = loadConfig()
836
+ const sections = []
837
+
838
+ sections.push("=".repeat(60))
839
+ sections.push(`hiai-opencode diagnose - ${timestamp}`)
840
+ sections.push("=".repeat(60))
841
+ sections.push("")
842
+
843
+ sections.push("ENVIRONMENT (keys only, no values):")
844
+ const envKeys = [
845
+ "FIRECRAWL_API_KEY", "STITCH_AI_API_KEY", "CONTEXT7_API_KEY",
846
+ "EXA_API_KEY", "TAVILY_API_KEY", "OPENCODE_RAG_URL",
847
+ "MEMPALACE_PYTHON", "HIAI_PLAYWRIGHT_INSTALL_BROWSERS", "HIAI_MCP_AUTO_INSTALL",
848
+ ]
849
+ for (const key of envKeys) {
850
+ const hasValue = !!process.env[key]?.trim()
851
+ sections.push(` ${key}: ${hasValue ? "(set)" : "(not set)"}`)
852
+ }
853
+ sections.push("")
854
+
855
+ sections.push("CONFIGURATION:")
856
+ sections.push(` Config path: ${configPath ?? "(defaults)"}`)
857
+ if (error) sections.push(` Config parse warning: ${error}`)
858
+ const modelKeys = Object.keys(config?.models ?? {})
859
+ const mcpKeys = Object.keys(config?.mcp ?? {})
860
+ sections.push(` models configured: ${modelKeys.length} [${modelKeys.join(", ") || "none"}]`)
861
+ sections.push(` mcp servers in config: ${mcpKeys.length} [${mcpKeys.join(", ") || "none"}]`)
862
+ sections.push("")
863
+
864
+ sections.push("TOOLS REGISTERED:")
865
+ const toolCount = 26
866
+ sections.push(` ~${toolCount} tools (from tool-registry.ts)`)
867
+ sections.push("")
868
+
869
+ sections.push("AGENTS:")
870
+ const agents = ["bob", "coder", "strategist", "guard", "critic", "designer", "researcher", "manager", "brainstormer", "vision"]
871
+ for (const agent of agents) {
872
+ const model = config?.models?.[agent]?.model
873
+ sections.push(` ${agent}: ${model ? `model=${model}` : "(default)"}`)
874
+ }
875
+ sections.push("")
876
+
877
+ sections.push("MCP SERVERS:")
878
+ for (const [name, entry] of Object.entries(MCP_REGISTRY)) {
879
+ const userEntry = config?.mcp?.[name]
880
+ const enabled = userEntry?.enabled ?? entry.defaultEnabled
881
+ sections.push(` ${name}: ${enabled ? "enabled" : "disabled"}`)
882
+ }
883
+ sections.push("")
884
+
885
+ sections.push("FILE PATHS:")
886
+ sections.push(` CWD: ${process.cwd()}`)
887
+ sections.push(` Package root: ${PACKAGE_ROOT}`)
888
+ sections.push(` Config: ${configPath ?? "(none)"}`)
889
+ sections.push(` Static MCP: ${join(process.cwd(), ".mcp.json")}`)
890
+ sections.push("")
891
+
892
+ sections.push("=".repeat(60))
893
+ sections.push("Diagnose complete. File written to: " + defaultPath)
894
+ sections.push("NO secrets or API keys are included in this output.")
895
+ sections.push("=".repeat(60))
896
+
897
+ mkdirSync(dirname(defaultPath), { recursive: true })
898
+ writeFileSync(defaultPath, sections.join("\n") + "\n")
899
+ console.log(`Diagnose written to: ${defaultPath}`)
254
900
  }
255
901
 
256
902
  async function main() {
@@ -265,6 +911,21 @@ async function main() {
265
911
  return
266
912
  }
267
913
 
914
+ if (command === "doctor") {
915
+ await mcpStatus({ doctor: true })
916
+ return
917
+ }
918
+
919
+ if (command === "export-mcp") {
920
+ exportMcp(process.argv[3])
921
+ return
922
+ }
923
+
924
+ if (command === "diagnose") {
925
+ await runDiagnose(process.argv[3])
926
+ return
927
+ }
928
+
268
929
  console.error(`Unknown command: ${command}`)
269
930
  usage()
270
931
  process.exit(1)