@ikarmic/forge-core 5.1.1 → 5.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.
Files changed (81) hide show
  1. package/dist/agent/agent.js +208 -57
  2. package/dist/agent/enhance-prompt.js +2 -1
  3. package/dist/agent/goal.d.ts +5 -8
  4. package/dist/agent/prompts.d.ts +15 -0
  5. package/dist/agent/prompts.js +34 -0
  6. package/dist/agent/race-abort.d.ts +8 -0
  7. package/dist/agent/race-abort.js +24 -0
  8. package/dist/agent/retry.d.ts +50 -0
  9. package/dist/agent/retry.js +192 -0
  10. package/dist/agent/shell-command.d.ts +23 -0
  11. package/dist/agent/shell-command.js +93 -0
  12. package/dist/agent/stack-facts.d.ts +23 -0
  13. package/dist/agent/stack-facts.js +150 -0
  14. package/dist/catalog/models-dev-snapshot.d.ts +1 -0
  15. package/dist/catalog/models-dev-snapshot.js +3 -0
  16. package/dist/catalog/models-dev.d.ts +35 -0
  17. package/dist/catalog/models-dev.js +143 -0
  18. package/dist/code-index/parser.js +48 -3
  19. package/dist/context/compaction.js +8 -2
  20. package/dist/git/auto-commit.d.ts +4 -2
  21. package/dist/git/auto-commit.js +16 -5
  22. package/dist/github/github-tools.d.ts +2 -18
  23. package/dist/index.d.ts +11 -3
  24. package/dist/index.js +9 -2
  25. package/dist/journey/answers-block.js +14 -1
  26. package/dist/journey/offer.js +2 -1
  27. package/dist/mcp/json-schema-to-zod.js +12 -1
  28. package/dist/memory/session-digest.js +2 -1
  29. package/dist/orchestrator/index.d.ts +1 -1
  30. package/dist/orchestrator/index.js +1 -1
  31. package/dist/orchestrator/orchestrator.d.ts +45 -6
  32. package/dist/orchestrator/orchestrator.js +62 -12
  33. package/dist/orchestrator/router.d.ts +1 -1
  34. package/dist/orchestrator/router.js +4 -1
  35. package/dist/orchestrator/specialists.d.ts +6 -1
  36. package/dist/orchestrator/specialists.js +165 -68
  37. package/dist/orchestrator/types.d.ts +1 -1
  38. package/dist/policy/policy.d.ts +6 -59
  39. package/dist/pricing.d.ts +2 -0
  40. package/dist/pricing.js +47 -16
  41. package/dist/structured/schema.js +9 -12
  42. package/dist/tools/apply-patch.d.ts +1 -5
  43. package/dist/tools/ask-user.d.ts +2 -10
  44. package/dist/tools/ask-user.js +12 -4
  45. package/dist/tools/browser-tools.d.ts +15 -31
  46. package/dist/tools/codebase-search.d.ts +1 -7
  47. package/dist/tools/command-sandbox.d.ts +24 -0
  48. package/dist/tools/command-sandbox.js +73 -0
  49. package/dist/tools/dependency-audit.d.ts +30 -0
  50. package/dist/tools/dependency-audit.js +197 -0
  51. package/dist/tools/edit-file.d.ts +9 -15
  52. package/dist/tools/edit-file.js +64 -16
  53. package/dist/tools/fetch-url.d.ts +1 -18
  54. package/dist/tools/find-references.d.ts +1 -5
  55. package/dist/tools/find-symbol.d.ts +10 -8
  56. package/dist/tools/git-diff.d.ts +13 -0
  57. package/dist/tools/git-diff.js +85 -0
  58. package/dist/tools/index.d.ts +3 -1
  59. package/dist/tools/index.js +5 -1
  60. package/dist/tools/list-files.d.ts +1 -19
  61. package/dist/tools/list-symbols.d.ts +1 -5
  62. package/dist/tools/memory.d.ts +23 -43
  63. package/dist/tools/notebook.d.ts +11 -17
  64. package/dist/tools/read-file.d.ts +1 -11
  65. package/dist/tools/read-image.d.ts +1 -12
  66. package/dist/tools/read-only.d.ts +1 -1
  67. package/dist/tools/read-only.js +2 -0
  68. package/dist/tools/run-command.d.ts +1 -17
  69. package/dist/tools/run-command.js +12 -2
  70. package/dist/tools/search-files.d.ts +1 -17
  71. package/dist/tools/search-files.js +8 -49
  72. package/dist/tools/search-worker.d.ts +23 -0
  73. package/dist/tools/search-worker.js +77 -0
  74. package/dist/tools/sub-agent-wait.d.ts +2 -8
  75. package/dist/tools/sub-agent.d.ts +2 -70
  76. package/dist/tools/sub-agent.js +1 -0
  77. package/dist/tools/todo.d.ts +8 -20
  78. package/dist/tools/web-search.d.ts +1 -7
  79. package/dist/tools/write-file.d.ts +1 -7
  80. package/dist/types.d.ts +60 -1
  81. package/package.json +3 -3
