@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
package/src/index.ts CHANGED
@@ -14,26 +14,20 @@ import { createPluginDispose, type PluginDispose } from "./plugin-dispose"
14
14
  import { loadPluginConfig } from "./plugin-config"
15
15
  import { createModelCacheState } from "./plugin-state"
16
16
  import { createFirstMessageVariantGate } from "./shared/first-message-variant"
17
- import { injectServerAuthIntoClient, log } from "./shared"
17
+ import { injectServerAuthIntoClient, log, logWarn, logError } from "./shared"
18
18
  import { hydratePluginConfigWithPlatformDefaults } from "./shared/runtime-plugin-config"
19
19
  import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shared/external-plugin-detector"
20
20
  import { PLUGIN_NAME } from "./shared/plugin-identity"
21
+ import { autoExportStaticMcpJson } from "./shared/mcp-static-export"
21
22
  import { warnIfListPluginEntry, warnMissingRequiredMcpEnv } from "./shared/startup-diagnostics"
23
+ import { lintModeAgentCapabilities } from "./plugin/startup-diagnostics"
22
24
  import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash"
23
25
  import { lspManager } from "./tools/lsp/client"
24
26
 
25
- import { loadConfig, resolveEnvVars } from "./config/loader"
27
+ import { loadConfig } from "./config/loader"
26
28
  import type { HiaiOpencodeConfig } from "./config/types"
27
29
 
28
30
  import { createPlugin as createSubtask2Plugin } from "./internals/plugins/subtask2/core/plugin"
29
- import WebsearchCitedPlugin, {
30
- GOOGLE_PROVIDER_ID,
31
- OPENAI_PROVIDER_ID,
32
- OPENROUTER_PROVIDER_ID,
33
- WebsearchCitedGooglePlugin,
34
- WebsearchCitedOpenAIPlugin
35
- } from "./internals/plugins/websearch-cited/index"
36
- import type { WebsearchCitedFallback } from "./internals/plugins/websearch-cited/index"
37
31
  import { createBuiltinSkills } from "./features/builtin-skills"
