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