@ikarmic/forge-core 5.1.0 → 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 (87) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/agent.js +39 -5
  3. package/dist/agent/goal.d.ts +5 -8
  4. package/dist/agent/prompts.d.ts +25 -1
  5. package/dist/agent/prompts.js +61 -17
  6. package/dist/agent/race-abort.d.ts +8 -0
  7. package/dist/agent/race-abort.js +24 -0
  8. package/dist/agent/stack-facts.d.ts +23 -0
  9. package/dist/agent/stack-facts.js +150 -0
  10. package/dist/code-index/parser.js +48 -3
  11. package/dist/github/github-tools.d.ts +2 -18
  12. package/dist/index.d.ts +7 -3
  13. package/dist/index.js +8 -3
  14. package/dist/journey/answers-block.d.ts +17 -0
  15. package/dist/journey/answers-block.js +28 -0
  16. package/dist/journey/index.d.ts +9 -0
  17. package/dist/journey/index.js +8 -0
  18. package/dist/journey/journey.d.ts +13 -0
  19. package/dist/journey/journey.js +40 -0
  20. package/dist/journey/offer.d.ts +25 -0
  21. package/dist/journey/offer.js +49 -0
  22. package/dist/journey/questions.d.ts +23 -0
  23. package/dist/journey/questions.js +49 -0
  24. package/dist/journey/stages.d.ts +13 -0
  25. package/dist/journey/stages.js +107 -0
  26. package/dist/journey/survey.d.ts +10 -0
  27. package/dist/journey/survey.js +203 -0
  28. package/dist/journey/trigger.d.ts +19 -0
  29. package/dist/journey/trigger.js +79 -0
  30. package/dist/journey/types.d.ts +72 -0
  31. package/dist/journey/types.js +1 -0
  32. package/dist/mcp/json-schema-to-zod.js +12 -1
  33. package/dist/orchestrator/index.d.ts +1 -1
  34. package/dist/orchestrator/index.js +1 -1
  35. package/dist/orchestrator/orchestrator.d.ts +45 -6
  36. package/dist/orchestrator/orchestrator.js +62 -12
  37. package/dist/orchestrator/router.d.ts +1 -1
  38. package/dist/orchestrator/router.js +4 -1
  39. package/dist/orchestrator/specialists.d.ts +6 -1
  40. package/dist/orchestrator/specialists.js +165 -68
  41. package/dist/orchestrator/types.d.ts +1 -1
  42. package/dist/policy/policy.d.ts +6 -59
  43. package/dist/pricing.d.ts +2 -0
  44. package/dist/pricing.js +35 -16
  45. package/dist/skills/builtin.js +1 -1
  46. package/dist/specs/generate.d.ts +18 -9
  47. package/dist/specs/generate.js +174 -14
  48. package/dist/specs/index.d.ts +2 -2
  49. package/dist/specs/index.js +1 -1
  50. package/dist/specs/store.d.ts +14 -2
  51. package/dist/specs/store.js +26 -1
  52. package/dist/specs/types.d.ts +11 -0
  53. package/dist/structured/schema.js +9 -12
  54. package/dist/tools/apply-patch.d.ts +1 -5
  55. package/dist/tools/ask-user.d.ts +2 -10
  56. package/dist/tools/ask-user.js +39 -3
  57. package/dist/tools/browser-tools.d.ts +15 -31
  58. package/dist/tools/codebase-search.d.ts +1 -7
  59. package/dist/tools/dependency-audit.d.ts +30 -0
  60. package/dist/tools/dependency-audit.js +197 -0
  61. package/dist/tools/edit-file.d.ts +1 -15
  62. package/dist/tools/fetch-url.d.ts +1 -18
  63. package/dist/tools/find-references.d.ts +1 -5
  64. package/dist/tools/find-symbol.d.ts +10 -8
  65. package/dist/tools/git-diff.d.ts +13 -0
  66. package/dist/tools/git-diff.js +85 -0
  67. package/dist/tools/index.d.ts +3 -1
  68. package/dist/tools/index.js +5 -1
  69. package/dist/tools/list-files.d.ts +1 -19
  70. package/dist/tools/list-symbols.d.ts +1 -5
  71. package/dist/tools/memory.d.ts +23 -43
  72. package/dist/tools/notebook.d.ts +11 -17
  73. package/dist/tools/read-file.d.ts +1 -11
  74. package/dist/tools/read-file.js +5 -0
  75. package/dist/tools/read-image.d.ts +1 -12
  76. package/dist/tools/read-only.d.ts +1 -1
  77. package/dist/tools/read-only.js +2 -0
  78. package/dist/tools/run-command.d.ts +1 -17
  79. package/dist/tools/search-files.d.ts +1 -17
  80. package/dist/tools/sub-agent-wait.d.ts +2 -8
  81. package/dist/tools/sub-agent.d.ts +2 -70
  82. package/dist/tools/sub-agent.js +3 -0
  83. package/dist/tools/todo.d.ts +8 -20
  84. package/dist/tools/web-search.d.ts +1 -7
  85. package/dist/tools/write-file.d.ts +1 -7
  86. package/dist/types.d.ts +27 -0
  87. package/package.json +3 -3
