@iinm/plain-agent 1.8.2 → 1.8.4
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 +6 -2
- package/bin/plain +1 -1
- package/config/config.predefined.json +1 -1
- package/config/prompts.predefined/shortcuts/configure.md +1 -1
- package/dist/main.mjs +473 -0
- package/dist/main.mjs.map +7 -0
- package/package.json +5 -7
- package/src/agent.d.ts +0 -52
- package/src/agent.mjs +0 -204
- package/src/agentLoop.mjs +0 -419
- package/src/agentState.mjs +0 -41
- package/src/claudeCodePlugin.mjs +0 -164
- package/src/cliArgs.mjs +0 -175
- package/src/cliBatch.mjs +0 -144
- package/src/cliCommands.mjs +0 -283
- package/src/cliCompleter.mjs +0 -227
- package/src/cliCost.mjs +0 -309
- package/src/cliFormatter.mjs +0 -413
- package/src/cliInteractive.mjs +0 -526
- package/src/cliInterruptTransform.mjs +0 -51
- package/src/cliMuteTransform.mjs +0 -26
- package/src/cliPasteTransform.mjs +0 -183
- package/src/config.d.ts +0 -36
- package/src/config.mjs +0 -197
- package/src/context/loadAgentRoles.mjs +0 -283
- package/src/context/loadPrompts.mjs +0 -324
- package/src/context/loadUserMessageContext.mjs +0 -147
- package/src/costTracker.mjs +0 -210
- package/src/env.mjs +0 -44
- package/src/main.mjs +0 -278
- package/src/mcpClient.mjs +0 -351
- package/src/mcpIntegration.mjs +0 -160
- package/src/model.d.ts +0 -109
- package/src/modelCaller.mjs +0 -32
- package/src/modelDefinition.d.ts +0 -92
- package/src/prompt.mjs +0 -138
- package/src/providers/anthropic.d.ts +0 -248
- package/src/providers/anthropic.mjs +0 -587
- package/src/providers/bedrock.d.ts +0 -249
- package/src/providers/bedrock.mjs +0 -700
- package/src/providers/gemini.d.ts +0 -208
- package/src/providers/gemini.mjs +0 -754
- package/src/providers/openai.d.ts +0 -281
- package/src/providers/openai.mjs +0 -544
- package/src/providers/openaiCompatible.d.ts +0 -147
- package/src/providers/openaiCompatible.mjs +0 -652
- package/src/providers/platform/awsSigV4.mjs +0 -184
- package/src/providers/platform/azure.mjs +0 -42
- package/src/providers/platform/bedrock.mjs +0 -78
- package/src/providers/platform/googleCloud.mjs +0 -34
- package/src/subagent.mjs +0 -265
- package/src/tmpfile.mjs +0 -27
- package/src/tool.d.ts +0 -74
- package/src/toolExecutor.mjs +0 -236
- package/src/toolInputValidator.mjs +0 -183
- package/src/toolUseApprover.mjs +0 -99
- package/src/tools/askURL.mjs +0 -209
- package/src/tools/askWeb.mjs +0 -208
- package/src/tools/compactContext.d.ts +0 -4
- package/src/tools/compactContext.mjs +0 -87
- package/src/tools/delegateToSubagent.d.ts +0 -4
- package/src/tools/delegateToSubagent.mjs +0 -48
- package/src/tools/execCommand.d.ts +0 -22
- package/src/tools/execCommand.mjs +0 -200
- package/src/tools/patchFile.d.ts +0 -4
- package/src/tools/patchFile.mjs +0 -133
- package/src/tools/reportAsSubagent.d.ts +0 -3
- package/src/tools/reportAsSubagent.mjs +0 -44
- package/src/tools/tmuxCommand.d.ts +0 -14
- package/src/tools/tmuxCommand.mjs +0 -194
- package/src/tools/writeFile.d.ts +0 -4
- package/src/tools/writeFile.mjs +0 -56
- package/src/usageStore.mjs +0 -167
- package/src/utils/evalJSONConfig.mjs +0 -72
- package/src/utils/matchValue.d.ts +0 -6
- package/src/utils/matchValue.mjs +0 -40
- package/src/utils/noThrow.mjs +0 -31
- package/src/utils/notify.mjs +0 -29
- package/src/utils/parseFileRange.mjs +0 -18
- package/src/utils/readFileRange.mjs +0 -33
- package/src/utils/retryOnError.mjs +0 -41
- package/src/voiceInput.mjs +0 -61
- package/src/voiceInputGemini.mjs +0 -105
- package/src/voiceInputOpenAI.mjs +0 -104
- package/src/voiceInputSession.mjs +0 -543
- package/src/voiceToggleKey.mjs +0 -62
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
/** @import { ClaudeCodePlugin } from "../claudeCodePlugin.mjs" */
|
|
2
|
-
|
|
3
|
-
import { execFileSync } from "node:child_process";
|
|
4
|
-
import crypto from "node:crypto";
|
|
5
|
-
import fs from "node:fs/promises";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { parse as parseYaml } from "yaml";
|
|
8
|
-
import {
|
|
9
|
-
AGENT_CACHE_DIR,
|
|
10
|
-
AGENT_PROJECT_METADATA_DIR,
|
|
11
|
-
AGENT_ROOT,
|
|
12
|
-
AGENT_USER_CONFIG_DIR,
|
|
13
|
-
} from "../env.mjs";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {Object} Prompt
|
|
17
|
-
* @property {string} id
|
|
18
|
-
* @property {string} description
|
|
19
|
-
* @property {string} content
|
|
20
|
-
* @property {string} filePath
|
|
21
|
-
* @property {boolean} claudeOriginated
|
|
22
|
-
* @property {string} [import]
|
|
23
|
-
* @property {boolean} [userInvocable]
|
|
24
|
-
* @property {boolean} [isShortcut]
|
|
25
|
-
* @property {boolean} [isSkill]
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Load all prompts from the predefined directories.
|
|
30
|
-
* @param {ClaudeCodePlugin[]} [claudeCodePlugins]
|
|
31
|
-
* @returns {Promise<Map<string, Prompt>>}
|
|
32
|
-
*/
|
|
33
|
-
export async function loadPrompts(claudeCodePlugins) {
|
|
34
|
-
/** @type {Array<{dir: string, idPrefix: string, only?: RegExp}>} */
|
|
35
|
-
const promptDirs = [
|
|
36
|
-
{
|
|
37
|
-
dir: path.resolve(AGENT_ROOT, "config", "prompts.predefined"),
|
|
38
|
-
idPrefix: "",
|
|
39
|
-
},
|
|
40
|
-
{ dir: path.resolve(AGENT_USER_CONFIG_DIR, "prompts"), idPrefix: "" },
|
|
41
|
-
{ dir: path.resolve(AGENT_PROJECT_METADATA_DIR, "prompts"), idPrefix: "" },
|
|
42
|
-
{
|
|
43
|
-
dir: path.resolve(process.cwd(), ".claude", "commands"),
|
|
44
|
-
idPrefix: "claude/commands:",
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
dir: path.resolve(process.cwd(), ".claude", "skills"),
|
|
48
|
-
idPrefix: "claude/skills:",
|
|
49
|
-
},
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
// Add plugin directories if provided
|
|
53
|
-
if (claudeCodePlugins) {
|
|
54
|
-
for (const plugin of claudeCodePlugins) {
|
|
55
|
-
// Commands
|
|
56
|
-
promptDirs.push({
|
|
57
|
-
dir: path.join(plugin.path, "commands"),
|
|
58
|
-
idPrefix: `claude/${plugin.name}/commands:`,
|
|
59
|
-
only: plugin.only,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Skills
|
|
63
|
-
promptDirs.push({
|
|
64
|
-
dir: path.join(plugin.path, "skills"),
|
|
65
|
-
idPrefix: `claude/${plugin.name}/skills:`,
|
|
66
|
-
only: plugin.only,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** @type {Map<string, Prompt>} */
|
|
72
|
-
const prompts = new Map();
|
|
73
|
-
|
|
74
|
-
for (const { dir, idPrefix, only } of promptDirs) {
|
|
75
|
-
const files = await getMarkdownFiles(dir).catch((err) => {
|
|
76
|
-
if (err.code !== "ENOENT") {
|
|
77
|
-
console.warn(`Failed to list prompts in ${dir}:`, err);
|
|
78
|
-
}
|
|
79
|
-
return [];
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
for (const file of files) {
|
|
83
|
-
const fullPath = path.join(dir, file);
|
|
84
|
-
const content = await fs.readFile(fullPath, "utf-8").catch((err) => {
|
|
85
|
-
console.warn(`Failed to read prompt file ${fullPath}:`, err);
|
|
86
|
-
return null;
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (content === null) continue;
|
|
90
|
-
|
|
91
|
-
// Filter by only pattern if specified
|
|
92
|
-
if (only && !only.test(file)) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Ignore all files in the skills/ directory except for SKILL.md.
|
|
97
|
-
if (fullPath.match(/\/skills\//) && !file.endsWith("/SKILL.md")) {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let prompt = parsePrompt(file, content, fullPath, idPrefix);
|
|
102
|
-
if (prompt.import) {
|
|
103
|
-
prompt = await mergeRemotePrompt(prompt, file, fullPath);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (prompt.userInvocable === false) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
prompts.set(prompt.id, prompt);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return prompts;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Merges a remote prompt into a local prompt if an import URL is provided.
|
|
119
|
-
* @param {Prompt} localPrompt
|
|
120
|
-
* @param {string} relativePath
|
|
121
|
-
* @param {string} fullPath
|
|
122
|
-
* @returns {Promise<Prompt>}
|
|
123
|
-
*/
|
|
124
|
-
async function mergeRemotePrompt(localPrompt, relativePath, fullPath) {
|
|
125
|
-
const importUrl = localPrompt.import;
|
|
126
|
-
if (!importUrl) {
|
|
127
|
-
return localPrompt;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const fetchedContent = await fetchAndCachePrompt(importUrl).catch((err) => {
|
|
131
|
-
console.warn(`Failed to fetch prompt from ${importUrl}:`, err);
|
|
132
|
-
return null;
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
if (!fetchedContent) {
|
|
136
|
-
return localPrompt;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const remotePrompt = parsePrompt(relativePath, fetchedContent, fullPath);
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
...remotePrompt,
|
|
143
|
-
...localPrompt, // Local overrides
|
|
144
|
-
content: `${remotePrompt.content}\n\n---\n\n${localPrompt.content}`.trim(),
|
|
145
|
-
description: localPrompt.description || remotePrompt.description || "",
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Fetch a prompt from a URL and cache it.
|
|
151
|
-
* @param {string} url
|
|
152
|
-
* @returns {Promise<string>}
|
|
153
|
-
*/
|
|
154
|
-
async function fetchAndCachePrompt(url) {
|
|
155
|
-
const hash = crypto.createHash("sha256").update(url).digest("hex");
|
|
156
|
-
const cacheDir = path.join(AGENT_CACHE_DIR, "prompts");
|
|
157
|
-
const cachePath = path.join(cacheDir, hash);
|
|
158
|
-
|
|
159
|
-
const cachedContent = await fs.readFile(cachePath, "utf-8").catch(() => null);
|
|
160
|
-
if (cachedContent !== null) {
|
|
161
|
-
return cachedContent;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const fetchedContent = await fetchContent(url);
|
|
165
|
-
|
|
166
|
-
// Attempt to cache, but don't block or fail on errors
|
|
167
|
-
fs.mkdir(cacheDir, { recursive: true })
|
|
168
|
-
.then(() => fs.writeFile(cachePath, fetchedContent, "utf-8"))
|
|
169
|
-
.catch((err) => {
|
|
170
|
-
console.warn(`Failed to write cache for ${url}:`, err);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
return fetchedContent;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Fetch content from a URL.
|
|
178
|
-
* @param {string} url
|
|
179
|
-
* @returns {Promise<string>}
|
|
180
|
-
*/
|
|
181
|
-
async function fetchContent(url) {
|
|
182
|
-
const githubMatch = url.match(
|
|
183
|
-
/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/,
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
if (githubMatch) {
|
|
187
|
-
const [, owner, repo, ref, path] = githubMatch;
|
|
188
|
-
const apiUrl = `repos/${owner}/${repo}/contents/${path}?ref=${ref}`;
|
|
189
|
-
try {
|
|
190
|
-
return execFileSync(
|
|
191
|
-
"gh",
|
|
192
|
-
["api", "-H", "Accept: application/vnd.github.v3.raw", apiUrl],
|
|
193
|
-
{ encoding: "utf-8" },
|
|
194
|
-
);
|
|
195
|
-
} catch (err) {
|
|
196
|
-
throw new Error(`Failed to fetch from GitHub via gh CLI: ${err}`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const response = await fetch(url);
|
|
201
|
-
if (!response.ok) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
`Failed to fetch prompt from ${url}: ${response.status} ${response.statusText}`,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
return response.text();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Recursively get all markdown files in a directory.
|
|
211
|
-
* @param {string} dir
|
|
212
|
-
* @param {string} [baseDir]
|
|
213
|
-
* @returns {Promise<string[]>}
|
|
214
|
-
*/
|
|
215
|
-
async function getMarkdownFiles(dir, baseDir = dir) {
|
|
216
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
217
|
-
const files = [];
|
|
218
|
-
|
|
219
|
-
for (const entry of entries) {
|
|
220
|
-
const fullPath = path.join(dir, entry.name);
|
|
221
|
-
let isDirectory = entry.isDirectory();
|
|
222
|
-
let isFile = entry.isFile();
|
|
223
|
-
|
|
224
|
-
if (entry.isSymbolicLink()) {
|
|
225
|
-
const stat = await fs.stat(fullPath).catch(() => null);
|
|
226
|
-
if (!stat) continue;
|
|
227
|
-
isDirectory = stat.isDirectory();
|
|
228
|
-
isFile = stat.isFile();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (isDirectory) {
|
|
232
|
-
files.push(...(await getMarkdownFiles(fullPath, baseDir)));
|
|
233
|
-
} else if (isFile && entry.name.endsWith(".md")) {
|
|
234
|
-
files.push(path.relative(baseDir, fullPath));
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return files;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Parse a prompt file content.
|
|
243
|
-
* @param {string} relativePath
|
|
244
|
-
* @param {string} fileContent
|
|
245
|
-
* @param {string} fullPath
|
|
246
|
-
* @param {string} [idPrefix=""]
|
|
247
|
-
* @returns {Prompt}
|
|
248
|
-
*/
|
|
249
|
-
function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
|
|
250
|
-
const rawId = relativePath.replace(/\/SKILL\.md$/, "").replace(/\.md$/, "");
|
|
251
|
-
const isSkill = relativePath.endsWith("SKILL.md");
|
|
252
|
-
const isShortcut = rawId.startsWith("shortcuts/");
|
|
253
|
-
const id = isShortcut
|
|
254
|
-
? idPrefix + rawId.replace(/^shortcuts\//, "")
|
|
255
|
-
: idPrefix + rawId;
|
|
256
|
-
const claudeOriginated = idPrefix.startsWith("claude");
|
|
257
|
-
|
|
258
|
-
// Match YAML frontmatter
|
|
259
|
-
const match = fileContent.match(
|
|
260
|
-
/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
if (!match) {
|
|
264
|
-
return {
|
|
265
|
-
id,
|
|
266
|
-
description: "",
|
|
267
|
-
content: fileContent.trim(),
|
|
268
|
-
filePath: fullPath,
|
|
269
|
-
claudeOriginated,
|
|
270
|
-
isShortcut,
|
|
271
|
-
isSkill,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const content = match[2].trim();
|
|
276
|
-
|
|
277
|
-
/** @type {{description?:string; import?:string; "user-invocable"?:boolean}} */
|
|
278
|
-
let frontmatter;
|
|
279
|
-
try {
|
|
280
|
-
frontmatter =
|
|
281
|
-
/** @type {{description?:string; import?:string; "user-invocable"?:boolean}} */ (
|
|
282
|
-
parseYaml(match[1])
|
|
283
|
-
);
|
|
284
|
-
} catch (_err) {
|
|
285
|
-
return {
|
|
286
|
-
id,
|
|
287
|
-
description: parseFrontmatterField(match[1], "description") ?? "",
|
|
288
|
-
content,
|
|
289
|
-
filePath: fullPath,
|
|
290
|
-
claudeOriginated,
|
|
291
|
-
import: parseFrontmatterField(match[1], "import"),
|
|
292
|
-
userInvocable:
|
|
293
|
-
parseFrontmatterField(match[1], "user-invocable") === "true" ||
|
|
294
|
-
undefined,
|
|
295
|
-
isShortcut,
|
|
296
|
-
isSkill,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
const userInvocable = frontmatter["user-invocable"];
|
|
300
|
-
|
|
301
|
-
return {
|
|
302
|
-
id,
|
|
303
|
-
description: frontmatter.description ?? "",
|
|
304
|
-
content,
|
|
305
|
-
filePath: fullPath,
|
|
306
|
-
claudeOriginated,
|
|
307
|
-
import: frontmatter.import,
|
|
308
|
-
userInvocable: userInvocable ?? undefined,
|
|
309
|
-
isShortcut,
|
|
310
|
-
isSkill: relativePath.endsWith("SKILL.md"),
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Parse a field from YAML frontmatter.
|
|
316
|
-
* @param {string} frontmatter
|
|
317
|
-
* @param {string} field
|
|
318
|
-
* @returns {string | undefined}
|
|
319
|
-
*/
|
|
320
|
-
function parseFrontmatterField(frontmatter, field) {
|
|
321
|
-
const regex = new RegExp(`^${field}:\\s*(.*)$`, "m");
|
|
322
|
-
const match = frontmatter.match(regex);
|
|
323
|
-
return match ? match[1].trim() : undefined;
|
|
324
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { MessageContentText, MessageContentImage } from "../model";
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { styleText } from "node:util";
|
|
8
|
-
import { noThrow } from "../utils/noThrow.mjs";
|
|
9
|
-
import { parseFileRange } from "../utils/parseFileRange.mjs";
|
|
10
|
-
import { readFileRange } from "../utils/readFileRange.mjs";
|
|
11
|
-
|
|
12
|
-
/** @type {ReadonlyMap<string, string>} */
|
|
13
|
-
const IMAGE_MIME_TYPES = new Map([
|
|
14
|
-
[".png", "image/png"],
|
|
15
|
-
[".jpg", "image/jpeg"],
|
|
16
|
-
[".jpeg", "image/jpeg"],
|
|
17
|
-
[".gif", "image/gif"],
|
|
18
|
-
[".webp", "image/webp"],
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {string} message
|
|
23
|
-
* @returns {Promise<(MessageContentText | MessageContentImage)[]>}
|
|
24
|
-
*/
|
|
25
|
-
export async function loadUserMessageContext(message) {
|
|
26
|
-
const workingDir = process.cwd();
|
|
27
|
-
|
|
28
|
-
/** @type {string[]} */
|
|
29
|
-
const text = [];
|
|
30
|
-
/** @type {string[]} */
|
|
31
|
-
const contexts = [];
|
|
32
|
-
/** @type {MessageContentImage[]} */
|
|
33
|
-
const images = [];
|
|
34
|
-
|
|
35
|
-
let cursor = 0;
|
|
36
|
-
for (const match of message.matchAll(
|
|
37
|
-
/(^|\s)@(?:'([^']+)'|((?:\\ |[^\s])+))/g,
|
|
38
|
-
)) {
|
|
39
|
-
if (cursor < match.index) {
|
|
40
|
-
text.push(message.slice(cursor, match.index));
|
|
41
|
-
}
|
|
42
|
-
cursor = match.index + match[0].length;
|
|
43
|
-
const [entireMatch, leading, quoted, escaped] = match;
|
|
44
|
-
const reference = quoted ?? escaped.replace(/\\ /g, " ");
|
|
45
|
-
|
|
46
|
-
const ext = path.extname(reference).toLowerCase();
|
|
47
|
-
if (IMAGE_MIME_TYPES.has(ext)) {
|
|
48
|
-
const imageContent = await loadImageContent(reference);
|
|
49
|
-
if (imageContent instanceof Error) {
|
|
50
|
-
warn(`Failed to load image from ${reference}: ${imageContent.message}`);
|
|
51
|
-
text.push(entireMatch);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
images.push(imageContent);
|
|
55
|
-
text.push(`${leading}[Image #${images.length}:${reference}]`);
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const contextSnippet = await loadContextSnippet(reference, workingDir);
|
|
60
|
-
if (contextSnippet) {
|
|
61
|
-
contexts.push(contextSnippet);
|
|
62
|
-
}
|
|
63
|
-
text.push(entireMatch);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (cursor < message.length) {
|
|
67
|
-
text.push(message.slice(cursor));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return [
|
|
71
|
-
{ type: "text", text: [text.join(""), ...contexts].join("\n\n") },
|
|
72
|
-
...images,
|
|
73
|
-
];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* @param {string} reference
|
|
78
|
-
* @param {string} workingDir
|
|
79
|
-
* @returns {Promise<string | null>}
|
|
80
|
-
*/
|
|
81
|
-
async function loadContextSnippet(reference, workingDir) {
|
|
82
|
-
const fileRange = parseFileRange(reference);
|
|
83
|
-
if (fileRange instanceof Error) {
|
|
84
|
-
warn(`Failed to parse context reference ${reference}: ${fileRange}`);
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const absolutePath = path.resolve(fileRange.filePath);
|
|
89
|
-
const relativePath = path.relative(workingDir, absolutePath);
|
|
90
|
-
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
91
|
-
warn(
|
|
92
|
-
`Refusing to load context from outside working directory: ${absolutePath}`,
|
|
93
|
-
);
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const fileContent = await readFileRange(fileRange);
|
|
98
|
-
if (fileContent instanceof Error) {
|
|
99
|
-
warn(`Failed to load context from ${reference}: ${fileContent}`);
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return [`<context location="${reference}">`, fileContent, "</context>"].join(
|
|
104
|
-
"\n",
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @param {string} imagePath
|
|
110
|
-
* @returns {Promise<MessageContentImage | Error>}
|
|
111
|
-
*/
|
|
112
|
-
async function loadImageContent(imagePath) {
|
|
113
|
-
const absolutePath = path.resolve(imagePath);
|
|
114
|
-
|
|
115
|
-
return await noThrow(async () => {
|
|
116
|
-
const data = await readFile(absolutePath);
|
|
117
|
-
return {
|
|
118
|
-
type: "image",
|
|
119
|
-
data: data.toString("base64"),
|
|
120
|
-
mimeType: inferMimeType(absolutePath),
|
|
121
|
-
};
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* @param {string} filePath
|
|
127
|
-
* @returns {string}
|
|
128
|
-
*/
|
|
129
|
-
function inferMimeType(filePath) {
|
|
130
|
-
const extension = path.extname(filePath).toLowerCase();
|
|
131
|
-
const mimeType = IMAGE_MIME_TYPES.get(extension);
|
|
132
|
-
if (!mimeType) {
|
|
133
|
-
throw new Error(
|
|
134
|
-
`Unsupported image extension: ${extension} (file: ${filePath})`,
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return mimeType;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* @param {string} message
|
|
143
|
-
* @returns {void}
|
|
144
|
-
*/
|
|
145
|
-
function warn(message) {
|
|
146
|
-
console.warn(styleText("yellow", message));
|
|
147
|
-
}
|
package/src/costTracker.mjs
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { ProviderTokenUsage } from "./model"
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @typedef {Object} TokenBreakdown
|
|
7
|
-
* @property {number} tokens - Token count
|
|
8
|
-
* @property {number | undefined} cost - Cost (undefined if no pricing)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @typedef {Object} CostSummary
|
|
13
|
-
* @property {string} currency - Currency code (e.g., "USD")
|
|
14
|
-
* @property {string} unit - Unit size (e.g., "1M")
|
|
15
|
-
* @property {Record<string, TokenBreakdown>} breakdown - Token breakdown
|
|
16
|
-
* @property {number | undefined} totalCost - Total cost (undefined if no pricing)
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @typedef {Object} CostConfig
|
|
21
|
-
* @property {string} currency
|
|
22
|
-
* @property {string} unit
|
|
23
|
-
* @property {Record<string, number>} costs
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @typedef {Object} CostTracker
|
|
28
|
-
* @property {(usage: ProviderTokenUsage) => void} recordUsage - Record token usage
|
|
29
|
-
* @property {() => Record<string, number>} getAggregatedUsage - Get aggregated usage
|
|
30
|
-
* @property {() => CostSummary} calculateCost - Calculate cost summary
|
|
31
|
-
* @property {() => boolean} hasUsage - Check if any usage recorded
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Validate a cost configuration object at runtime.
|
|
36
|
-
* @param {unknown} config
|
|
37
|
-
*/
|
|
38
|
-
function validateCostConfig(config) {
|
|
39
|
-
if (config === undefined) return;
|
|
40
|
-
if (typeof config !== "object" || config === null) {
|
|
41
|
-
throw new TypeError("CostConfig must be an object");
|
|
42
|
-
}
|
|
43
|
-
const c = /** @type {Record<string, unknown>} */ (config);
|
|
44
|
-
if (typeof c.currency !== "string") {
|
|
45
|
-
throw new TypeError("CostConfig.currency must be a string");
|
|
46
|
-
}
|
|
47
|
-
if (typeof c.unit !== "string") {
|
|
48
|
-
throw new TypeError("CostConfig.unit must be a string");
|
|
49
|
-
}
|
|
50
|
-
if (typeof c.costs !== "object" || c.costs === null) {
|
|
51
|
-
throw new TypeError("CostConfig.costs must be an object");
|
|
52
|
-
}
|
|
53
|
-
for (const [key, value] of Object.entries(
|
|
54
|
-
/** @type {Record<string, unknown>} */ (c.costs),
|
|
55
|
-
)) {
|
|
56
|
-
if (typeof value !== "number") {
|
|
57
|
-
throw new TypeError(
|
|
58
|
-
`CostConfig.costs["${key}"] must be a number, got ${typeof value}`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Create a cost tracker for session token usage
|
|
66
|
-
* @param {CostConfig} [costConfig] - Optional cost configuration
|
|
67
|
-
* @returns {CostTracker}
|
|
68
|
-
*/
|
|
69
|
-
export function createCostTracker(costConfig) {
|
|
70
|
-
validateCostConfig(costConfig);
|
|
71
|
-
|
|
72
|
-
/** @type {ProviderTokenUsage[]} */
|
|
73
|
-
const usageHistory = [];
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Record token usage from a provider.
|
|
77
|
-
* Throws when usage is not a non-null object.
|
|
78
|
-
* @param {ProviderTokenUsage} usage
|
|
79
|
-
* @throws {TypeError} when usage is null, undefined, or not an object
|
|
80
|
-
*/
|
|
81
|
-
function recordUsage(usage) {
|
|
82
|
-
if (typeof usage !== "object" || usage === null) {
|
|
83
|
-
throw new TypeError("usage must be a non-null object");
|
|
84
|
-
}
|
|
85
|
-
usageHistory.push(usage);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Get aggregated token usage
|
|
90
|
-
* @returns {Record<string, number>}
|
|
91
|
-
*/
|
|
92
|
-
function getAggregatedUsage() {
|
|
93
|
-
return aggregateTokens(usageHistory);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Calculate cost summary
|
|
98
|
-
* @returns {CostSummary}
|
|
99
|
-
*/
|
|
100
|
-
function calculateCost() {
|
|
101
|
-
const aggregated = aggregateTokens(usageHistory);
|
|
102
|
-
return calculateCostFromConfig(aggregated, costConfig);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Check if any usage recorded
|
|
107
|
-
* @returns {boolean}
|
|
108
|
-
*/
|
|
109
|
-
function hasUsage() {
|
|
110
|
-
return usageHistory.length > 0;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return Object.freeze({
|
|
114
|
-
recordUsage,
|
|
115
|
-
getAggregatedUsage,
|
|
116
|
-
calculateCost,
|
|
117
|
-
hasUsage,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Aggregate token usage history by key
|
|
123
|
-
* @param {ProviderTokenUsage[]} usageHistory
|
|
124
|
-
* @returns {Record<string, number>}
|
|
125
|
-
*/
|
|
126
|
-
function aggregateTokens(usageHistory) {
|
|
127
|
-
/** @type {Record<string, number>} */
|
|
128
|
-
const aggregated = {};
|
|
129
|
-
|
|
130
|
-
for (const usage of usageHistory) {
|
|
131
|
-
recursivelySumValues(usage, [], aggregated);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return aggregated;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Recursively sum numeric values in token usage
|
|
139
|
-
* @param {ProviderTokenUsage} obj
|
|
140
|
-
* @param {string[]} path
|
|
141
|
-
* @param {Record<string, number>} result
|
|
142
|
-
*/
|
|
143
|
-
function recursivelySumValues(obj, path, result) {
|
|
144
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
145
|
-
const currentPath = [...path, key];
|
|
146
|
-
const pathStr = currentPath.join(".");
|
|
147
|
-
|
|
148
|
-
if (typeof value === "number") {
|
|
149
|
-
result[pathStr] = (result[pathStr] || 0) + value;
|
|
150
|
-
} else if (
|
|
151
|
-
typeof value === "object" &&
|
|
152
|
-
value !== null &&
|
|
153
|
-
!Array.isArray(value)
|
|
154
|
-
) {
|
|
155
|
-
recursivelySumValues(value, currentPath, result);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Calculate cost from aggregated tokens and config
|
|
162
|
-
* @param {Record<string, number>} aggregated
|
|
163
|
-
* @param {CostConfig | undefined} config
|
|
164
|
-
* @returns {CostSummary}
|
|
165
|
-
*/
|
|
166
|
-
function calculateCostFromConfig(aggregated, config) {
|
|
167
|
-
/** @type {Record<string, TokenBreakdown>} */
|
|
168
|
-
const breakdown = {};
|
|
169
|
-
let totalCost = 0;
|
|
170
|
-
|
|
171
|
-
for (const [key, tokens] of Object.entries(aggregated)) {
|
|
172
|
-
breakdown[key] = Object.freeze({ tokens, cost: undefined });
|
|
173
|
-
|
|
174
|
-
if (!config?.costs?.[key]) {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const costValue = config.costs[key];
|
|
179
|
-
const unitSize = parseUnit(config.unit);
|
|
180
|
-
|
|
181
|
-
if (typeof costValue !== "number") {
|
|
182
|
-
throw new TypeError(
|
|
183
|
-
`config.costs["${key}"] must be a number, got ${typeof costValue}`,
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const cost = (tokens * costValue) / unitSize;
|
|
188
|
-
breakdown[key] = Object.freeze({ tokens, cost });
|
|
189
|
-
totalCost += cost;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return Object.freeze({
|
|
193
|
-
currency: config?.currency ?? "USD",
|
|
194
|
-
unit: config?.unit ?? "1M",
|
|
195
|
-
breakdown,
|
|
196
|
-
totalCost: config?.costs ? totalCost : undefined,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Parse unit string to number.
|
|
202
|
-
* @param {string} unit
|
|
203
|
-
* @returns {number}
|
|
204
|
-
* @throws {Error} when the unit is not recognized
|
|
205
|
-
*/
|
|
206
|
-
function parseUnit(unit) {
|
|
207
|
-
if (unit === "1M") return 1_000_000;
|
|
208
|
-
if (unit === "1K") return 1_000;
|
|
209
|
-
throw new Error(`Unknown cost unit: "${unit}"`);
|
|
210
|
-
}
|