@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
|
@@ -0,0 +1,200 @@
|
|
|
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 {AskWebToolGeminiOptions | AskWebToolGeminiVertexAIOptions} AskWebToolOptions */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} AskWebToolGeminiOptions
|
|
13
|
+
* @property {"gemini"} provider
|
|
14
|
+
* @property {string=} baseURL
|
|
15
|
+
* @property {string} apiKey
|
|
16
|
+
* @property {string} model
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} AskWebToolGeminiVertexAIOptions
|
|
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 {AskWebToolOptions} config
|
|
34
|
+
* @returns {Tool}
|
|
35
|
+
*/
|
|
36
|
+
export function createAskWebTool(config) {
|
|
37
|
+
/**
|
|
38
|
+
* @param {AskWebInput} input
|
|
39
|
+
* @param {number} retryCount
|
|
40
|
+
* @returns {Promise<string | Error>}
|
|
41
|
+
*/
|
|
42
|
+
async function askWeb(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
|
+
google_search: {},
|
|
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 askWeb(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_web",
|
|
180
|
+
description:
|
|
181
|
+
"Use the web search to answer questions that need up-to-date information or supporting sources.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
question: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: "The question to ask",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
required: ["question"],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @param {AskWebInput} input
|
|
196
|
+
* @returns {Promise<string | Error>}
|
|
197
|
+
*/
|
|
198
|
+
impl: async (input) => await noThrow(async () => askWeb(input, 0)),
|
|
199
|
+
};
|
|
200
|
+
}
|
package/src/tools/askGoogle.mjs
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
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
|
-
/**
|
|
10
|
-
* @typedef {Object} AskGoogleToolOptions
|
|
11
|
-
* @property {"vertex-ai"=} platform
|
|
12
|
-
* @property {string=} baseURL
|
|
13
|
-
* @property {string=} apiKey - API key for Google AI Studio
|
|
14
|
-
* @property {string=} account - The Google Cloud account to use for Vertex AI
|
|
15
|
-
* @property {string=} model
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @typedef {Object} AskGoogleInput
|
|
20
|
-
* @property {string} question
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {AskGoogleToolOptions} config
|
|
25
|
-
* @returns {Tool}
|
|
26
|
-
*/
|
|
27
|
-
export function createAskGoogleTool(config) {
|
|
28
|
-
/**
|
|
29
|
-
* @param {AskGoogleInput} input
|
|
30
|
-
* @param {number} retryCount
|
|
31
|
-
* @returns {Promise<string | Error>}
|
|
32
|
-
*/
|
|
33
|
-
async function askGoogle(input, retryCount = 0) {
|
|
34
|
-
const model = config.model ?? "gemini-3-flash-preview";
|
|
35
|
-
const url =
|
|
36
|
-
config.platform === "vertex-ai" && config.baseURL
|
|
37
|
-
? `${config.baseURL}/publishers/google/models/${model}:generateContent`
|
|
38
|
-
: config.baseURL
|
|
39
|
-
? `${config.baseURL}/models/${model}:generateContent`
|
|
40
|
-
: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
41
|
-
|
|
42
|
-
/** @type {Record<string,string>} */
|
|
43
|
-
const authHeader =
|
|
44
|
-
config.platform === "vertex-ai"
|
|
45
|
-
? {
|
|
46
|
-
Authorization: `Bearer ${await getGoogleCloudAccessToken(config.account)}`,
|
|
47
|
-
}
|
|
48
|
-
: {
|
|
49
|
-
"x-goog-api-key": config.apiKey ?? "",
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const data = {
|
|
53
|
-
contents: [
|
|
54
|
-
{
|
|
55
|
-
role: "user",
|
|
56
|
-
parts: [
|
|
57
|
-
{
|
|
58
|
-
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.
|
|
59
|
-
|
|
60
|
-
Question: ${input.question}`,
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
tools: [
|
|
66
|
-
{
|
|
67
|
-
google_search: {},
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const response = await fetch(url, {
|
|
73
|
-
method: "POST",
|
|
74
|
-
headers: {
|
|
75
|
-
...authHeader,
|
|
76
|
-
"Content-Type": "application/json",
|
|
77
|
-
},
|
|
78
|
-
body: JSON.stringify(data),
|
|
79
|
-
signal: AbortSignal.timeout(120 * 1000),
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
if (response.status === 429 || response.status >= 500) {
|
|
83
|
-
const interval = Math.min(2 * 2 ** retryCount, 16);
|
|
84
|
-
console.error(
|
|
85
|
-
styleText(
|
|
86
|
-
"yellow",
|
|
87
|
-
`Google API returned ${response.status}. Retrying in ${interval} seconds...`,
|
|
88
|
-
),
|
|
89
|
-
);
|
|
90
|
-
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
91
|
-
return askGoogle(input, retryCount + 1);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!response.ok) {
|
|
95
|
-
return new Error(
|
|
96
|
-
`Failed to ask Google: status=${response.status}, body=${await response.text()}`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const body = await response.json();
|
|
101
|
-
|
|
102
|
-
const answer = body.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
103
|
-
|
|
104
|
-
if (typeof answer !== "string") {
|
|
105
|
-
return new Error(
|
|
106
|
-
`Unexpected response format from Google: ${JSON.stringify(body)}`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return answer;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
def: {
|
|
115
|
-
name: "ask_google",
|
|
116
|
-
description: "Ask Google a question using natural language",
|
|
117
|
-
inputSchema: {
|
|
118
|
-
type: "object",
|
|
119
|
-
properties: {
|
|
120
|
-
question: {
|
|
121
|
-
type: "string",
|
|
122
|
-
description: "The question to ask Google",
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
required: ["question"],
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* @param {AskGoogleInput} input
|
|
131
|
-
* @returns {Promise<string | Error>}
|
|
132
|
-
*/
|
|
133
|
-
impl: async (input) => await noThrow(async () => askGoogle(input, 0)),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { Tool } from '../tool'
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { writeTmpFile } from "../tmpfile.mjs";
|
|
6
|
-
import { noThrow } from "../utils/noThrow.mjs";
|
|
7
|
-
|
|
8
|
-
const MAX_CONTENT_LENGTH = 1024 * 8;
|
|
9
|
-
|
|
10
|
-
/** @type {Tool} */
|
|
11
|
-
export const fetchWebPageTool = {
|
|
12
|
-
def: {
|
|
13
|
-
name: "fetch_web_page",
|
|
14
|
-
description:
|
|
15
|
-
"Fetch and extract web page content from a given URL, returning it as Markdown.",
|
|
16
|
-
inputSchema: {
|
|
17
|
-
type: "object",
|
|
18
|
-
properties: {
|
|
19
|
-
url: {
|
|
20
|
-
type: "string",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
required: ["url"],
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @param {Record<string, unknown>} input
|
|
29
|
-
* @returns {Record<string, unknown>}
|
|
30
|
-
*/
|
|
31
|
-
maskApprovalInput: (input) => {
|
|
32
|
-
try {
|
|
33
|
-
const url = new URL(String(input.url));
|
|
34
|
-
return { url: url.hostname };
|
|
35
|
-
} catch {
|
|
36
|
-
return input;
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @param {{url: string}} input
|
|
42
|
-
* @returns {Promise<string | Error>}
|
|
43
|
-
*/
|
|
44
|
-
impl: async (input) =>
|
|
45
|
-
await noThrow(async () => {
|
|
46
|
-
const { Readability } = await import("@mozilla/readability");
|
|
47
|
-
const { JSDOM } = await import("jsdom");
|
|
48
|
-
const TurndownService = (await import("turndown")).default;
|
|
49
|
-
|
|
50
|
-
const response = await fetch(input.url, {
|
|
51
|
-
signal: AbortSignal.timeout(30 * 1000),
|
|
52
|
-
});
|
|
53
|
-
const html = await response.text();
|
|
54
|
-
const dom = new JSDOM(html, { url: input.url });
|
|
55
|
-
const reader = new Readability(dom.window.document);
|
|
56
|
-
const article = reader.parse();
|
|
57
|
-
|
|
58
|
-
if (!article?.content) {
|
|
59
|
-
return "";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const turndownService = new TurndownService({
|
|
63
|
-
headingStyle: "atx",
|
|
64
|
-
bulletListMarker: "-",
|
|
65
|
-
codeBlockStyle: "fenced",
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const markdown = turndownService.turndown(article.content);
|
|
69
|
-
const trimmedMarkdown = markdown.trim();
|
|
70
|
-
|
|
71
|
-
if (trimmedMarkdown.length <= MAX_CONTENT_LENGTH) {
|
|
72
|
-
return trimmedMarkdown;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const filePath = await writeTmpFile(
|
|
76
|
-
trimmedMarkdown,
|
|
77
|
-
"read_web_page",
|
|
78
|
-
"md",
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
const lineCount = trimmedMarkdown.split("\n").length;
|
|
82
|
-
|
|
83
|
-
return [
|
|
84
|
-
`Content is large (${trimmedMarkdown.length} characters, ${lineCount} lines) and saved to ${filePath}`,
|
|
85
|
-
"- Use rg / awk to read specific parts",
|
|
86
|
-
].join("\n");
|
|
87
|
-
}),
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Playground
|
|
91
|
-
// (async () => {
|
|
92
|
-
// const input = {
|
|
93
|
-
// url: "https://devin.ai/agents101",
|
|
94
|
-
// };
|
|
95
|
-
// console.log(await fetchWebPageTool.impl(input));
|
|
96
|
-
// })();
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { Tool } from '../tool'
|
|
3
|
-
* @import { TavilySearchInput } from './tavilySearch'
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { noThrow } from "../utils/noThrow.mjs";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param {{tavilyApiKey?: string}} config
|
|
10
|
-
* @returns {Tool}
|
|
11
|
-
*/
|
|
12
|
-
export function createTavilySearchTool(config) {
|
|
13
|
-
return {
|
|
14
|
-
def: {
|
|
15
|
-
name: "search_web",
|
|
16
|
-
description: "Search the web for information",
|
|
17
|
-
inputSchema: {
|
|
18
|
-
type: "object",
|
|
19
|
-
properties: {
|
|
20
|
-
query: {
|
|
21
|
-
type: "string",
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
required: ["query"],
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {TavilySearchInput} input
|
|
30
|
-
* @returns {Promise<string | Error>}
|
|
31
|
-
*/
|
|
32
|
-
impl: async (input) =>
|
|
33
|
-
await noThrow(async () => {
|
|
34
|
-
const response = await fetch("https://api.tavily.com/search", {
|
|
35
|
-
method: "POST",
|
|
36
|
-
headers: {
|
|
37
|
-
Authorization: `Bearer ${config.tavilyApiKey}`,
|
|
38
|
-
"Content-Type": "application/json",
|
|
39
|
-
},
|
|
40
|
-
body: JSON.stringify({
|
|
41
|
-
...input,
|
|
42
|
-
max_results: 5,
|
|
43
|
-
}),
|
|
44
|
-
signal: AbortSignal.timeout(120 * 1000),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
if (!response.ok) {
|
|
48
|
-
return new Error(
|
|
49
|
-
`Failed to search: status=${response.status}, body=${await response.text()}`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const body = await response.json();
|
|
54
|
-
return JSON.stringify(body);
|
|
55
|
-
}),
|
|
56
|
-
};
|
|
57
|
-
}
|