@ikarmic/forge-core 5.1.1 → 5.2.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/dist/agent/agent.js +14 -3
- package/dist/agent/goal.d.ts +5 -8
- package/dist/agent/race-abort.d.ts +8 -0
- package/dist/agent/race-abort.js +24 -0
- package/dist/agent/stack-facts.d.ts +23 -0
- package/dist/agent/stack-facts.js +150 -0
- package/dist/code-index/parser.js +48 -3
- package/dist/github/github-tools.d.ts +2 -18
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/journey/answers-block.js +14 -1
- package/dist/journey/offer.js +2 -1
- package/dist/mcp/json-schema-to-zod.js +12 -1
- package/dist/orchestrator/index.d.ts +1 -1
- package/dist/orchestrator/index.js +1 -1
- package/dist/orchestrator/orchestrator.d.ts +45 -6
- package/dist/orchestrator/orchestrator.js +62 -12
- package/dist/orchestrator/router.d.ts +1 -1
- package/dist/orchestrator/router.js +4 -1
- package/dist/orchestrator/specialists.d.ts +6 -1
- package/dist/orchestrator/specialists.js +165 -68
- package/dist/orchestrator/types.d.ts +1 -1
- package/dist/policy/policy.d.ts +6 -59
- package/dist/pricing.d.ts +2 -0
- package/dist/pricing.js +35 -16
- package/dist/structured/schema.js +9 -12
- package/dist/tools/apply-patch.d.ts +1 -5
- package/dist/tools/ask-user.d.ts +2 -10
- package/dist/tools/ask-user.js +12 -4
- package/dist/tools/browser-tools.d.ts +15 -31
- package/dist/tools/codebase-search.d.ts +1 -7
- package/dist/tools/dependency-audit.d.ts +30 -0
- package/dist/tools/dependency-audit.js +197 -0
- package/dist/tools/edit-file.d.ts +1 -15
- package/dist/tools/fetch-url.d.ts +1 -18
- package/dist/tools/find-references.d.ts +1 -5
- package/dist/tools/find-symbol.d.ts +10 -8
- package/dist/tools/git-diff.d.ts +13 -0
- package/dist/tools/git-diff.js +85 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/list-files.d.ts +1 -19
- package/dist/tools/list-symbols.d.ts +1 -5
- package/dist/tools/memory.d.ts +23 -43
- package/dist/tools/notebook.d.ts +11 -17
- package/dist/tools/read-file.d.ts +1 -11
- package/dist/tools/read-image.d.ts +1 -12
- package/dist/tools/read-only.d.ts +1 -1
- package/dist/tools/read-only.js +2 -0
- package/dist/tools/run-command.d.ts +1 -17
- package/dist/tools/search-files.d.ts +1 -17
- package/dist/tools/sub-agent-wait.d.ts +2 -8
- package/dist/tools/sub-agent.d.ts +1 -69
- package/dist/tools/todo.d.ts +8 -20
- package/dist/tools/web-search.d.ts +1 -7
- package/dist/tools/write-file.d.ts +1 -7
- package/dist/types.d.ts +2 -0
- package/package.json +3 -3
package/dist/pricing.js
CHANGED
|
@@ -9,6 +9,9 @@ const PRICING = {
|
|
|
9
9
|
// OpenAI ----------------------------------------------------
|
|
10
10
|
'gpt-4o': { input: 2.5, output: 10 },
|
|
11
11
|
'gpt-4o-mini': { input: 0.15, output: 0.6 },
|
|
12
|
+
'gpt-4.1': { input: 2, output: 8 },
|
|
13
|
+
'gpt-4.1-mini': { input: 0.4, output: 1.6 },
|
|
14
|
+
'gpt-4.1-nano': { input: 0.1, output: 0.4 },
|
|
12
15
|
'gpt-4-turbo': { input: 10, output: 30 },
|
|
13
16
|
'gpt-4': { input: 30, output: 60 },
|
|
14
17
|
'gpt-3.5-turbo': { input: 0.5, output: 1.5 },
|
|
@@ -26,7 +29,9 @@ const PRICING = {
|
|
|
26
29
|
'claude-sonnet-4-5': { input: 3, output: 15 },
|
|
27
30
|
'claude-opus-4': { input: 15, output: 75 },
|
|
28
31
|
'claude-opus-4-1': { input: 15, output: 75 },
|
|
32
|
+
'claude-opus-4-5': { input: 5, output: 25 },
|
|
29
33
|
'claude-haiku-4': { input: 0.8, output: 4 },
|
|
34
|
+
'claude-haiku-4-5': { input: 1, output: 5 },
|
|
30
35
|
// Google ----------------------------------------------------
|
|
31
36
|
'gemini-1.5-pro': { input: 1.25, output: 5 },
|
|
32
37
|
'gemini-1.5-flash': { input: 0.075, output: 0.3 },
|
|
@@ -35,25 +40,11 @@ const PRICING = {
|
|
|
35
40
|
'gemini-2.0-flash-lite': { input: 0.025, output: 0.1 },
|
|
36
41
|
'gemini-2.5-pro': { input: 1.25, output: 10 },
|
|
37
42
|
'gemini-2.5-flash': { input: 0.3, output: 2.5 },
|
|
38
|
-
// Ollama is local: no API cost. We surface this as 0/$0.00 in the UI so
|
|
39
|
-
// users see the meter running with $0.00 instead of "unpriced" churn.
|
|
40
|
-
// List a few popular model id prefixes so substring matching resolves them.
|
|
41
|
-
'llama3.1': { input: 0, output: 0 },
|
|
42
|
-
'llama3.2': { input: 0, output: 0 },
|
|
43
|
-
'llama3.3': { input: 0, output: 0 },
|
|
44
|
-
'qwen2.5': { input: 0, output: 0 },
|
|
45
|
-
'mistral': { input: 0, output: 0 },
|
|
46
|
-
'mixtral': { input: 0, output: 0 },
|
|
47
|
-
'gemma2': { input: 0, output: 0 },
|
|
48
|
-
'codellama': { input: 0, output: 0 },
|
|
49
|
-
'deepseek-coder': { input: 0, output: 0 },
|
|
50
|
-
'phi3': { input: 0, output: 0 },
|
|
51
43
|
// OpenRouter ----------------------------------------------------
|
|
52
44
|
// OpenRouter passes through third-party models. We list the popular ones
|
|
53
45
|
// explicitly; everything else falls back to "unpriced" but still works
|
|
54
46
|
// (cost events just get suppressed). Prices track OpenRouter's published
|
|
55
47
|
// list at the time of writing.
|
|
56
|
-
'openrouter/auto': { input: 0, output: 0 }, // dynamic routing, unknown
|
|
57
48
|
'anthropic/claude-3.5-sonnet': { input: 3, output: 15 },
|
|
58
49
|
'anthropic/claude-3.5-haiku': { input: 0.8, output: 4 },
|
|
59
50
|
'anthropic/claude-3-opus': { input: 15, output: 75 },
|
|
@@ -70,6 +61,30 @@ const PRICING = {
|
|
|
70
61
|
// MiniMax --------------------------------------------------
|
|
71
62
|
'minimax-m3': { input: 0.3, output: 1.2 },
|
|
72
63
|
};
|
|
64
|
+
/**
|
|
65
|
+
* Providers that run on the person's own machine. Their models cost nothing
|
|
66
|
+
* whatever they are called, and only they may be priced at $0: a free entry
|
|
67
|
+
* keyed by a model name ("mistral") once priced the paid hosted model of the
|
|
68
|
+
* same name at $0 too, so a cost cap never stopped it.
|
|
69
|
+
*/
|
|
70
|
+
const LOCAL_PROVIDERS = new Set(['ollama', 'lmstudio']);
|
|
71
|
+
/**
|
|
72
|
+
* Whether `key` names the model `id` at `at`, rather than an older version of
|
|
73
|
+
* it or part of another word. `claude-opus-4` is inside `claude-opus-4-5` and
|
|
74
|
+
* `gpt-4` inside `gpt-4.1`, and each once priced the newer model at the older
|
|
75
|
+
* one's rate. A date (`-20241022`), revision (`-002`), size (`:7b`) or variant
|
|
76
|
+
* (`-latest`) after the key is still the same model.
|
|
77
|
+
*/
|
|
78
|
+
function namesModelAt(id, key, at) {
|
|
79
|
+
if (at > 0 && /[a-z0-9]/.test(id[at - 1]))
|
|
80
|
+
return false;
|
|
81
|
+
const rest = id.slice(at + key.length);
|
|
82
|
+
if (rest === '')
|
|
83
|
+
return true;
|
|
84
|
+
if (/^[a-z0-9]/.test(rest))
|
|
85
|
+
return false;
|
|
86
|
+
return !/^(?:\.\d|-\d{1,2}(?=$|[-.:@_]))/.test(rest);
|
|
87
|
+
}
|
|
73
88
|
/** Provider prompt-cache billing relative to the normal input rate. */
|
|
74
89
|
function cacheMultipliersFor(model) {
|
|
75
90
|
switch (model.provider) {
|
|
@@ -103,6 +118,8 @@ export function lookupPricing(model, override) {
|
|
|
103
118
|
const configured = overrideCost(model);
|
|
104
119
|
if (configured)
|
|
105
120
|
return { ...cache, ...configured, known: true };
|
|
121
|
+
if (typeof model !== 'string' && LOCAL_PROVIDERS.has(model.provider))
|
|
122
|
+
return { input: 0, output: 0, known: true };
|
|
106
123
|
const id = (typeof model === 'string' ? model : model.model).toLowerCase();
|
|
107
124
|
// Strip only the OpenRouter-style 'models/' prefix so 'models/gemini-1.5-pro'
|
|
108
125
|
// resolves the same way as 'gemini-1.5-pro'. We do NOT strip provider prefixes
|
|
@@ -115,8 +132,10 @@ export function lookupPricing(model, override) {
|
|
|
115
132
|
// Substring match: longest key wins, so 'gpt-4o-mini' beats 'gpt-4o'.
|
|
116
133
|
const keys = Object.keys(PRICING).sort((a, b) => b.length - a.length);
|
|
117
134
|
for (const key of keys) {
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
for (let at = stripped.indexOf(key); at !== -1; at = stripped.indexOf(key, at + 1)) {
|
|
136
|
+
if (namesModelAt(stripped, key, at))
|
|
137
|
+
return { ...PRICING[key], ...cache, known: true, matchedKey: key };
|
|
138
|
+
}
|
|
120
139
|
}
|
|
121
140
|
return { input: 0, output: 0, known: false };
|
|
122
141
|
}
|
|
@@ -258,36 +258,33 @@ export function formatZodError(err) {
|
|
|
258
258
|
return err.issues
|
|
259
259
|
.map((issue) => {
|
|
260
260
|
const pathStr = issue.path.length > 0 ? issue.path.join('.') : '(root)';
|
|
261
|
-
const expected = issueCode(issue
|
|
261
|
+
const expected = issueCode(issue);
|
|
262
262
|
const received = describeReceived(issue);
|
|
263
263
|
return `${pathStr}: ${issue.message}${expected ? ` (expected ${expected}${received ? `, got ${received}` : ''})` : ''}`;
|
|
264
264
|
})
|
|
265
265
|
.join('\n');
|
|
266
266
|
}
|
|
267
|
-
function issueCode(
|
|
268
|
-
switch (code) {
|
|
267
|
+
function issueCode(issue) {
|
|
268
|
+
switch (issue.code) {
|
|
269
269
|
case 'invalid_type':
|
|
270
270
|
return 'correct type';
|
|
271
|
-
case '
|
|
271
|
+
case 'invalid_format':
|
|
272
272
|
return 'string matching format';
|
|
273
273
|
case 'too_small':
|
|
274
274
|
return 'value >= min';
|
|
275
275
|
case 'too_big':
|
|
276
276
|
return 'value <= max';
|
|
277
|
-
case '
|
|
278
|
-
return 'one of the allowed values';
|
|
277
|
+
case 'invalid_value':
|
|
278
|
+
return issue.values.length === 1 ? 'the literal value' : 'one of the allowed values';
|
|
279
279
|
case 'unrecognized_keys':
|
|
280
280
|
return 'no extra keys';
|
|
281
281
|
case 'invalid_union':
|
|
282
282
|
return 'one of the union variants';
|
|
283
|
-
case 'invalid_literal':
|
|
284
|
-
return 'the literal value';
|
|
285
283
|
default:
|
|
286
284
|
return '';
|
|
287
285
|
}
|
|
288
286
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return JSON.stringify(issue.received);
|
|
287
|
+
// Zod 4 dropped `received` from the issue; its message already says "received <type>".
|
|
288
|
+
function describeReceived(_issue) {
|
|
289
|
+
return '';
|
|
293
290
|
}
|
|
@@ -2,11 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import type { ToolDefinition } from '../types.js';
|
|
3
3
|
declare const params: z.ZodObject<{
|
|
4
4
|
patch: z.ZodString;
|
|
5
|
-
},
|
|
6
|
-
patch: string;
|
|
7
|
-
}, {
|
|
8
|
-
patch: string;
|
|
9
|
-
}>;
|
|
5
|
+
}, z.core.$strip>;
|
|
10
6
|
export type PatchOp = {
|
|
11
7
|
kind: 'add';
|
|
12
8
|
path: string;
|
package/dist/tools/ask-user.d.ts
CHANGED
|
@@ -7,17 +7,9 @@ import type { ToolDefinition } from '../types.js';
|
|
|
7
7
|
*/
|
|
8
8
|
declare const params: z.ZodObject<{
|
|
9
9
|
question: z.ZodString;
|
|
10
|
-
options: z.ZodOptional<z.ZodArray<z.ZodString
|
|
10
|
+
options: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
11
11
|
allowCustom: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
-
},
|
|
13
|
-
question: string;
|
|
14
|
-
options?: string[] | undefined;
|
|
15
|
-
allowCustom?: boolean | undefined;
|
|
16
|
-
}, {
|
|
17
|
-
question: string;
|
|
18
|
-
options?: string[] | undefined;
|
|
19
|
-
allowCustom?: boolean | undefined;
|
|
20
|
-
}>;
|
|
12
|
+
}, z.core.$strip>;
|
|
21
13
|
export interface AskUserResult {
|
|
22
14
|
answer: string;
|
|
23
15
|
unavailable?: boolean;
|
package/dist/tools/ask-user.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { raceAbort } from '../agent/race-abort.js';
|
|
2
3
|
/**
|
|
3
4
|
* Ask the person a question with optional choices. The agent only offers this tool when a surface can answer
|
|
4
5
|
* (AgentConfig.askUser) and the run is not autonomous; if it is called anyway, or the run is stopped while the
|
|
@@ -45,16 +46,23 @@ export const askUserTool = {
|
|
|
45
46
|
ctx.askBudget.used += 1;
|
|
46
47
|
}
|
|
47
48
|
const budget = ctx.askBudget;
|
|
49
|
+
const refund = () => {
|
|
50
|
+
if (budget)
|
|
51
|
+
budget.used = Math.max(0, budget.used - 1);
|
|
52
|
+
};
|
|
48
53
|
const answered = ctx.askUser(args, signal).then((answer) => {
|
|
49
54
|
// A question the person dismissed resolves to an empty answer and must not
|
|
50
55
|
// cost part of the budget: four dismissals would otherwise end an interview
|
|
51
56
|
// without a single question being answered. Refunding keeps the budget a
|
|
52
57
|
// count of questions actually answered.
|
|
53
|
-
if (
|
|
54
|
-
|
|
58
|
+
if (answer.trim() === '')
|
|
59
|
+
refund();
|
|
55
60
|
return { answer };
|
|
61
|
+
}, (err) => {
|
|
62
|
+
// A surface that failed never showed the question, so it costs nothing either.
|
|
63
|
+
refund();
|
|
64
|
+
throw err;
|
|
56
65
|
});
|
|
57
|
-
|
|
58
|
-
return Promise.race([answered, aborted]);
|
|
66
|
+
return raceAbort(answered, signal, UNAVAILABLE);
|
|
59
67
|
},
|
|
60
68
|
};
|
|
@@ -4,15 +4,15 @@ import type { ToolContext, ToolDefinition } from '../types.js';
|
|
|
4
4
|
/** Throws unless `rawUrl` is an http(s) URL the browser may visit. */
|
|
5
5
|
export declare function assertBrowsableUrl(rawUrl: string, policy: ToolContext['policy']): Promise<void>;
|
|
6
6
|
declare const observeParams: z.ZodObject<{
|
|
7
|
-
action: z.ZodEnum<
|
|
7
|
+
action: z.ZodEnum<{
|
|
8
|
+
close: "close";
|
|
9
|
+
text: "text";
|
|
10
|
+
open: "open";
|
|
11
|
+
screenshot: "screenshot";
|
|
12
|
+
console: "console";
|
|
13
|
+
}>;
|
|
8
14
|
url: z.ZodOptional<z.ZodString>;
|
|
9
|
-
},
|
|
10
|
-
action: "close" | "text" | "open" | "screenshot" | "console";
|
|
11
|
-
url?: string | undefined;
|
|
12
|
-
}, {
|
|
13
|
-
action: "close" | "text" | "open" | "screenshot" | "console";
|
|
14
|
-
url?: string | undefined;
|
|
15
|
-
}>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
16
|
export type BrowserResult = {
|
|
17
17
|
action: 'open';
|
|
18
18
|
url: string;
|
|
@@ -48,30 +48,14 @@ export declare function createBrowserTools(session: BrowserSession): [
|
|
|
48
48
|
note?: string;
|
|
49
49
|
}>
|
|
50
50
|
];
|
|
51
|
-
declare const actParams: z.
|
|
52
|
-
action: z.ZodEnum<
|
|
51
|
+
declare const actParams: z.ZodObject<{
|
|
52
|
+
action: z.ZodEnum<{
|
|
53
|
+
type: "type";
|
|
54
|
+
click: "click";
|
|
55
|
+
evaluate: "evaluate";
|
|
56
|
+
}>;
|
|
53
57
|
selector: z.ZodOptional<z.ZodString>;
|
|
54
58
|
text: z.ZodOptional<z.ZodString>;
|
|
55
59
|
expression: z.ZodOptional<z.ZodString>;
|
|
56
|
-
},
|
|
57
|
-
action: "type" | "click" | "evaluate";
|
|
58
|
-
text?: string | undefined;
|
|
59
|
-
expression?: string | undefined;
|
|
60
|
-
selector?: string | undefined;
|
|
61
|
-
}, {
|
|
62
|
-
action: "type" | "click" | "evaluate";
|
|
63
|
-
text?: string | undefined;
|
|
64
|
-
expression?: string | undefined;
|
|
65
|
-
selector?: string | undefined;
|
|
66
|
-
}>, {
|
|
67
|
-
action: "type" | "click" | "evaluate";
|
|
68
|
-
text?: string | undefined;
|
|
69
|
-
expression?: string | undefined;
|
|
70
|
-
selector?: string | undefined;
|
|
71
|
-
}, {
|
|
72
|
-
action: "type" | "click" | "evaluate";
|
|
73
|
-
text?: string | undefined;
|
|
74
|
-
expression?: string | undefined;
|
|
75
|
-
selector?: string | undefined;
|
|
76
|
-
}>;
|
|
60
|
+
}, z.core.$strip>;
|
|
77
61
|
export {};
|
|
@@ -5,13 +5,7 @@ import type { ToolDefinition } from '../types.js';
|
|
|
5
5
|
declare const params: z.ZodObject<{
|
|
6
6
|
query: z.ZodString;
|
|
7
7
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
8
|
-
},
|
|
9
|
-
query: string;
|
|
10
|
-
limit?: number | undefined;
|
|
11
|
-
}, {
|
|
12
|
-
query: string;
|
|
13
|
-
limit?: number | undefined;
|
|
14
|
-
}>;
|
|
8
|
+
}, z.core.$strip>;
|
|
15
9
|
export interface CodebaseSearchResult {
|
|
16
10
|
query: string;
|
|
17
11
|
embedder: string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ToolDefinition } from '../types.js';
|
|
3
|
+
export declare const AUDIT_TIMEOUT_MS = 60000;
|
|
4
|
+
export declare const MAX_AUDIT_CHARS = 60000;
|
|
5
|
+
export interface AuditFinding {
|
|
6
|
+
package: string;
|
|
7
|
+
installed?: string;
|
|
8
|
+
advisory: string;
|
|
9
|
+
severity: string;
|
|
10
|
+
fixedIn?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AuditorPlan {
|
|
13
|
+
ecosystem: 'npm' | 'pnpm' | 'yarn' | 'python' | 'cargo' | 'go';
|
|
14
|
+
program: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
lockfile: string;
|
|
17
|
+
}
|
|
18
|
+
/** The auditors for the lockfiles at the workspace root, in a fixed order. */
|
|
19
|
+
export declare function pickAuditors(cwd: string): AuditorPlan[];
|
|
20
|
+
/** npm 7+ (`auditReportVersion: 2`): one finding per package with its own advisory; entries that only inherit one are skipped. */
|
|
21
|
+
export declare function parseNpmAudit(text: string): AuditFinding[];
|
|
22
|
+
/** pnpm (npm 6 format): `advisories` keyed by id. */
|
|
23
|
+
export declare function parsePnpmAudit(text: string): AuditFinding[];
|
|
24
|
+
/** pip-audit `-f json`: `{ dependencies: [...] }` (older versions print the list itself). pip-audit reports no severity. */
|
|
25
|
+
export declare function parsePipAudit(text: string): AuditFinding[];
|
|
26
|
+
/** cargo-audit `--json`: `vulnerabilities.list`. */
|
|
27
|
+
export declare function parseCargoAudit(text: string): AuditFinding[];
|
|
28
|
+
declare const params: z.ZodObject<{}, z.core.$strip>;
|
|
29
|
+
export declare const dependencyAuditTool: ToolDefinition<z.infer<typeof params>, string>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dependency_audit`: known vulnerabilities in the workspace's dependencies. The auditor is chosen from the
|
|
3
|
+
* lockfiles at the workspace root and run with fixed arguments; the model chooses nothing. It still asks first
|
|
4
|
+
* (RB-41): an auditor follows the repository's own settings, so `yarnPath` in .yarnrc.yml runs a script the
|
|
5
|
+
* repository ships, pip-audit builds sdists, and a cargo alias can replace `cargo audit`. The auditors also
|
|
6
|
+
* send the dependency list to their registry's advisory service, which the description says.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { resolveOnPath } from '../lsp/client.js';
|
|
13
|
+
import { shellEnv } from './shell-env.js';
|
|
14
|
+
export const AUDIT_TIMEOUT_MS = 60_000;
|
|
15
|
+
export const MAX_AUDIT_CHARS = 60_000;
|
|
16
|
+
const INSTALL_HINT = {
|
|
17
|
+
npm: 'install Node.js, which ships npm',
|
|
18
|
+
pnpm: 'npm install -g pnpm',
|
|
19
|
+
yarn: 'npm install -g yarn',
|
|
20
|
+
python: 'pip install pip-audit',
|
|
21
|
+
cargo: 'cargo install cargo-audit',
|
|
22
|
+
go: 'go install golang.org/x/vuln/cmd/govulncheck@latest',
|
|
23
|
+
};
|
|
24
|
+
/** A requirements file name that is safe to hand to a program a shell may start (npm-style `.cmd` shims on Windows). */
|
|
25
|
+
const REQUIREMENTS = /^requirements[A-Za-z0-9._-]*\.txt$/;
|
|
26
|
+
/** The auditors for the lockfiles at the workspace root, in a fixed order. */
|
|
27
|
+
export function pickAuditors(cwd) {
|
|
28
|
+
const has = (name) => existsSync(path.join(cwd, name));
|
|
29
|
+
const plans = [];
|
|
30
|
+
if (has('package-lock.json'))
|
|
31
|
+
plans.push({ ecosystem: 'npm', program: 'npm', args: ['audit', '--json'], lockfile: 'package-lock.json' });
|
|
32
|
+
if (has('pnpm-lock.yaml'))
|
|
33
|
+
plans.push({ ecosystem: 'pnpm', program: 'pnpm', args: ['audit', '--json'], lockfile: 'pnpm-lock.yaml' });
|
|
34
|
+
if (has('yarn.lock')) {
|
|
35
|
+
let berry = false;
|
|
36
|
+
try {
|
|
37
|
+
berry = readFileSync(path.join(cwd, 'yarn.lock'), 'utf8').includes('__metadata:');
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Unreadable: treat as classic.
|
|
41
|
+
}
|
|
42
|
+
plans.push({ ecosystem: 'yarn', program: 'yarn', args: berry ? ['npm', 'audit', '--json', '--recursive'] : ['audit', '--json'], lockfile: 'yarn.lock' });
|
|
43
|
+
}
|
|
44
|
+
let entries = [];
|
|
45
|
+
try {
|
|
46
|
+
entries = readdirSync(cwd).sort();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// An unreadable folder has no requirements files.
|
|
50
|
+
}
|
|
51
|
+
for (const name of entries.filter((n) => REQUIREMENTS.test(n))) {
|
|
52
|
+
plans.push({ ecosystem: 'python', program: 'pip-audit', args: ['-f', 'json', '-r', name], lockfile: name });
|
|
53
|
+
}
|
|
54
|
+
const pyLock = ['poetry.lock', 'uv.lock'].find(has);
|
|
55
|
+
if (pyLock)
|
|
56
|
+
plans.push({ ecosystem: 'python', program: 'pip-audit', args: ['-f', 'json', '.'], lockfile: pyLock });
|
|
57
|
+
if (has('Cargo.lock'))
|
|
58
|
+
plans.push({ ecosystem: 'cargo', program: 'cargo', args: ['audit', '--json'], lockfile: 'Cargo.lock' });
|
|
59
|
+
if (has('go.sum'))
|
|
60
|
+
plans.push({ ecosystem: 'go', program: 'govulncheck', args: ['-json', './...'], lockfile: 'go.sum' });
|
|
61
|
+
return plans;
|
|
62
|
+
}
|
|
63
|
+
const rec = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? v : {});
|
|
64
|
+
const str = (v) => (typeof v === 'string' && v ? v : undefined);
|
|
65
|
+
function parseJson(text) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(text);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function finding(f) {
|
|
74
|
+
return Object.fromEntries(Object.entries(f).filter(([, v]) => v !== undefined));
|
|
75
|
+
}
|
|
76
|
+
/** npm 7+ (`auditReportVersion: 2`): one finding per package with its own advisory; entries that only inherit one are skipped. */
|
|
77
|
+
export function parseNpmAudit(text) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const [name, raw] of Object.entries(rec(rec(parseJson(text)).vulnerabilities))) {
|
|
80
|
+
const v = rec(raw);
|
|
81
|
+
const via = Array.isArray(v.via) ? v.via.map(rec).find((x) => str(x.url) || str(x.title)) : undefined;
|
|
82
|
+
if (!via)
|
|
83
|
+
continue;
|
|
84
|
+
const url = str(via.url) ?? '';
|
|
85
|
+
const fix = rec(v.fixAvailable);
|
|
86
|
+
out.push(finding({
|
|
87
|
+
package: name,
|
|
88
|
+
advisory: url.match(/GHSA-[a-z0-9-]+/i)?.[0] ?? str(via.title) ?? url,
|
|
89
|
+
severity: str(v.severity) ?? 'unknown',
|
|
90
|
+
fixedIn: str(fix.name) === name ? str(fix.version) : undefined,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** pnpm (npm 6 format): `advisories` keyed by id. */
|
|
96
|
+
export function parsePnpmAudit(text) {
|
|
97
|
+
return Object.values(rec(rec(parseJson(text)).advisories)).map((raw) => {
|
|
98
|
+
const a = rec(raw);
|
|
99
|
+
const first = Array.isArray(a.findings) ? rec(a.findings[0]) : {};
|
|
100
|
+
return finding({
|
|
101
|
+
package: str(a.module_name) ?? 'unknown',
|
|
102
|
+
installed: str(first.version),
|
|
103
|
+
advisory: str(a.github_advisory_id) ?? str(a.url) ?? str(a.title) ?? 'unknown',
|
|
104
|
+
severity: str(a.severity) ?? 'unknown',
|
|
105
|
+
fixedIn: str(a.patched_versions),
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** pip-audit `-f json`: `{ dependencies: [...] }` (older versions print the list itself). pip-audit reports no severity. */
|
|
110
|
+
export function parsePipAudit(text) {
|
|
111
|
+
const parsed = parseJson(text);
|
|
112
|
+
const deps = Array.isArray(parsed) ? parsed : rec(parsed).dependencies;
|
|
113
|
+
if (!Array.isArray(deps))
|
|
114
|
+
return [];
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const d of deps.map(rec)) {
|
|
117
|
+
for (const v of Array.isArray(d.vulns) ? d.vulns.map(rec) : []) {
|
|
118
|
+
const fixes = Array.isArray(v.fix_versions) ? v.fix_versions.filter((x) => typeof x === 'string') : [];
|
|
119
|
+
out.push(finding({ package: str(d.name) ?? 'unknown', installed: str(d.version), advisory: str(v.id) ?? 'unknown', severity: 'unknown', fixedIn: fixes.join(', ') || undefined }));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** cargo-audit `--json`: `vulnerabilities.list`. */
|
|
125
|
+
export function parseCargoAudit(text) {
|
|
126
|
+
const list = rec(rec(parseJson(text)).vulnerabilities).list;
|
|
127
|
+
if (!Array.isArray(list))
|
|
128
|
+
return [];
|
|
129
|
+
return list.map(rec).map((v) => {
|
|
130
|
+
const adv = rec(v.advisory);
|
|
131
|
+
const pkg = rec(v.package);
|
|
132
|
+
const patched = rec(v.versions).patched;
|
|
133
|
+
return finding({
|
|
134
|
+
package: str(pkg.name) ?? str(adv.package) ?? 'unknown',
|
|
135
|
+
installed: str(pkg.version),
|
|
136
|
+
advisory: str(adv.id) ?? 'unknown',
|
|
137
|
+
severity: str(adv.severity) ?? 'unknown',
|
|
138
|
+
fixedIn: Array.isArray(patched) ? patched.filter((x) => typeof x === 'string').join(', ') || undefined : undefined,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const PARSERS = {
|
|
143
|
+
npm: parseNpmAudit,
|
|
144
|
+
pnpm: parsePnpmAudit,
|
|
145
|
+
python: parsePipAudit,
|
|
146
|
+
cargo: parseCargoAudit,
|
|
147
|
+
};
|
|
148
|
+
function runAuditor(program, args, cwd) {
|
|
149
|
+
// npm-style `.cmd` shims need a shell on Windows; the arguments are fixed or checked above, never the model's.
|
|
150
|
+
const shell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(program);
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
execFile(shell ? `"${program}"` : program, args, { cwd, env: shellEnv(process.env), shell, timeout: AUDIT_TIMEOUT_MS, maxBuffer: 64 * 1024 * 1024, windowsHide: true }, (err, stdout, stderr) => {
|
|
153
|
+
// Auditors exit non-zero when they find something: the JSON on stdout is still the answer.
|
|
154
|
+
if (err && !String(stdout).trim())
|
|
155
|
+
resolve({ stdout: '', error: (String(stderr).trim() || err.message).split('\n')[0] });
|
|
156
|
+
else
|
|
157
|
+
resolve({ stdout: String(stdout) });
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function formatFindings(plan, findings) {
|
|
162
|
+
const head = `${plan.program} ${plan.args.join(' ')} (${plan.lockfile}): ${findings.length} known vulnerabilit${findings.length === 1 ? 'y' : 'ies'}`;
|
|
163
|
+
const lines = findings.map((f) => `- ${f.package}${f.installed ? ` ${f.installed}` : ''}: ${f.advisory} (${f.severity})${f.fixedIn ? `, fixed in ${f.fixedIn}` : ''}`);
|
|
164
|
+
return [head, ...lines].join('\n');
|
|
165
|
+
}
|
|
166
|
+
const params = z.object({});
|
|
167
|
+
export const dependencyAuditTool = {
|
|
168
|
+
name: 'dependency_audit',
|
|
169
|
+
description: 'Check the project dependencies for known vulnerabilities with the auditor that fits its lockfile (npm, pnpm, yarn, pip-audit, cargo audit, govulncheck). ' +
|
|
170
|
+
"It changes no files, but the auditor follows the project's own package-manager settings and can run code the project ships, so it asks first. It contacts the package registry: the dependency list is sent to its advisory service. Takes no arguments.",
|
|
171
|
+
parameters: params,
|
|
172
|
+
destructive: true,
|
|
173
|
+
execute: async (_args, ctx) => {
|
|
174
|
+
const plans = pickAuditors(ctx.cwd);
|
|
175
|
+
if (plans.length === 0)
|
|
176
|
+
return 'Nothing to audit: no lockfile or requirements file at the project root.';
|
|
177
|
+
const env = shellEnv(process.env);
|
|
178
|
+
const parts = [];
|
|
179
|
+
for (const plan of plans) {
|
|
180
|
+
const program = resolveOnPath(plan.program, env, process.platform);
|
|
181
|
+
if (!program) {
|
|
182
|
+
parts.push(`${plan.program} is not installed, so ${plan.lockfile} was not audited. To install it: ${INSTALL_HINT[plan.ecosystem]}.`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const { stdout, error } = await runAuditor(program, plan.args, ctx.cwd);
|
|
186
|
+
if (error) {
|
|
187
|
+
const missing = /no such (sub)?command|unknown command/i.test(error);
|
|
188
|
+
parts.push(missing ? `${plan.program} ${plan.args[0]} is not installed. To install it: ${INSTALL_HINT[plan.ecosystem]}.` : `${plan.program} failed: ${error}`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const parse = PARSERS[plan.ecosystem];
|
|
192
|
+
parts.push(parse ? formatFindings(plan, parse(stdout)) : `${plan.program} output (not parsed):\n${stdout}`);
|
|
193
|
+
}
|
|
194
|
+
const text = parts.join('\n\n');
|
|
195
|
+
return text.length > MAX_AUDIT_CHARS ? `${text.slice(0, MAX_AUDIT_CHARS)}\n[audit output cut: ${text.length - MAX_AUDIT_CHARS} characters left out]` : text;
|
|
196
|
+
},
|
|
197
|
+
};
|
|
@@ -4,22 +4,8 @@ declare const params: z.ZodObject<{
|
|
|
4
4
|
path: z.ZodString;
|
|
5
5
|
old_text: z.ZodString;
|
|
6
6
|
new_text: z.ZodString;
|
|
7
|
-
/**
|
|
8
|
-
* If true (default), require old_text to match exactly once. If false,
|
|
9
|
-
* replace_all mode is used and every occurrence is replaced.
|
|
10
|
-
*/
|
|
11
7
|
replace_all: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
-
},
|
|
13
|
-
path: string;
|
|
14
|
-
old_text: string;
|
|
15
|
-
new_text: string;
|
|
16
|
-
replace_all?: boolean | undefined;
|
|
17
|
-
}, {
|
|
18
|
-
path: string;
|
|
19
|
-
old_text: string;
|
|
20
|
-
new_text: string;
|
|
21
|
-
replace_all?: boolean | undefined;
|
|
22
|
-
}>;
|
|
8
|
+
}, z.core.$strip>;
|
|
23
9
|
export interface EditFileResult {
|
|
24
10
|
path: string;
|
|
25
11
|
replacements: number;
|
|
@@ -2,27 +2,10 @@ import { z } from 'zod';
|
|
|
2
2
|
import type { ToolDefinition } from '../types.js';
|
|
3
3
|
declare const params: z.ZodObject<{
|
|
4
4
|
url: z.ZodString;
|
|
5
|
-
/**
|
|
6
|
-
* Hard cap on response body size in bytes. Defaults to 200KB. The fetch
|
|
7
|
-
* is aborted as soon as the cap is exceeded, so a runaway response cannot
|
|
8
|
-
* blow up the agent's context window.
|
|
9
|
-
*/
|
|
10
5
|
max_bytes: z.ZodOptional<z.ZodNumber>;
|
|
11
|
-
/** Per-request timeout in ms. Default 10 seconds. Set 0 to disable. */
|
|
12
6
|
timeout_ms: z.ZodOptional<z.ZodNumber>;
|
|
13
|
-
/** Follow HTTP redirects. Default true. */
|
|
14
7
|
follow_redirects: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
-
},
|
|
16
|
-
url: string;
|
|
17
|
-
timeout_ms?: number | undefined;
|
|
18
|
-
max_bytes?: number | undefined;
|
|
19
|
-
follow_redirects?: boolean | undefined;
|
|
20
|
-
}, {
|
|
21
|
-
url: string;
|
|
22
|
-
timeout_ms?: number | undefined;
|
|
23
|
-
max_bytes?: number | undefined;
|
|
24
|
-
follow_redirects?: boolean | undefined;
|
|
25
|
-
}>;
|
|
8
|
+
}, z.core.$strip>;
|
|
26
9
|
export interface FetchUrlResult {
|
|
27
10
|
/** Final URL after redirects (may differ from the input). */
|
|
28
11
|
url: string;
|
|
@@ -2,11 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import type { ToolDefinition } from '../types.js';
|
|
3
3
|
declare const params: z.ZodObject<{
|
|
4
4
|
name: z.ZodString;
|
|
5
|
-
},
|
|
6
|
-
name: string;
|
|
7
|
-
}, {
|
|
8
|
-
name: string;
|
|
9
|
-
}>;
|
|
5
|
+
}, z.core.$strip>;
|
|
10
6
|
export interface ReferenceHit {
|
|
11
7
|
path: string;
|
|
12
8
|
line: number;
|
|
@@ -3,14 +3,16 @@ import type { SymbolKind } from '../code-index/types.js';
|
|
|
3
3
|
import type { ToolDefinition } from '../types.js';
|
|
4
4
|
declare const params: z.ZodObject<{
|
|
5
5
|
name: z.ZodString;
|
|
6
|
-
kind: z.ZodOptional<z.ZodEnum<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
kind: z.ZodOptional<z.ZodEnum<{
|
|
7
|
+
function: "function";
|
|
8
|
+
class: "class";
|
|
9
|
+
interface: "interface";
|
|
10
|
+
type: "type";
|
|
11
|
+
enum: "enum";
|
|
12
|
+
const: "const";
|
|
13
|
+
method: "method";
|
|
14
|
+
}>>;
|
|
15
|
+
}, z.core.$strip>;
|
|
14
16
|
export interface FindSymbolMatch {
|
|
15
17
|
name: string;
|
|
16
18
|
kind: SymbolKind;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ToolDefinition } from '../types.js';
|
|
3
|
+
/** The most diff text handed to the model; the rest is cut with a note. */
|
|
4
|
+
export declare const MAX_DIFF_CHARS = 60000;
|
|
5
|
+
declare const params: z.ZodObject<{
|
|
6
|
+
base: z.ZodOptional<z.ZodString>;
|
|
7
|
+
staged: z.ZodOptional<z.ZodBoolean>;
|
|
8
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
/** A ref git will read as a ref, never as an option: no leading dash, only the characters refs and ranges use. */
|
|
11
|
+
export declare function isSafeGitRef(ref: string): boolean;
|
|
12
|
+
export declare const gitDiffTool: ToolDefinition<z.infer<typeof params>, string>;
|
|
13
|
+
export {};
|