@@ -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,24 @@
1
+ import type { CommandSandbox } from '../types.js';
2
+ export type SandboxedCommand = {
3
+ kind: 'wrapped';
4
+ command: string;
5
+ } | {
6
+ kind: 'unavailable';
7
+ command: string;
8
+ } | {
9
+ kind: 'refused';
10
+ reason: string;
11
+ };
12
+ /** Quote one argument for /bin/sh. */
13
+ export declare function shellQuote(arg: string): string;
14
+ interface Options {
15
+ cwd: string;
16
+ sandbox: CommandSandbox;
17
+ platform: NodeJS.Platform;
18
+ tmpDir: string;
19
+ findBinary?: (name: string) => string | undefined;
20
+ realpath?: (p: string) => string;
21
+ }
22
+ /** The command line that runs `command` inside the sandbox, or why it cannot be sandboxed. */
23
+ export declare function sandboxCommand(command: string, opts: Options): SandboxedCommand;
24
+ export {};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * OS sandbox for run_command (after Kilo's kilo-sandbox): bubblewrap on Linux, sandbox-exec (Seatbelt) on macOS.
3
+ * Writes are confined to the workspace, the temp folder and any extra folders the user named; the workspace's .git
4
+ * stays read-only, since a hook written there would later run outside the sandbox; the network is cut unless
5
+ * allowed. Windows has no comparable tool, so there the command runs as before (approval still applies).
6
+ */
7
+ import { existsSync, realpathSync } from 'node:fs';
8
+ import path from 'node:path';
9
+ import { resolveOnPath } from '../lsp/client.js';
10
+ /** Quote one argument for /bin/sh. */
11
+ export function shellQuote(arg) {
12
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
13
+ }
14
+ function defaultFind(name) {
15
+ for (const dir of ['/usr/bin', '/bin', '/usr/local/bin']) {
16
+ const full = path.posix.join(dir, name);
17
+ if (existsSync(full))
18
+ return full;
19
+ }
20
+ return resolveOnPath(name, process.env, process.platform);
21
+ }
22
+ function safeRealpath(p) {
23
+ try {
24
+ return realpathSync(p);
25
+ }
26
+ catch {
27
+ return p;
28
+ }
29
+ }
30
+ /** The command line that runs `command` inside the sandbox, or why it cannot be sandboxed. */
31
+ export function sandboxCommand(command, opts) {
32
+ const find = opts.findBinary ?? defaultFind;
33
+ const real = opts.realpath ?? safeRealpath;
34
+ if (opts.platform === 'linux') {
35
+ const bwrap = find('bwrap');
36
+ if (!bwrap)
37
+ return { kind: 'refused', reason: 'The command sandbox is on, but bubblewrap (bwrap) is not installed. Install it (apt install bubblewrap) or turn the sandbox off.' };
38
+ const writable = [opts.cwd, opts.tmpDir, ...(opts.sandbox.extraWritable ?? [])];
39
+ const gitDir = path.join(opts.cwd, '.git');
40
+ const args = [
41
+ bwrap,
42
+ '--die-with-parent',
43
+ '--new-session',
44
+ '--unshare-pid',
45
+ ...(opts.sandbox.network ? [] : ['--unshare-net']),
46
+ '--ro-bind', '/', '/',
47
+ '--dev', '/dev',
48
+ '--proc', '/proc',
49
+ ...writable.flatMap((p) => ['--bind', p, p]),
50
+ ...(existsSync(gitDir) ? ['--ro-bind', gitDir, gitDir] : []),
51
+ '--chdir', opts.cwd,
52
+ '--', '/bin/sh', '-c', command,
53
+ ];
54
+ return { kind: 'wrapped', command: args.map(shellQuote).join(' ') };
55
+ }
56
+ if (opts.platform === 'darwin') {
57
+ const exec = find('sandbox-exec');
58
+ if (!exec)
59
+ return { kind: 'refused', reason: 'The command sandbox is on, but sandbox-exec was not found. Turn the sandbox off to run commands.' };
60
+ const quote = (p) => JSON.stringify(real(p));
61
+ const writable = [opts.cwd, opts.tmpDir, ...(opts.sandbox.extraWritable ?? [])].map((p) => `(subpath ${quote(p)})`);
62
+ const profile = [
63
+ '(version 1)',
64
+ '(allow default)',
65
+ '(deny file-write*)',
66
+ `(allow file-write* ${writable.join(' ')} (subpath "/private/var/folders") (literal "/dev/null") (literal "/dev/stdout") (literal "/dev/stderr") (regex #"^/dev/tty"))`,
67
+ `(deny file-write* (subpath ${quote(path.posix.join(opts.cwd, '.git'))}))`,
68
+ ...(opts.sandbox.network ? [] : ['(deny network* (remote ip))']),
69
+ ].join('\n');
70
+ return { kind: 'wrapped', command: [exec, '-p', profile, '/bin/sh', '-c', command].map(shellQuote).join(' ') };
71
+ }
72
+ return { kind: 'unavailable', command };
73
+ }
@@ -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,26 +4,20 @@ 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;
26
12
  bytes: number;
27
13
  }
