@gajae-code/coding-agent 0.2.3 → 0.2.5

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 (197) hide show
  1. package/CHANGELOG.md +34 -8600
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +61 -0
  4. package/dist/types/cli/update-cli.d.ts +3 -0
  5. package/dist/types/config/settings-schema.d.ts +27 -3
  6. package/dist/types/config/settings.d.ts +1 -1
  7. package/dist/types/defaults/gjc-defaults.d.ts +19 -6
  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/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
  11. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  12. package/dist/types/modes/components/settings-selector.d.ts +4 -0
  13. package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
  14. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  15. package/dist/types/modes/interactive-mode.d.ts +2 -0
  16. package/dist/types/modes/theme/defaults/index.d.ts +45 -9351
  17. package/dist/types/modes/theme/theme.d.ts +6 -5
  18. package/dist/types/modes/types.d.ts +2 -0
  19. package/dist/types/sdk.d.ts +2 -0
  20. package/dist/types/session/streaming-output.d.ts +11 -0
  21. package/dist/types/skill-state/active-state.d.ts +1 -0
  22. package/dist/types/task/types.d.ts +1 -0
  23. package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
  24. package/dist/types/tools/bash.d.ts +24 -0
  25. package/dist/types/tools/cron.d.ts +110 -0
  26. package/dist/types/tools/index.d.ts +4 -0
  27. package/dist/types/tools/monitor.d.ts +54 -0
  28. package/dist/types/web/search/index.d.ts +1 -0
  29. package/dist/types/web/search/provider.d.ts +11 -4
  30. package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
  31. package/dist/types/web/search/types.d.ts +1 -1
  32. package/package.json +7 -7
  33. package/src/async/job-manager.ts +224 -0
  34. package/src/cli/agents-cli.ts +3 -0
  35. package/src/cli/update-cli.ts +67 -16
  36. package/src/config/settings-schema.ts +30 -2
  37. package/src/config/settings.ts +44 -7
  38. package/src/defaults/gjc/skills/deep-interview/SKILL.md +48 -6
  39. package/src/defaults/gjc/skills/deep-interview/auto-answer-uncertain.md +37 -0
  40. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +42 -0
  41. package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
  42. package/src/defaults/gjc/skills/ultragoal/SKILL.md +9 -6
  43. package/src/defaults/gjc-defaults.ts +68 -16
  44. package/src/discovery/helpers.ts +5 -0
  45. package/src/eval/js/shared/rewrite-imports.ts +1 -2
  46. package/src/exec/bash-executor.ts +20 -9
  47. package/src/gjc-runtime/deep-interview-runtime.ts +44 -0
  48. package/src/gjc-runtime/ralplan-runtime.ts +2 -0
  49. package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
  50. package/src/gjc-runtime/state-runtime.ts +3 -2
  51. package/src/goals/tools/goal-tool.ts +5 -1
  52. package/src/hooks/skill-state.ts +1 -1
  53. package/src/internal-urls/docs-index.generated.ts +8 -4
  54. package/src/lsp/render.ts +1 -1
  55. package/src/memories/index.ts +5 -4
  56. package/src/modes/acp/acp-agent.ts +1 -1
  57. package/src/modes/acp/acp-client-bridge.ts +1 -1
  58. package/src/modes/components/agent-dashboard.ts +1 -1
  59. package/src/modes/components/diff.ts +2 -2
  60. package/src/modes/components/settings-selector.ts +25 -14
  61. package/src/modes/components/skill-hud/render.ts +7 -2
  62. package/src/modes/controllers/command-controller.ts +1 -1
  63. package/src/modes/controllers/input-controller.ts +10 -2
  64. package/src/modes/controllers/selector-controller.ts +67 -0
  65. package/src/modes/interactive-mode.ts +34 -3
  66. package/src/modes/theme/defaults/blue-crab.json +126 -0
  67. package/src/modes/theme/defaults/index.ts +2 -196
  68. package/src/modes/theme/theme.ts +75 -36
  69. package/src/modes/types.ts +2 -0
  70. package/src/prompts/agents/architect.md +5 -1
  71. package/src/prompts/agents/critic.md +5 -1
  72. package/src/prompts/agents/frontmatter.md +1 -0
  73. package/src/prompts/agents/planner.md +5 -1
  74. package/src/prompts/memories/unavailable.md +9 -0
  75. package/src/prompts/tools/bash.md +9 -0
  76. package/src/prompts/tools/cron.md +25 -0
  77. package/src/prompts/tools/monitor.md +30 -0
  78. package/src/runtime-mcp/oauth-flow.ts +4 -2
  79. package/src/sdk.ts +7 -0
  80. package/src/session/agent-session.ts +16 -5
  81. package/src/session/streaming-output.ts +21 -0
  82. package/src/skill-state/active-state.ts +163 -12
  83. package/src/slash-commands/builtin-registry.ts +11 -1
  84. package/src/task/agents.ts +1 -0
  85. package/src/task/executor.ts +1 -0
  86. package/src/task/types.ts +1 -0
  87. package/src/tools/bash-allowed-prefixes.ts +169 -0
  88. package/src/tools/bash.ts +190 -29
  89. package/src/tools/browser/tab-worker.ts +1 -1
  90. package/src/tools/cron.ts +665 -0
  91. package/src/tools/index.ts +20 -2
  92. package/src/tools/monitor.ts +136 -0
  93. package/src/vim/engine.ts +3 -3
  94. package/src/web/search/index.ts +31 -18
  95. package/src/web/search/provider.ts +57 -12
  96. package/src/web/search/providers/duckduckgo.ts +279 -0
  97. package/src/web/search/types.ts +2 -0
  98. package/src/modes/theme/dark.json +0 -95
  99. package/src/modes/theme/defaults/alabaster.json +0 -93
  100. package/src/modes/theme/defaults/amethyst.json +0 -96
  101. package/src/modes/theme/defaults/anthracite.json +0 -93
  102. package/src/modes/theme/defaults/basalt.json +0 -91
  103. package/src/modes/theme/defaults/birch.json +0 -95
  104. package/src/modes/theme/defaults/dark-abyss.json +0 -91
  105. package/src/modes/theme/defaults/dark-arctic.json +0 -104
  106. package/src/modes/theme/defaults/dark-aurora.json +0 -95
  107. package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
  108. package/src/modes/theme/defaults/dark-cavern.json +0 -91
  109. package/src/modes/theme/defaults/dark-copper.json +0 -95
  110. package/src/modes/theme/defaults/dark-cosmos.json +0 -90
  111. package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
  112. package/src/modes/theme/defaults/dark-dracula.json +0 -98
  113. package/src/modes/theme/defaults/dark-eclipse.json +0 -91
  114. package/src/modes/theme/defaults/dark-ember.json +0 -95
  115. package/src/modes/theme/defaults/dark-equinox.json +0 -90
  116. package/src/modes/theme/defaults/dark-forest.json +0 -96
  117. package/src/modes/theme/defaults/dark-github.json +0 -105
  118. package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
  119. package/src/modes/theme/defaults/dark-lavender.json +0 -95
  120. package/src/modes/theme/defaults/dark-lunar.json +0 -89
  121. package/src/modes/theme/defaults/dark-midnight.json +0 -95
  122. package/src/modes/theme/defaults/dark-monochrome.json +0 -94
  123. package/src/modes/theme/defaults/dark-monokai.json +0 -98
  124. package/src/modes/theme/defaults/dark-nebula.json +0 -90
  125. package/src/modes/theme/defaults/dark-nord.json +0 -97
  126. package/src/modes/theme/defaults/dark-ocean.json +0 -101
  127. package/src/modes/theme/defaults/dark-one.json +0 -100
  128. package/src/modes/theme/defaults/dark-poimandres.json +0 -141
  129. package/src/modes/theme/defaults/dark-rainforest.json +0 -91
  130. package/src/modes/theme/defaults/dark-reef.json +0 -91
  131. package/src/modes/theme/defaults/dark-retro.json +0 -92
  132. package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
  133. package/src/modes/theme/defaults/dark-sakura.json +0 -95
  134. package/src/modes/theme/defaults/dark-slate.json +0 -95
  135. package/src/modes/theme/defaults/dark-solarized.json +0 -97
  136. package/src/modes/theme/defaults/dark-solstice.json +0 -90
  137. package/src/modes/theme/defaults/dark-starfall.json +0 -91
  138. package/src/modes/theme/defaults/dark-sunset.json +0 -99
  139. package/src/modes/theme/defaults/dark-swamp.json +0 -90
  140. package/src/modes/theme/defaults/dark-synthwave.json +0 -103
  141. package/src/modes/theme/defaults/dark-taiga.json +0 -91
  142. package/src/modes/theme/defaults/dark-terminal.json +0 -95
  143. package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
  144. package/src/modes/theme/defaults/dark-tundra.json +0 -91
  145. package/src/modes/theme/defaults/dark-twilight.json +0 -91
  146. package/src/modes/theme/defaults/dark-volcanic.json +0 -91
  147. package/src/modes/theme/defaults/graphite.json +0 -92
  148. package/src/modes/theme/defaults/light-arctic.json +0 -107
  149. package/src/modes/theme/defaults/light-aurora-day.json +0 -91
  150. package/src/modes/theme/defaults/light-canyon.json +0 -91
  151. package/src/modes/theme/defaults/light-catppuccin.json +0 -106
  152. package/src/modes/theme/defaults/light-cirrus.json +0 -90
  153. package/src/modes/theme/defaults/light-coral.json +0 -95
  154. package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
  155. package/src/modes/theme/defaults/light-dawn.json +0 -90
  156. package/src/modes/theme/defaults/light-dunes.json +0 -91
  157. package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
  158. package/src/modes/theme/defaults/light-forest.json +0 -100
  159. package/src/modes/theme/defaults/light-frost.json +0 -95
  160. package/src/modes/theme/defaults/light-github.json +0 -115
  161. package/src/modes/theme/defaults/light-glacier.json +0 -91
  162. package/src/modes/theme/defaults/light-gruvbox.json +0 -108
  163. package/src/modes/theme/defaults/light-haze.json +0 -90
  164. package/src/modes/theme/defaults/light-honeycomb.json +0 -95
  165. package/src/modes/theme/defaults/light-lagoon.json +0 -91
  166. package/src/modes/theme/defaults/light-lavender.json +0 -95
  167. package/src/modes/theme/defaults/light-meadow.json +0 -91
  168. package/src/modes/theme/defaults/light-mint.json +0 -95
  169. package/src/modes/theme/defaults/light-monochrome.json +0 -101
  170. package/src/modes/theme/defaults/light-ocean.json +0 -99
  171. package/src/modes/theme/defaults/light-one.json +0 -99
  172. package/src/modes/theme/defaults/light-opal.json +0 -91
  173. package/src/modes/theme/defaults/light-orchard.json +0 -91
  174. package/src/modes/theme/defaults/light-paper.json +0 -95
  175. package/src/modes/theme/defaults/light-poimandres.json +0 -141
  176. package/src/modes/theme/defaults/light-prism.json +0 -90
  177. package/src/modes/theme/defaults/light-retro.json +0 -98
  178. package/src/modes/theme/defaults/light-sand.json +0 -95
  179. package/src/modes/theme/defaults/light-savanna.json +0 -91
  180. package/src/modes/theme/defaults/light-solarized.json +0 -102
  181. package/src/modes/theme/defaults/light-soleil.json +0 -90
  182. package/src/modes/theme/defaults/light-sunset.json +0 -99
  183. package/src/modes/theme/defaults/light-synthwave.json +0 -98
  184. package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
  185. package/src/modes/theme/defaults/light-wetland.json +0 -91
  186. package/src/modes/theme/defaults/light-zenith.json +0 -89
  187. package/src/modes/theme/defaults/limestone.json +0 -94
  188. package/src/modes/theme/defaults/mahogany.json +0 -97
  189. package/src/modes/theme/defaults/marble.json +0 -93
  190. package/src/modes/theme/defaults/obsidian.json +0 -91
  191. package/src/modes/theme/defaults/onyx.json +0 -91
  192. package/src/modes/theme/defaults/pearl.json +0 -93
  193. package/src/modes/theme/defaults/porcelain.json +0 -91
  194. package/src/modes/theme/defaults/quartz.json +0 -96
  195. package/src/modes/theme/defaults/sandstone.json +0 -95
  196. package/src/modes/theme/defaults/titanium.json +0 -90
  197. package/src/modes/theme/light.json +0 -93