@@ -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 {};
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `git_diff`: the current changes, read-only, so Review and Security can see what changed without a
3
+ * `run_command` approval card. It runs one fixed program with fixed flags; the model chooses only a base
4
+ * ref, staged or not, and paths. A base that looks like an option is refused: `git diff --output=<file>`
5
+ * writes a file, and an option passed where a ref belongs is how a model (or a prompt it read) gets there.
6
+ */
7
+ import { execFile } from 'node:child_process';
8
+ import path from 'node:path';
9
+ import { promisify } from 'node:util';
10
+ import { z } from 'zod';
11
+ import { resolveOnPath } from '../lsp/client.js';
12
+ import { resolveAndCheckPath } from './shared.js';
13
+ import { shellEnv } from './shell-env.js';
14
+ const run = promisify(execFile);
15
+ /** The most diff text handed to the model; the rest is cut with a note. */
16
+ export const MAX_DIFF_CHARS = 60_000;
17
+ const params = z.object({
18
+ base: z
19
+ .string()
20
+ .optional()
21
+ .describe('Compare against this ref (a branch, tag, commit or range such as main...HEAD). Default: HEAD, i.e. your uncommitted changes.'),
22
+ staged: z.boolean().optional().describe('Only the staged changes (git diff --cached).'),
23
+ paths: z.array(z.string()).optional().describe('Limit the diff to these files or folders inside the project.'),
24
+ });
25
+ /** A ref git will read as a ref, never as an option: no leading dash, only the characters refs and ranges use. */
26
+ export function isSafeGitRef(ref) {
27
+ return ref.length > 0 && !ref.startsWith('-') && /^[A-Za-z0-9._/~^@{}:-]+$/.test(ref);
28
+ }
29
+ async function git(args, cwd) {
30
+ const env = shellEnv(process.env);
31
+ const program = resolveOnPath('git', env, process.platform) ?? 'git';
32
+ try {
33
+ // No repository code and no lock on the person's index: core.fsmonitor names a program git would run for
34
+ // status, and an optional index refresh can collide with the person's own commit (RB-41).
35
+ const safe = ['-c', 'core.fsmonitor=false', '--no-optional-locks', ...args];
36
+ const { stdout, stderr } = await run(program, safe, { cwd, env, maxBuffer: 64 * 1024 * 1024, windowsHide: true });
37
+ return { ok: true, stdout, stderr };
38
+ }
39
+ catch (err) {
40
+ const e = err;
41
+ return { ok: false, stdout: e.stdout ?? '', stderr: e.stderr ?? e.message };
42
+ }
43
+ }
44
+ function cut(text) {
45
+ if (text.length <= MAX_DIFF_CHARS)
46
+ return text;
47
+ const left = text.length - MAX_DIFF_CHARS;
48
+ return `${text.slice(0, MAX_DIFF_CHARS)}\n[diff cut: ${left} characters left out. Pass paths to see one file at a time.]`;
49
+ }
50
+ export const gitDiffTool = {
51
+ name: 'git_diff',
52
+ description: 'Show the current changes as a unified diff, plus new untracked files. Read-only: it never changes files or the repository. ' +
53
+ 'By default it compares your working tree and staged changes with HEAD; pass base to compare with a branch or commit, staged for staged changes only, paths to narrow it.',
54
+ parameters: params,
55
+ execute: async (args, ctx) => {
56
+ if (args.base !== undefined && !isSafeGitRef(args.base)) {
57
+ return `git_diff refused the base "${args.base}": a base must be a branch, tag, commit or range, and must not start with "-".`;
58
+ }
59
+ const rels = (args.paths ?? []).map((p) => path.relative(ctx.cwd, resolveAndCheckPath(p, ctx)) || '.');
60
+ const inside = await git(['rev-parse', '--is-inside-work-tree'], ctx.cwd);
61
+ if (!inside.ok || inside.stdout.trim() !== 'true')
62
+ return 'This folder is not a git repository, so there is no diff to show.';
63
+ const diffArgs = ['diff', '--no-ext-diff', '--no-textconv', '--no-color'];
64
+ // A repository with no commit has no HEAD: its staged files are the change.
65
+ const hasHead = (await git(['rev-parse', '--verify', '--quiet', 'HEAD'], ctx.cwd)).ok;
66
+ if (args.staged || (!args.base && !hasHead))
67
+ diffArgs.push('--cached');
68
+ else if (!args.base)
69
+ diffArgs.push('HEAD');
70
+ if (args.base)
71
+ diffArgs.push(args.base);
72
+ diffArgs.push('--', ...rels);
73
+ const diff = await git(diffArgs, ctx.cwd);
74
+ if (!diff.ok)
75
+ return `git diff failed: ${diff.stderr.trim()}`;
76
+ const status = await git(['status', '--porcelain=v1', '--', ...rels], ctx.cwd);
77
+ const parts = [diff.stdout.trim() ? diff.stdout : '(no changes in tracked files)'];
78
+ if (status.ok && status.stdout.trim())
79
+ parts.push(`git status:\n${status.stdout}`);
80
+ if (status.ok && /^\?\? /m.test(status.stdout)) {
81
+ parts.push('Untracked files (marked ??) are new and not shown in the diff above: read them with read_file.');
82
+ }
83
+ return cut(parts.join('\n'));
84
+ },
85
+ };
@@ -3,6 +3,8 @@ import { editFileTool } from './edit-file.js';
3
3
  import { fetchUrlTool } from './fetch-url.js';