28
14
  export declare const editFileTool: ToolDefinition<z.infer<typeof params>, EditFileResult>;
15
+ /**
16
+ * What edit_file writes for `original`; throws the message the model sees when old_text cannot be placed.
17
+ * Shared with the chat's diff preview so the two never disagree.
18
+ */
19
+ export declare function applyTextEdit(original: string, oldTextIn: string, newTextIn: string, replaceAll: boolean, file: string): {
20
+ content: string;
21
+ replacements: number;
22
+ };
29
23
  export {};
@@ -24,29 +24,77 @@ export const editFileTool = {
24
24
  throw new Error(`File not found: ${args.path}`);
25
25
  throw err;
26
26
  });
27
- const occurrences = countOccurrences(original, args.old_text);
28
- if (occurrences === 0) {
29
- throw new Error(`old_text not found in ${args.path}. Re-read the file (it may have changed) and try again.`);
30
- }
31
- const replaceAll = args.replace_all === true;
32
- if (!replaceAll && occurrences > 1) {
33
- throw new Error(`old_text matches ${occurrences} times in ${args.path}. Pass more context to make it unique, or set replace_all=true to replace all occurrences.`);
34
- }
35
- const updated = replaceAll
36
- ? original.split(args.old_text).join(args.new_text)
37
- // Use a replacer FUNCTION so new_text is inserted literally: a plain
38
- // string 2nd arg to String.prototype.replace still honors special
39
- // patterns like $&, $1, $$ even though old_text isn't a regex.
40
- : original.replace(args.old_text, () => args.new_text);
41
- const bytes = encodeText(updated, encoding);
27
+ const { content, replacements } = applyTextEdit(original, args.old_text, args.new_text, args.replace_all === true, args.path);
28
+ const bytes = encodeText(content, encoding);
42
29
  await writeFile(resolved, bytes);