@@ -31,6 +31,7 @@ import { BashTool } from "./bash";
31
31
  import { BrowserTool } from "./browser";
32
32
  import { CalculatorTool } from "./calculator";
33
33
  import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
34
+ import { CronCreateTool, CronDeleteTool, CronListTool } from "./cron";
34
35
  import { DebugTool } from "./debug";
35
36
  import { EvalTool } from "./eval";
36
37
  import { FindTool } from "./find";
@@ -41,6 +42,7 @@ import { HindsightRetainTool } from "./hindsight-retain";
41
42
  import { InspectImageTool } from "./inspect-image";
42
43
  import { IrcTool } from "./irc";
43
44
  import { JobTool } from "./job";
45
+ import { MonitorTool } from "./monitor";
44
46
  import { wrapToolWithMetaNotice } from "./output-meta";
45
47
  import { ReadTool } from "./read";
46
48
  import { RecipeTool } from "./recipe";
@@ -69,6 +71,7 @@ export * from "./bash";
69
71
  export * from "./browser";
70
72
  export * from "./calculator";
71
73
  export * from "./checkpoint";
74
+ export * from "./cron";
72
75
  export * from "./debug";
73
76
  export * from "./eval";
74
77
  export * from "./find";
@@ -80,6 +83,7 @@ export * from "./image-gen";
80
83
  export * from "./inspect-image";
