@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.
Files changed (58) hide show
  1. package/dist/agent/agent.js +14 -3
  2. package/dist/agent/goal.d.ts +5 -8
  3. package/dist/agent/race-abort.d.ts +8 -0
  4. package/dist/agent/race-abort.js +24 -0
  5. package/dist/agent/stack-facts.d.ts +23 -0
  6. package/dist/agent/stack-facts.js +150 -0
  7. package/dist/code-index/parser.js +48 -3
  8. package/dist/github/github-tools.d.ts +2 -18
  9. package/dist/index.d.ts +5 -2
  10. package/dist/index.js +5 -2
  11. package/dist/journey/answers-block.js +14 -1
  12. package/dist/journey/offer.js +2 -1
  13. package/dist/mcp/json-schema-to-zod.js +12 -1
  14. package/dist/orchestrator/index.d.ts +1 -1
  15. package/dist/orchestrator/index.js +1 -1
  16. package/dist/orchestrator/orchestrator.d.ts +45 -6
  17. package/dist/orchestrator/orchestrator.js +62 -12
  18. package/dist/orchestrator/router.d.ts +1 -1
  19. package/dist/orchestrator/router.js +4 -1
  20. package/dist/orchestrator/specialists.d.ts +6 -1
  21. package/dist/orchestrator/specialists.js +165 -68
  22. package/dist/orchestrator/types.d.ts +1 -1
  23. package/dist/policy/policy.d.ts +6 -59
  24. package/dist/pricing.d.ts +2 -0
  25. package/dist/pricing.js +35 -16
  26. package/dist/structured/schema.js +9 -12
  27. package/dist/tools/apply-patch.d.ts +1 -5
  28. package/dist/tools/ask-user.d.ts +2 -10
  29. package/dist/tools/ask-user.js +12 -4
  30. package/dist/tools/browser-tools.d.ts +15 -31
  31. package/dist/tools/codebase-search.d.ts +1 -7
  32. package/dist/tools/dependency-audit.d.ts +30 -0
  33. package/dist/tools/dependency-audit.js +197 -0
  34. package/dist/tools/edit-file.d.ts +1 -15
  35. package/dist/tools/fetch-url.d.ts +1 -18
  36. package/dist/tools/find-references.d.ts +1 -5
  37. package/dist/tools/find-symbol.d.ts +10 -8
  38. package/dist/tools/git-diff.d.ts +13 -0
  39. package/dist/tools/git-diff.js +85 -0
  40. package/dist/tools/index.d.ts +3 -1
  41. package/dist/tools/index.js +5 -1
  42. package/dist/tools/list-files.d.ts +1 -19
  43. package/dist/tools/list-symbols.d.ts +1 -5
  44. package/dist/tools/memory.d.ts +23 -43
  45. package/dist/tools/notebook.d.ts +11 -17
  46. package/dist/tools/read-file.d.ts +1 -11
  47. package/dist/tools/read-image.d.ts +1 -12
  48. package/dist/tools/read-only.d.ts +1 -1
  49. package/dist/tools/read-only.js +2 -0
  50. package/dist/tools/run-command.d.ts +1 -17
  51. package/dist/tools/search-files.d.ts +1 -17
  52. package/dist/tools/sub-agent-wait.d.ts +2 -8
  53. package/dist/tools/sub-agent.d.ts +1 -69
  54. package/dist/tools/todo.d.ts +8 -20
  55. package/dist/tools/web-search.d.ts +1 -7
  56. package/dist/tools/write-file.d.ts +1 -7
  57. package/dist/types.d.ts +2 -0
  58. 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
- if (stripped.includes(key))
119
- return { ...PRICING[key], ...cache, known: true };
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.code);
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(code) {
268
- switch (code) {
267
+ function issueCode(issue) {
268
+ switch (issue.code) {
269
269
  case 'invalid_type':
270
270
  return 'correct type';
271
- case 'invalid_string':
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 'invalid_enum_value':
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
- function describeReceived(issue) {
290
- if (issue.code !== 'invalid_type')
291
- return '';
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
- }, "strip", z.ZodTypeAny, {
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;
@@ -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, "many">>;
10
+ options: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
11
  allowCustom: z.ZodOptional<z.ZodBoolean>;
12
- }, "strip", z.ZodTypeAny, {
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;
@@ -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 (budget && answer.trim() === '')
54
- budget.used = Math.max(0, budget.used - 1);
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
- const aborted = new Promise((resolve) => signal?.addEventListener('abort', () => resolve(UNAVAILABLE), { once: true }));
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<["open", "screenshot", "text", "console", "close"]>;
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
- }, "strip", z.ZodTypeAny, {
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.ZodEffects<z.ZodObject<{
52
- action: z.ZodEnum<["click", "type", "evaluate"]>;
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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
- }, "strip", z.ZodTypeAny, {
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<["function", "class", "interface", "type", "enum", "const", "method"]>>;
7
- }, "strip", z.ZodTypeAny, {
8
- name: string;
9
- kind?: "function" | "class" | "interface" | "type" | "enum" | "const" | "method" | undefined;
10
- }, {
11
- name: string;
12
- kind?: "function" | "class" | "interface" | "type" | "enum" | "const" | "method" | undefined;
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 {};