march-cli 0.1.8 → 0.1.10
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/package.json +1 -1
- package/src/agent/editing/lsp-report.mjs +69 -0
- package/src/agent/file-edit-tool.mjs +10 -24
- package/src/agent/model-payload-dumper.mjs +11 -4
- package/src/agent/runner/runner-utils.mjs +18 -0
- package/src/agent/runner.mjs +29 -28
- package/src/agent/runtime/runner-runtime-host.mjs +2 -0
- package/src/agent/turn/turn-logging.mjs +30 -0
- package/src/agent/turn/turn-runner.mjs +40 -0
- package/src/cli/commands/status-command.mjs +45 -0
- package/src/cli/permissions.mjs +1 -1
- package/src/cli/startup/runtime-close.mjs +23 -0
- package/src/cli/status-line-updater.mjs +1 -0
- package/src/cli/tui/tool-rendering.mjs +1 -1
- package/src/config/loader.mjs +28 -1
- package/src/debug/logger.mjs +141 -0
- package/src/lsp/client.mjs +2 -2
- package/src/lsp/diagnostic-store.mjs +5 -2
- package/src/{context/diagnostics.mjs → lsp/diagnostics-format.mjs} +6 -4
- package/src/lsp/managed-node-server.mjs +94 -0
- package/src/lsp/path-match.mjs +10 -0
- package/src/lsp/servers.mjs +97 -21
- package/src/lsp/service.mjs +57 -12
- package/src/lsp/status-message.mjs +9 -0
- package/src/lsp/typescript-project-resolver.mjs +186 -0
- package/src/main.mjs +17 -24
- package/src/platform/spawn-command.mjs +27 -0
- package/src/provider/hosted-tools.mjs +111 -0
- package/src/web/tools.mjs +2 -2
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const CONFIG_RE = /^(?:tsconfig(?:\..+)?|jsconfig)\.json$/;
|
|
5
|
+
const DEFAULT_EXCLUDES = ["node_modules", "bower_components", "jspm_packages"];
|
|
6
|
+
const MATCH_EXTENSIONS = [".ts", ".tsx", ".d.ts", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
|
|
7
|
+
|
|
8
|
+
export function resolveTypeScriptProjectRoot({ filePath, workspaceRoot }) {
|
|
9
|
+
const configs = findTypeScriptConfigs(dirname(filePath), workspaceRoot);
|
|
10
|
+
if (configs.length === 0) return null;
|
|
11
|
+
|
|
12
|
+
const included = configs.find((config) => configIncludesFile(config, filePath));
|
|
13
|
+
return dirname(included ?? configs[0]);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function findTypeScriptConfigs(start, stop) {
|
|
17
|
+
const configs = [];
|
|
18
|
+
let dir = resolve(start);
|
|
19
|
+
const boundary = resolve(stop);
|
|
20
|
+
for (;;) {
|
|
21
|
+
configs.push(...configFilesIn(dir));
|
|
22
|
+
if (dir === boundary || dirname(dir) === dir) return configs;
|
|
23
|
+
dir = dirname(dir);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function configFilesIn(dir) {
|
|
28
|
+
if (!existsSync(dir)) return [];
|
|
29
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
30
|
+
.filter((entry) => entry.isFile() && CONFIG_RE.test(entry.name))
|
|
31
|
+
.map((entry) => entry.name)
|
|
32
|
+
.sort(configSort)
|
|
33
|
+
.map((name) => join(dir, name));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function configSort(a, b) {
|
|
37
|
+
return configRank(a) - configRank(b) || a.localeCompare(b);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function configRank(name) {
|
|
41
|
+
if (name === "tsconfig.json") return 0;
|
|
42
|
+
if (name === "jsconfig.json") return 1;
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function configIncludesFile(configPath, filePath) {
|
|
47
|
+
const config = readConfigChain(configPath);
|
|
48
|
+
if (!config) return false;
|
|
49
|
+
|
|
50
|
+
const exclude = config.exclude ?? DEFAULT_EXCLUDES.map((pattern) => ({ base: dirname(configPath), pattern }));
|
|
51
|
+
if (exclude.some(({ base, pattern }) => matchesPatternFromBase(filePath, base, pattern))) return false;
|
|
52
|
+
|
|
53
|
+
if (config.files) {
|
|
54
|
+
return config.files.some(({ base, pattern }) => normalizePath(relative(base, filePath)) === normalizePath(pattern));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (config.include) {
|
|
58
|
+
return config.include.some(({ base, pattern }) => matchesPatternFromBase(filePath, base, pattern));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const file = normalizePath(relative(dirname(configPath), filePath));
|
|
62
|
+
return !isOutside(file) && MATCH_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readConfigChain(path, seen = new Set()) {
|
|
66
|
+
if (seen.has(path)) return null;
|
|
67
|
+
seen.add(path);
|
|
68
|
+
|
|
69
|
+
const raw = readConfig(path);
|
|
70
|
+
if (!raw) return null;
|
|
71
|
+
const base = resolveExtends(path, raw.extends, seen);
|
|
72
|
+
return {
|
|
73
|
+
files: patternList(raw.files, dirname(path)) ?? base?.files ?? null,
|
|
74
|
+
include: patternList(raw.include, dirname(path)) ?? base?.include ?? null,
|
|
75
|
+
exclude: patternList(raw.exclude, dirname(path)) ?? base?.exclude ?? null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveExtends(configPath, value, seen) {
|
|
80
|
+
if (typeof value !== "string" || !value.startsWith(".")) return null;
|
|
81
|
+
const resolved = resolve(dirname(configPath), value.endsWith(".json") ? value : `${value}.json`);
|
|
82
|
+
return existsSync(resolved) ? readConfigChain(resolved, seen) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readConfig(path) {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(stripJsonComments(readFileSync(path, "utf8")));
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function patternList(value, base) {
|
|
94
|
+
const items = arrayOfStrings(value);
|
|
95
|
+
return items ? items.map((pattern) => ({ base, pattern })) : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function arrayOfStrings(value) {
|
|
99
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isOutside(path) {
|
|
103
|
+
return path === ".." || path.startsWith("../") || path.includes(":");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function matchesPatternFromBase(filePath, base, pattern) {
|
|
107
|
+
const file = normalizePath(relative(base, filePath));
|
|
108
|
+
return !isOutside(file) && matchesConfigPattern(file, pattern);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function matchesConfigPattern(file, pattern) {
|
|
112
|
+
const normalized = normalizePath(pattern);
|
|
113
|
+
if (!normalized.includes("*") && file === normalized) return true;
|
|
114
|
+
const glob = normalized.includes("*") ? normalized : `${trimSlash(normalized)}/**/*`;
|
|
115
|
+
return globToRegExp(glob).test(file);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function globToRegExp(glob) {
|
|
119
|
+
let source = "^";
|
|
120
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
121
|
+
const char = glob[i];
|
|
122
|
+
if (char === "*") {
|
|
123
|
+
if (glob[i + 1] === "*") {
|
|
124
|
+
i += 1;
|
|
125
|
+
if (glob[i + 1] === "/") {
|
|
126
|
+
i += 1;
|
|
127
|
+
source += "(?:.*/)?";
|
|
128
|
+
} else {
|
|
129
|
+
source += ".*";
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
source += "[^/]*";
|
|
133
|
+
}
|
|
134
|
+
} else if (char === "?") {
|
|
135
|
+
source += "[^/]";
|
|
136
|
+
} else {
|
|
137
|
+
source += escapeRegExp(char);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return new RegExp(`${source}$`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function trimSlash(path) {
|
|
144
|
+
return path.replace(/\/+$/, "");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizePath(path) {
|
|
148
|
+
return path.replaceAll("\\", "/");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function escapeRegExp(value) {
|
|
152
|
+
return value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function stripJsonComments(source) {
|
|
156
|
+
let out = "";
|
|
157
|
+
let inString = false;
|
|
158
|
+
let quote = "";
|
|
159
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
160
|
+
const char = source[i];
|
|
161
|
+
const next = source[i + 1];
|
|
162
|
+
if (inString) {
|
|
163
|
+
out += char;
|
|
164
|
+
if (char === "\\") {
|
|
165
|
+
out += next ?? "";
|
|
166
|
+
i += 1;
|
|
167
|
+
} else if (char === quote) {
|
|
168
|
+
inString = false;
|
|
169
|
+
}
|
|
170
|
+
} else if (char === '"' || char === "'") {
|
|
171
|
+
inString = true;
|
|
172
|
+
quote = char;
|
|
173
|
+
out += char;
|
|
174
|
+
} else if (char === "/" && next === "/") {
|
|
175
|
+
while (i < source.length && source[i] !== "\n") i += 1;
|
|
176
|
+
out += "\n";
|
|
177
|
+
} else if (char === "/" && next === "*") {
|
|
178
|
+
i += 2;
|
|
179
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i += 1;
|
|
180
|
+
i += 1;
|
|
181
|
+
} else {
|
|
182
|
+
out += char;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out.replace(/,\s*([}\]])/g, "$1");
|
|
186
|
+
}
|
package/src/main.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { createInputHistoryStore } from "./cli/input/history-store.mjs";
|
|
|
10
10
|
import { createModeState } from "./cli/input/mode-state.mjs";
|
|
11
11
|
import { loadPromptTemplates } from "./cli/input/prompt-templates.mjs";
|
|
12
12
|
import { runInteractiveRepl, runSingleShotPrompt } from "./cli/repl-loop.mjs";
|
|
13
|
+
import { closeMarchRuntime } from "./cli/startup/runtime-close.mjs";
|
|
13
14
|
import { createStatusLineUpdater } from "./cli/status-line-updater.mjs";
|
|
14
15
|
import { wireTuiHandlers } from "./cli/tui/tui-handlers.mjs";
|
|
15
16
|
import { createMarchAuthStorage } from "./auth/storage.mjs";
|
|
@@ -28,6 +29,7 @@ import { formatStartupBanner } from "./cli/startup/startup-banner.mjs";
|
|
|
28
29
|
import { initializeMcp } from "./mcp/index.mjs";
|
|
29
30
|
import { createWebToolsFromConfig } from "./web/tools.mjs";
|
|
30
31
|
import { createModelContextDumper } from "./debug/model-context-dumper.mjs";
|
|
32
|
+
import { createLogger, installProcessLogHandlers } from "./debug/logger.mjs";
|
|
31
33
|
import { defaultCenterMemoryPath } from "./context/center-memory.mjs";
|
|
32
34
|
import { runProviderConfigCommand } from "./provider/config-command.mjs";
|
|
33
35
|
import { runWebSearchConfigCommand } from "./web/config-command.mjs";
|
|
@@ -68,6 +70,15 @@ export async function run(argv) {
|
|
|
68
70
|
|
|
69
71
|
const stateRoot = join(homedir(), ".march");
|
|
70
72
|
if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
|
|
73
|
+
const logger = createLogger({ logDir: join(stateRoot, "logs") });
|
|
74
|
+
installProcessLogHandlers(logger);
|
|
75
|
+
logger.event("process.start", {
|
|
76
|
+
cwd,
|
|
77
|
+
argv,
|
|
78
|
+
version: process.version,
|
|
79
|
+
platform: process.platform,
|
|
80
|
+
logPath: logger.path,
|
|
81
|
+
});
|
|
71
82
|
|
|
72
83
|
const provider = args.provider ?? config.provider ?? null;
|
|
73
84
|
const serviceTier = config.serviceTier ?? null;
|
|
@@ -96,7 +107,6 @@ export async function run(argv) {
|
|
|
96
107
|
const memoryStore = new MarkdownMemoryStore({ root: memoryRoot });
|
|
97
108
|
const memoryTools = createMarkdownMemoryTools(memoryStore);
|
|
98
109
|
const currentProject = basename(cwd);
|
|
99
|
-
|
|
100
110
|
const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
|
|
101
111
|
|
|
102
112
|
// MCP: connect to configured MCP servers
|
|
@@ -182,9 +192,11 @@ export async function run(argv) {
|
|
|
182
192
|
authStorage: authConfig.authStorage,
|
|
183
193
|
maxTurns: config.maxTurns ?? undefined,
|
|
184
194
|
trimBatch: config.trimBatch ?? undefined,
|
|
195
|
+
hostedTools: config.hostedTools,
|
|
185
196
|
permissionController,
|
|
186
197
|
modelContextDumper,
|
|
187
198
|
turnNotifier,
|
|
199
|
+
logger,
|
|
188
200
|
onModelPayload: ({ estimatedTokens }) => {
|
|
189
201
|
refreshStatusBar?.({ contextTokens: estimatedTokens });
|
|
190
202
|
},
|
|
@@ -235,8 +247,9 @@ export async function run(argv) {
|
|
|
235
247
|
});
|
|
236
248
|
} finally {
|
|
237
249
|
turnRunning = false;
|
|
238
|
-
await closeMarchRuntime({ runner, memoryStore, ui, blankLine: true });
|
|
250
|
+
await closeMarchRuntime({ runner, memoryStore, ui, logger, blankLine: true });
|
|
239
251
|
}
|
|
252
|
+
logger.event("process.exit", { code: 0 });
|
|
240
253
|
return 0;
|
|
241
254
|
}
|
|
242
255
|
|
|
@@ -264,32 +277,12 @@ export async function run(argv) {
|
|
|
264
277
|
modeState,
|
|
265
278
|
});
|
|
266
279
|
} finally {
|
|
267
|
-
await closeMarchRuntime({ runner, memoryStore, ui });
|
|
280
|
+
await closeMarchRuntime({ runner, memoryStore, ui, logger });
|
|
268
281
|
}
|
|
282
|
+
logger.event("process.exit", { code: 0 });
|
|
269
283
|
return 0;
|
|
270
284
|
}
|
|
271
285
|
|
|
272
|
-
async function closeMarchRuntime({ runner, memoryStore, ui, blankLine = false }) {
|
|
273
|
-
let firstError = null;
|
|
274
|
-
try {
|
|
275
|
-
await runner.dispose();
|
|
276
|
-
} catch (err) {
|
|
277
|
-
firstError ??= err;
|
|
278
|
-
}
|
|
279
|
-
try {
|
|
280
|
-
memoryStore.close();
|
|
281
|
-
} catch (err) {
|
|
282
|
-
firstError ??= err;
|
|
283
|
-
}
|
|
284
|
-
try {
|
|
285
|
-
if (blankLine) ui.writeln("");
|
|
286
|
-
await ui.close();
|
|
287
|
-
} catch (err) {
|
|
288
|
-
firstError ??= err;
|
|
289
|
-
}
|
|
290
|
-
if (firstError) throw firstError;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
286
|
function resolveMemoryRoot(configured, stateRoot) {
|
|
294
287
|
if (configured) return resolve(String(configured));
|
|
295
288
|
if (process.env.MARCH_MEMORY_ROOT) return resolve(process.env.MARCH_MEMORY_ROOT);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function spawnCommand(command, args = [], options = {}) {
|
|
4
|
+
const resolved = resolveSpawnCommand(command, args);
|
|
5
|
+
return spawn(resolved.command, resolved.args, { ...options, ...resolved.options });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function resolveSpawnCommand(command, args = []) {
|
|
9
|
+
if (process.platform !== "win32" || !isWindowsScriptCommand(command)) {
|
|
10
|
+
return { command, args };
|
|
11
|
+
}
|
|
12
|
+
// Node can fail to spawn .cmd/.bat directly on Windows; cmd.exe runs them reliably.
|
|
13
|
+
return {
|
|
14
|
+
command: "cmd.exe",
|
|
15
|
+
args: ["/d", "/s", "/c", `"${[quoteCmdArg(command), ...args.map(quoteCmdArg)].join(" ")}"`],
|
|
16
|
+
options: { windowsVerbatimArguments: true },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isWindowsScriptCommand(command) {
|
|
21
|
+
const lower = command.toLowerCase();
|
|
22
|
+
return lower.endsWith(".cmd") || lower.endsWith(".bat");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function quoteCmdArg(value) {
|
|
26
|
+
return /[\s&()^|<>"%]/.test(value) ? `"${value.replaceAll('"', '""')}"` : value;
|
|
27
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const OPENAI_PROVIDERS = new Set(["openai"]);
|
|
2
|
+
const OPENAI_CODEX_PROVIDERS = new Set(["openai-codex"]);
|
|
3
|
+
const AZURE_OPENAI_PROVIDERS = new Set(["azure-openai-responses"]);
|
|
4
|
+
const ANTHROPIC_PROVIDERS = new Set(["anthropic"]);
|
|
5
|
+
const GOOGLE_PROVIDERS = new Set(["google", "google-vertex"]);
|
|
6
|
+
const XAI_PROVIDERS = new Set(["xai", "supergrok-oauth", "xai-oauth"]);
|
|
7
|
+
|
|
8
|
+
export function injectHostedTools(payload, model, config = {}) {
|
|
9
|
+
const capabilities = resolveHostedToolCapabilities(model).filter((tool) => isToolEnabled(tool, config));
|
|
10
|
+
if (capabilities.length === 0) return payload;
|
|
11
|
+
return injectPayloadHostedTools(payload, capabilities);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveHostedTools(model, config = {}) {
|
|
15
|
+
return resolveHostedToolCapabilities(model).filter((tool) => isToolEnabled(tool, config)).map(createHostedTool);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveHostedToolCapabilities(model) {
|
|
19
|
+
if (!model || typeof model !== "object") return [];
|
|
20
|
+
if (OPENAI_PROVIDERS.has(model.provider) && isOpenAiResponsesApi(model.api)) return ["openai.webSearch"];
|
|
21
|
+
if (OPENAI_CODEX_PROVIDERS.has(model.provider) && model.api === "openai-codex-responses") {
|
|
22
|
+
return ["openaiCodex.webSearch"];
|
|
23
|
+
}
|
|
24
|
+
if (AZURE_OPENAI_PROVIDERS.has(model.provider) && model.api === "azure-openai-responses") {
|
|
25
|
+
return ["azureOpenai.webSearch"];
|
|
26
|
+
}
|
|
27
|
+
if (ANTHROPIC_PROVIDERS.has(model.provider) && model.api === "anthropic-messages") return ["anthropic.webSearch"];
|
|
28
|
+
if (GOOGLE_PROVIDERS.has(model.provider) && isGoogleApi(model.api)) return ["google.webSearch"];
|
|
29
|
+
if (XAI_PROVIDERS.has(model.provider) && model.api === "openai-responses") return ["xai.webSearch", "xai.xSearch"];
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isToolEnabled(tool, config) {
|
|
34
|
+
const [provider, name] = tool.split(".");
|
|
35
|
+
const value = config?.[provider]?.[name] ?? "auto";
|
|
36
|
+
return value !== false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createHostedTool(tool) {
|
|
40
|
+
if (tool === "openai.webSearch" || tool === "azureOpenai.webSearch") return { type: "web_search_preview" };
|
|
41
|
+
if (tool === "openaiCodex.webSearch") return { type: "web_search" };
|
|
42
|
+
if (tool === "anthropic.webSearch") return { type: "web_search_20250305", name: "web_search" };
|
|
43
|
+
if (tool === "google.webSearch") return { googleSearch: {} };
|
|
44
|
+
if (tool === "xai.webSearch") return { type: "web_search", enable_image_understanding: true };
|
|
45
|
+
if (tool === "xai.xSearch") {
|
|
46
|
+
return { type: "x_search", enable_image_understanding: true, enable_video_understanding: true };
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Unsupported hosted tool capability: ${tool}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isOpenAiResponsesApi(api) {
|
|
52
|
+
return api === "openai-responses";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isGoogleApi(api) {
|
|
56
|
+
return api === "google-generative-ai" || api === "google-vertex";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function injectPayloadHostedTools(payload, capabilities) {
|
|
60
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
61
|
+
if (payload.body && typeof payload.body === "object") {
|
|
62
|
+
return { ...payload, body: injectPayloadHostedTools(payload.body, capabilities) };
|
|
63
|
+
}
|
|
64
|
+
if (typeof payload.body === "string") return injectStringBodyHostedTools(payload, capabilities);
|
|
65
|
+
return capabilities.reduce((next, capability) => injectPayloadHostedTool(next, capability), payload);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function injectPayloadHostedTool(payload, capability) {
|
|
69
|
+
if (capability.startsWith("google.")) return appendGoogleTool(payload, createHostedTool(capability));
|
|
70
|
+
return appendTopLevelTool(payload, createHostedTool(capability));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function injectStringBodyHostedTools(payload, capabilities) {
|
|
74
|
+
try {
|
|
75
|
+
const body = JSON.parse(payload.body);
|
|
76
|
+
return { ...payload, body: JSON.stringify(injectPayloadHostedTools(body, capabilities)) };
|
|
77
|
+
} catch {
|
|
78
|
+
return payload;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function appendTopLevelTool(payload, tool) {
|
|
83
|
+
if (!Array.isArray(payload.tools)) return { ...payload, tools: [tool] };
|
|
84
|
+
return { ...payload, tools: mergeTools(payload.tools, [tool], getToolKey) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function appendGoogleTool(payload, tool) {
|
|
88
|
+
const config = payload.config && typeof payload.config === "object" ? payload.config : {};
|
|
89
|
+
const tools = Array.isArray(config.tools) ? config.tools : [];
|
|
90
|
+
return {
|
|
91
|
+
...payload,
|
|
92
|
+
config: {
|
|
93
|
+
...config,
|
|
94
|
+
tools: mergeTools(tools, [tool], getGoogleToolKey),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function mergeTools(existing, added, keyForTool) {
|
|
100
|
+
const keys = new Set(existing.map(keyForTool).filter(Boolean));
|
|
101
|
+
return [...existing, ...added.filter((tool) => !keys.has(keyForTool(tool)))];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getToolKey(tool) {
|
|
105
|
+
return tool?.type ?? tool?.name;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getGoogleToolKey(tool) {
|
|
109
|
+
if (tool?.googleSearch) return "googleSearch";
|
|
110
|
+
return tool?.functionDeclarations ? "functionDeclarations" : getToolKey(tool);
|
|
111
|
+
}
|
package/src/web/tools.mjs
CHANGED
|
@@ -6,8 +6,8 @@ import { fetchWebPage } from "./fetch.mjs";
|
|
|
6
6
|
|
|
7
7
|
export function createWebTools({ tavilyKey, braveKey } = {}) {
|
|
8
8
|
const webSearchTool = defineTool({
|
|
9
|
-
name: "
|
|
10
|
-
label: "Web Search",
|
|
9
|
+
name: "external_web_search",
|
|
10
|
+
label: "External Web Search",
|
|
11
11
|
description:
|
|
12
12
|
"Search the web for current information on any topic. " +
|
|
13
13
|
"Requires TAVILY_API_KEY or BRAVE_API_KEY; if neither is configured, use web_fetch when you already know the URL. " +
|