@gajae-code/coding-agent 0.2.4 → 0.3.0

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 (266) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +145 -2
  4. package/dist/types/commands/harness.d.ts +37 -0
  5. package/dist/types/config/settings-schema.d.ts +13 -3
  6. package/dist/types/config/settings.d.ts +3 -1
  7. package/dist/types/deep-interview/render-middleware.d.ts +5 -0
  8. package/dist/types/discovery/helpers.d.ts +1 -0
  9. package/dist/types/exec/bash-executor.d.ts +8 -1
  10. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/extensibility/shared-events.d.ts +1 -0
  13. package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
  14. package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
  15. package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
  16. package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
  17. package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
  18. package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
  19. package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
  20. package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
  21. package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
  22. package/dist/types/harness-control-plane/classifier.d.ts +13 -0
  23. package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
  24. package/dist/types/harness-control-plane/finalize.d.ts +47 -0
  25. package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
  26. package/dist/types/harness-control-plane/operate.d.ts +35 -0
  27. package/dist/types/harness-control-plane/owner.d.ts +46 -0
  28. package/dist/types/harness-control-plane/preserve.d.ts +19 -0
  29. package/dist/types/harness-control-plane/receipts.d.ts +88 -0
  30. package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
  31. package/dist/types/harness-control-plane/seams.d.ts +21 -0
  32. package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
  33. package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
  34. package/dist/types/harness-control-plane/storage.d.ts +53 -0
  35. package/dist/types/harness-control-plane/types.d.ts +162 -0
  36. package/dist/types/hooks/skill-keywords.d.ts +2 -1
  37. package/dist/types/hooks/skill-state.d.ts +2 -29
  38. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  39. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  40. package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
  41. package/dist/types/modes/interactive-mode.d.ts +2 -0
  42. package/dist/types/modes/theme/defaults/index.d.ts +45 -9477
  43. package/dist/types/modes/theme/theme.d.ts +1 -5
  44. package/dist/types/modes/types.d.ts +2 -0
  45. package/dist/types/sdk.d.ts +4 -0
  46. package/dist/types/session/agent-session.d.ts +8 -0
  47. package/dist/types/session/streaming-output.d.ts +11 -0
  48. package/dist/types/skill-state/active-state.d.ts +3 -0
  49. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
  50. package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
  51. package/dist/types/task/executor.d.ts +3 -0
  52. package/dist/types/task/types.d.ts +56 -3
  53. package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
  54. package/dist/types/tools/bash.d.ts +24 -0
  55. package/dist/types/tools/cron.d.ts +110 -0
  56. package/dist/types/tools/index.d.ts +4 -0
  57. package/dist/types/tools/monitor.d.ts +54 -0
  58. package/dist/types/tools/subagent.d.ts +11 -1
  59. package/dist/types/web/search/index.d.ts +1 -0
  60. package/dist/types/web/search/provider.d.ts +11 -4
  61. package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
  62. package/dist/types/web/search/types.d.ts +1 -1
  63. package/package.json +7 -7
  64. package/src/async/job-manager.ts +522 -6
  65. package/src/cli/agents-cli.ts +3 -0
  66. package/src/cli/auth-broker-cli.ts +1 -0
  67. package/src/cli/config-cli.ts +10 -2
  68. package/src/cli.ts +2 -0
  69. package/src/commands/harness.ts +592 -0
  70. package/src/commands/team.ts +36 -39
  71. package/src/config/settings-schema.ts +15 -2
  72. package/src/config/settings.ts +49 -7
  73. package/src/deep-interview/render-middleware.ts +366 -0
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +9 -2
  75. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  76. package/src/defaults/gjc/skills/team/SKILL.md +47 -21
  77. package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
  78. package/src/discovery/helpers.ts +5 -0
  79. package/src/eval/js/shared/rewrite-imports.ts +1 -2
  80. package/src/exec/bash-executor.ts +20 -9
  81. package/src/extensibility/custom-tools/types.ts +1 -0
  82. package/src/extensibility/extensions/types.ts +6 -0
  83. package/src/extensibility/shared-events.ts +1 -0
  84. package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
  85. package/src/gjc-runtime/goal-mode-request.ts +11 -3
  86. package/src/gjc-runtime/ralplan-runtime.ts +27 -10
  87. package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
  88. package/src/gjc-runtime/state-graph.ts +86 -0
  89. package/src/gjc-runtime/state-migrations.ts +132 -0
  90. package/src/gjc-runtime/state-renderer.ts +345 -0
  91. package/src/gjc-runtime/state-runtime.ts +733 -21
  92. package/src/gjc-runtime/state-validation.ts +49 -0
  93. package/src/gjc-runtime/state-writer.ts +718 -0
  94. package/src/gjc-runtime/team-runtime.ts +1083 -89
  95. package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
  96. package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
  97. package/src/gjc-runtime/workflow-manifest.ts +425 -0
  98. package/src/harness-control-plane/classifier.ts +128 -0
  99. package/src/harness-control-plane/control-endpoint.ts +137 -0
  100. package/src/harness-control-plane/finalize.ts +222 -0
  101. package/src/harness-control-plane/frame-mapper.ts +286 -0
  102. package/src/harness-control-plane/operate.ts +225 -0
  103. package/src/harness-control-plane/owner.ts +553 -0
  104. package/src/harness-control-plane/preserve.ts +102 -0
  105. package/src/harness-control-plane/receipts.ts +216 -0
  106. package/src/harness-control-plane/rpc-adapter.ts +276 -0
  107. package/src/harness-control-plane/seams.ts +39 -0
  108. package/src/harness-control-plane/session-lease.ts +388 -0
  109. package/src/harness-control-plane/state-machine.ts +97 -0
  110. package/src/harness-control-plane/storage.ts +257 -0
  111. package/src/harness-control-plane/types.ts +214 -0
  112. package/src/hooks/skill-keywords.ts +4 -2
  113. package/src/hooks/skill-state.ts +25 -42
  114. package/src/internal-urls/docs-index.generated.ts +6 -4
  115. package/src/lsp/render.ts +1 -1
  116. package/src/modes/acp/acp-agent.ts +1 -1
  117. package/src/modes/acp/acp-client-bridge.ts +1 -1
  118. package/src/modes/components/agent-dashboard.ts +1 -1
  119. package/src/modes/components/assistant-message.ts +5 -1
  120. package/src/modes/components/diff.ts +2 -2
  121. package/src/modes/components/hook-selector.ts +72 -2
  122. package/src/modes/components/skill-hud/render.ts +7 -2
  123. package/src/modes/controllers/event-controller.ts +71 -6
  124. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  125. package/src/modes/controllers/input-controller.ts +19 -3
  126. package/src/modes/controllers/selector-controller.ts +3 -2
  127. package/src/modes/interactive-mode.ts +21 -2
  128. package/src/modes/theme/defaults/index.ts +0 -196
  129. package/src/modes/theme/theme.ts +35 -35
  130. package/src/modes/types.ts +2 -0
  131. package/src/prompts/agents/architect.md +5 -1
  132. package/src/prompts/agents/critic.md +5 -1
  133. package/src/prompts/agents/executor.md +13 -0
  134. package/src/prompts/agents/frontmatter.md +1 -0
  135. package/src/prompts/agents/planner.md +5 -1
  136. package/src/prompts/tools/bash.md +9 -0
  137. package/src/prompts/tools/cron.md +25 -0
  138. package/src/prompts/tools/monitor.md +30 -0
  139. package/src/prompts/tools/subagent.md +33 -3
  140. package/src/runtime-mcp/oauth-flow.ts +4 -2
  141. package/src/sdk.ts +7 -0
  142. package/src/session/agent-session.ts +247 -38
  143. package/src/session/session-manager.ts +13 -1
  144. package/src/session/streaming-output.ts +21 -0
  145. package/src/skill-state/active-state.ts +222 -78
  146. package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
  147. package/src/skill-state/initial-phase.ts +2 -0
  148. package/src/skill-state/workflow-state-contract.ts +26 -0
  149. package/src/task/agents.ts +1 -0
  150. package/src/task/executor.ts +51 -8
  151. package/src/task/index.ts +120 -8
  152. package/src/task/render.ts +6 -3
  153. package/src/task/types.ts +57 -3
  154. package/src/tools/ask.ts +28 -7
  155. package/src/tools/bash-allowed-prefixes.ts +169 -0
  156. package/src/tools/bash.ts +190 -29
  157. package/src/tools/browser/tab-worker.ts +1 -1
  158. package/src/tools/cron.ts +665 -0
  159. package/src/tools/index.ts +20 -2
  160. package/src/tools/monitor.ts +136 -0
  161. package/src/tools/subagent.ts +255 -64
  162. package/src/vim/engine.ts +3 -3
  163. package/src/web/search/index.ts +31 -18
  164. package/src/web/search/provider.ts +57 -12
  165. package/src/web/search/providers/duckduckgo.ts +279 -0
  166. package/src/web/search/types.ts +2 -0
  167. package/src/modes/theme/dark.json +0 -95
  168. package/src/modes/theme/defaults/alabaster.json +0 -93
  169. package/src/modes/theme/defaults/amethyst.json +0 -96
  170. package/src/modes/theme/defaults/anthracite.json +0 -93
  171. package/src/modes/theme/defaults/basalt.json +0 -91
  172. package/src/modes/theme/defaults/birch.json +0 -95
  173. package/src/modes/theme/defaults/dark-abyss.json +0 -91
  174. package/src/modes/theme/defaults/dark-arctic.json +0 -104
  175. package/src/modes/theme/defaults/dark-aurora.json +0 -95
  176. package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
  177. package/src/modes/theme/defaults/dark-cavern.json +0 -91
  178. package/src/modes/theme/defaults/dark-copper.json +0 -95
  179. package/src/modes/theme/defaults/dark-cosmos.json +0 -90
  180. package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
  181. package/src/modes/theme/defaults/dark-dracula.json +0 -98
  182. package/src/modes/theme/defaults/dark-eclipse.json +0 -91
  183. package/src/modes/theme/defaults/dark-ember.json +0 -95
  184. package/src/modes/theme/defaults/dark-equinox.json +0 -90
  185. package/src/modes/theme/defaults/dark-forest.json +0 -96
  186. package/src/modes/theme/defaults/dark-github.json +0 -105
  187. package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
  188. package/src/modes/theme/defaults/dark-lavender.json +0 -95
  189. package/src/modes/theme/defaults/dark-lunar.json +0 -89
  190. package/src/modes/theme/defaults/dark-midnight.json +0 -95
  191. package/src/modes/theme/defaults/dark-monochrome.json +0 -94
  192. package/src/modes/theme/defaults/dark-monokai.json +0 -98
  193. package/src/modes/theme/defaults/dark-nebula.json +0 -90
  194. package/src/modes/theme/defaults/dark-nord.json +0 -97
  195. package/src/modes/theme/defaults/dark-ocean.json +0 -101
  196. package/src/modes/theme/defaults/dark-one.json +0 -100
  197. package/src/modes/theme/defaults/dark-poimandres.json +0 -141
  198. package/src/modes/theme/defaults/dark-rainforest.json +0 -91
  199. package/src/modes/theme/defaults/dark-reef.json +0 -91
  200. package/src/modes/theme/defaults/dark-retro.json +0 -92
  201. package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
  202. package/src/modes/theme/defaults/dark-sakura.json +0 -95
  203. package/src/modes/theme/defaults/dark-slate.json +0 -95
  204. package/src/modes/theme/defaults/dark-solarized.json +0 -97
  205. package/src/modes/theme/defaults/dark-solstice.json +0 -90
  206. package/src/modes/theme/defaults/dark-starfall.json +0 -91
  207. package/src/modes/theme/defaults/dark-sunset.json +0 -99
  208. package/src/modes/theme/defaults/dark-swamp.json +0 -90
  209. package/src/modes/theme/defaults/dark-synthwave.json +0 -103
  210. package/src/modes/theme/defaults/dark-taiga.json +0 -91
  211. package/src/modes/theme/defaults/dark-terminal.json +0 -95
  212. package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
  213. package/src/modes/theme/defaults/dark-tundra.json +0 -91
  214. package/src/modes/theme/defaults/dark-twilight.json +0 -91
  215. package/src/modes/theme/defaults/dark-volcanic.json +0 -91
  216. package/src/modes/theme/defaults/graphite.json +0 -92
  217. package/src/modes/theme/defaults/light-arctic.json +0 -107
  218. package/src/modes/theme/defaults/light-aurora-day.json +0 -91
  219. package/src/modes/theme/defaults/light-canyon.json +0 -91
  220. package/src/modes/theme/defaults/light-catppuccin.json +0 -106
  221. package/src/modes/theme/defaults/light-cirrus.json +0 -90
  222. package/src/modes/theme/defaults/light-coral.json +0 -95
  223. package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
  224. package/src/modes/theme/defaults/light-dawn.json +0 -90
  225. package/src/modes/theme/defaults/light-dunes.json +0 -91
  226. package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
  227. package/src/modes/theme/defaults/light-forest.json +0 -100
  228. package/src/modes/theme/defaults/light-frost.json +0 -95
  229. package/src/modes/theme/defaults/light-github.json +0 -115
  230. package/src/modes/theme/defaults/light-glacier.json +0 -91
  231. package/src/modes/theme/defaults/light-gruvbox.json +0 -108
  232. package/src/modes/theme/defaults/light-haze.json +0 -90
  233. package/src/modes/theme/defaults/light-honeycomb.json +0 -95
  234. package/src/modes/theme/defaults/light-lagoon.json +0 -91
  235. package/src/modes/theme/defaults/light-lavender.json +0 -95
  236. package/src/modes/theme/defaults/light-meadow.json +0 -91
  237. package/src/modes/theme/defaults/light-mint.json +0 -95
  238. package/src/modes/theme/defaults/light-monochrome.json +0 -101
  239. package/src/modes/theme/defaults/light-ocean.json +0 -99
  240. package/src/modes/theme/defaults/light-one.json +0 -99
  241. package/src/modes/theme/defaults/light-opal.json +0 -91
  242. package/src/modes/theme/defaults/light-orchard.json +0 -91
  243. package/src/modes/theme/defaults/light-paper.json +0 -95
  244. package/src/modes/theme/defaults/light-poimandres.json +0 -141
  245. package/src/modes/theme/defaults/light-prism.json +0 -90
  246. package/src/modes/theme/defaults/light-retro.json +0 -98
  247. package/src/modes/theme/defaults/light-sand.json +0 -95
  248. package/src/modes/theme/defaults/light-savanna.json +0 -91
  249. package/src/modes/theme/defaults/light-solarized.json +0 -102
  250. package/src/modes/theme/defaults/light-soleil.json +0 -90
  251. package/src/modes/theme/defaults/light-sunset.json +0 -99
  252. package/src/modes/theme/defaults/light-synthwave.json +0 -98
  253. package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
  254. package/src/modes/theme/defaults/light-wetland.json +0 -91
  255. package/src/modes/theme/defaults/light-zenith.json +0 -89
  256. package/src/modes/theme/defaults/limestone.json +0 -94
  257. package/src/modes/theme/defaults/mahogany.json +0 -97
  258. package/src/modes/theme/defaults/marble.json +0 -93
  259. package/src/modes/theme/defaults/obsidian.json +0 -91
  260. package/src/modes/theme/defaults/onyx.json +0 -91
  261. package/src/modes/theme/defaults/pearl.json +0 -93
  262. package/src/modes/theme/defaults/porcelain.json +0 -91
  263. package/src/modes/theme/defaults/quartz.json +0 -96
  264. package/src/modes/theme/defaults/sandstone.json +0 -95
  265. package/src/modes/theme/defaults/titanium.json +0 -90
  266. package/src/modes/theme/light.json +0 -93
