@gajae-code/coding-agent 0.2.4 → 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.
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +61 -0
- package/dist/types/config/settings-schema.d.ts +7 -3
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +8 -1
- package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
- package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/defaults/index.d.ts +45 -9477
- package/dist/types/modes/theme/theme.d.ts +1 -5
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/skill-state/active-state.d.ts +1 -0
- package/dist/types/task/types.d.ts +1 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +24 -0
- package/dist/types/tools/cron.d.ts +110 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/monitor.d.ts +54 -0
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/provider.d.ts +11 -4
- package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +224 -0
- package/src/cli/agents-cli.ts +3 -0
- package/src/config/settings-schema.ts +8 -2
- package/src/config/settings.ts +44 -7
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +9 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/discovery/helpers.ts +5 -0
- package/src/eval/js/shared/rewrite-imports.ts +1 -2
- package/src/exec/bash-executor.ts +20 -9
- package/src/gjc-runtime/ralplan-runtime.ts +2 -0
- package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
- package/src/hooks/skill-state.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +5 -3
- package/src/lsp/render.ts +1 -1
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/acp/acp-client-bridge.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/skill-hud/render.ts +7 -2
- package/src/modes/controllers/input-controller.ts +10 -2
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +20 -2
- package/src/modes/theme/defaults/index.ts +0 -196
- package/src/modes/theme/theme.ts +35 -35
- package/src/modes/types.ts +1 -0
- package/src/prompts/agents/architect.md +5 -1
- package/src/prompts/agents/critic.md +5 -1
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/agents/planner.md +5 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/cron.md +25 -0
- package/src/prompts/tools/monitor.md +30 -0
- package/src/runtime-mcp/oauth-flow.ts +4 -2
- package/src/sdk.ts +3 -0
- package/src/session/agent-session.ts +16 -5
- package/src/session/streaming-output.ts +21 -0
- package/src/skill-state/active-state.ts +163 -12
- package/src/task/agents.ts +1 -0
- package/src/task/executor.ts +1 -0
- package/src/task/types.ts +1 -0
- package/src/tools/bash-allowed-prefixes.ts +169 -0
- package/src/tools/bash.ts +190 -29
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/cron.ts +665 -0
- package/src/tools/index.ts +20 -2
- package/src/tools/monitor.ts +136 -0
- package/src/vim/engine.ts +3 -3
- package/src/web/search/index.ts +31 -18
- package/src/web/search/provider.ts +57 -12
- package/src/web/search/providers/duckduckgo.ts +279 -0
- package/src/web/search/types.ts +2 -0
- package/src/modes/theme/dark.json +0 -95
- package/src/modes/theme/defaults/alabaster.json +0 -93
- package/src/modes/theme/defaults/amethyst.json +0 -96
- package/src/modes/theme/defaults/anthracite.json +0 -93
- package/src/modes/theme/defaults/basalt.json +0 -91
- package/src/modes/theme/defaults/birch.json +0 -95
- package/src/modes/theme/defaults/dark-abyss.json +0 -91
- package/src/modes/theme/defaults/dark-arctic.json +0 -104
- package/src/modes/theme/defaults/dark-aurora.json +0 -95
- package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
- package/src/modes/theme/defaults/dark-cavern.json +0 -91
- package/src/modes/theme/defaults/dark-copper.json +0 -95
- package/src/modes/theme/defaults/dark-cosmos.json +0 -90
- package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
- package/src/modes/theme/defaults/dark-dracula.json +0 -98
- package/src/modes/theme/defaults/dark-eclipse.json +0 -91
- package/src/modes/theme/defaults/dark-ember.json +0 -95
- package/src/modes/theme/defaults/dark-equinox.json +0 -90
- package/src/modes/theme/defaults/dark-forest.json +0 -96
- package/src/modes/theme/defaults/dark-github.json +0 -105
- package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
- package/src/modes/theme/defaults/dark-lavender.json +0 -95
- package/src/modes/theme/defaults/dark-lunar.json +0 -89
- package/src/modes/theme/defaults/dark-midnight.json +0 -95
- package/src/modes/theme/defaults/dark-monochrome.json +0 -94
- package/src/modes/theme/defaults/dark-monokai.json +0 -98
- package/src/modes/theme/defaults/dark-nebula.json +0 -90
- package/src/modes/theme/defaults/dark-nord.json +0 -97
- package/src/modes/theme/defaults/dark-ocean.json +0 -101
- package/src/modes/theme/defaults/dark-one.json +0 -100
- package/src/modes/theme/defaults/dark-poimandres.json +0 -141
- package/src/modes/theme/defaults/dark-rainforest.json +0 -91
- package/src/modes/theme/defaults/dark-reef.json +0 -91
- package/src/modes/theme/defaults/dark-retro.json +0 -92
- package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
- package/src/modes/theme/defaults/dark-sakura.json +0 -95
- package/src/modes/theme/defaults/dark-slate.json +0 -95
- package/src/modes/theme/defaults/dark-solarized.json +0 -97
- package/src/modes/theme/defaults/dark-solstice.json +0 -90
- package/src/modes/theme/defaults/dark-starfall.json +0 -91
- package/src/modes/theme/defaults/dark-sunset.json +0 -99
- package/src/modes/theme/defaults/dark-swamp.json +0 -90
- package/src/modes/theme/defaults/dark-synthwave.json +0 -103
- package/src/modes/theme/defaults/dark-taiga.json +0 -91
- package/src/modes/theme/defaults/dark-terminal.json +0 -95
- package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
- package/src/modes/theme/defaults/dark-tundra.json +0 -91
- package/src/modes/theme/defaults/dark-twilight.json +0 -91
- package/src/modes/theme/defaults/dark-volcanic.json +0 -91
- package/src/modes/theme/defaults/graphite.json +0 -92
- package/src/modes/theme/defaults/light-arctic.json +0 -107
- package/src/modes/theme/defaults/light-aurora-day.json +0 -91
- package/src/modes/theme/defaults/light-canyon.json +0 -91
- package/src/modes/theme/defaults/light-catppuccin.json +0 -106
- package/src/modes/theme/defaults/light-cirrus.json +0 -90
- package/src/modes/theme/defaults/light-coral.json +0 -95
- package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
- package/src/modes/theme/defaults/light-dawn.json +0 -90
- package/src/modes/theme/defaults/light-dunes.json +0 -91
- package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
- package/src/modes/theme/defaults/light-forest.json +0 -100
- package/src/modes/theme/defaults/light-frost.json +0 -95
- package/src/modes/theme/defaults/light-github.json +0 -115
- package/src/modes/theme/defaults/light-glacier.json +0 -91
- package/src/modes/theme/defaults/light-gruvbox.json +0 -108
- package/src/modes/theme/defaults/light-haze.json +0 -90
- package/src/modes/theme/defaults/light-honeycomb.json +0 -95
- package/src/modes/theme/defaults/light-lagoon.json +0 -91
- package/src/modes/theme/defaults/light-lavender.json +0 -95
- package/src/modes/theme/defaults/light-meadow.json +0 -91
- package/src/modes/theme/defaults/light-mint.json +0 -95
- package/src/modes/theme/defaults/light-monochrome.json +0 -101
- package/src/modes/theme/defaults/light-ocean.json +0 -99
- package/src/modes/theme/defaults/light-one.json +0 -99
- package/src/modes/theme/defaults/light-opal.json +0 -91
- package/src/modes/theme/defaults/light-orchard.json +0 -91
- package/src/modes/theme/defaults/light-paper.json +0 -95
- package/src/modes/theme/defaults/light-poimandres.json +0 -141
- package/src/modes/theme/defaults/light-prism.json +0 -90
- package/src/modes/theme/defaults/light-retro.json +0 -98
- package/src/modes/theme/defaults/light-sand.json +0 -95
- package/src/modes/theme/defaults/light-savanna.json +0 -91
- package/src/modes/theme/defaults/light-solarized.json +0 -102
- package/src/modes/theme/defaults/light-soleil.json +0 -90
- package/src/modes/theme/defaults/light-sunset.json +0 -99
- package/src/modes/theme/defaults/light-synthwave.json +0 -98
- package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
- package/src/modes/theme/defaults/light-wetland.json +0 -91
- package/src/modes/theme/defaults/light-zenith.json +0 -89
- package/src/modes/theme/defaults/limestone.json +0 -94
- package/src/modes/theme/defaults/mahogany.json +0 -97
- package/src/modes/theme/defaults/marble.json +0 -93
- package/src/modes/theme/defaults/obsidian.json +0 -91
- package/src/modes/theme/defaults/onyx.json +0 -91
- package/src/modes/theme/defaults/pearl.json +0 -93
- package/src/modes/theme/defaults/porcelain.json +0 -91
- package/src/modes/theme/defaults/quartz.json +0 -96
- package/src/modes/theme/defaults/sandstone.json +0 -95
- package/src/modes/theme/defaults/titanium.json +0 -90
- package/src/modes/theme/light.json +0 -93
package/src/tools/index.ts
CHANGED
|
@@ -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
|
|
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")
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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" };
|
package/src/web/search/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* is
|
|
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
|
|
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
|
-
|
|
198
|
+
chain.push(preferredProvider);
|
|
161
199
|
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
}
|