81
84
  export * from "./irc";
82
85
  export * from "./job";
86
+ export * from "./monitor";
83
87
  export * from "./read";
84
88
  export * from "./recipe";
85
89
  export * from "./render-mermaid";
@@ -164,6 +168,8 @@ export interface ToolSession {
164
168
  getToolByName?: (name: string) => AgentTool | undefined;
165
169
  /** Agent registry for IRC routing across live sessions. */
166
170
  agentRegistry?: AgentRegistry;
171
+ /** Optional restricted bash command prefixes for read-only role agents. */
172
+ bashAllowedPrefixes?: string[];
167
173
  /** Get artifacts directory for artifact:// URLs */
168
174
  getArtifactsDir?: () => string | null;
169
175
  /** Get the ArtifactManager backing this session (shared across parent + subagents). */
@@ -319,6 +325,10 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
319
325
  task: s => TaskTool.create(s),
320
326
  subagent: s => new SubagentTool(s),
321
327
  job: JobTool.createIf,
328
+ monitor: MonitorTool.createIf,
329
+ CronCreate: CronCreateTool.createIf,
330
+ CronList: CronListTool.createIf,
331
+ CronDelete: CronDeleteTool.createIf,
322
332
  recipe: RecipeTool.createIf,
323
333
  irc: IrcTool.createIf,
324
334
  todo_write: s => new TodoWriteTool(s),
@@ -466,6 +476,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
466
476
  const discoveryActive = effectiveDiscoveryMode !== "off";
467
477
 
468
478
  const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
479
+ const allToolFactoryEntries = Object.entries(allTools) as Array<[string, ToolFactory]>;
480
+ const allToolsByRequestName = new Map<string, [string, ToolFactory]>();
481
+ for (const [name, factory] of allToolFactoryEntries) {
482
+ allToolsByRequestName.set(name.toLowerCase(), [name, factory]);
483
+ }
469
484
  const isToolAllowed = (name: string) => {
470
485
  if (name === "goal") return goalEnabled && session.getGoalRuntime !== undefined;
471
486
  if (goalStateToolNames.includes(name as (typeof GOAL_MODE_TOOL_NAMES)[number])) return goalEnabled;
@@ -509,10 +524,13 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
509
524
  requestedTools.push("yield");
510
525
  }
511
526
 
512
- const filteredRequestedTools = requestedTools?.filter(name => name in allTools && isToolAllowed(name));
527
+ const filteredRequestedTools = requestedTools
528
+ ?.map(name => allToolsByRequestName.get(name))
529
+ .filter((entry): entry is [string, ToolFactory] => entry !== undefined)
530
+ .filter(([name]) => isToolAllowed(name));
513
531
  const baseEntries =
514
532
  filteredRequestedTools !== undefined
515
- ? filteredRequestedTools.filter(name => name !== "resolve").map(name => [name, allTools[name]] as const)
533
+ ? filteredRequestedTools.filter(([name]) => name !== "resolve")
516
534
  : [
517
535
  ...Object.entries(BUILTIN_TOOLS)
518
536
  .filter(([name]) => isToolAllowed(name))
@@ -0,0 +1,136 @@
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
2
+ import { logger, prompt } from "@gajae-code/utils";
3
+ import * as z from "zod/v4";
4
+ import { AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
5
+ import monitorDescription from "../prompts/tools/monitor.md" with { type: "text" };
6
+ import { BashTool } from "./bash";
7
+ import type { ToolSession } from "./index";
8
+ import { ToolError } from "./tool-errors";
9
+
10
+ const monitorKindEnum = z.enum(["log", "poll", "watch", "other"]);
11
+
12
+ const monitorSchema = z.object({
13
+ command: z
14
+ .string()
15
+ .describe(
16
+ "Shell command to run as a background monitor. Each stdout line is delivered as a separate task-notification event.",
17
+ ),
18
+ kind: monitorKindEnum.describe(
19
+ "Category of monitor. 'log' tails a log file, 'poll' polls a status endpoint, 'watch' watches a directory, 'other' for arbitrary streams.",
20
+ ),
21
+ description: z
22
+ .string()
23
+ .describe("Short human-readable description of what is being monitored. Appears in task listings."),
24
+ timeout: z
25
+ .number()
26
+ .min(1)
27
+ .optional()
28
+ .describe(
29
+ "Optional maximum wall-clock seconds the monitor may run before automatic shutdown. Omit for indefinite (subject to session lifetime).",
30
+ ),
31
+ persistent: z
32
+ .boolean()
33
+ .optional()
34
+ .describe(
35
+ "Whether to keep the monitor running past the originating turn. Persistent monitors survive until session end or explicit kill via the background-task stop tool.",
36
+ ),
37
+ });
38
+
39
+ export type MonitorParams = z.infer<typeof monitorSchema>;
40
+
41
+ export interface MonitorToolDetails {
42
+ taskId: string;
43
+ kind: z.infer<typeof monitorKindEnum>;
44
+ description: string;
45
+ command: string;
46
+ persistent: boolean;
47
+ }
48
+
49
+ const MONITOR_LABEL_MAX = 120;
50
+
51
+ function buildMonitorLabel(params: MonitorParams): string {
52
+ const base = `[monitor:${params.kind}] ${params.description}`;
53
+ if (base.length <= MONITOR_LABEL_MAX) return base;
54
+ return `${base.slice(0, MONITOR_LABEL_MAX - 3)}...`;
55
+ }
56
+
57
+ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolDetails> {
58
+ readonly name = "monitor";
59
+ readonly label = "Monitor";
60
+ readonly summary = "Start a background monitor that streams stdout lines as task notifications";
61
+ readonly description: string;
62
+ readonly parameters = monitorSchema;
63
+ readonly strict = true;
64
+ readonly loadMode = "discoverable";
65
+
66
+ constructor(private readonly session: ToolSession) {
67
+ this.description = prompt.render(monitorDescription);
68
+ }
69
+
70
+ static createIf(session: ToolSession): MonitorTool | null {
71
+ if (!isBackgroundJobSupportEnabled(session.settings)) return null;
72
+ return new MonitorTool(session);
73
+ }
74
+
75
+ async execute(
76
+ _toolCallId: string,
77
+ params: MonitorParams,
78
+ _signal?: AbortSignal,
79
+ _onUpdate?: AgentToolUpdateCallback<MonitorToolDetails>,
80
+ context?: AgentToolContext,
81
+ ): Promise<AgentToolResult<MonitorToolDetails>> {
82
+ const manager = AsyncJobManager.instance();
83
+ if (!manager) {
84
+ throw new ToolError("Async execution is disabled; the monitor tool is unavailable in this session.");
85
+ }
86
+
87
+ const persistent = params.persistent ?? false;
88
+ const label = buildMonitorLabel(params);
89
+ const ownerId = this.session.getAgentId?.() ?? undefined;
90
+ const bash = new BashTool(this.session);
91
+ let deliveredFirstLine = false;
92
+ const monitorJob = await bash.startMonitorJob(
93
+ { command: params.command, timeout: params.timeout },
94
+ {
95
+ ownerId,
96
+ label,
97
+ ctx: context,
98
+ onRawLine: (line, jobId) => {
99
+ if (!persistent && deliveredFirstLine) return;
100
+ deliveredFirstLine = true;
101
+ const content = `<task-notification>\nMonitor task ${jobId} (${params.kind}: ${params.description}) emitted:\n${line}\n</task-notification>`;
102
+ const details = { taskId: jobId, kind: params.kind, description: params.description };
103
+ const sendPromise = this.session.sendCustomMessage?.(
104
+ { customType: "task-notification", content, display: false, attribution: "agent", details },
105
+ { triggerTurn: true, deliverAs: "followUp" },
106
+ );
107
+ if (sendPromise) {
108
+ void sendPromise.catch(error => {
109
+ logger.warn("Monitor task-notification delivery failed", {
110
+ error: error instanceof Error ? error.message : String(error),
111
+ });
112
+ });
113
+ } else {
114
+ this.session.steer?.({ customType: "task-notification", content, details });
115
+ }
116
+ if (!persistent) {
117
+ manager.cancel(jobId, ownerId ? { ownerId } : undefined);
118
+ }
119
+ },
120
+ },
121
+ );
122
+
123
+ const startedText = `Monitor started · task ${monitorJob.jobId} · persistent: ${persistent}`;
124
+
125
+ return {
126
+ content: [{ type: "text", text: startedText }],
127
+ details: {
128
+ taskId: monitorJob.jobId,
129
+ kind: params.kind,
130
+ description: params.description,
131
+ command: params.command,
132
+ persistent,
133
+ },
134
+ };
135
+ }
136
+ }
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
  }