package/src/vim/engine.ts CHANGED
@@ -858,7 +858,7 @@ export class VimEngine {
858
858
  }
859
859
  case "r": {
860
860
  const replacement = tokens[nextIndex + 1];
861
- if (!replacement || replacement.value.length !== 1) {
861
+ if (replacement?.value.length !== 1) {
862
862
  throw new VimError("Visual replace requires a literal character", opToken);
863
863
  }
864
864
  const visual = expandVisualOffsets(
@@ -1109,7 +1109,7 @@ export class VimEngine {
1109
1109
  return nextIndex + 1;
1110
1110
  case "r": {
1111
1111
  const replacement = tokens[nextIndex + 1];
1112
- if (!replacement || replacement.value.length !== 1) {
1112
+ if (replacement?.value.length !== 1) {
1113
1113
  throw new VimError("r requires a replacement character", token);
1114
1114
  }
1115
1115
  await this.#applyAtomicChange(["r", replacement.value], () => {
@@ -1746,7 +1746,7 @@ export class VimEngine {
1746
1746
  case "t":
1747
1747
  case "T": {
1748
1748
  const searchToken = tokens[index + 1];
1749
- if (!searchToken || searchToken.value.length !== 1) {
1749
+ if (searchToken?.value.length !== 1) {
1750
1750
  throw new VimError(`${token.value} requires a literal character`, token);
1751
1751
  }
1752
1752
  this.lastCharFind = { char: searchToken.value, mode: token.value as "f" | "F" | "t" | "T" };
@@ -8,6 +8,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
8
8
  import type { AuthStorage } from "@gajae-code/ai";
9
9
  import { prompt } from "@gajae-code/utils";
10
10
  import * as z from "zod/v4";
11
+ import { parseModelString } from "../../config/model-resolver";
11
12
  import type { CustomTool, CustomToolContext, RenderResultOptions } from "../../extensibility/custom-tools/types";
12
13
  import type { Theme } from "../../modes/theme/theme";
13
14
  import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { type: "text" };
@@ -16,7 +17,7 @@ import { discoverAuthStorage } from "../../sdk";
16
17
  import type { ToolSession } from "../../tools";
17
18
  import { formatAge } from "../../tools/render-utils";
18
19
  import { throwIfAborted } from "../../tools/tool-errors";
19
- import { getSearchProvider, getSearchProviderLabel, resolveProviderChain, type SearchProvider } from "./provider";
20
+ import { getSearchProviderLabel, resolveProviderChain, type SearchProvider } from "./provider";
20
21
  import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render";
21
22
  import type { SearchProviderId, SearchResponse } from "./types";
22
23
  import { SearchProviderError } from "./types";
@@ -115,10 +116,21 @@ function formatForLLM(response: SearchResponse): string {
115
116
  return parts.join("\n");
116
117
  }
117
118
 
119
+ /** Best-effort active model provider: prefer the resolved Model, fall back to parsing the model string. */
120
+ function resolveActiveModelProvider(
121
+ modelProvider: string | undefined,
122
+ modelString: string | undefined,
123
+ ): string | undefined {
124
+ if (modelProvider) return modelProvider;
125
+ if (modelString) return parseModelString(modelString)?.provider;
126
+ return undefined;
127
+ }
128
+
118
129
  interface ExecuteSearchOptions {
119
130
  authStorage: AuthStorage;
120
131
  sessionId?: string;
121
132
  signal?: AbortSignal;
133
+ activeModelProvider?: string;
122
134
  }
123
135
 
124
136
  /** Execute web search */
@@ -127,20 +139,11 @@ async function executeSearch(
127
139
  params: SearchQueryParams,
128
140
  options: ExecuteSearchOptions,
129
141
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
130
- const { authStorage, sessionId, signal } = options;
131
- const providers =
132
- params.provider && params.provider !== "auto"
133
- ? await getSearchProvider(params.provider).then(async provider =>
134
- (await provider.isAvailable(authStorage)) ? [provider] : resolveProviderChain(authStorage, "auto"),
135
- )
136
- : await resolveProviderChain(authStorage);
137
- if (providers.length === 0) {
138
- const message = "No web search provider configured.";
139
- return {
140
- content: [{ type: "text" as const, text: `Error: ${message}` }],
141
- details: { response: { provider: "none", sources: [] }, error: message },
142
- };
143
- }
142
+ const { authStorage, sessionId, signal, activeModelProvider } = options;
143
+ // Pass `params.provider` straight through: when omitted (the normal model-facing
144
+ // path) it is `undefined`, so `resolveProviderChain` applies the settings-configured
145
+ // preferred provider. Coalescing to "auto" here would silently bypass that preference.
146
+ const providers = await resolveProviderChain(authStorage, params.provider, activeModelProvider);
144
147
 
145
148
  const failures: Array<{ provider: SearchProvider; error: unknown }> = [];
146
149
  let lastProvider = providers[0];
@@ -207,13 +210,14 @@ async function executeSearch(
207
210
  */
208
211
  export async function runSearchQuery(
209
212
  params: SearchQueryParams,
210
- options: { authStorage?: AuthStorage; sessionId?: string; signal?: AbortSignal } = {},
213
+ options: { authStorage?: AuthStorage; sessionId?: string; signal?: AbortSignal; activeModelProvider?: string } = {},
211
214
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
212
215
  const authStorage = options.authStorage ?? (await discoverAuthStorage());
213
216
  return executeSearch("cli-web-search", params, {
214
217
  authStorage,
215
218
  sessionId: options.sessionId,
216
219
  signal: options.signal,
220
+ activeModelProvider: options.activeModelProvider,
217
221
  });
218
222
  }
219
223
 
@@ -247,7 +251,11 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
247
251
  ): Promise<AgentToolResult<SearchRenderDetails>> {
248
252
  const authStorage = this.#session.authStorage ?? (await discoverAuthStorage());
249
253
  const sessionId = this.#session.getSessionId?.() ?? undefined;
250
- return executeSearch(_toolCallId, params, { authStorage, sessionId, signal });
254
+ const activeModelProvider = resolveActiveModelProvider(
255
+ this.#session.model?.provider,
256
+ this.#session.getActiveModelString?.(),
257
+ );
258
+ return executeSearch(_toolCallId, params, { authStorage, sessionId, signal, activeModelProvider });
251
259
  }
252
260
  }
253
261
 
@@ -267,7 +275,12 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
267
275
  ) {
268
276
  const authStorage = ctx.modelRegistry?.authStorage ?? (await discoverAuthStorage());
269
277
  const sessionId = ctx.sessionManager.getSessionId();
270
- return executeSearch(toolCallId, params, { authStorage, sessionId, signal });
278
+ return executeSearch(toolCallId, params, {
279
+ authStorage,
280
+ sessionId,
281
+ signal,
282
+ activeModelProvider: ctx.model?.provider,
283
+ });
271
284
  },
272
285
 
273
286
  renderCall(args: SearchToolParams, options: RenderResultOptions, theme: Theme) {
@@ -93,6 +93,11 @@ const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
93
93
  label: "SearXNG",
94
94
  load: async () => new (await import("./providers/searxng")).SearXNGProvider(),
95
95
  },
96
+ duckduckgo: {
97
+ id: "duckduckgo",
98
+ label: "DuckDuckGo",
99
+ load: async () => new (await import("./providers/duckduckgo")).DuckDuckGoProvider(),
100
+ },
96
101
  };
97
102
 
98
103
  const instanceCache = new Map<SearchProviderId, SearchProvider>();
@@ -119,6 +124,7 @@ export async function getSearchProvider(id: SearchProviderId): Promise<SearchPro
119
124
  }
120
125
 
121
126
  export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
127
+ "duckduckgo",
122
128
  "tavily",
123
129
  "perplexity",
124
130
  "brave",
@@ -135,6 +141,30 @@ export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
135
141
  "searxng",
136
142
  ];
137
143
 
144
+ /**
145
+ * Map an active model's provider string to its own native web-search provider.
146
+ * Keys are real model provider ids (see packages/ai/src/types.ts KnownProvider);
147
+ * a few aliases (gemini/kimi) and API strings (openai-responses) are tolerated
148
+ * defensively. Providers absent from this map (custom/unknown) fall through to
149
+ * DuckDuckGo.
150
+ */
151
+ const MODEL_PROVIDER_TO_SEARCH: Record<string, SearchProviderId> = {
152
+ openai: "codex",
153
+ "openai-codex": "codex",
154
+ "openai-responses": "codex",
155
+ anthropic: "anthropic",
156
+ google: "gemini",
157
+ "google-gemini-cli": "gemini",
158
+ "google-antigravity": "gemini",
159
+ gemini: "gemini",
160
+ moonshot: "kimi",
161
+ "kimi-code": "kimi",
162
+ kimi: "kimi",
163
+ zai: "zai",
164
+ perplexity: "perplexity",
165
+ synthetic: "synthetic",
166
+ };
167
+
138
168
  /** Preferred provider set via settings (default: auto) */
139
169
  let preferredProvId: SearchProviderId | "auto" = "auto";
140
170
 
@@ -144,30 +174,45 @@ export function setPreferredSearchProvider(provider: SearchProviderId | "auto"):
144
174
  }
145
175
 
146
176
  /**
147
- * Determine which providers are configured and currently available.
148
- * Each candidate is loaded (and its `isAvailable()` called) only as the chain
149
- * is walked, so unconfigured providers never pay the load cost.
177
+ * Resolve the ordered provider chain for a search request.
178
+ *
179
+ * Resolution is active-model-gated, never credential-scanning:
180
+ * 1. An explicitly preferred provider (settings) that is available is primary.
181
+ * 2. Otherwise the active model's own native search is primary, but only when
182
+ * that provider's own credentials are present (its `isAvailable()`).
183
+ * 3. DuckDuckGo (keyless) is always appended as the terminal fallback, so a
184
+ * missing primary — or a primary runtime failure — still returns results
185
+ * with zero configuration. Keyed standalone providers are never
186
+ * auto-selected; they are reachable only via explicit selection (step 1).
150
187
  */
151
188
  export async function resolveProviderChain(
152
189
  authStorage: AuthStorage,
153
190
  preferredProvider: SearchProviderId | "auto" = preferredProvId,
191
+ activeModelProvider?: string,
154
192
  ): Promise<SearchProvider[]> {
155
- const providers: SearchProvider[] = [];
193
+ const chain: SearchProviderId[] = [];
156
194
 
157
195
  if (preferredProvider !== "auto") {
158
196
  const provider = await getSearchProvider(preferredProvider);
159
197
  if (await provider.isAvailable(authStorage)) {
160
- providers.push(provider);
198
+ chain.push(preferredProvider);
161
199
  }
162
- }
163
-
164
- for (const id of SEARCH_PROVIDER_ORDER) {
165
- if (id === preferredProvider) continue;
166
- const provider = await getSearchProvider(id);
167
- if (await provider.isAvailable(authStorage)) {
168
- providers.push(provider);
200
+ } else if (activeModelProvider) {
201
+ const nativeId = MODEL_PROVIDER_TO_SEARCH[activeModelProvider.toLowerCase()];
202
+ if (nativeId) {
203
+ const provider = await getSearchProvider(nativeId);
204
+ if (await provider.isAvailable(authStorage)) {
205
+ chain.push(nativeId);
206
+ }
169
207
  }
170
208
  }
171
209
 
210
+ // DuckDuckGo is the permissionless terminal fallback (deduped).
211
+ if (!chain.includes("duckduckgo")) chain.push("duckduckgo");
212
+
213
+ const providers: SearchProvider[] = [];
214
+ for (const id of chain) {
215
+ providers.push(await getSearchProvider(id));
216
+ }
172
217
  return providers;
173
218
  }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * DuckDuckGo Web Search Provider
3
+ *
4
+ * Keyless, permissionless web search. Scrapes DuckDuckGo's no-JavaScript HTML
5
+ * endpoints and maps anchors/snippets into the unified SearchResponse shape
6
+ * (sources only — DuckDuckGo does not synthesize an answer).
7
+ *
8
+ * This is the zero-config default/fallback backend: it requires no API key and
9
+ * no OAuth, so `isAvailable()` is always true. Because DuckDuckGo applies
10
+ * anti-bot rate limiting (HTTP 202 / 403 / empty responses) from datacenter and
11
+ * VPN IPs, the provider is best-effort: it retries with backoff, rotates the
12
+ * user-agent, and alternates between the `html` and `lite` endpoints. When every
13
+ * attempt fails it throws a {@link SearchProviderError} rather than returning an
14
+ * empty success — it never falls through to keyed providers.
15
+ *
16
+ * Endpoints:
17
+ * https://html.duckduckgo.com/html/ (primary)
18
+ * https://lite.duckduckgo.com/lite/ (fallback markup)
19
+ *
20
+ * The HTML markup is liable to drift; the parser is deliberately small and is
21
+ * pinned by fixture-driven tests (see test/tools/web-search-duckduckgo.test.ts).
22
+ */
23
+
24
+ import type { AuthStorage } from "@gajae-code/ai";
25
+
26
+ import type { SearchResponse, SearchSource } from "../../../web/search/types";
27
+ import { SearchProviderError } from "../../../web/search/types";
28
+ import { clampNumResults } from "../utils";
29
+ import type { SearchParams } from "./base";
30
+ import { SearchProvider } from "./base";
31
+ import { classifyProviderHttpError, withHardTimeout } from "./utils";
32
+
33
+ const HTML_ENDPOINT = "https://html.duckduckgo.com/html/";
34
+ const LITE_ENDPOINT = "https://lite.duckduckgo.com/lite/";
35
+
36
+ const DEFAULT_NUM_RESULTS = 10;
37
+ const MAX_NUM_RESULTS = 20;
38
+
39
+ /** Endpoint order across retry attempts; rotates markup and user-agent. */
40
+ const ATTEMPTS: Array<"html" | "lite"> = ["html", "lite", "html"];
41
+
42
+ /** Backoff (ms) applied between attempts. Index 0 is unused (first attempt). */
43
+ const BACKOFF_MS = [0, 400, 800];
44
+
45
+ /** Realistic desktop user-agents rotated per attempt to dodge naive blocks. */
46
+ const USER_AGENTS = [
47
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
48
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
49
+ "Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0",
50
+ ];
51
+
52
+ /** Map our recency filter to DuckDuckGo's `df` time parameter. */
53
+ const RECENCY_MAP: Record<"day" | "week" | "month" | "year", string> = {
54
+ day: "d",
55
+ week: "w",
56
+ month: "m",
57
+ year: "y",
58
+ };
59
+
60
+ interface ParsedResult {
61
+ title: string;
62
+ url: string;
63
+ snippet?: string;
64
+ }
65
+
66
+ /** Decode a small set of HTML entities without pulling in a DOM library. */
67
+ function decodeEntities(input: string): string {
68
+ return input
69
+ .replace(/&amp;/g, "&")
70
+ .replace(/&lt;/g, "<")
71
+ .replace(/&gt;/g, ">")
72
+ .replace(/&quot;/g, '"')
73
+ .replace(/&#0*39;|&#x0*27;|&apos;/gi, "'")
74
+ .replace(/&#x0*2f;/gi, "/")
75
+ .replace(/&#(\d+);/g, (_, dec: string) => String.fromCodePoint(Number(dec)))
76
+ .replace(/&#x([0-9a-f]+);/gi, (_, hex: string) => String.fromCodePoint(Number.parseInt(hex, 16)))
77
+ .replace(/&nbsp;/g, " ");
78
+ }
79
+
80
+ /** Strip tags, decode entities, and collapse whitespace from an HTML fragment. */
81
+ function cleanText(fragment: string): string {
82
+ return decodeEntities(fragment.replace(/<[^>]+>/g, ""))
83
+ .replace(/\s+/g, " ")
84
+ .trim();
85
+ }
86
+
87
+ /**
88
+ * Resolve a DuckDuckGo result href to the real destination URL. DuckDuckGo wraps
89
+ * external links in a `/l/?uddg=<encoded>` redirect; `lite` sometimes links
90
+ * directly. Returns null for unusable or internal links (so ads/redirect shells
91
+ * are dropped).
92
+ */
93
+ export function decodeResultUrl(href: string): string | null {
94
+ let h = decodeEntities(href.trim());
95
+ if (!h || h.startsWith("#")) return null;
96
+ if (h.startsWith("//")) h = `https:${h}`;
97
+ let parsed: URL;
98
+ try {
99
+ parsed = new URL(h, "https://duckduckgo.com");
100
+ } catch {
101
+ return null;
102
+ }
103
+ const uddg = parsed.searchParams.get("uddg");
104
+ if (uddg) {
105
+ try {
106
+ const target = new URL(uddg);
107
+ if (target.protocol !== "http:" && target.protocol !== "https:") return null;
108
+ if (target.hostname.endsWith("duckduckgo.com")) return null;
109
+ return target.toString();
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+ // No redirect wrapper: accept only real external http(s) links.
115
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
116
+ if (parsed.hostname.endsWith("duckduckgo.com")) return null;
117
+ return parsed.toString();
118
+ }
119
+
120
+ /** Parse results from the `html.duckduckgo.com/html/` markup. */
121
+ export function parseHtmlResults(html: string): ParsedResult[] {
122
+ const titleRe = /<a\b[^>]*class="[^"]*\bresult__a\b[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
123
+ const snippetRe = /<a\b[^>]*class="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
124
+ const snippets: string[] = [];
125
+ for (const m of html.matchAll(snippetRe)) snippets.push(cleanText(m[1]));
126
+
127
+ const results: ParsedResult[] = [];
128
+ let idx = 0;
129
+ for (const m of html.matchAll(titleRe)) {
130
+ const url = decodeResultUrl(m[1]);
131
+ const title = cleanText(m[2]);
132
+ const snippet = snippets[idx];
133
+ idx++;
134
+ if (!url || !title) continue;
135
+ results.push({ title, url, snippet: snippet || undefined });
136
+ }
137
+ return results;
138
+ }
139
+
140
+ /** Parse results from the `lite.duckduckgo.com/lite/` markup. */
141
+ export function parseLiteResults(html: string): ParsedResult[] {
142
+ const linkRe = /<a\b[^>]*class="[^"]*\bresult-link\b[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
143
+ const snippetRe = /<td\b[^>]*class="[^"]*\bresult-snippet\b[^"]*"[^>]*>([\s\S]*?)<\/td>/gi;
144
+ const snippets: string[] = [];
145
+ for (const m of html.matchAll(snippetRe)) snippets.push(cleanText(m[1]));
146
+
147
+ const results: ParsedResult[] = [];
148
+ let idx = 0;
149
+ for (const m of html.matchAll(linkRe)) {
150
+ const url = decodeResultUrl(m[1]);
151
+ const title = cleanText(m[2]);
152
+ const snippet = snippets[idx];
153
+ idx++;
154
+ if (!url || !title) continue;
155
+ results.push({ title, url, snippet: snippet || undefined });
156
+ }
157
+ return results;
158
+ }
159
+
160
+ function delay(ms: number, signal?: AbortSignal): Promise<void> {
161
+ return new Promise((resolve, reject) => {
162
+ if (signal?.aborted) {
163
+ reject(new DOMException("Aborted", "AbortError"));
164
+ return;
165
+ }
166
+ const timer = setTimeout(resolve, ms);
167
+ signal?.addEventListener(
168
+ "abort",
169
+ () => {
170
+ clearTimeout(timer);
171
+ reject(new DOMException("Aborted", "AbortError"));
172
+ },
173
+ { once: true },
174
+ );
175
+ });
176
+ }
177
+
178
+ /** Fetch one endpoint and parse it. Throws on HTTP error, rate-limit, or empty parse. */
179
+ async function fetchAndParse(
180
+ endpoint: "html" | "lite",
181
+ query: string,
182
+ df: string | undefined,
183
+ userAgent: string,
184
+ signal: AbortSignal | undefined,
185
+ ): Promise<ParsedResult[]> {
186
+ const url = endpoint === "html" ? HTML_ENDPOINT : LITE_ENDPOINT;
187
+ const body = new URLSearchParams({ q: query });
188
+ if (df) body.set("df", df);
189
+
190
+ const response = await fetch(url, {
191
+ method: "POST",
192
+ headers: {
193
+ "User-Agent": userAgent,
194
+ Accept: "text/html,application/xhtml+xml",
195
+ "Content-Type": "application/x-www-form-urlencoded",
196
+ "Accept-Language": "en-US,en;q=0.9",
197
+ },
198
+ body,
199
+ signal: withHardTimeout(signal),
200
+ });
201
+
202
+ // DuckDuckGo signals soft blocks with 202 (which is still response.ok).
203
+ if (response.status === 202) {
204
+ throw new SearchProviderError("duckduckgo", "duckduckgo: rate-limited (202)", 202);
205
+ }
206
+ if (!response.ok) {
207
+ const errorText = await response.text();
208
+ const classified = classifyProviderHttpError("duckduckgo", response.status, errorText);
209
+ if (classified) throw classified;
210
+ throw new SearchProviderError("duckduckgo", `DuckDuckGo error (${response.status})`, response.status);
211
+ }
212
+
213
+ const text = await response.text();
214
+ const parsed = endpoint === "html" ? parseHtmlResults(text) : parseLiteResults(text);
215
+ if (parsed.length === 0) {
216
+ throw new SearchProviderError("duckduckgo", "duckduckgo: no parseable results (possible block)");
217
+ }
218
+ return parsed;
219
+ }
220
+
221
+ /** Execute a keyless DuckDuckGo web search with light resilience. */
222
+ export async function searchDuckDuckGo(params: {
223
+ query: string;
224
+ num_results?: number;
225
+ recency?: "day" | "week" | "month" | "year";
226
+ signal?: AbortSignal;
227
+ }): Promise<SearchResponse> {
228
+ const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
229
+ const df = params.recency ? RECENCY_MAP[params.recency] : undefined;
230
+
231
+ let lastError: unknown;
232
+ for (let attempt = 0; attempt < ATTEMPTS.length; attempt++) {
233
+ if (params.signal?.aborted) throw new DOMException("Aborted", "AbortError");
234
+ if (BACKOFF_MS[attempt] > 0) await delay(BACKOFF_MS[attempt], params.signal);
235
+
236
+ const endpoint = ATTEMPTS[attempt];
237
+ const userAgent = USER_AGENTS[attempt % USER_AGENTS.length];
238
+ try {
239
+ const parsed = await fetchAndParse(endpoint, params.query, df, userAgent, params.signal);
240
+ const sources: SearchSource[] = parsed.slice(0, numResults).map(result => ({
241
+ title: result.title,
242
+ url: result.url,
243
+ snippet: result.snippet,
244
+ }));
245
+ return { provider: "duckduckgo", sources };
246
+ } catch (error) {
247
+ // A caller cancellation must abort immediately, never silently retry.
248
+ if (params.signal?.aborted) throw error;
249
+ lastError = error;
250
+ }
251
+ }
252
+
253
+ if (lastError instanceof SearchProviderError) throw lastError;
254
+ throw new SearchProviderError(
255
+ "duckduckgo",
256
+ `DuckDuckGo search failed after ${ATTEMPTS.length} attempts${
257
+ lastError instanceof Error ? `: ${lastError.message}` : ""
258
+ }`,
259
+ );
260
+ }
261
+
262
+ /** Keyless, permissionless web search provider backed by DuckDuckGo. */
263
+ export class DuckDuckGoProvider extends SearchProvider {
264
+ readonly id = "duckduckgo";
265
+ readonly label = "DuckDuckGo";
266
+
267
+ isAvailable(_authStorage: AuthStorage): boolean {
268
+ return true;
269
+ }
270
+
271
+ search(params: SearchParams): Promise<SearchResponse> {
272
+ return searchDuckDuckGo({
273
+ query: params.query,
274
+ num_results: params.numSearchResults ?? params.limit,
275
+ recency: params.recency,
276
+ signal: params.signal,
277
+ });
278
+ }
279
+ }
@@ -6,6 +6,7 @@
6
6
 
7
7
  /** Supported web search providers */
8
8
  export type SearchProviderId =
9
+ | "duckduckgo"
9
10
  | "exa"
10
11
  | "brave"
11
12
  | "jina"
@@ -23,6 +24,7 @@ export type SearchProviderId =
23
24
 
24
25
  export function isSearchProviderId(value: string): value is SearchProviderId {
25
26
  return [
27
+ "duckduckgo",
26
28
  "exa",
27
29
  "brave",
28
30
  "jina",
@@ -1,95 +0,0 @@
1
- {
2
- "$schema": "https://raw.githubusercontent.com/can1357/gajae-code/main/packages/coding-agent/theme-schema.json",
3
- "name": "dark",
4
- "vars": {
5
- "cyan": "#0088fa",
6
- "blue": "#178fb9",
7
- "green": "#89d281",
8
- "red": "#fc3a4b",
9
- "yellow": "#e4c00f",
10
- "gray": "#777d88",
11
- "dimGray": "#5f6673",
12
- "darkGray": "#3d424a",
13
- "accent": "#febc38",
14
- "selectedBg": "#31363f",
15
- "userMsgBg": "#221d1a",
16
- "toolPendingBg": "#1d2129",
17
- "toolSuccessBg": "#161a1f",
18
- "toolErrorBg": "#291d1d",
19
- "customMsgBg": "#2a2530"
20
- },
21
- "colors": {
22
- "accent": "accent",
23
- "border": "blue",
24
- "borderAccent": "cyan",
25
- "borderMuted": "darkGray",
26
- "success": "green",
27
- "error": "red",
28
- "warning": "yellow",
29
- "muted": "gray",
30
- "dim": "dimGray",
31
- "text": "",
32
- "thinkingText": "gray",
33
- "selectedBg": "selectedBg",
34
- "userMessageBg": "userMsgBg",
35
- "userMessageText": "",
36
- "customMessageBg": "customMsgBg",
37
- "customMessageText": "",
38
- "customMessageLabel": "#b281d6",
39
- "toolPendingBg": "toolPendingBg",
40
- "toolSuccessBg": "toolSuccessBg",
41
- "toolErrorBg": "toolErrorBg",
42
- "toolTitle": "",
43
- "toolOutput": "gray",
44
- "mdHeading": "#febc38",
45
- "mdLink": "#0088fa",
46
- "mdLinkUrl": "dimGray",
47
- "mdCode": "#e5c1ff",
48
- "mdCodeBlock": "#9CDCFE",
49
- "mdCodeBlockBorder": "darkGray",
50
- "mdQuote": "gray",
51
- "mdQuoteBorder": "darkGray",
52
- "mdHr": "darkGray",
53
- "mdListBullet": "accent",
54
- "toolDiffAdded": "green",
55
- "toolDiffRemoved": "red",
56
- "toolDiffContext": "gray",
57
- "link": "#0088fa",
58
- "syntaxComment": "#6A9955",
59
- "syntaxKeyword": "#569CD6",
60
- "syntaxFunction": "#DCDCAA",
61
- "syntaxVariable": "#9CDCFE",
62
- "syntaxString": "#CE9178",
63
- "syntaxNumber": "#B5CEA8",
64
- "syntaxType": "#4EC9B0",
65
- "syntaxOperator": "#D4D4D4",
66
- "syntaxPunctuation": "#D4D4D4",
67
- "thinkingOff": "darkGray",
68
- "thinkingMinimal": "dimGray",
69
- "thinkingLow": "#178fb9",
70
- "thinkingMedium": "#0088fa",
71
- "thinkingHigh": "#b281d6",
72
- "thinkingXhigh": "#e5c1ff",
73
- "bashMode": "cyan",
74
- "statusLineBg": "#121212",
75
- "statusLineSep": 244,
76
- "statusLineModel": "#d787af",
77
- "statusLinePath": "#00afaf",
78
- "statusLineGitClean": "#5faf5f",
79
- "statusLineGitDirty": "#d7af5f",
80
- "statusLineContext": "#8787af",
81
- "statusLineSpend": "#5fafaf",
82
- "statusLineStaged": 70,
83
- "statusLineDirty": 178,
84
- "statusLineUntracked": 39,
85
- "statusLineOutput": 205,
86
- "statusLineCost": 205,
87
- "statusLineSubagents": "accent",
88
- "pythonMode": "yellow"
89
- },
90
- "export": {
91
- "pageBg": "#18181e",
92
- "cardBg": "#1e1e24",
93
- "infoBg": "#3c3728"
94
- }
95
- }