38
32
  import {
39
33
  materializeBuiltinSkills,
@@ -42,39 +36,6 @@ import {
42
36
 
43
37
  let activePluginDispose: PluginDispose | null = null
44
38
 
45
- function createWebsearchFallback(config: HiaiOpencodeConfig): WebsearchCitedFallback | undefined {
46
- const model = config.agents?.researcher?.model?.trim()
47
- if (!model) {
48
- return undefined
49
- }
50
-
51
- if (model.startsWith("openrouter/")) {
52
- return {
53
- providerID: OPENROUTER_PROVIDER_ID,
54
- model: model.slice("openrouter/".length),
55
- }
56
- }
57
-
58
- if (model.startsWith("openai/")) {
59
- return {
60
- providerID: OPENAI_PROVIDER_ID,
61
- model: model.slice("openai/".length),
62
- }
63
- }
64
-
65
- if (model.startsWith("google/")) {
66
- return {
67
- providerID: GOOGLE_PROVIDER_ID,
68
- model: model.slice("google/".length),
69
- }
70
- }
71
-
72
- return {
73
- providerID: OPENROUTER_PROVIDER_ID,
74
- model,
75
- }
76
- }
77
-
78
39
  function configureBundledBunPtyLibrary(): void {
79
40
  if (process.env.BUN_PTY_LIB?.trim()) {
80
41
  return
@@ -111,7 +72,7 @@ const HiaiOpenCodePlugin: Plugin = async (ctx) => {
111
72
 
112
73
  const skillPluginCheck = detectExternalSkillPlugin(ctx.directory)
113
74
  if (skillPluginCheck.detected && skillPluginCheck.pluginName) {
114
- console.warn(getSkillPluginConflictWarning(skillPluginCheck.pluginName))
75
+ logWarn(getSkillPluginConflictWarning(skillPluginCheck.pluginName))
115
76
  }
116
77
 
117
78
  injectServerAuthIntoClient(ctx.client)
@@ -126,6 +87,8 @@ const HiaiOpenCodePlugin: Plugin = async (ctx) => {
126
87
  pluginConfig,
127
88
  platformConfig: internalConfig,
128
89
  })
90
+ lintModeAgentCapabilities()
91
+ autoExportStaticMcpJson(ctx.directory, internalConfig)
129
92
 
130
93
  materializeBuiltinSkills(
131
94
  createBuiltinSkills({
@@ -205,12 +168,8 @@ const HiaiOpenCodePlugin: Plugin = async (ctx) => {
205
168
  const mod = await import("./internals/plugins/pty/plugin");
206
169
  ptyResult = await mod.PTYPlugin(ctx);
207
170
  } catch (err) {
208
- console.error("[hiai-opencode] PTYPlugin failed to load:", err);
171
+ logError("PTYPlugin failed to load:", err)
209
172
  }
210
- const websearchResult = await WebsearchCitedPlugin(ctx, createWebsearchFallback(internalConfig))
211
- const websearchGoogleResult = await WebsearchCitedGooglePlugin(ctx)
212
- const websearchOpenAIResult = await WebsearchCitedOpenAIPlugin(ctx)
213
-
214
173
  const combinedResult = {
215
174
  name: PLUGIN_NAME,
216
175
  ...pluginInterface,
@@ -219,7 +178,6 @@ const HiaiOpenCodePlugin: Plugin = async (ctx) => {
219
178
  tool: {
220
179
  ...pluginInterface.tool,
221
180
  ...ptyResult.tool,
222
- ...websearchResult.tool,
223
181
  },
224
182
 
225
183
  // Chain hooks: command.execute.before
@@ -250,7 +208,6 @@ const HiaiOpenCodePlugin: Plugin = async (ctx) => {
250
208
  config: async (input: any) => {
251
209
  await pluginInterface.config?.(input);
252
210
  await (subtask2Result as any).config?.(input);
253
- await (websearchResult as any).config?.(input);
254
211
  },
255
212
 
256
213
  // Merge Events
@@ -260,33 +217,6 @@ const HiaiOpenCodePlugin: Plugin = async (ctx) => {
260
217
  await (ptyResult as any).event?.(input);
261
218
  },
262
219
 
263
- // Auth (Consolidated)
264
- auth: {
265
- provider: "hiai-opencode",
266
- methods: [
267
- { type: "api" as const, label: "Google Search API key" },
268
- ],
269
- loader: async (getAuth: any) => {
270
- const authData = await getAuth();
271
- const { registerGetAuth, GOOGLE_PROVIDER_ID, OPENAI_PROVIDER_ID, OPENROUTER_PROVIDER_ID } = await import("./internals/plugins/websearch-cited/index");
272
-
273
- const getConfiguredKey = (configKey?: string) => {
274
- if (configKey) return resolveEnvVars(configKey);
275
- return undefined;
276
- };
277
-
278
- const googleKey = authData["Google Search API key"] || getConfiguredKey(internalConfig.auth?.googleSearch);
279
- const openaiKey = getConfiguredKey(internalConfig.auth?.openai);
280
- const openRouterKey = getConfiguredKey(internalConfig.auth?.openrouter);
281
-
282
- if (googleKey) registerGetAuth(GOOGLE_PROVIDER_ID, () => Promise.resolve({ type: "api", key: googleKey }));
283
- if (openaiKey) registerGetAuth(OPENAI_PROVIDER_ID, () => Promise.resolve({ type: "api", key: openaiKey }));
284
- if (openRouterKey) registerGetAuth(OPENROUTER_PROVIDER_ID, () => Promise.resolve({ type: "api", key: openRouterKey }));
285
-
286
- return {};
287
- },
288
- },
289
-
290
220
  "experimental.session.compacting": async (
291
221
  _input: { sessionID: string },
292
222
  output: { context: string[] },
@@ -3,11 +3,7 @@
3
3
  import { join } from "path";
4
4
  import { getOpenCodeConfigDir } from "../../../../shared/opencode-config-dir";
5
5
  import type { CommandConfig } from "../types";
6
- import {
7
- parseFrontmatter,
8
- getTemplateBody,
9
- parseParallelConfig,
10
- } from "../parsing";
6
+ import { parseFrontmatter, getTemplateBody, parseParallelConfig, parseLoopConfig } from "../parsing";
11
7
 
12
8
  /**
13
9
  * Commands: Manifest building
@@ -43,7 +39,7 @@ export async function buildManifest(): Promise<Record<string, CommandConfig>> {
43
39
  agent: fm.agent as string | undefined,
44
40
  description: fm.description as string | undefined,
45
41
  template: getTemplateBody(content),
46
- loop: fm.loop as any,
42
+ loop: parseLoopConfig(fm.loop),
47
43
  model: fm.model as string | undefined,
48
44
  auto: fm.subtask2 === "auto",
49
45
  };
@@ -261,11 +261,11 @@ export async function commandExecuteBefore(
261
261
  log(
262
262
  `cmd.before: registered parent for prompt (${part.prompt.length} chars)`
263
263
  );
264
- if ((part as any).as) {
264
+ if ("as" in part && part.as) {
265
265
  registerPendingResultCaptureByPrompt(
266
266
  part.prompt,
267
267
  input.sessionID,
268
- (part as any).as
268
+ String(part.as)
269
269
  );
270
270
  }
271
271
  }
@@ -177,7 +177,7 @@ export async function chatMessagesTransform(input: any, output: any) {
177
177
  if (msg.info?.role !== "user") continue;
178
178
 
179
179
  // Track processed messages by ID to avoid infinite loop
180
- const msgId = (msg.info as any)?.id;
180
+ const msgId = msg.info?.id;
181
181
  if (msgId && hasProcessedS2Message(msgId)) continue;
182
182
 
183
183
  for (const part of msg.parts) {
@@ -27,11 +27,14 @@ export function parseLoopConfig(loop: unknown): LoopConfig | undefined {
27
27
  }
28
28
 
29
29
  // Parse a parallel item - handles "/cmd {model:...} args" syntax, plain "cmd", or {command, arguments} object
30
+ function isParallelCommandLike(p: unknown): p is { command: unknown; arguments?: unknown; prompt?: unknown; model?: unknown; agent?: unknown; loop?: unknown; as?: unknown; inline?: unknown } {
31
+ return typeof p === "object" && p !== null && "command" in p
32
+ }
33
+
30
34
  export function parseParallelItem(p: unknown): ParallelCommand | null {
31
35
  if (typeof p === "string") {
32
36
  const trimmed = p.trim();
33
37
  if (trimmed.startsWith("/")) {
34
- // Parse /command {overrides} args syntax
35
38
  const parsed = parseCommandWithOverrides(trimmed);
36
39
  if (parsed.isInlineSubtask) {
37
40
  return {
@@ -55,16 +58,16 @@ export function parseParallelItem(p: unknown): ParallelCommand | null {
55
58
  }
56
59
  return { command: trimmed };
57
60
  }
58
- if (typeof p === "object" && p !== null && (p as any).command) {
61
+ if (isParallelCommandLike(p)) {
59
62
  return {
60
- command: (p as any).command,
61
- arguments: (p as any).arguments,
62
- prompt: (p as any).prompt,
63
- model: (p as any).model,
64
- agent: (p as any).agent,
65
- loop: (p as any).loop,
66
- as: (p as any).as,
67
- inline: (p as any).inline,
63
+ command: String(p.command),
64
+ arguments: p.arguments !== undefined ? String(p.arguments) : undefined,
65
+ prompt: p.prompt !== undefined ? String(p.prompt) : undefined,
66
+ model: p.model !== undefined ? String(p.model) : undefined,
67
+ agent: p.agent !== undefined ? String(p.agent) : undefined,
68
+ loop: p.loop !== undefined ? parseLoopConfig(p.loop) : undefined,
69
+ as: p.as !== undefined ? String(p.as) : undefined,
70
+ inline: p.inline !== undefined ? Boolean(p.inline) : undefined,
68
71
  };
69
72
  }
70
73
  return null;
package/src/mcp/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- export { createBuiltinMcps } from "./omo-mcp-index"
2
1
  export { McpNameSchema, type McpName, type AnyMcpName } from "./types"
3
2
 
4
3
  import type { McpServerConfig } from "../config/types.js";
@@ -10,6 +10,8 @@ export type HiaiMcpName =
10
10
  | "rag"
11
11
  | "context7"
12
12
  | "mempalace"
13
+ | "websearch"
14
+ | "grep_app"
13
15
 
14
16
  export type HiaiMcpInstallKind = "bundled" | "npm" | "python" | "remote" | "user-service"
15
17
 
@@ -121,6 +123,31 @@ export const HIAI_MCP_REGISTRY: Record<HiaiMcpName, HiaiMcpRegistryEntry> = {
121
123
  timeout: 600000,
122
124
  },
123
125
  },
126
+ websearch: {
127
+ name: "websearch",
128
+ enabledByDefault: true,
129
+ install: "remote",
130
+ optionalEnv: ["EXA_API_KEY", "TAVILY_API_KEY"],
131
+ config: {
132
+ enabled: true,
133
+ type: "remote",
134
+ url: "https://mcp.exa.ai/mcp?tools=web_search_exa",
135
+ headers: { "x-api-key": "{env:EXA_API_KEY}" },
136
+ timeout: 600000,
137
+ provider: "exa",
138
+ },
139
+ },
140
+ grep_app: {
141
+ name: "grep_app",
142
+ enabledByDefault: true,
143
+ install: "remote",
144
+ config: {
145
+ enabled: true,
146
+ type: "remote",
147
+ url: "https://mcp.grep.app",
148
+ timeout: 600000,
149
+ },
150
+ },
124
151
  }
125
152
 
126
153
  export function createDefaultMcpConfig(): Record<HiaiMcpName, McpServerConfig> {
@@ -236,8 +236,6 @@ export function createChatMessageHandler(args: {
236
236
  await hooks.thinkMode?.["chat.message"]?.(input, output)
237
237
  await hooks.claudeCodeHooks?.["chat.message"]?.(input, output)
238
238
  await hooks.autoSlashCommand?.["chat.message"]?.(input, output)
239
- await hooks.noBobGpt?.["chat.message"]?.(input, output)
240
- await hooks.noCoderNonGpt?.["chat.message"]?.(input, output)
241
239
  if (hooks.startWork && isStartWorkHookOutput(output)) {
242
240
  const promptText = extractPromptText(output.parts)
243
241
  if (isStartWorkFallbackTemplate(promptText)) {
@@ -19,8 +19,6 @@ import {
19
19
  createStartWorkHook,
20
20
  createStrategistMdOnlyHook,
21
21
  createBobJuniorNotepadHook,
22
- createNoBobGptHook,
23
- createNoCoderNonGptHook,
24
22
  createQuestionLabelTruncatorHook,
25
23
  createPreemptiveCompactionHook,
26
24
  createRuntimeFallbackHook,
@@ -54,8 +52,6 @@ export type SessionHooks = {
54
52
  startWork: ReturnType<typeof createStartWorkHook> | null
55
53
  strategistMdOnly: ReturnType<typeof createStrategistMdOnlyHook> | null
56
54
  bobJuniorNotepad: ReturnType<typeof createBobJuniorNotepadHook> | null
57
- noBobGpt: ReturnType<typeof createNoBobGptHook> | null
58
- noCoderNonGpt: ReturnType<typeof createNoCoderNonGptHook> | null
59
55
  questionLabelTruncator: ReturnType<typeof createQuestionLabelTruncatorHook> | null
60
56
  taskResumeInfo: ReturnType<typeof createTaskResumeInfoHook> | null
61
57
  anthropicEffort: ReturnType<typeof createAnthropicEffortHook> | null
@@ -219,17 +215,6 @@ export function createSessionHooks(args: {
219
215
  ? safeHook("sub-notepad", () => createBobJuniorNotepadHook(ctx))
220
216
  : null
221
217
 
222
- const noBobGpt = isHookEnabled("no-bob-gpt")
223
- ? safeHook("no-bob-gpt", () => createNoBobGptHook(ctx))
224
- : null
225
-
226
- const noCoderNonGpt = isHookEnabled("no-coder-non-gpt")
227
- ? safeHook("no-coder-non-gpt", () =>
228
- createNoCoderNonGptHook(ctx, {
229
- allowNonGptModel: pluginConfig.agents?.coder?.allow_non_gpt_model,
230
- }))
231
- : null
232
-
233
218
  const questionLabelTruncator = isHookEnabled("question-label-truncator")
234
219
  ? safeHook("question-label-truncator", () => createQuestionLabelTruncatorHook())
235
220
  : null
@@ -275,8 +260,6 @@ export function createSessionHooks(args: {
275
260
  startWork,
276
261
  strategistMdOnly,
277
262
  bobJuniorNotepad,
278
- noBobGpt,
279
- noCoderNonGpt,
280
263
  questionLabelTruncator,
281
264
  taskResumeInfo,
282
265
  anthropicEffort,
@@ -0,0 +1,27 @@
1
+ import { MODE_TO_AGENT } from "../shared/mode-routing"
2
+
3
+ const WRITE_CAPABLE_AGENTS = new Set(["coder", "sub", "designer", "brainstormer"])
4
+
5
+ const WRITE_MODES = new Set([
6
+ "quick",
7
+ "bounded",
8
+ "deep",
9
+ "cross-module",
10
+ "visual-engineering",
11
+ "artistry",
12
+ "writing",
13
+ ])
14
+
15
+ const READONLY_MODES = new Set(["ultrabrain", "git", "git-ops"])
16
+
17
+ export function lintModeAgentCapabilities(): void {
18
+ for (const [mode, agent] of Object.entries(MODE_TO_AGENT)) {
19
+ if (WRITE_MODES.has(mode) && !WRITE_CAPABLE_AGENTS.has(agent)) {
20
+ console.warn(`[startup-lint] Mode "${mode}" targets agent "${agent}" which cannot write/edit. Tasks using this mode may fail when attempting file operations.`)
21
+ }
22
+
23
+ if (READONLY_MODES.has(mode) && WRITE_CAPABLE_AGENTS.has(agent)) {
24
+ console.warn(`[startup-lint] Mode "${mode}" targets agent "${agent}" which can write. Consider verifying this is intentional.`)
25
+ }
26
+ }
27
+ }
@@ -81,7 +81,7 @@ const RUNTIME_AGENT_DESCRIPTIONS: Partial<Record<string, string>> = {
81
81
  "Compatibility wrapper for review functions now folded into Critic. (Quality Guardian - HiaiOpenCode)",
82
82
  };
83
83
 
84
- function forceVisiblePrimaryAgent(agent: unknown, name: string): unknown {
84
+ function forceVisiblePrimaryAgent(agent: unknown, name: string, forceMode?: "primary" | "all"): unknown {
85
85
  if (typeof agent !== "object" || agent === null) {
86
86
  return agent;
87
87
  }
@@ -91,7 +91,7 @@ function forceVisiblePrimaryAgent(agent: unknown, name: string): unknown {
91
91
  ...base,
92
92
  name,
93
93
  hidden: false,
94
- mode: "primary",
94
+ mode: forceMode ?? "all",
95
95
  ...(typeof base.description === "string" && base.description.trim().length > 0
96
96
  ? {}
97
97
  : { description: RUNTIME_AGENT_DESCRIPTIONS[name] }),
@@ -475,6 +475,7 @@ export async function applyAgentConfig(params: {
475
475
  normalizedAgents[name] = forceVisiblePrimaryAgent(
476
476
  normalizedAgents[name],
477
477
  name,
478
+ name === "Bob" ? "primary" : "all",
478
479
  );
479
480
  }
480
481
  }
@@ -0,0 +1,63 @@
1
+ import { expect, test } from "bun:test"
2
+
3
+ import { applyMcpConfig } from "./mcp-config-handler"
4
+
5
+ const emptyPluginComponents = {
6
+ commands: {},
7
+ skills: {},
8
+ agents: {},
9
+ mcpServers: {},
10
+ hooksConfigs: [],
11
+ plugins: [],
12
+ errors: [],
13
+ }
14
+
15
+ test("context7 auth fallback from hiai-opencode config is used when env placeholder is empty", async () => {
16
+ const config: Record<string, unknown> = {}
17
+
18
+ await applyMcpConfig({
19
+ config,
20
+ pluginConfig: {
21
+ auth: { context7: "ctx-test-key" },
22
+ claude_code: { mcp: false },
23
+ __platformDefaults: {
24
+ mcp: {
25
+ context7: {
26
+ enabled: true,
27
+ type: "remote",
28
+ url: "https://mcp.context7.com/mcp",
29
+ headers: { "X-API-KEY": "{env:CONTEXT7_API_KEY}" },
30
+ },
31
+ },
32
+ },
33
+ } as any,
34
+ pluginComponents: emptyPluginComponents,
35
+ })
36
+
37
+ expect((config.mcp as any).context7.headers).toEqual({ "X-API-KEY": "ctx-test-key" })
38
+ })
39
+
40
+ test("firecrawl auth fallback from hiai-opencode config is used when env placeholder is empty", async () => {
41
+ const config: Record<string, unknown> = {}
42
+
43
+ await applyMcpConfig({
44
+ config,
45
+ pluginConfig: {
46
+ auth: { firecrawl: "fc-test-key" },
47
+ claude_code: { mcp: false },
48
+ __platformDefaults: {
49
+ mcp: {
50
+ firecrawl: {
51
+ enabled: true,
52
+ type: "remote",
53
+ url: "http://localhost/firecrawl",
54
+ environment: { FIRECRAWL_API_KEY: "{env:FIRECRAWL_API_KEY}" },
55
+ },
56
+ },
57
+ },
58
+ } as any,
59
+ pluginComponents: emptyPluginComponents,
60
+ })
61
+
62
+ expect((config.mcp as any).firecrawl.environment).toEqual({ FIRECRAWL_API_KEY: "fc-test-key" })
63
+ })
@@ -2,7 +2,6 @@ import type { HiaiOpenCodeConfig } from "../config";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
4
  import { loadMcpConfigs } from "../features/claude-code-mcp-loader";
5
- import { createBuiltinMcps } from "../mcp";
6
5
  import type { PluginComponents } from "./plugin-components-loader";
7
6
  import { log } from "../shared";
8
7
  import { getPlatformMcpDefaults } from "../shared/runtime-plugin-config";
@@ -14,8 +13,20 @@ function resolveHeaderAuthFallback(
14
13
  pluginConfig: HiaiOpenCodeConfig,
15
14
  name: string,
16
15
  ): Record<string, string> | undefined {
17
- if (name === "stitch" && pluginConfig.auth?.stitch?.trim()) {
18
- return { "X-Goog-Api-Key": pluginConfig.auth.stitch.trim() };
16
+ const resolveAuth = (value: string | undefined): string | undefined => {
17
+ if (!value?.trim()) return undefined;
18
+ const resolved = resolveEnvVars(value).trim();
19
+ return resolved.length > 0 ? resolved : undefined;
20
+ };
21
+
22
+ if (name === "stitch") {
23
+ const key = resolveAuth(pluginConfig.auth?.stitch);
24
+ return key ? { "X-Goog-Api-Key": key } : undefined;
25
+ }
26
+
27
+ if (name === "context7") {
28
+ const key = resolveAuth(pluginConfig.auth?.context7);
29
+ return key ? { "X-API-KEY": key } : undefined;
19
30
  }
20
31
 
21
32
  return undefined;
@@ -25,8 +36,9 @@ function resolveEnvironmentAuthFallback(
25
36
  pluginConfig: HiaiOpenCodeConfig,
26
37
  name: string,
27
38
  ): Record<string, string> | undefined {
28
- if (name === "firecrawl" && pluginConfig.auth?.firecrawl?.trim()) {
29
- return { FIRECRAWL_API_KEY: pluginConfig.auth.firecrawl.trim() };
39
+ if (name === "firecrawl") {
40
+ const key = pluginConfig.auth?.firecrawl ? resolveEnvVars(pluginConfig.auth.firecrawl).trim() : "";
41
+ return key ? { FIRECRAWL_API_KEY: key } : undefined;
30
42
  }
31
43
 
32
44
  return undefined;
@@ -122,18 +134,20 @@ function normalizePlatformMcpDefaults(
122
134
  );
123
135
  const headerFallback = resolveHeaderAuthFallback(pluginConfig, name);
124
136
  const environmentFallback = resolveEnvironmentAuthFallback(pluginConfig, name);
137
+ const usableHeaders = headers && !hasMissingResolvedValue(headers) ? headers : undefined;
138
+ const usableEnvironment = environment && !hasMissingResolvedValue(environment) ? environment : undefined;
125
139
  const headersWithFallback =
126
- headers && headerFallback
127
- ? { ...headerFallback, ...headers }
128
- : headers ?? headerFallback;
140
+ usableHeaders && headerFallback
141
+ ? { ...headerFallback, ...usableHeaders }
142
+ : usableHeaders ?? headerFallback;
129
143
  const environmentWithFallback =
130
- environment && environmentFallback
131
- ? { ...environmentFallback, ...environment }
132
- : environment ?? environmentFallback;
144
+ usableEnvironment && environmentFallback
145
+ ? { ...environmentFallback, ...usableEnvironment }
146
+ : usableEnvironment ?? environmentFallback;
133
147
 
134
148
  const missingResolvedValues =
135
- hasMissingResolvedValue(headersWithFallback) ||
136
- hasMissingResolvedValue(environmentWithFallback);
149
+ (!!headers && !usableHeaders && !headerFallback) ||
150
+ (!!environment && !usableEnvironment && !environmentFallback);
137
151
 
138
152
  if (missingResolvedValues) {
139
153
  log(`MCP server "${name}" is missing environment-backed auth; keeping it visible in config`);
@@ -152,6 +166,8 @@ function normalizePlatformMcpDefaults(
152
166
  nextEntry.environment = environmentWithFallback;
153
167
  }
154
168
 
169
+ delete nextEntry.provider;
170
+
155
171
  normalized[name] = nextEntry;
156
172
  }
157
173
 
@@ -184,7 +200,6 @@ export async function applyMcpConfig(params: {
184
200
  getPlatformMcpDefaults(params.pluginConfig) as unknown as Record<string, unknown>,
185
201
  params.pluginConfig,
186
202
  ),
187
- ...createBuiltinMcps(disabledMcps, params.pluginConfig),
188
203
  ...mcpResult.servers,
189
204
  ...(userMcp ?? {}),
190
205
  ...params.pluginComponents.mcpServers,
@@ -98,7 +98,7 @@ export async function buildStrategistAgentConfig(params: {
98
98
  const base: Record<string, unknown> = {
99
99
  ...(resolvedModel ? { model: resolvedModel } : {}),
100
100
  ...(variantToUse ? { variant: variantToUse } : {}),
101
- mode: "primary",
101
+ mode: "all",
102
102
  prompt: getStrategistPrompt(resolvedModel, params.disabledTools) + "\n\n" + CLOSURE_SCHEMA_PROMPT,
103
103
  permission: PROMETHEUS_PERMISSION,
104
104
  description: `${(params.configAgentPlan?.description as string) ?? "Plan agent"} (Strategist - HiaiOpenCode)`,
@@ -0,0 +1,9 @@
1
+ import { expect, test } from "bun:test"
2
+
3
+ import { getAgentConfigKey } from "./agent-display-names"
4
+
5
+ test("writer aliases resolve to brainstormer", () => {
6
+ expect(getAgentConfigKey("writer")).toBe("brainstormer")
7
+ expect(getAgentConfigKey("copywriter")).toBe("brainstormer")
8
+ expect(getAgentConfigKey("content-writer")).toBe("brainstormer")
9
+ })
@@ -110,6 +110,11 @@ const LEGACY_DISPLAY_NAMES: Record<string, string> = {
110
110
  "platform manager (utility)": "platform-manager",
111
111
  "platform manager - utility": "platform-manager",
112
112
  "brainstormer - idea explorer": "brainstormer",
113
+ "writer": "brainstormer",
114
+ "copywriter": "brainstormer",
115
+ "content-writer": "brainstormer",
116
+ "content writer": "brainstormer",
117
+ "website-writer": "brainstormer",
113
118
  "agent skills - skill composer": "agent-skills",
114
119
  "subagent": "sub",
115
120
  "ui": "multimodal",
@@ -1,5 +1,5 @@
1
1
  import { checkForLegacyPluginEntry } from "./legacy-plugin-warning"
2
- import { log } from "./logger"
2
+ import { log, logWarn } from "./logger"
3
3
  import { migrateLegacyPluginEntry } from "./migrate-legacy-plugin-entry"
4
4
  import { toCanonicalEntry } from "./plugin-entry-migrator"
5
5
  import { LEGACY_PLUGIN_NAME, PLUGIN_NAME } from "./plugin-identity"
@@ -28,18 +28,16 @@ export function logLegacyPluginStartupWarning(deps: LogLegacyPluginStartupWarnin
28
28
  hasCanonicalEntry: result.hasCanonicalEntry,
29
29
  })
30
30
 
31
- console.warn(
32
- `[hiai-opencode] WARNING: Your opencode.json uses the legacy package name "${LEGACY_PLUGIN_NAME}".`
31
+ logWarn(`Your opencode.json uses the legacy package name "${LEGACY_PLUGIN_NAME}".`
33
32
  + ` The package has been renamed to "${PLUGIN_NAME}".`
34
- + ` Attempting auto-migration...`,
35
- )
33
+ + ` Attempting auto-migration...`)
36
34
 
37
35
  const migrated = migrateLegacyPluginEntryFn(result.configPath!)
38
36
  if (migrated) {
39
- console.warn(`[hiai-opencode] Auto-migrated opencode.json: ${result.legacyEntries.join(", ")} -> ${suggestedEntries.join(", ")}`)
37
+ logWarn(`Auto-migrated opencode.json: ${result.legacyEntries.join(", ")} -> ${suggestedEntries.join(", ")}`)
40
38
  } else {
41
- console.warn(
42
- `[hiai-opencode] Could not auto-migrate. Please manually update your opencode.json:`
39
+ logWarn(
40
+ `Could not auto-migrate. Please manually update your opencode.json:`
43
41
  + ` ${result.legacyEntries.map((e, i) => `"${e}" -> "${suggestedEntries[i]}"`).join(", ")}`,
44
42
  )
45
43
  }
@@ -43,6 +43,14 @@ export function log(message: string, data?: unknown): void {
43
43
  }
44
44
  }
45
45
 
46
+ export function logWarn(message: string, data?: unknown): void {
47
+ log(`WARN: ${message}`, data)
48
+ }
49
+
50
+ export function logError(message: string, data?: unknown): void {
51
+ log(`ERROR: ${message}`, data)
52
+ }
53
+
46
54
  export function getLogFilePath(): string {
47
55
  return logFile
48
56
  }