4
4
  import { findReferencesTool } from './find-references.js';
5
5
  import { findSymbolTool } from './find-symbol.js';
6
+ import { dependencyAuditTool } from './dependency-audit.js';
7
+ import { gitDiffTool } from './git-diff.js';
6
8
  import { listFilesTool } from './list-files.js';
7
9
  import { listSymbolsTool } from './list-symbols.js';
8
10
  import { memoryForgetTool, memoryReadTool, memoryWriteTool } from './memory.js';
@@ -30,7 +32,7 @@ type AnyToolDefinition = ToolDefinition<any, any>;
30
32
  * intentional: the model sees this order in its tool list.
31
33
  */
32
34
  export declare const BUILTIN_TOOLS: readonly AnyToolDefinition[];
33
- export { applyPatchTool, askUserTool, editFileTool, editNotebookTool, fetchUrlTool, findReferencesTool, findSymbolTool, listFilesTool, listSymbolsTool, memoryForgetTool, memoryReadTool, memoryWriteTool, readFileTool, readImageTool, runCommandTool, searchFilesTool, subAgentTool, subAgentWaitTool, todoReadTool, todoWriteTool, writeFileTool, };
35
+ export { applyPatchTool, askUserTool, dependencyAuditTool, editFileTool, editNotebookTool, fetchUrlTool, findReferencesTool, findSymbolTool, gitDiffTool, listFilesTool, listSymbolsTool, memoryForgetTool, memoryReadTool, memoryWriteTool, readFileTool, readImageTool, runCommandTool, searchFilesTool, subAgentTool, subAgentWaitTool, todoReadTool, todoWriteTool, writeFileTool, };
34
36
  export type { ToolContext, ToolDefinition, ToolPolicy, ToolSandbox } from '../types.js';
