@iinm/plain-agent 1.1.1 → 1.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.
- package/README.md +106 -36
- package/package.json +4 -8
- package/src/claudeCodePlugin.mjs +164 -0
- package/src/cliArgs.mjs +114 -36
- package/src/cliFormatter.mjs +0 -9
- package/src/cliInteractive.mjs +5 -3
- package/src/config.d.ts +6 -20
- package/src/config.mjs +12 -8
- package/src/context/loadAgentRoles.mjs +12 -6
- package/src/context/loadPrompts.mjs +17 -10
- package/src/main.mjs +41 -25
- package/src/mcp.mjs +4 -8
- package/src/tools/askURL.mjs +201 -0
- package/src/tools/askWeb.mjs +200 -0
- package/src/tools/askGoogle.mjs +0 -135
- package/src/tools/fetchWebPage.mjs +0 -96
- package/src/tools/tavilySearch.d.ts +0 -6
- package/src/tools/tavilySearch.mjs +0 -57
package/src/config.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { ModelDefinition, PlatformConfig } from "./modelDefinition";
|
|
2
2
|
import { ToolUsePattern } from "./tool";
|
|
3
|
+
import { AskURLToolOptions } from "./tools/askURL.mjs";
|
|
4
|
+
import { AskWebToolOptions } from "./tools/askWeb.mjs";
|
|
3
5
|
import { ExecCommandSanboxConfig } from "./tools/execCommand";
|
|
4
|
-
|
|
5
|
-
export type ClaudeCodePluginConfig = {
|
|
6
|
-
name: string;
|
|
7
|
-
path: string;
|
|
8
|
-
};
|
|
6
|
+
import { ClaudeCodePluginRepo } from "./claudeCodePlugin.mjs";
|
|
9
7
|
|
|
10
8
|
export type AppConfig = {
|
|
11
9
|
model?: string;
|
|
@@ -18,24 +16,12 @@ export type AppConfig = {
|
|
|
18
16
|
};
|
|
19
17
|
sandbox?: ExecCommandSanboxConfig;
|
|
20
18
|
tools?: {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* - AI Studio: requires apiKey
|
|
24
|
-
*/
|
|
25
|
-
askGoogle?: {
|
|
26
|
-
platform?: "vertex-ai";
|
|
27
|
-
baseURL?: string;
|
|
28
|
-
account?: string;
|
|
29
|
-
apiKey?: string;
|
|
30
|
-
model?: string;
|
|
31
|
-
};
|
|
32
|
-
searchWeb?: {
|
|
33
|
-
tavilyApiKey?: string;
|
|
34
|
-
};
|
|
19
|
+
askWeb?: AskWebToolOptions;
|
|
20
|
+
askURL?: AskURLToolOptions;
|
|
35
21
|
};
|
|
36
22
|
mcpServers?: Record<string, MCPServerConfig>;
|
|
37
23
|
notifyCmd?: string;
|
|
38
|
-
claudeCodePlugins?:
|
|
24
|
+
claudeCodePlugins?: ClaudeCodePluginRepo[];
|
|
39
25
|
};
|
|
40
26
|
|
|
41
27
|
export type MCPServerConfig = {
|
package/src/config.mjs
CHANGED
|
@@ -76,14 +76,18 @@ export async function loadAppConfig(options = {}) {
|
|
|
76
76
|
},
|
|
77
77
|
sandbox: config.sandbox ?? merged.sandbox,
|
|
78
78
|
tools: {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
askWeb: config.tools?.askWeb
|
|
80
|
+
? {
|
|
81
|
+
...(merged.tools?.askWeb ?? {}),
|
|
82
|
+
...config.tools.askWeb,
|
|
83
|
+
}
|
|
84
|
+
: merged.tools?.askWeb,
|
|
85
|
+
askURL: config.tools?.askURL
|
|
86
|
+
? {
|
|
87
|
+
...(merged.tools?.askURL ?? {}),
|
|
88
|
+
...config.tools.askURL,
|
|
89
|
+
}
|
|
90
|
+
: merged.tools?.askWeb,
|
|
87
91
|
},
|
|
88
92
|
mcpServers: {
|
|
89
93
|
...(merged.mcpServers ?? {}),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs" */
|
|
2
|
+
|
|
1
3
|
import crypto from "node:crypto";
|
|
2
4
|
import fs from "node:fs/promises";
|
|
3
5
|
import path from "node:path";
|
|
@@ -7,7 +9,6 @@ import {
|
|
|
7
9
|
AGENT_PROJECT_METADATA_DIR,
|
|
8
10
|
AGENT_ROOT,
|
|
9
11
|
AGENT_USER_CONFIG_DIR,
|
|
10
|
-
CLAUDE_CODE_PLUGIN_DIR,
|
|
11
12
|
} from "../env.mjs";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -21,10 +22,11 @@ import {
|
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Load all agent roles from the predefined directories.
|
|
24
|
-
* @param {
|
|
25
|
+
* @param {ClaudeCodePlugin[]} [claudeCodePlugins]
|
|
25
26
|
* @returns {Promise<Map<string, AgentRole>>}
|
|
26
27
|
*/
|
|
27
28
|
export async function loadAgentRoles(claudeCodePlugins) {
|
|
29
|
+
/** @type {Array<{dir: string, idPrefix: string, only?: RegExp}>} */
|
|
28
30
|
const agentDirs = [
|
|
29
31
|
{
|
|
30
32
|
dir: path.resolve(AGENT_ROOT, ".config", "agents.predefined"),
|
|
@@ -41,11 +43,10 @@ export async function loadAgentRoles(claudeCodePlugins) {
|
|
|
41
43
|
// Add plugin directories if provided
|
|
42
44
|
if (claudeCodePlugins) {
|
|
43
45
|
for (const plugin of claudeCodePlugins) {
|
|
44
|
-
const pluginBase = path.join(CLAUDE_CODE_PLUGIN_DIR, plugin.path);
|
|
45
|
-
|
|
46
46
|
agentDirs.push({
|
|
47
|
-
dir: path.join(
|
|
47
|
+
dir: path.join(plugin.path, "agents"),
|
|
48
48
|
idPrefix: `claude/${plugin.name}:`,
|
|
49
|
+
only: plugin.only,
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
}
|
|
@@ -53,7 +54,7 @@ export async function loadAgentRoles(claudeCodePlugins) {
|
|
|
53
54
|
/** @type {Map<string, AgentRole>} */
|
|
54
55
|
const roles = new Map();
|
|
55
56
|
|
|
56
|
-
for (const { dir, idPrefix } of agentDirs) {
|
|
57
|
+
for (const { dir, idPrefix, only } of agentDirs) {
|
|
57
58
|
const files = await getMarkdownFiles(dir).catch((err) => {
|
|
58
59
|
if (err.code !== "ENOENT") {
|
|
59
60
|
console.warn(`Failed to list agent roles in ${dir}:`, err);
|
|
@@ -70,6 +71,11 @@ export async function loadAgentRoles(claudeCodePlugins) {
|
|
|
70
71
|
|
|
71
72
|
if (content === null) continue;
|
|
72
73
|
|
|
74
|
+
// Filter by only pattern if specified
|
|
75
|
+
if (only && !only.test(file)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
let role = parseAgentRole(file, content, fullPath, idPrefix);
|
|
74
80
|
if (role.import) {
|
|
75
81
|
role = await mergeRemoteRole(role, file, fullPath);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs" */
|
|
2
|
+
|
|
1
3
|
import { execFileSync } from "node:child_process";
|
|
2
4
|
import crypto from "node:crypto";
|
|
3
5
|
import fs from "node:fs/promises";
|
|
@@ -8,7 +10,6 @@ import {
|
|
|
8
10
|
AGENT_PROJECT_METADATA_DIR,
|
|
9
11
|
AGENT_ROOT,
|
|
10
12
|
AGENT_USER_CONFIG_DIR,
|
|
11
|
-
CLAUDE_CODE_PLUGIN_DIR,
|
|
12
13
|
} from "../env.mjs";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -25,10 +26,11 @@ import {
|
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Load all prompts from the predefined directories.
|
|
28
|
-
* @param {
|
|
29
|
+
* @param {ClaudeCodePlugin[]} [claudeCodePlugins]
|
|
29
30
|
* @returns {Promise<Map<string, Prompt>>}
|
|
30
31
|
*/
|
|
31
32
|
export async function loadPrompts(claudeCodePlugins) {
|
|
33
|
+
/** @type {Array<{dir: string, idPrefix: string, only?: RegExp}>} */
|
|
32
34
|
const promptDirs = [
|
|
33
35
|
{
|
|
34
36
|
dir: path.resolve(AGENT_ROOT, ".config", "prompts.predefined"),
|
|
@@ -49,18 +51,18 @@ export async function loadPrompts(claudeCodePlugins) {
|
|
|
49
51
|
// Add plugin directories if provided
|
|
50
52
|
if (claudeCodePlugins) {
|
|
51
53
|
for (const plugin of claudeCodePlugins) {
|
|
52
|
-
const pluginBase = path.join(CLAUDE_CODE_PLUGIN_DIR, plugin.path);
|
|
53
|
-
|
|
54
54
|
// Commands
|
|
55
55
|
promptDirs.push({
|
|
56
|
-
dir: path.join(
|
|
56
|
+
dir: path.join(plugin.path, "commands"),
|
|
57
57
|
idPrefix: `claude/${plugin.name}/commands:`,
|
|
58
|
+
only: plugin.only,
|
|
58
59
|
});
|
|
59
60
|
|
|
60
61
|
// Skills
|
|
61
62
|
promptDirs.push({
|
|
62
|
-
dir: path.join(
|
|
63
|
+
dir: path.join(plugin.path, "skills"),
|
|
63
64
|
idPrefix: `claude/${plugin.name}/skills:`,
|
|
65
|
+
only: plugin.only,
|
|
64
66
|
});
|
|
65
67
|
}
|
|
66
68
|
}
|
|
@@ -68,7 +70,7 @@ export async function loadPrompts(claudeCodePlugins) {
|
|
|
68
70
|
/** @type {Map<string, Prompt>} */
|
|
69
71
|
const prompts = new Map();
|
|
70
72
|
|
|
71
|
-
for (const { dir, idPrefix } of promptDirs) {
|
|
73
|
+
for (const { dir, idPrefix, only } of promptDirs) {
|
|
72
74
|
const files = await getMarkdownFiles(dir).catch((err) => {
|
|
73
75
|
if (err.code !== "ENOENT") {
|
|
74
76
|
console.warn(`Failed to list prompts in ${dir}:`, err);
|
|
@@ -85,6 +87,11 @@ export async function loadPrompts(claudeCodePlugins) {
|
|
|
85
87
|
|
|
86
88
|
if (content === null) continue;
|
|
87
89
|
|
|
90
|
+
// Filter by only pattern if specified
|
|
91
|
+
if (only && !only.test(file)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
88
95
|
// Ignore all files in the skills/ directory except for SKILL.md.
|
|
89
96
|
if (fullPath.match(/\/skills\//) && !file.endsWith("/SKILL.md")) {
|
|
90
97
|
continue;
|
|
@@ -240,6 +247,7 @@ async function getMarkdownFiles(dir, baseDir = dir) {
|
|
|
240
247
|
*/
|
|
241
248
|
function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
|
|
242
249
|
const rawId = relativePath.replace(/\/SKILL\.md$/, "").replace(/\.md$/, "");
|
|
250
|
+
const isSkill = relativePath.endsWith("SKILL.md");
|
|
243
251
|
const isShortcut = rawId.startsWith("shortcuts/");
|
|
244
252
|
const id = isShortcut
|
|
245
253
|
? idPrefix + rawId.replace(/^shortcuts\//, "")
|
|
@@ -257,7 +265,7 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
|
|
|
257
265
|
content: fileContent.trim(),
|
|
258
266
|
filePath: fullPath,
|
|
259
267
|
isShortcut,
|
|
260
|
-
isSkill
|
|
268
|
+
isSkill,
|
|
261
269
|
};
|
|
262
270
|
}
|
|
263
271
|
|
|
@@ -281,7 +289,7 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
|
|
|
281
289
|
parseFrontmatterField(match[1], "user-invocable") === "true" ||
|
|
282
290
|
undefined,
|
|
283
291
|
isShortcut,
|
|
284
|
-
isSkill
|
|
292
|
+
isSkill,
|
|
285
293
|
};
|
|
286
294
|
}
|
|
287
295
|
const userInvocable = frontmatter["user-invocable"];
|
|
@@ -304,7 +312,6 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
|
|
|
304
312
|
* @param {string} field
|
|
305
313
|
* @returns {string | undefined}
|
|
306
314
|
*/
|
|
307
|
-
|
|
308
315
|
function parseFrontmatterField(frontmatter, field) {
|
|
309
316
|
const regex = new RegExp(`^${field}:\\s*(.*)$`, "m");
|
|
310
317
|
const match = frontmatter.match(regex);
|
package/src/main.mjs
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { styleText } from "node:util";
|
|
6
6
|
import { createAgent } from "./agent.mjs";
|
|
7
|
+
import {
|
|
8
|
+
installClaudeCodePlugins,
|
|
9
|
+
resolvePluginPaths,
|
|
10
|
+
} from "./claudeCodePlugin.mjs";
|
|
7
11
|
import { parseCliArgs, printHelp } from "./cliArgs.mjs";
|
|
8
12
|
import { startBatchSession } from "./cliBatch.mjs";
|
|
9
13
|
import { startInteractiveSession } from "./cliInteractive.mjs";
|
|
@@ -18,23 +22,22 @@ import {
|
|
|
18
22
|
import { setupMCPServer } from "./mcp.mjs";
|
|
19
23
|
import { createModelCaller } from "./modelCaller.mjs";
|
|
20
24
|
import { createPrompt } from "./prompt.mjs";
|
|
21
|
-
import {
|
|
25
|
+
import { createAskURLTool } from "./tools/askURL.mjs";
|
|
26
|
+
import { createAskWebTool } from "./tools/askWeb.mjs";
|
|
22
27
|
import { createDelegateToSubagentTool } from "./tools/delegateToSubagent.mjs";
|
|
23
28
|
import { createExecCommandTool } from "./tools/execCommand.mjs";
|
|
24
|
-
import { fetchWebPageTool } from "./tools/fetchWebPage.mjs";
|
|
25
29
|
import { patchFileTool } from "./tools/patchFile.mjs";
|
|
26
30
|
import { createReportAsSubagentTool } from "./tools/reportAsSubagent.mjs";
|
|
27
|
-
import { createTavilySearchTool } from "./tools/tavilySearch.mjs";
|
|
28
31
|
import { createTmuxCommandTool } from "./tools/tmuxCommand.mjs";
|
|
29
32
|
import { writeFileTool } from "./tools/writeFile.mjs";
|
|
30
33
|
import { createToolUseApprover } from "./toolUseApprover.mjs";
|
|
31
34
|
|
|
32
35
|
const cliArgs = parseCliArgs(process.argv);
|
|
33
|
-
if (cliArgs.
|
|
36
|
+
if (cliArgs.subcommand.type === "help") {
|
|
34
37
|
printHelp();
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
if (cliArgs.
|
|
40
|
+
if (cliArgs.subcommand.type === "list-models") {
|
|
38
41
|
const { appConfig } = await loadAppConfig({ skipTrustCheck: true });
|
|
39
42
|
if (!appConfig.models || appConfig.models.length === 0) {
|
|
40
43
|
console.error("No models found in configuration.");
|
|
@@ -49,6 +52,11 @@ if (cliArgs.listModels) {
|
|
|
49
52
|
process.exit(0);
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
if (cliArgs.subcommand.type === "install-claude-code-plugins") {
|
|
56
|
+
await installClaudeCodePlugins();
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
(async () => {
|
|
53
61
|
const startTime = new Date();
|
|
54
62
|
const sessionId = [
|
|
@@ -57,12 +65,18 @@ if (cliArgs.listModels) {
|
|
|
57
65
|
`0${startTime.getMinutes()}`.slice(-2),
|
|
58
66
|
].join("-");
|
|
59
67
|
const tmuxSessionId = `agent-${sessionId}`;
|
|
60
|
-
|
|
68
|
+
|
|
69
|
+
const isBatchMode = cliArgs.subcommand.type === "batch";
|
|
70
|
+
const configFiles =
|
|
71
|
+
cliArgs.subcommand.type === "batch" ||
|
|
72
|
+
cliArgs.subcommand.type === "interactive"
|
|
73
|
+
? cliArgs.subcommand.config
|
|
74
|
+
: [];
|
|
61
75
|
|
|
62
76
|
const { appConfig, loadedConfigPath } = await loadAppConfig({
|
|
63
77
|
skipUserConfig: isBatchMode,
|
|
64
78
|
skipTrustCheck: isBatchMode,
|
|
65
|
-
configFiles
|
|
79
|
+
configFiles,
|
|
66
80
|
});
|
|
67
81
|
|
|
68
82
|
// In batch mode, skip human-readable output
|
|
@@ -122,9 +136,17 @@ if (cliArgs.listModels) {
|
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
139
|
+
const modelFromConfig = appConfig.model || "";
|
|
140
|
+
const modelFromArgs =
|
|
141
|
+
cliArgs.subcommand.type === "batch" ||
|
|
142
|
+
cliArgs.subcommand.type === "interactive"
|
|
143
|
+
? cliArgs.subcommand.model
|
|
144
|
+
: null;
|
|
145
|
+
const modelNameWithVariant = modelFromArgs || modelFromConfig;
|
|
146
|
+
|
|
147
|
+
const pluginPaths = resolvePluginPaths(appConfig.claudeCodePlugins ?? []);
|
|
148
|
+
const agentRoles = await loadAgentRoles(pluginPaths);
|
|
149
|
+
const prompts = await loadPrompts(pluginPaths);
|
|
128
150
|
|
|
129
151
|
const prompt = createPrompt({
|
|
130
152
|
username: USER_NAME,
|
|
@@ -142,23 +164,16 @@ if (cliArgs.listModels) {
|
|
|
142
164
|
writeFileTool,
|
|
143
165
|
patchFileTool,
|
|
144
166
|
createTmuxCommandTool({ sandbox: appConfig.sandbox }),
|
|
145
|
-
fetchWebPageTool,
|
|
146
167
|
createDelegateToSubagentTool(),
|
|
147
168
|
createReportAsSubagentTool(),
|
|
148
169
|
];
|
|
149
170
|
|
|
150
|
-
if (appConfig.tools?.
|
|
151
|
-
builtinTools.push(
|
|
171
|
+
if (appConfig.tools?.askWeb) {
|
|
172
|
+
builtinTools.push(createAskWebTool(appConfig.tools.askWeb));
|
|
152
173
|
}
|
|
153
174
|
|
|
154
|
-
if (appConfig.tools?.
|
|
155
|
-
builtinTools.push(
|
|
156
|
-
createAskGoogleTool({
|
|
157
|
-
platform: appConfig.tools.askGoogle.platform,
|
|
158
|
-
baseURL: appConfig.tools.askGoogle.baseURL,
|
|
159
|
-
apiKey: appConfig.tools.askGoogle.apiKey,
|
|
160
|
-
}),
|
|
161
|
-
);
|
|
175
|
+
if (appConfig.tools?.askURL) {
|
|
176
|
+
builtinTools.push(createAskURLTool(appConfig.tools.askURL));
|
|
162
177
|
}
|
|
163
178
|
|
|
164
179
|
const toolUseApprover = createToolUseApprover({
|
|
@@ -224,19 +239,20 @@ if (cliArgs.listModels) {
|
|
|
224
239
|
},
|
|
225
240
|
};
|
|
226
241
|
|
|
227
|
-
if (
|
|
228
|
-
|
|
242
|
+
if (cliArgs.subcommand.type === "batch") {
|
|
243
|
+
const task = cliArgs.subcommand.task;
|
|
244
|
+
if (!task) {
|
|
229
245
|
throw new Error("Batch task is required in batch mode");
|
|
230
246
|
}
|
|
231
247
|
await startBatchSession({
|
|
232
248
|
...sessionOptions,
|
|
233
|
-
task
|
|
249
|
+
task,
|
|
234
250
|
});
|
|
235
251
|
} else {
|
|
236
252
|
startInteractiveSession({
|
|
237
253
|
...sessionOptions,
|
|
238
254
|
notifyCmd: appConfig.notifyCmd || AGENT_NOTIFY_CMD_DEFAULT,
|
|
239
|
-
claudeCodePlugins: appConfig.claudeCodePlugins,
|
|
255
|
+
claudeCodePlugins: resolvePluginPaths(appConfig.claudeCodePlugins ?? []),
|
|
240
256
|
});
|
|
241
257
|
}
|
|
242
258
|
})().catch((err) => {
|
package/src/mcp.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
-
* @import { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
2
|
* @import { StructuredToolResultContent, Tool, ToolImplementation } from "./tool";
|
|
5
3
|
* @import { MCPServerConfig } from "./config";
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
6
|
import { mkdir, open } from "node:fs/promises";
|
|
9
7
|
import path from "node:path";
|
|
8
|
+
import { Client } from "@modelcontextprotocol/client";
|
|
10
9
|
import { AGENT_PROJECT_METADATA_DIR } from "./env.mjs";
|
|
11
10
|
import { writeTmpFile } from "./tmpfile.mjs";
|
|
12
11
|
import { noThrow } from "./utils/noThrow.mjs";
|
|
@@ -52,7 +51,7 @@ export async function setupMCPServer(serverName, serverConfig) {
|
|
|
52
51
|
/**
|
|
53
52
|
* @typedef {Object} MCPClientOptions
|
|
54
53
|
* @property {string} serverName - The name of the MCP server.
|
|
55
|
-
* @property {StdioServerParameters} params - The transport to use for the client.
|
|
54
|
+
* @property {import("@modelcontextprotocol/client").StdioServerParameters} params - The transport to use for the client.
|
|
56
55
|
*/
|
|
57
56
|
|
|
58
57
|
/**
|
|
@@ -60,10 +59,7 @@ export async function setupMCPServer(serverName, serverConfig) {
|
|
|
60
59
|
* @returns {Promise<{client: Client; cleanup: () => void}>} - The MCP client and cleanup function.
|
|
61
60
|
*/
|
|
62
61
|
async function startMCPServer(options) {
|
|
63
|
-
const mcpClient = await import("@modelcontextprotocol/
|
|
64
|
-
const mcpClientStdio = await import(
|
|
65
|
-
"@modelcontextprotocol/sdk/client/stdio.js"
|
|
66
|
-
);
|
|
62
|
+
const mcpClient = await import("@modelcontextprotocol/client");
|
|
67
63
|
|
|
68
64
|
const client = new mcpClient.Client({
|
|
69
65
|
name: "undefined",
|
|
@@ -83,7 +79,7 @@ async function startMCPServer(options) {
|
|
|
83
79
|
const logPath = path.join(logDir, `mcp--${options.serverName}.stderr`);
|
|
84
80
|
const stderrLogFile = await open(logPath, "a");
|
|
85
81
|
|
|
86
|
-
const transport = new
|
|
82
|
+
const transport = new mcpClient.StdioClientTransport({
|
|
87
83
|
...restParams,
|
|
88
84
|
env: env ? { ...defaultEnv, ...env } : undefined,
|
|
89
85
|
stderr: stderrLogFile.fd,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Tool } from '../tool'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { styleText } from "node:util";
|
|
6
|
+
import { getGoogleCloudAccessToken } from "../providers/platform/googleCloud.mjs";
|
|
7
|
+
import { noThrow } from "../utils/noThrow.mjs";
|
|
8
|
+
|
|
9
|
+
/** @typedef {AskURLToolGeminiOptions | AskURLToolGeminiVertexAIOptions} AskURLToolOptions */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} AskURLToolGeminiOptions
|
|
13
|
+
* @property {"gemini"} provider
|
|
14
|
+
* @property {string=} baseURL
|
|
15
|
+
* @property {string} apiKey
|
|
16
|
+
* @property {string} model
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} AskURLToolGeminiVertexAIOptions
|
|
21
|
+
* @property {"gemini-vertex-ai"} provider
|
|
22
|
+
* @property {string} baseURL
|
|
23
|
+
* @property {string=} account
|
|
24
|
+
* @property {string} model
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} AskWebInput
|
|
29
|
+
* @property {string} question
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {AskURLToolOptions} config
|
|
34
|
+
* @returns {Tool}
|
|
35
|
+
*/
|
|
36
|
+
export function createAskURLTool(config) {
|
|
37
|
+
/**
|
|
38
|
+
* @param {AskWebInput} input
|
|
39
|
+
* @param {number} retryCount
|
|
40
|
+
* @returns {Promise<string | Error>}
|
|
41
|
+
*/
|
|
42
|
+
async function askURL(input, retryCount = 0) {
|
|
43
|
+
const model = config.model ?? "gemini-3-flash-preview";
|
|
44
|
+
const url =
|
|
45
|
+
config.provider === "gemini-vertex-ai"
|
|
46
|
+
? `${config.baseURL}/publishers/google/models/${config.model}:generateContent`
|
|
47
|
+
: config.baseURL
|
|
48
|
+
? `${config.baseURL}/models/${model}:generateContent`
|
|
49
|
+
: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
50
|
+
|
|
51
|
+
/** @type {Record<string,string>} */
|
|
52
|
+
const authHeader =
|
|
53
|
+
config.provider === "gemini-vertex-ai"
|
|
54
|
+
? {
|
|
55
|
+
Authorization: `Bearer ${await getGoogleCloudAccessToken(config.account)}`,
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
"x-goog-api-key": config.apiKey ?? "",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const data = {
|
|
62
|
+
contents: [
|
|
63
|
+
{
|
|
64
|
+
role: "user",
|
|
65
|
+
parts: [
|
|
66
|
+
{
|
|
67
|
+
text: `I need a comprehensive answer to this question. Please note that I don't have access to external URLs, so include all relevant facts, data, or explanations directly in your response. Avoid referencing links I can't open.
|
|
68
|
+
|
|
69
|
+
Question: ${input.question}`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
tools: [
|
|
75
|
+
{
|
|
76
|
+
url_context: {},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
...authHeader,
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
},
|
|
87
|
+
body: JSON.stringify(data),
|
|
88
|
+
signal: AbortSignal.timeout(120 * 1000),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (response.status === 429 || response.status >= 500) {
|
|
92
|
+
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
93
|
+
console.error(
|
|
94
|
+
styleText(
|
|
95
|
+
"yellow",
|
|
96
|
+
`Google API returned ${response.status}. Retrying in ${interval} seconds...`,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
100
|
+
return askURL(input, retryCount + 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
return new Error(
|
|
105
|
+
`Failed to ask Web: status=${response.status}, body=${await response.text()}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const body = await response.json();
|
|
110
|
+
|
|
111
|
+
const candidate = body.candidates?.[0];
|
|
112
|
+
const text = candidate?.content?.parts?.[0]?.text;
|
|
113
|
+
/** @type {{segment?:{startIndex:number,endIndex:number,text:string},groundingChunkIndices?:number[]}[] | undefined} */
|
|
114
|
+
const supports = candidate?.groundingMetadata?.groundingSupports;
|
|
115
|
+
/** @type {{web?:{uri:string,title:string}}[] | undefined} */
|
|
116
|
+
const chunks = candidate?.groundingMetadata?.groundingChunks;
|
|
117
|
+
|
|
118
|
+
if (typeof text !== "string") {
|
|
119
|
+
return new Error(
|
|
120
|
+
`Unexpected response format from Google: ${JSON.stringify(body)}`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {string} source
|
|
126
|
+
* @param {number} byteIndex
|
|
127
|
+
* @param {string} insertText
|
|
128
|
+
*/
|
|
129
|
+
const insertTextAtUtf8ByteIndex = (source, byteIndex, insertText) => {
|
|
130
|
+
const sourceBuffer = Buffer.from(source, "utf8");
|
|
131
|
+
const normalizedByteIndex = Math.max(
|
|
132
|
+
0,
|
|
133
|
+
Math.min(byteIndex, sourceBuffer.length),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return Buffer.concat([
|
|
137
|
+
sourceBuffer.subarray(0, normalizedByteIndex),
|
|
138
|
+
Buffer.from(insertText, "utf8"),
|
|
139
|
+
sourceBuffer.subarray(normalizedByteIndex),
|
|
140
|
+
]).toString("utf8");
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Sort by end_index desc because Gemini grounding indexes are byte offsets
|
|
144
|
+
// into the original UTF-8 text.
|
|
145
|
+
const sortedSupports = supports?.toSorted(
|
|
146
|
+
(a, b) => (b.segment?.endIndex ?? 0) - (a.segment?.endIndex ?? 0),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Insert citations using UTF-8 byte offsets.
|
|
150
|
+
let textWithCitations = text;
|
|
151
|
+
for (const support of sortedSupports ?? []) {
|
|
152
|
+
const endIndex = support.segment?.endIndex;
|
|
153
|
+
if (
|
|
154
|
+
typeof endIndex !== "number" ||
|
|
155
|
+
!support.groundingChunkIndices?.length
|
|
156
|
+
) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
textWithCitations = insertTextAtUtf8ByteIndex(
|
|
161
|
+
textWithCitations,
|
|
162
|
+
endIndex,
|
|
163
|
+
` [${support.groundingChunkIndices.map((i) => i + 1).join(", ")}] `,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const chunkString = (chunks ?? [])
|
|
168
|
+
.map(
|
|
169
|
+
(chunk, index) =>
|
|
170
|
+
`- [${index + 1} - ${chunk.web?.title}](${chunk.web?.uri})`,
|
|
171
|
+
)
|
|
172
|
+
.join("\n");
|
|
173
|
+
|
|
174
|
+
return [textWithCitations, chunkString].join("\n\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
def: {
|
|
179
|
+
name: "ask_url",
|
|
180
|
+
description:
|
|
181
|
+
"Use one or more provided URLs to answer a question. Include the URLs in your question.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
question: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description:
|
|
188
|
+
"The question to ask, including one or more URLs to use as context.",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
required: ["question"],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {AskWebInput} input
|
|
197
|
+
* @returns {Promise<string | Error>}
|
|
198
|
+
*/
|
|
199
|
+
impl: async (input) => await noThrow(async () => askURL(input, 0)),
|
|
200
|
+
};
|
|
201
|
+
}
|