43
30
  return {
44
31
  path: resolved,
45
- replacements: replaceAll ? occurrences : 1,
32
+ replacements,
46
33
  bytes: bytes.length,
47
34
  };
48
35
  },
49
36
  };
37
+ /**
38
+ * What edit_file writes for `original`; throws the message the model sees when old_text cannot be placed.
39
+ * Shared with the chat's diff preview so the two never disagree.
40
+ */
41
+ export function applyTextEdit(original, oldTextIn, newTextIn, replaceAll, file) {
42
+ // read_file shows a CRLF file without its \r, so the model sends LF text: match on LF, write back the file's own ending.
43
+ const crlf = original.includes('\r\n');
44
+ const content = crlf ? toLf(original) : original;
45
+ const oldText = toLf(oldTextIn);
46
+ const newText = toLf(newTextIn);
47
+ const occurrences = countOccurrences(content, oldText);
48
+ let updated;
49
+ if (occurrences === 0) {
50
+ const span = replaceAll ? undefined : trimmedLineMatch(content, oldText, file);
51
+ if (!span) {
52
+ throw new Error(`old_text not found in ${file}. Re-read the file (it may have changed) and try again.`);
53
+ }
54
+ updated = content.slice(0, span.start) + newText + content.slice(span.end);
55
+ }
56
+ else {
57
+ if (!replaceAll && occurrences > 1) {
58
+ throw new Error(`old_text matches ${occurrences} times in ${file}. Pass more context to make it unique, or set replace_all=true to replace all occurrences.`);
59
+ }
60
+ updated = replaceAll
61
+ ? content.split(oldText).join(newText)
62
+ // Use a replacer FUNCTION so new_text is inserted literally: a plain
63
+ // string 2nd arg to String.prototype.replace still honors special
64
+ // patterns like $&, $1, $$ even though old_text isn't a regex.
65
+ : content.replace(oldText, () => newText);
66
+ }
67
+ return { content: crlf ? updated.replace(/\n/g, '\r\n') : updated, replacements: replaceAll ? occurrences : 1 };
68
+ }
69
+ function toLf(text) {
70
+ return text.replace(/\r\n/g, '\n');
71
+ }
72
+ /**
73
+ * Fallback when the exact text is absent: the block of whole lines whose trimmed text equals old_text's trimmed
74
+ * lines (a model often gets indentation or trailing spaces wrong). Only a unique block counts.
75
+ */
76
+ function trimmedLineMatch(content, oldText, file) {
77
+ const want = oldText.split('\n').map((l) => l.trim());
78
+ if (want.length > 1 && want[want.length - 1] === '')
79
+ want.pop();
80
+ if (want.every((l) => l === ''))
81
+ return undefined;
82
+ const lines = content.split('\n');
83
+ const starts = [];
84
+ for (let i = 0; i + want.length <= lines.length; i++) {
85
+ if (want.every((w, j) => lines[i + j].trim() === w))
86
+ starts.push(i);
87
+ }
88
+ if (starts.length === 0)
89
+ return undefined;
90
+ if (starts.length > 1) {
91
+ throw new Error(`old_text matches more than one place in ${file} once leading and trailing spaces are ignored. Pass more context to make it unique.`);
92
+ }
93
+ const offsetOf = (line) => lines.slice(0, line).reduce((n, l) => n + l.length + 1, 0);
94
+ const first = starts[0];
95
+ const last = first + want.length - 1;
96
+ return { start: offsetOf(first), end: offsetOf(last) + lines[last].length };
97
+ }
50
98
  function countOccurrences(haystack, needle) {
51
99
  if (needle.length === 0)
52
100
  return 0;
@@ -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 {};