@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
@@ -0,0 +1,119 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
2
+ import { dirname, join } from "node:path"
3
+
4
+ import type { HiaiOpencodeConfig, McpServerConfig } from "../config/types"
5
+ import { resolveEnvVars } from "../config/loader"
6
+ import { log, logWarn } from "./logger"
7
+
8
+ type StaticMcpServer = {
9
+ type?: "http" | "stdio"
10
+ url?: string
11
+ command?: string
12
+ args?: string[]
13
+ env?: Record<string, string>
14
+ headers?: Record<string, string>
15
+ }
16
+
17
+ export const MCP_EXPORT_MARKER = "hiai-opencode"
18
+
19
+ type StaticMcpJsonPayload = {
20
+ mcpServers: Record<string, StaticMcpServer>
21
+ _meta?: {
22
+ generatedBy: typeof MCP_EXPORT_MARKER
23
+ version: 1
24
+ generatedAt: string
25
+ }
26
+ }
27
+
28
+ function toStaticMcpServer(config: McpServerConfig): StaticMcpServer | null {
29
+ if (config.enabled === false) return null
30
+
31
+ if (config.type === "remote") {
32
+ const headers = config.headers
33
+ ? Object.fromEntries(Object.entries(config.headers).map(([key, value]) => [key, resolveEnvVars(value)]))
34
+ : undefined
35
+
36
+ return {
37
+ type: "http",
38
+ url: config.url,
39
+ ...(headers ? { headers } : {}),
40
+ }
41
+ }
42
+
43
+ const [command, ...args] = config.command ?? []
44
+ if (!command) return null
45
+
46
+ const env = config.environment
47
+ ? Object.fromEntries(Object.entries(config.environment).map(([key, value]) => [key, resolveEnvVars(value)]))
48
+ : undefined
49
+
50
+ return {
51
+ command,
52
+ ...(args.length > 0 ? { args } : {}),
53
+ ...(env ? { env } : {}),
54
+ }
55
+ }
56
+
57
+ export function buildStaticMcpJson(config: HiaiOpencodeConfig): StaticMcpJsonPayload {
58
+ const mcpServers: Record<string, StaticMcpServer> = {}
59
+
60
+ for (const [name, serverConfig] of Object.entries(config.mcp ?? {})) {
61
+ const converted = toStaticMcpServer(serverConfig)
62
+ if (converted) {
63
+ mcpServers[name] = converted
64
+ }
65
+ }
66
+
67
+ return {
68
+ _meta: {
69
+ generatedBy: MCP_EXPORT_MARKER,
70
+ version: 1,
71
+ generatedAt: new Date().toISOString(),
72
+ },
73
+ mcpServers,
74
+ }
75
+ }
76
+
77
+ export function isManagedStaticMcpFile(path: string): boolean {
78
+ if (!existsSync(path)) return false
79
+
80
+ try {
81
+ const raw = readFileSync(path, "utf-8")
82
+ const parsed = JSON.parse(raw) as StaticMcpJsonPayload
83
+ return parsed?._meta?.generatedBy === MCP_EXPORT_MARKER
84
+ } catch {
85
+ return false
86
+ }
87
+ }
88
+
89
+ export function autoExportStaticMcpJson(directory: string, config: HiaiOpencodeConfig): void {
90
+ const mode = process.env.HIAI_OPENCODE_AUTO_EXPORT_MCP?.trim().toLowerCase() || "if-missing"
91
+ if (mode === "0" || mode === "false" || mode === "no" || mode === "off") {
92
+ return
93
+ }
94
+
95
+ const outputPath = process.env.HIAI_OPENCODE_MCP_EXPORT_PATH?.trim() || join(directory, ".mcp.json")
96
+ if (mode === "if-missing" && existsSync(outputPath)) {
97
+ return
98
+ }
99
+
100
+ const isForceMode = mode === "force"
101
+ if (mode === "always" && existsSync(outputPath) && !isManagedStaticMcpFile(outputPath) && !isForceMode) {
102
+ logWarn(`refusing to overwrite non-managed static MCP config at ${outputPath}. `
103
+ + "Set HIAI_OPENCODE_AUTO_EXPORT_MCP=force to override.")
104
+ return
105
+ }
106
+
107
+ try {
108
+ mkdirSync(dirname(outputPath), { recursive: true })
109
+ const payload = buildStaticMcpJson(config)
110
+ writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`)
111
+ log("[hiai-opencode] exported static MCP config", {
112
+ outputPath,
113
+ servers: Object.keys(payload.mcpServers),
114
+ })
115
+ } catch (error) {
116
+ const message = error instanceof Error ? error.message : String(error)
117
+ logWarn(`failed to export static MCP config to ${outputPath}: ${message}`)
118
+ }
119
+ }
@@ -64,6 +64,13 @@ export const AGENT_NAME_MAP: Record<string, string> = {
64
64
  // Designer
65
65
  designer: "designer",
66
66
 
67
+ // Writer/copywriter aliases are handled by Brainstormer.
68
+ writer: "brainstormer",
69
+ copywriter: "brainstormer",
70
+ "content-writer": "brainstormer",
71
+ "content writer": "brainstormer",
72
+ "website-writer": "brainstormer",
73
+
67
74
  // Multimodal / Vision
68
75
  ui: "multimodal",
69
76
  vision: "multimodal",
@@ -80,6 +87,7 @@ export const BUILTIN_AGENT_NAMES = new Set([
80
87
  "multimodal",
81
88
  "platform-manager",
82
89
  "guard",
90
+ "brainstormer",
83
91
  ])
84
92
 
85
93
  export function migrateAgentNames(
@@ -5,7 +5,7 @@ export const HOOK_NAME_MAP: Record<string, string | null> = {
5
5
  "anthropic-auto-compact": "anthropic-context-window-limit-recovery",
6
6
  "bob-orchestrator": "guard",
7
7
 
8
- "bob-gpt-coder-reminder": "no-bob-gpt",
8
+ "bob-gpt-coder-reminder": null,
9
9
 
10
10
  // Removed hooks (v3.0.0) - will be filtered out and user warned
11
11
  "empty-message-sanitizer": null,
@@ -0,0 +1,88 @@
1
+ import { expect, test } from "bun:test"
2
+ import { normalizeMode, resolveModeAgent, MODE_TO_AGENT, MODE_NAMES } from "./mode-routing"
3
+
4
+ test("normalizeMode returns legacy aliases mapped to new names", () => {
5
+ expect(normalizeMode("unspecified-low")).toBe("bounded")
6
+ expect(normalizeMode("unspecified-high")).toBe("cross-module")
7
+ })
8
+
9
+ test("normalizeMode returns same name when not a legacy alias", () => {
10
+ expect(normalizeMode("quick")).toBe("quick")
11
+ expect(normalizeMode("deep")).toBe("deep")
12
+ expect(normalizeMode("writing")).toBe("writing")
13
+ })
14
+
15
+ test("resolveModeAgent maps quick to sub", () => {
16
+ expect(resolveModeAgent("quick")).toBe("sub")
17
+ })
18
+
19
+ test("resolveModeAgent maps writing to brainstormer", () => {
20
+ expect(resolveModeAgent("writing")).toBe("brainstormer")
21
+ })
22
+
23
+ test("resolveModeAgent maps deep to coder", () => {
24
+ expect(resolveModeAgent("deep")).toBe("coder")
25
+ })
26
+
27
+ test("resolveModeAgent maps ultrabrain to strategist", () => {
28
+ expect(resolveModeAgent("ultrabrain")).toBe("strategist")
29
+ })
30
+
31
+ test("resolveModeAgent maps visual-engineering to designer", () => {
32
+ expect(resolveModeAgent("visual-engineering")).toBe("designer")
33
+ })
34
+
35
+ test("resolveModeAgent maps artistry to designer", () => {
36
+ expect(resolveModeAgent("artistry")).toBe("designer")
37
+ })
38
+
39
+ test("resolveModeAgent maps git to platform-manager", () => {
40
+ expect(resolveModeAgent("git")).toBe("platform-manager")
41
+ })
42
+
43
+ test("resolveModeAgent maps git-ops to platform-manager", () => {
44
+ expect(resolveModeAgent("git-ops")).toBe("platform-manager")
45
+ })
46
+
47
+ test("resolveModeAgent maps bounded to sub", () => {
48
+ expect(resolveModeAgent("bounded")).toBe("sub")
49
+ })
50
+
51
+ test("resolveModeAgent maps cross-module to coder", () => {
52
+ expect(resolveModeAgent("cross-module")).toBe("coder")
53
+ })
54
+
55
+ test("resolveModeAgent falls back to coder for unknown mode", () => {
56
+ expect(resolveModeAgent("unknown-mode")).toBe("coder")
57
+ })
58
+
59
+ test("resolveModeAgent handles legacy aliases", () => {
60
+ expect(resolveModeAgent("unspecified-low")).toBe("sub")
61
+ expect(resolveModeAgent("unspecified-high")).toBe("coder")
62
+ })
63
+
64
+ test("MODE_NAMES contains all mode names", () => {
65
+ expect(MODE_NAMES).toContain("quick")
66
+ expect(MODE_NAMES).toContain("writing")
67
+ expect(MODE_NAMES).toContain("deep")
68
+ expect(MODE_NAMES).toContain("ultrabrain")
69
+ expect(MODE_NAMES).toContain("visual-engineering")
70
+ expect(MODE_NAMES).toContain("artistry")
71
+ expect(MODE_NAMES).toContain("git")
72
+ expect(MODE_NAMES).toContain("git-ops")
73
+ expect(MODE_NAMES).toContain("bounded")
74
+ expect(MODE_NAMES).toContain("cross-module")
75
+ })
76
+
77
+ test("MODE_TO_AGENT has correct mapping", () => {
78
+ expect(MODE_TO_AGENT.quick).toBe("sub")
79
+ expect(MODE_TO_AGENT.writing).toBe("brainstormer")
80
+ expect(MODE_TO_AGENT.deep).toBe("coder")
81
+ expect(MODE_TO_AGENT.ultrabrain).toBe("strategist")
82
+ expect(MODE_TO_AGENT["visual-engineering"]).toBe("designer")
83
+ expect(MODE_TO_AGENT.artistry).toBe("designer")
84
+ expect(MODE_TO_AGENT.git).toBe("platform-manager")
85
+ expect(MODE_TO_AGENT["git-ops"]).toBe("platform-manager")
86
+ expect(MODE_TO_AGENT.bounded).toBe("sub")
87
+ expect(MODE_TO_AGENT["cross-module"]).toBe("coder")
88
+ })
@@ -0,0 +1,30 @@
1
+ import type { CanonicalDelegateAgentKey } from "../tools/delegate-task/sub-agent"
2
+
3
+ export const MODE_TO_AGENT: Record<string, CanonicalDelegateAgentKey> = {
4
+ quick: "sub",
5
+ writing: "brainstormer",
6
+ deep: "coder",
7
+ ultrabrain: "strategist",
8
+ "visual-engineering": "designer",
9
+ artistry: "designer",
10
+ git: "platform-manager",
11
+ "git-ops": "platform-manager",
12
+ bounded: "sub",
13
+ "cross-module": "coder",
14
+ }
15
+
16
+ const LEGACY_MODE_ALIASES: Record<string, string> = {
17
+ "unspecified-low": "bounded",
18
+ "unspecified-high": "cross-module",
19
+ }
20
+
21
+ export type ModeName = keyof typeof MODE_TO_AGENT
22
+ export const MODE_NAMES = Object.keys(MODE_TO_AGENT) as ModeName[]
23
+
24
+ export function normalizeMode(modeName: string): string {
25
+ return LEGACY_MODE_ALIASES[modeName] ?? modeName
26
+ }
27
+
28
+ export function resolveModeAgent(modeName: string): CanonicalDelegateAgentKey {
29
+ return MODE_TO_AGENT[normalizeMode(modeName)] ?? "coder"
30
+ }
@@ -5,6 +5,7 @@ import type { HiaiOpenCodeConfig, HiaiOpencodeConfig } from "../config"
5
5
  import { HIAI_MCP_REGISTRY } from "../mcp/registry"
6
6
  import { parseJsoncSafe } from "./jsonc-parser"
7
7
  import { getOpenCodeConfigPaths } from "./opencode-config-dir"
8
+ import { logWarn } from "./logger"
8
9
  import { PLUGIN_NAME } from "./plugin-identity"
9
10
 
10
11
  interface OpenCodeConfig {
@@ -38,9 +39,9 @@ export function warnIfListPluginEntry(directory: string): void {
38
39
  const plugins = readPlugins(configPath)
39
40
  if (!plugins.includes("list")) continue
40
41
 
41
- console.warn(`[hiai-opencode] WARNING: ${configPath} contains plugin: ["list"].`)
42
- console.warn("[hiai-opencode] This can prevent hiai-opencode MCP servers from loading from that config scope.")
43
- console.warn(`[hiai-opencode] Update it to: plugin: ["${PLUGIN_NAME}"]`)
42
+ logWarn(`${configPath} contains plugin: ["list"].`)
43
+ logWarn("This can prevent hiai-opencode MCP servers from loading from that config scope.")
44
+ logWarn(`Update it to: plugin: ["${PLUGIN_NAME}"]`)
44
45
  }
45
46
  }
46
47
 
@@ -69,9 +70,7 @@ export function warnMissingRequiredMcpEnv(args: {
69
70
 
70
71
  if (missing.length === 0) continue
71
72
 
72
- console.warn(
73
- `[hiai-opencode] MCP "${name}" is enabled but missing required env: ${missing.join(", ")}.`
74
- + " The plugin will continue to load; set the key or disable this MCP in hiai-opencode.json.",
75
- )
73
+ logWarn(`MCP "${name}" is enabled but missing required env: ${missing.join(", ")}.`
74
+ + " The plugin will continue to load; set the key or disable this MCP in hiai-opencode.json.")
76
75
  }
77
76
  }
@@ -1,5 +1,5 @@
1
1
  export type { EnvironmentCheckResult } from "./environment-check"
2
2
  export { checkEnvironment, formatEnvironmentCheck } from "./environment-check"
3
- export { CLI_LANGUAGES, NAPI_LANGUAGES, LANG_EXTENSIONS } from "./language-support"
3
+ export { CLI_LANGUAGES, LANG_EXTENSIONS } from "./language-support"
4
4
  export { DEFAULT_TIMEOUT_MS, DEFAULT_MAX_OUTPUT_BYTES, DEFAULT_MAX_MATCHES } from "./language-support"
5
5
  export { findSgCliPathSync, getSgCliPath, setSgCliPath } from "./sg-cli-path"
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "fs"
2
2
 
3
- import { CLI_LANGUAGES, NAPI_LANGUAGES } from "./language-support"
3
+ import { CLI_LANGUAGES } from "./language-support"
4
4
  import { getSgCliPath } from "./sg-cli-path"
5
5
 
6
6
  export interface EnvironmentCheckResult {
@@ -9,14 +9,10 @@ export interface EnvironmentCheckResult {
9
9
  path: string
10
10
  error?: string
11
11
  }
12
- napi: {
13
- available: boolean
14
- error?: string
15
- }
16
12
  }
17
13
 
18
14
  /**
19
- * Check if ast-grep CLI and NAPI are available.
15
+ * Check if ast-grep CLI is available.
20
16
  * Call this at startup to provide early feedback about missing dependencies.
21
17
  */
22
18
  export function checkEnvironment(): EnvironmentCheckResult {
@@ -26,9 +22,6 @@ export function checkEnvironment(): EnvironmentCheckResult {
26
22
  available: false,
27
23
  path: cliPath ?? "not found",
28
24
  },
29
- napi: {
30
- available: false,
31
- },
32
25
  }
33
26
 
34
27
  if (cliPath && existsSync(cliPath)) {
@@ -39,17 +32,6 @@ export function checkEnvironment(): EnvironmentCheckResult {
39
32
  result.cli.error = `Binary not found: ${cliPath}`
40
33
  }
41
34
 
42
- // Check NAPI availability
43
- try {
44
- require("@ast-grep/napi")
45
- result.napi.available = true
46
- } catch (error) {
47
- result.napi.available = false
48
- result.napi.error = `@ast-grep/napi not installed: ${
49
- error instanceof Error ? error.message : String(error)
50
- }`
51
- }
52
-
53
35
  return result
54
36
  }
55
37
 
@@ -70,20 +52,8 @@ export function formatEnvironmentCheck(result: EnvironmentCheckResult): string {
70
52
  lines.push(" Install: bun add -D @ast-grep/cli")
71
53
  }
72
54
 
73
- // NAPI status
74
- if (result.napi.available) {
75
- lines.push("[OK] NAPI: Available")
76
- } else {
77
- lines.push("[X] NAPI: Not available")
78
- if (result.napi.error) {
79
- lines.push(` Error: ${result.napi.error}`)
80
- }
81
- lines.push(" Install: bun add -D @ast-grep/napi")
82
- }
83
-
84
55
  lines.push("")
85
56
  lines.push(`CLI supports ${CLI_LANGUAGES.length} languages`)
86
- lines.push(`NAPI supports ${NAPI_LANGUAGES.length} languages: ${NAPI_LANGUAGES.join(", ")}`)
87
57
 
88
58
  return lines.join("\n")
89
59
  }
@@ -27,9 +27,6 @@ export const CLI_LANGUAGES = [
27
27
  "yaml",
28
28
  ] as const
29
29
 
30
- // NAPI supported languages (5 total - native bindings)
31
- export const NAPI_LANGUAGES = ["html", "javascript", "tsx", "css", "typescript"] as const
32
-
33
30
  export const DEFAULT_TIMEOUT_MS = 300_000
34
31
  export const DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024
35
32
  export const DEFAULT_MAX_MATCHES = 500
@@ -1,7 +1,6 @@
1
- import type { CLI_LANGUAGES, NAPI_LANGUAGES } from "./constants"
1
+ import type { CLI_LANGUAGES } from "./constants"
2
2
 
3
3
  export type CliLanguage = (typeof CLI_LANGUAGES)[number]
4
- export type NapiLanguage = (typeof NAPI_LANGUAGES)[number]
5
4
 
6
5
  export interface Position {
7
6
  line: number
@@ -10,6 +10,7 @@ import type { DelegatedModelConfig } from "../../shared/model-resolution-types"
10
10
  import type { FallbackEntry } from "../../shared/model-requirements"
11
11
  import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
12
12
  import { getAgentConfigKey, stripInvisibleAgentCharacters } from "../../shared/agent-display-names"
13
+ import { resolveCanonicalDelegateAgentKey } from "../../tools/delegate-task/sub-agent"
13
14
  import { normalizeFallbackModels } from "../../shared/model-resolver"
14
15
  import { buildFallbackChainFromModels } from "../../shared/fallback-chain-from-models"
15
16
  import { log } from "../../shared"
@@ -139,22 +140,28 @@ export function createCallOmoAgent(
139
140
  async execute(args: CallOmoAgentArgs, toolContext) {
140
141
  const toolCtx = toolContext as ToolContextWithMetadata;
141
142
  log(
142
- `[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`,
143
+ `[call_omo_agent] DEPRECATED: call_omo_agent is deprecated. Use task(agent="...", load_skills=[], run_in_background=...) instead.`,
143
144
  );
144
145
 
145
146
  const callableAgents = await resolveCallableAgents(ctx.client);
146
147
 
147
148
  // Strip ZWSP and case-insensitive agent validation - allows canonical names and compatibility aliases.
148
149
  const strippedAgentType = stripInvisibleAgentCharacters(args.subagent_type)
150
+ const preCanonicalAgentType = getAgentConfigKey(strippedAgentType)
151
+ const canonicalAgentType = resolveCanonicalDelegateAgentKey(preCanonicalAgentType)
152
+ const wasLegacyAlias = canonicalAgentType !== preCanonicalAgentType.toLowerCase()
153
+ if (wasLegacyAlias) {
154
+ log("[call_omo_agent] Legacy agent alias resolved", { from: preCanonicalAgentType, to: canonicalAgentType })
155
+ }
149
156
  if (
150
157
  !callableAgents.some(
151
- (name) => name.toLowerCase() === strippedAgentType.toLowerCase(),
158
+ (name) => name.toLowerCase() === canonicalAgentType.toLowerCase(),
152
159
  )
153
160
  ) {
154
- return `Error: Invalid agent type "${args.subagent_type}". Only ${callableAgents.join(", ")} are allowed.`;
161
+ return `Error: Invalid agent type "${args.subagent_type}". Available agents: ${callableAgents.join(", ")}. Legacy aliases: oracle→strategist, hephaestus→coder, metis→strategist, momus→critic, sisyphus-junior→sub, multimodal-looker→multimodal, librarian→researcher, explore→researcher.`;
155
162
  }
156
163
 
157
- const normalizedAgent = strippedAgentType.toLowerCase();
164
+ const normalizedAgent = canonicalAgentType.toLowerCase();
158
165
  args = { ...args, subagent_type: normalizedAgent };
159
166
 
160
167
  // Check if agent is disabled
@@ -4,7 +4,7 @@ const UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND = `<Category_Context>
4
4
  You are working on tasks that don't fit specific categories but require moderate effort.
5
5
 
6
6
  <Routing_Policy>
7
- Executor contour: coder (fast bounded execution). Keep the scope bounded unless the task clearly requires deep or cross-system work.
7
+ Executor contour: sub (cheap fast-tier executor). Keep the scope bounded unless the task clearly requires deep or cross-system work; in that case escalate to unspecified-high or deep.
8
8
  </Routing_Policy>
9
9
 
10
10
  <Selection_Gate>
@@ -50,13 +50,13 @@ export const ANTHROPIC_CATEGORIES: BuiltinCategoryDefinition[] = [
50
50
  {
51
51
  name: "unspecified-low",
52
52
  config: {},
53
- description: "Unclassifiable moderate tasks with bounded scope. Uses coder execution contour.",
53
+ description: "Unclassifiable moderate tasks with bounded scope. Uses sub (cheap fast-tier) execution contour.",
54
54
  promptAppend: UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND,
55
55
  },
56
56
  {
57
57
  name: "unspecified-high",
58
58
  config: {},
59
- description: "Unclassifiable substantial tasks across modules. Uses coder execution contour.",
59
+ description: "Unclassifiable substantial tasks across modules. Uses coder (deep execution) with max-variant model.",
60
60
  promptAppend: UNSPECIFIED_HIGH_CATEGORY_PROMPT_APPEND,
61
61
  },
62
62
  ]
@@ -1,6 +1,7 @@
1
1
  import type { CategoryConfig } from "../../config/schema"
2
2
  import { ANTHROPIC_CATEGORIES } from "./anthropic-categories"
3
3
  import type { BuiltinCategoryDefinition } from "./builtin-category-definition"
4
+ import { GIT_CATEGORIES } from "./git-categories"
4
5
  import { GOOGLE_CATEGORIES } from "./google-categories"
5
6
  import { KIMI_CATEGORIES } from "./kimi-categories"
6
7
  import { OPENAI_CATEGORIES } from "./openai-categories"
@@ -10,6 +11,7 @@ const BUILTIN_CATEGORIES: BuiltinCategoryDefinition[] = [
10
11
  ...OPENAI_CATEGORIES,
11
12
  ...ANTHROPIC_CATEGORIES,
12
13
  ...KIMI_CATEGORIES,
14
+ ...GIT_CATEGORIES,
13
15
  ]
14
16
 
15
17
  function buildCategoryRecord<TValue>(
@@ -0,0 +1,87 @@
1
+ import { expect, test } from "bun:test"
2
+ import { resolveCategoryConfig } from "./categories"
3
+
4
+ test("resolveCategoryConfig returns null for unknown category with no config", () => {
5
+ const result = resolveCategoryConfig("nonexistent-category", {
6
+ userCategories: {},
7
+ availableModels: new Set(["openrouter/minimax/minimax-m2.7"]),
8
+ })
9
+ expect(result).toBeNull()
10
+ })
11
+
12
+ test("resolveCategoryConfig returns null when category is disabled", () => {
13
+ const result = resolveCategoryConfig("quick", {
14
+ userCategories: { quick: { disable: true } },
15
+ })
16
+ expect(result).toBeNull()
17
+ })
18
+
19
+ test("resolveCategoryConfig merges default config with user config", () => {
20
+ const result = resolveCategoryConfig("quick", {
21
+ userCategories: { quick: { temperature: 0.9 } },
22
+ availableModels: new Set(["openrouter/minimax/minimax-m2.7"]),
23
+ })
24
+ expect(result).not.toBeNull()
25
+ expect(result!.config.temperature).toBe(0.9)
26
+ })
27
+
28
+ test("resolveCategoryConfig user model takes precedence over default", () => {
29
+ const result = resolveCategoryConfig("quick", {
30
+ userCategories: { quick: { model: "openrouter/deepseek/deepseek-v4-flash" } },
31
+ availableModels: new Set(["openrouter/deepseek/deepseek-v4-flash"]),
32
+ })
33
+ expect(result).not.toBeNull()
34
+ expect(result!.model).toBe("openrouter/deepseek/deepseek-v4-flash")
35
+ })
36
+
37
+ test("resolveCategoryConfig returns promptAppend for category", () => {
38
+ const result = resolveCategoryConfig("deep", {
39
+ userCategories: {},
40
+ availableModels: new Set(["openrouter/minimax/minimax-m2.7"]),
41
+ })
42
+ expect(result).not.toBeNull()
43
+ expect(result!.promptAppend).toBeDefined()
44
+ })
45
+
46
+ test("resolveCategoryConfig isUserConfiguredModel true when user provides model", () => {
47
+ const result = resolveCategoryConfig("quick", {
48
+ userCategories: { quick: { model: "openrouter/deepseek/deepseek-v4-flash" } },
49
+ availableModels: new Set(["openrouter/deepseek/deepseek-v4-flash"]),
50
+ })
51
+ expect(result).not.toBeNull()
52
+ expect(result!.isUserConfiguredModel).toBe(true)
53
+ })
54
+
55
+ test("resolveCategoryConfig isUserConfiguredModel false when no user model", () => {
56
+ const result = resolveCategoryConfig("quick", {
57
+ userCategories: { quick: {} },
58
+ availableModels: new Set(["openrouter/minimax/minimax-m2.7"]),
59
+ })
60
+ expect(result).not.toBeNull()
61
+ expect(result!.isUserConfiguredModel).toBe(false)
62
+ })
63
+
64
+ test("resolveCategoryConfig concatenates prompt_append with default", () => {
65
+ const result = resolveCategoryConfig("quick", {
66
+ userCategories: { quick: { prompt_append: "Extra instructions" } },
67
+ availableModels: new Set(["openrouter/minimax/minimax-m2.7"]),
68
+ })
69
+ expect(result).not.toBeNull()
70
+ expect(result!.promptAppend).toContain("Extra instructions")
71
+ })
72
+
73
+ test("resolveCategoryConfig returns null when category not in defaults or user config", () => {
74
+ const result = resolveCategoryConfig("nonexistent", {
75
+ userCategories: {},
76
+ })
77
+ expect(result).toBeNull()
78
+ })
79
+
80
+ test("resolveCategoryConfig applies user variant over default variant", () => {
81
+ const result = resolveCategoryConfig("deep", {
82
+ userCategories: { deep: { variant: "high" } },
83
+ availableModels: new Set(["openrouter/minimax/minimax-m2.7"]),
84
+ })
85
+ expect(result).not.toBeNull()
86
+ expect(result!.config.variant).toBe("high")
87
+ })
@@ -3,9 +3,6 @@ import type { DelegateTaskArgs } from "./types"
3
3
  import type { ExecutorContext } from "./executor-types"
4
4
  import type { FallbackEntry } from "../../shared/model-requirements"
5
5
  import { mergeCategories } from "../../shared/merge-categories"
6
- import {
7
- CODER_AGENT_CONFIG_KEY,
8
- } from "./sub-agent"
9
6
  import { resolveCategoryConfig } from "./categories"
10
7
  import { parseModelString } from "./model-string-parser"
11
8
  import { CATEGORY_MODEL_REQUIREMENTS } from "../../shared/model-requirements"
@@ -15,6 +12,7 @@ import { CONFIG_BASENAME } from "../../shared/plugin-identity"
15
12
  import { getAvailableModelsForDelegateTask } from "./available-models"
16
13
  import { resolveModelForDelegateTask } from "./model-selection"
17
14
  import { getAgentDisplayName } from "../../shared/agent-display-names"
15
+ import { resolveModeAgent } from "../../shared/mode-routing"
18
16
 
19
17
  import type { CategoryConfig } from "../../config/schema"
20
18
  import type { DelegatedModelConfig } from "./types"
@@ -42,11 +40,11 @@ export interface CategoryResolutionResult {
42
40
  }
43
41
 
44
42
  function resolveCategoryExecutorAgentKey(
45
- _categoryName: string,
43
+ categoryName: string,
46
44
  config: CategoryConfig,
47
- ): typeof CODER_AGENT_CONFIG_KEY {
45
+ ): string {
48
46
  void config
49
- return CODER_AGENT_CONFIG_KEY
47
+ return resolveModeAgent(categoryName)
50
48
  }
51
49
 
52
50
  export async function resolveCategoryExecution(
@@ -120,7 +118,7 @@ Available categories: ${allCategoryNames}`,
120
118
  }
121
119
 
122
120
  const categoryExecutorAgentKey = resolveCategoryExecutorAgentKey(categoryName, resolved.config)
123
- const categoryExecutorAgentName = getAgentDisplayName(CODER_AGENT_CONFIG_KEY)
121
+ const categoryExecutorAgentName = getAgentDisplayName(categoryExecutorAgentKey)
124
122
  const categoryExecutorOverride = agentOverrides?.[categoryExecutorAgentKey as keyof typeof agentOverrides]
125
123
  ?? (agentOverrides
126
124
  ? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === categoryExecutorAgentKey)?.[1]
@@ -141,8 +139,9 @@ Available categories: ${allCategoryNames}`,
141
139
  const explicitCategoryModel = userCategories?.[args.category!]?.model
142
140
 
143
141
  if (!requirement) {
144
- // Precedence: explicit category model > routed executor override model > category resolved model.
145
- // This preserves per-category overrides while allowing category routing to stay on coder.
142
+ // Precedence: explicit category model > routed executor agent's override model > category resolved model.
143
+ // The routed executor agent is selected per-mode via resolveModeAgent (see shared/mode-routing.ts);
144
+ // its agentOverrides[<agent>].model serves as the fallback when no explicit category model is set.
146
145
  actualModel = explicitCategoryModel ?? overrideModel ?? resolved.model
147
146
  if (actualModel) {
148
147
  modelInfo = explicitCategoryModel || overrideModel
@@ -0,0 +1,30 @@
1
+ import type { BuiltinCategoryDefinition } from "./builtin-category-definition"
2
+
3
+ const GIT_CATEGORY_PROMPT_APPEND = `<Category_Context>
4
+ You are working on GIT / VERSION CONTROL tasks.
5
+
6
+ <Routing_Policy>
7
+ Executor contour: platform-manager (git operations specialist).
8
+ </Routing_Policy>
9
+
10
+ Git operations mindset:
11
+ - Atomic commits with clear messages
12
+ - Clean history: rebase/squash before push
13
+ - Safe operations: never force-push to main/master
14
+ - Clear handoff notes for session continuity
15
+
16
+ Approach:
17
+ - Understand the current branch state
18
+ - Plan atomic commit strategy
19
+ - Execute with minimal noise
20
+ - Verify state after operation
21
+ </Category_Context>`
22
+
23
+ export const GIT_CATEGORIES: BuiltinCategoryDefinition[] = [
24
+ {
25
+ name: "git",
26
+ config: {},
27
+ description: "Git operations: commits, branching, history, rebasing. Uses platform-manager execution contour.",
28
+ promptAppend: GIT_CATEGORY_PROMPT_APPEND,
29
+ },
30
+ ]