35
37
  export { checkPolicyViolation, globToRegex, mergeToolSandbox, normalizePathPattern, pathMatchesAny } from './policy.js';
36
38
  export { redact, countRedactions, buildRules } from './redact.js';
@@ -3,6 +3,8 @@ import { editFileTool } from './edit-file.js';
3
3
  import { fetchUrlTool } from './fetch-url.js';
4
4
  import { findReferencesTool } from './find-references.js';
5
5
  import { findSymbolTool } from './find-symbol.js';
6
+ import { dependencyAuditTool } from './dependency-audit.js';
7
+ import { gitDiffTool } from './git-diff.js';
6
8
  import { listFilesTool } from './list-files.js';
7
9
  import { listSymbolsTool } from './list-symbols.js';
8
10
  import { memoryForgetTool, memoryReadTool, memoryWriteTool } from './memory.js';
@@ -30,6 +32,8 @@ export const BUILTIN_TOOLS = [
30
32
  findSymbolTool,
31
33
  listSymbolsTool,
32
34
  findReferencesTool,
35
+ gitDiffTool,
36
+ dependencyAuditTool,
33
37
  writeFileTool,
34
38
  editFileTool,
35
39
  applyPatchTool,
@@ -45,7 +49,7 @@ export const BUILTIN_TOOLS = [
45
49
  todoReadTool,
46
50
  askUserTool,
47
51
  ];
48
- export { applyPatchTool, askUserTool, editFileTool, editNotebookTool, fetchUrlTool, findReferencesTool, findSymbolTool, listFilesTool, listSymbolsTool, memoryForgetTool, memoryReadTool, memoryWriteTool, readFileTool, readImageTool, runCommandTool, searchFilesTool, subAgentTool, subAgentWaitTool, todoReadTool, todoWriteTool, writeFileTool, };
52
+ export { applyPatchTool, askUserTool, dependencyAuditTool, editFileTool, editNotebookTool, fetchUrlTool, findReferencesTool, findSymbolTool, gitDiffTool, listFilesTool, listSymbolsTool, memoryForgetTool, memoryReadTool, memoryWriteTool, readFileTool, readImageTool, runCommandTool, searchFilesTool, subAgentTool, subAgentWaitTool, todoReadTool, todoWriteTool, writeFileTool, };
49
53
  export { checkPolicyViolation, globToRegex, mergeToolSandbox, normalizePathPattern, pathMatchesAny } from './policy.js';
50
54
  export { redact, countRedactions, buildRules } from './redact.js';
51
55
  export { ToolResultCache, buildCacheKey, cachedExecute } from './result-cache.js';
@@ -2,30 +2,12 @@ import { z } from 'zod';
2
2
  import type { ToolDefinition } from '../types.js';
3
3
  declare const params: z.ZodObject<{
4
4
  path: z.ZodOptional<z.ZodString>;
5
- /** Maximum entries to return. Defaults to 500. */
6
5
  max_entries: z.ZodOptional<z.ZodNumber>;
7
- /** Include dotfiles. Default false. */
8
6
  include_hidden: z.ZodOptional<z.ZodBoolean>;
9
7
  include_ignored: z.ZodOptional<z.ZodBoolean>;
10
- /** Recurse into subdirectories. Default false. */
11
8
  recursive: z.ZodOptional<z.ZodBoolean>;
12
- /** Max recursion depth when recursive=true. Default 4. */
13
9
  max_depth: z.ZodOptional<z.ZodNumber>;
14
- }, "strip", z.ZodTypeAny, {
15
- path?: string | undefined;
16
- recursive?: boolean | undefined;
17
- max_entries?: number | undefined;
18
- include_hidden?: boolean | undefined;
19
- include_ignored?: boolean | undefined;
20
- max_depth?: number | undefined;
21
- }, {
22
- path?: string | undefined;
23
- recursive?: boolean | undefined;
24
- max_entries?: number | undefined;
25
- include_hidden?: boolean | undefined;
26
- include_ignored?: boolean | undefined;
27
- max_depth?: number | undefined;
28
- }>;
10
+ }, z.core.$strip>;
29
11
  export interface ListEntry {
30
12
  path: string;
31
13
  type: 'file' | 'directory' | 'other';
@@ -3,11 +3,7 @@ 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
  path: z.ZodString;
6
- }, "strip", z.ZodTypeAny, {
7
- path: string;
8
- }, {
9
- path: string;
10
- }>;
6
+ }, z.core.$strip>;
11
7
  export interface SymbolOutlineEntry {
12
8
  name: string;
13
9
  kind: SymbolKind;
@@ -2,21 +2,13 @@ import { z } from 'zod';
2
2
  import { type MemoryType } from '../memory/memory-store.js';
3
3
  import type { ToolDefinition } from '../types.js';
4
4
  declare const readParams: z.ZodObject<{
5
- /**
6
- * Which memory to read: 'project' (default), 'global', or 'both'. 'both'
7
- * returns the merged view with project overriding global on heading
8
- * collisions: the same view the system-prompt injector sees.
9
- */
10
- scope: z.ZodOptional<z.ZodEnum<["project", "global", "both"]>>;
11
- /** Optional heading to filter by. Returns just that section's body. */
5
+ scope: z.ZodOptional<z.ZodEnum<{
6
+ project: "project";
7
+ global: "global";
8
+ both: "both";
9
+ }>>;
12
10
  section: z.ZodOptional<z.ZodString>;
13
- }, "strip", z.ZodTypeAny, {
14
- section?: string | undefined;
15
- scope?: "project" | "global" | "both" | undefined;
16
- }, {
17
- section?: string | undefined;
18
- scope?: "project" | "global" | "both" | undefined;
19
- }>;
11
+ }, z.core.$strip>;
20
12
  export interface MemoryReadResult {
21
13
  scope: 'project' | 'global' | 'both';
22
14
  content: string;
@@ -31,30 +23,21 @@ export declare const memoryReadTool: ToolDefinition<z.infer<typeof readParams>,
31
23
  /** Longest body `memory_write` accepts. Memory holds short facts, not documents. */
32
24
  export declare const MAX_BODY_CHARS = 1200;
33
25
  declare const writeParams: z.ZodObject<{
34
- /** Where to write: 'project' (default) or 'global'. */
35
- scope: z.ZodOptional<z.ZodEnum<["project", "global"]>>;
36
- /** Section heading. If the heading already exists (ignoring case and spacing), replaces its body. */
26
+ scope: z.ZodOptional<z.ZodEnum<{
27
+ project: "project";
28
+ global: "global";
29
+ }>>;
37
30
  heading: z.ZodString;
38
- /** Section body. Markdown allowed. */
39
31
  body: z.ZodString;
40
- type: z.ZodOptional<z.ZodEnum<["convention", "decision", "correction", "reference"]>>;
41
- refs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
32
+ type: z.ZodOptional<z.ZodEnum<{
33
+ convention: "convention";
34
+ decision: "decision";
35
+ correction: "correction";
36
+ reference: "reference";
37
+ }>>;
38
+ refs: z.ZodOptional<z.ZodArray<z.ZodString>>;
42
39
  pin: z.ZodOptional<z.ZodBoolean>;
43
- }, "strip", z.ZodTypeAny, {
44
- body: string;
45
- heading: string;
46
- type?: "convention" | "decision" | "correction" | "reference" | undefined;
47
- refs?: string[] | undefined;
48
- pin?: boolean | undefined;
49
- scope?: "project" | "global" | undefined;
50
- }, {
51
- body: string;
52
- heading: string;
53
- type?: "convention" | "decision" | "correction" | "reference" | undefined;
54
- refs?: string[] | undefined;
55
- pin?: boolean | undefined;
56
- scope?: "project" | "global" | undefined;
57
- }>;
40
+ }, z.core.$strip>;
58
41
  export interface MemoryWriteResult {
59
42
  scope: 'project' | 'global';
60
43
  path: string;
@@ -69,15 +52,12 @@ export interface MemoryWriteResult {
69
52
  }
70
53
  export declare const memoryWriteTool: ToolDefinition<z.infer<typeof writeParams>, MemoryWriteResult>;
71
54
  declare const forgetParams: z.ZodObject<{
72
- scope: z.ZodOptional<z.ZodEnum<["project", "global"]>>;
55
+ scope: z.ZodOptional<z.ZodEnum<{
56
+ project: "project";
57
+ global: "global";
58
+ }>>;
73
59
  heading: z.ZodString;
74
- }, "strip", z.ZodTypeAny, {
75
- heading: string;
76
- scope?: "project" | "global" | undefined;
77
- }, {
78
- heading: string;
79
- scope?: "project" | "global" | undefined;
80
- }>;
60
+ }, z.core.$strip>;
81
61
  export interface MemoryForgetResult {
82
62
  scope: 'project' | 'global';
83
63
  heading: string;
@@ -39,26 +39,20 @@ export declare function outputText(output: unknown): string;
39
39
  export declare function renderNotebook(nb: Notebook): string;
40
40
  declare const params: z.ZodObject<{
41
41
  path: z.ZodString;
42
- mode: z.ZodOptional<z.ZodEnum<["replace", "insert", "delete"]>>;
42
+ mode: z.ZodOptional<z.ZodEnum<{
43
+ replace: "replace";
44
+ delete: "delete";
45
+ insert: "insert";
46
+ }>>;
43
47
  cell_index: z.ZodOptional<z.ZodNumber>;
44
48
  cell_id: z.ZodOptional<z.ZodString>;
45
49
  source: z.ZodOptional<z.ZodString>;
46
- cell_type: z.ZodOptional<z.ZodEnum<["code", "markdown", "raw"]>>;
47
- }, "strip", z.ZodTypeAny, {
48
- path: string;
49
- mode?: "replace" | "delete" | "insert" | undefined;
50
- source?: string | undefined;
51
- cell_type?: "code" | "raw" | "markdown" | undefined;
52
- cell_index?: number | undefined;
53
- cell_id?: string | undefined;
54
- }, {
55
- path: string;
56
- mode?: "replace" | "delete" | "insert" | undefined;
57
- source?: string | undefined;
58
- cell_type?: "code" | "raw" | "markdown" | undefined;
59
- cell_index?: number | undefined;
60
- cell_id?: string | undefined;
61
- }>;
50
+ cell_type: z.ZodOptional<z.ZodEnum<{
51
+ code: "code";
52
+ raw: "raw";
53
+ markdown: "markdown";
54
+ }>>;
55
+ }, z.core.$strip>;
62
56
  export interface EditNotebookResult {
63
57
  path: string;
64
58
  mode: 'replace' | 'insert' | 'delete';
@@ -2,19 +2,9 @@ import { z } from 'zod';
2
2
  import type { ToolDefinition } from '../types.js';
3
3
  declare const params: z.ZodObject<{
4
4
  path: z.ZodString;
5
- /** 1-based line number to start reading from. Omit to start at the beginning. */
6
5
  offset: z.ZodOptional<z.ZodNumber>;
7
- /** Maximum number of lines to return. Defaults to 2000. */
8
6
  max_lines: z.ZodOptional<z.ZodNumber>;
9
- }, "strip", z.ZodTypeAny, {
10
- path: string;
11
- offset?: number | undefined;
12
- max_lines?: number | undefined;
13
- }, {
14
- path: string;
15
- offset?: number | undefined;
16
- max_lines?: number | undefined;
17
- }>;
7
+ }, z.core.$strip>;
18
8
  export interface ReadFileResult {
19
9
  path: string;
20
10
  totalLines: number;
@@ -55,6 +55,11 @@ export const readFileTool = {
55
55
  }
56
56
  const text = decoded.text;
57
57
  const lines = text.split(/\r?\n/);
58
+ // A file that ends with a newline splits into one extra, empty piece. It is not a line. Numbering it told the model a
59
+ // 37-line file had 38 lines and that the last was empty (a live spec caught it), and made a file of exactly max_lines
60
+ // lines look truncated. A genuinely blank last line ("a\n\n") still counts: only the piece after the final newline goes.
61
+ if (lines.length > 1 && lines[lines.length - 1] === '')
62
+ lines.pop();
58
63
  const offset = args.offset ?? 1;
59
64
  const maxLines = args.max_lines ?? 2000;
60
65
  const slice = lines.slice(offset - 1, offset - 1 + maxLines);