@ikarmic/forge-core 5.0.0 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent/agent.d.ts +8 -0
- package/dist/agent/agent.js +148 -24
- package/dist/agent/background.d.ts +58 -0
- package/dist/agent/background.js +139 -0
- package/dist/agent/enhance-prompt.d.ts +3 -1
- package/dist/agent/enhance-prompt.js +6 -2
- package/dist/agent/goal.d.ts +54 -0
- package/dist/agent/goal.js +65 -0
- package/dist/agent/prompts.d.ts +25 -1
- package/dist/agent/prompts.js +61 -17
- package/dist/agents/agent-files.d.ts +43 -0
- package/dist/agents/agent-files.js +109 -0
- package/dist/checkpoints/store.js +5 -1
- package/dist/commands/custom-commands.d.ts +9 -4
- package/dist/commands/custom-commands.js +13 -2
- package/dist/config-file.d.ts +43 -0
- package/dist/config-file.js +249 -5
- package/dist/config-merge.d.ts +13 -0
- package/dist/config-merge.js +67 -0
- package/dist/config-schema.d.ts +340 -0
- package/dist/config-schema.js +126 -0
- package/dist/config.js +2 -1
- package/dist/context/compaction.js +4 -0
- package/dist/context/mask.js +1 -1
- package/dist/custom-providers.d.ts +95 -0
- package/dist/custom-providers.js +120 -0
- package/dist/git/auto-commit.d.ts +2 -2
- package/dist/git/auto-commit.js +5 -4
- package/dist/github/github-tools.d.ts +2 -2
- package/dist/hooks/runner.js +3 -2
- package/dist/import/claude-code.d.ts +39 -0
- package/dist/import/claude-code.js +254 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.js +27 -4
- package/dist/journey/answers-block.d.ts +17 -0
- package/dist/journey/answers-block.js +15 -0
- package/dist/journey/index.d.ts +9 -0
- package/dist/journey/index.js +8 -0
- package/dist/journey/journey.d.ts +13 -0
- package/dist/journey/journey.js +40 -0
- package/dist/journey/offer.d.ts +25 -0
- package/dist/journey/offer.js +48 -0
- package/dist/journey/questions.d.ts +23 -0
- package/dist/journey/questions.js +49 -0
- package/dist/journey/stages.d.ts +13 -0
- package/dist/journey/stages.js +107 -0
- package/dist/journey/survey.d.ts +10 -0
- package/dist/journey/survey.js +203 -0
- package/dist/journey/trigger.d.ts +19 -0
- package/dist/journey/trigger.js +79 -0
- package/dist/journey/types.d.ts +72 -0
- package/dist/journey/types.js +1 -0
- package/dist/lsp/client.d.ts +5 -0
- package/dist/lsp/client.js +39 -3
- package/dist/mcp/index.d.ts +5 -1
- package/dist/mcp/index.js +3 -1
- package/dist/mcp/manager.d.ts +17 -3
- package/dist/mcp/manager.js +27 -2
- package/dist/mcp/marketplace.d.ts +2 -0
- package/dist/mcp/marketplace.js +10 -4
- package/dist/mcp/mcp-client.d.ts +12 -2
- package/dist/mcp/mcp-client.js +74 -17
- package/dist/mcp/mcp-config.d.ts +2 -0
- package/dist/mcp/mcp-config.js +1 -0
- package/dist/mcp/oauth.d.ts +55 -0
- package/dist/mcp/oauth.js +197 -0
- package/dist/mcp/resource-tools.d.ts +8 -0
- package/dist/mcp/resource-tools.js +56 -0
- package/dist/memory/session-digest.d.ts +12 -0
- package/dist/memory/session-digest.js +47 -0
- package/dist/memory/stale.js +2 -1
- package/dist/model-alias.d.ts +18 -2
- package/dist/model-alias.js +27 -3
- package/dist/model-catalog.js +0 -1
- package/dist/model-list.js +2 -1
- package/dist/model-overrides.d.ts +30 -0
- package/dist/model-overrides.js +48 -0
- package/dist/orchestrator/specialists-registry.js +2 -3
- package/dist/orchestrator/specialists.js +4 -4
- package/dist/policy/ca-certs.d.ts +8 -0
- package/dist/policy/ca-certs.js +30 -0
- package/dist/policy/proxy.d.ts +10 -3
- package/dist/policy/proxy.js +33 -6
- package/dist/pricing.js +4 -0
- package/dist/providers.d.ts +7 -0
- package/dist/providers.js +39 -5
- package/dist/semantic/embedder.d.ts +15 -0
- package/dist/semantic/embedder.js +25 -0
- package/dist/semantic/semantic-index.d.ts +10 -2
- package/dist/semantic/semantic-index.js +16 -7
- package/dist/sessions/session-store.js +2 -1
- package/dist/skills/builtin.d.ts +2 -0
- package/dist/skills/builtin.js +68 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/loader.d.ts +10 -3
- package/dist/skills/loader.js +20 -7
- package/dist/skills/marketplace.js +2 -1
- package/dist/skills/remote.d.ts +14 -0
- package/dist/skills/remote.js +131 -0
- package/dist/skills/types.d.ts +10 -0
- package/dist/specs/generate.d.ts +18 -9
- package/dist/specs/generate.js +174 -14
- package/dist/specs/index.d.ts +2 -2
- package/dist/specs/index.js +1 -1
- package/dist/specs/store.d.ts +14 -2
- package/dist/specs/store.js +26 -1
- package/dist/specs/types.d.ts +11 -0
- package/dist/steering/extra-instructions.d.ts +8 -0
- package/dist/steering/extra-instructions.js +104 -0
- package/dist/steering/folder-instructions.d.ts +8 -0
- package/dist/steering/folder-instructions.js +28 -0
- package/dist/steering/select.js +2 -1
- package/dist/timed-fetch.d.ts +12 -0
- package/dist/timed-fetch.js +83 -0
- package/dist/tools/apply-patch.d.ts +52 -0
- package/dist/tools/apply-patch.js +197 -0
- package/dist/tools/ask-user.d.ts +27 -0
- package/dist/tools/ask-user.js +60 -0
- package/dist/tools/browser-tools.d.ts +6 -6
- package/dist/tools/browser-tools.js +3 -13
- package/dist/tools/codebase-search.d.ts +4 -2
- package/dist/tools/codebase-search.js +2 -2
- package/dist/tools/edit-file.js +5 -3
- package/dist/tools/fetch-url.d.ts +7 -0
- package/dist/tools/fetch-url.js +52 -19
- package/dist/tools/find-symbol.d.ts +2 -2
- package/dist/tools/ignore-rules.js +2 -1
- package/dist/tools/index.d.ts +5 -1
- package/dist/tools/index.js +10 -1
- package/dist/tools/memory.d.ts +6 -6
- package/dist/tools/notebook.d.ts +2 -2
- package/dist/tools/permission-rules.d.ts +32 -0
- package/dist/tools/permission-rules.js +182 -0
- package/dist/tools/read-file.d.ts +5 -0
- package/dist/tools/read-file.js +12 -2
- package/dist/tools/read-only.d.ts +1 -1
- package/dist/tools/read-only.js +2 -0
- package/dist/tools/run-command.d.ts +4 -2
- package/dist/tools/run-command.js +12 -8
- package/dist/tools/sandbox-loader.js +3 -13
- package/dist/tools/search-files.d.ts +8 -2
- package/dist/tools/search-files.js +43 -0
- package/dist/tools/shared.d.ts +5 -0
- package/dist/tools/shared.js +30 -2
- package/dist/tools/shell-env.d.ts +7 -0
- package/dist/tools/shell-env.js +9 -0
- package/dist/tools/sub-agent-wait.d.ts +29 -0
- package/dist/tools/sub-agent-wait.js +60 -0
- package/dist/tools/sub-agent.d.ts +50 -4
- package/dist/tools/sub-agent.js +213 -125
- package/dist/tools/text-encoding.d.ts +10 -0
- package/dist/tools/text-encoding.js +40 -0
- package/dist/tools/todo.d.ts +36 -0
- package/dist/tools/todo.js +42 -0
- package/dist/tools/write-file.js +15 -9
- package/dist/tools/write-tools.d.ts +12 -0
- package/dist/tools/write-tools.js +18 -0
- package/dist/types.d.ts +98 -0
- package/dist/worktree/worktree.d.ts +3 -0
- package/dist/worktree/worktree.js +22 -0
- package/package.json +1 -1
|
@@ -21,6 +21,11 @@ export interface ReadFileResult {
|
|
|
21
21
|
offset: number;
|
|
22
22
|
content: string;
|
|
23
23
|
truncated: boolean;
|
|
24
|
+
/** `AGENTS.md` files of the folders this file sits in, shown the first time a file there is read. */
|
|
25
|
+
folderInstructions?: Array<{
|
|
26
|
+
file: string;
|
|
27
|
+
text: string;
|
|
28
|
+
}>;
|
|
24
29
|
}
|
|
25
30
|
/** A PDF comes back whole; the agent attaches it so document-capable models read it natively. */
|
|
26
31
|
export interface ReadPdfResult {
|
package/dist/tools/read-file.js
CHANGED
|
@@ -2,7 +2,9 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { isNotebookPath, parseNotebook, renderNotebook } from './notebook.js';
|
|
4
4
|
import { isPdf, MAX_ATTACHMENT_BYTES } from '../attachments.js';
|
|
5
|
+
import { folderInstructionsFor } from '../steering/folder-instructions.js';
|
|
5
6
|
import { resolveAndCheckPath, truncateByLines } from './shared.js';
|
|
7
|
+
import { decodeText } from './text-encoding.js';
|
|
6
8
|
const params = z.object({
|
|
7
9
|
path: z.string().describe('Path to the file to read, relative to the working directory or absolute.'),
|
|
8
10
|
/** 1-based line number to start reading from. Omit to start at the beginning. */
|
|
@@ -47,22 +49,30 @@ export const readFileTool = {
|
|
|
47
49
|
return { path: resolved, mime: 'application/pdf', bytes: raw.length, dataUrl: `data:application/pdf;base64,${raw.toString('base64')}` };
|
|
48
50
|
}
|
|
49
51
|
// Reject obvious binary content.
|
|
50
|
-
|
|
52
|
+
const decoded = decodeText(raw);
|
|
53
|
+
if (decoded.encoding === 'utf8' && looksBinary(raw)) {
|
|
51
54
|
throw new Error(`Refusing to read binary file: ${args.path} (${raw.length} bytes)`);
|
|
52
55
|
}
|
|
53
|
-
const text =
|
|
56
|
+
const text = decoded.text;
|
|
54
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();
|
|
55
63
|
const offset = args.offset ?? 1;
|
|
56
64
|
const maxLines = args.max_lines ?? 2000;
|
|
57
65
|
const slice = lines.slice(offset - 1, offset - 1 + maxLines);
|
|
58
66
|
const endIdx = offset - 1 + maxLines;
|
|
59
67
|
const truncated = endIdx < lines.length;
|
|
68
|
+
const folderInstructions = ctx.shownFolderInstructions ? await folderInstructionsFor(resolved, ctx.cwd, ctx.shownFolderInstructions) : undefined;
|
|
60
69
|
return {
|
|
61
70
|
path: resolved,
|
|
62
71
|
totalLines: lines.length,
|
|
63
72
|
offset,
|
|
64
73
|
content: truncateByLines(slice.map((l, i) => `${String(offset + i).padStart(6)}\t${l}`).join('\n'), maxLines),
|
|
65
74
|
truncated,
|
|
75
|
+
...(folderInstructions ? { folderInstructions } : {}),
|
|
66
76
|
};
|
|
67
77
|
},
|
|
68
78
|
};
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
* anything. Asking before each of them is noise, so they are approved without a prompt by default;
|
|
4
4
|
* edits, commands, web access and sub-agents still ask.
|
|
5
5
|
*/
|
|
6
|
-
export declare const READ_ONLY_TOOLS: readonly ["read_file", "list_files", "search_files", "find_symbol", "list_symbols", "find_references", "codebase_search", "memory_read"];
|
|
6
|
+
export declare const READ_ONLY_TOOLS: readonly ["read_file", "list_files", "search_files", "find_symbol", "list_symbols", "find_references", "codebase_search", "memory_read", "sub_agent_wait"];
|
|
7
7
|
/** The tools that run without a prompt: the user's list, or the read-only tools when none is set. */
|
|
8
8
|
export declare function resolveAutoApprove(configured?: readonly string[]): string[];
|
package/dist/tools/read-only.js
CHANGED
|
@@ -12,6 +12,8 @@ export const READ_ONLY_TOOLS = [
|
|
|
12
12
|
'find_references',
|
|
13
13
|
'codebase_search',
|
|
14
14
|
'memory_read',
|
|
15
|
+
// Only observes the sub-agents the person already let start.
|
|
16
|
+
'sub_agent_wait',
|
|
15
17
|
];
|
|
16
18
|
/** The tools that run without a prompt: the user's list, or the read-only tools when none is set. */
|
|
17
19
|
export function resolveAutoApprove(configured) {
|
|
@@ -14,12 +14,12 @@ declare const params: z.ZodObject<{
|
|
|
14
14
|
cwd: z.ZodOptional<z.ZodString>;
|
|
15
15
|
}, "strip", z.ZodTypeAny, {
|
|
16
16
|
command: string;
|
|
17
|
-
cwd?: string | undefined;
|
|
18
17
|
timeout_ms?: number | undefined;
|
|
18
|
+
cwd?: string | undefined;
|
|
19
19
|
}, {
|
|
20
20
|
command: string;
|
|
21
|
-
cwd?: string | undefined;
|
|
22
21
|
timeout_ms?: number | undefined;
|
|
22
|
+
cwd?: string | undefined;
|
|
23
23
|
}>;
|
|
24
24
|
export interface RunCommandResult {
|
|
25
25
|
command: string;
|
|
@@ -31,4 +31,6 @@ export interface RunCommandResult {
|
|
|
31
31
|
timedOut: boolean;
|
|
32
32
|
}
|
|
33
33
|
export declare const runCommandTool: ToolDefinition<z.infer<typeof params>, RunCommandResult>;
|
|
34
|
+
/** Cut a command's output to `max` characters, keeping the head and the tail. */
|
|
35
|
+
export declare function clampOutput(text: string, max?: number): string;
|
|
34
36
|
export {};
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { checkPolicyViolation } from './policy.js';
|
|
4
4
|
import { resolveAndCheckPath } from './shared.js';
|
|
5
|
+
import { shellEnv } from './shell-env.js';
|
|
5
6
|
/**
|
|
6
7
|
* Kill a child process and its whole tree. Windows: `taskkill /T`. POSIX: the child is started as the leader of its
|
|
7
8
|
* own process group (`detached`), and the signal goes to the group (`-pid`); signalling only the shell left whatever
|
|
@@ -77,8 +78,8 @@ export const runCommandTool = {
|
|
|
77
78
|
command: args.command,
|
|
78
79
|
exitCode: out.exitCode,
|
|
79
80
|
signal: out.signal ?? null,
|
|
80
|
-
stdout: clampOutput(out.stdout),
|
|
81
|
-
stderr: clampOutput(out.stderr),
|
|
81
|
+
stdout: clampOutput(out.stdout, ctx.maxOutputChars),
|
|
82
|
+
stderr: clampOutput(out.stderr, ctx.maxOutputChars),
|
|
82
83
|
durationMs: Date.now() - started,
|
|
83
84
|
timedOut: out.timedOut ?? false,
|
|
84
85
|
};
|
|
@@ -93,6 +94,8 @@ export const runCommandTool = {
|
|
|
93
94
|
const child = spawn(args.command, {
|
|
94
95
|
shell: true,
|
|
95
96
|
cwd,
|
|
97
|
+
// Windows: never run a same-named program the repository ships in its root (see shell-env.ts).
|
|
98
|
+
env: shellEnv(process.env),
|
|
96
99
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
97
100
|
windowsHide: true,
|
|
98
101
|
// POSIX: own process group, so a kill reaches everything the shell started (see killTree).
|
|
@@ -128,8 +131,8 @@ export const runCommandTool = {
|
|
|
128
131
|
command: args.command,
|
|
129
132
|
exitCode: code,
|
|
130
133
|
signal: signal,
|
|
131
|
-
stdout: clampOutput(stdout),
|
|
132
|
-
stderr: clampOutput(stderr),
|
|
134
|
+
stdout: clampOutput(stdout, ctx.maxOutputChars),
|
|
135
|
+
stderr: clampOutput(stderr, ctx.maxOutputChars),
|
|
133
136
|
durationMs: Date.now() - started,
|
|
134
137
|
timedOut,
|
|
135
138
|
});
|
|
@@ -137,11 +140,12 @@ export const runCommandTool = {
|
|
|
137
140
|
});
|
|
138
141
|
},
|
|
139
142
|
};
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
/** Cut a command's output to `max` characters, keeping the head and the tail. */
|
|
144
|
+
export function clampOutput(text, max = MAX_OUTPUT_CHARS) {
|
|
145
|
+
if (text.length <= max)
|
|
142
146
|
return text;
|
|
143
|
-
const half = Math.floor(
|
|
147
|
+
const half = Math.floor(max / 2);
|
|
144
148
|
return (text.slice(0, half) +
|
|
145
|
-
`\n\n... (${text.length -
|
|
149
|
+
`\n\n... (${text.length - max} characters truncated) ...\n\n` +
|
|
146
150
|
text.slice(text.length - half));
|
|
147
151
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import { globalSettingsPath, projectSettingsPath, readSettings } from '../hooks/loader.js';
|
|
4
2
|
import { validateToolSandboxConfig } from '../hooks/config.js';
|
|
3
|
+
import { updateJsonFile } from '../config-file.js';
|
|
5
4
|
const REGEX_FIELDS = ['allowedCommands', 'deniedCommands', 'allowedUrls', 'deniedUrls'];
|
|
6
5
|
const PATH_FIELDS = ['allowedPaths', 'deniedPaths'];
|
|
7
6
|
/** Compile a single policy's regex-string fields to RegExp, dropping any pattern that fails to compile. */
|
|
@@ -118,15 +117,6 @@ export async function loadToolSandboxConfig(cwd, opts, globalPathOverride) {
|
|
|
118
117
|
*/
|
|
119
118
|
export async function saveToolSandboxConfig(sandbox, settingsPath) {
|
|
120
119
|
const filePath = settingsPath ?? globalSettingsPath();
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const raw = await readFile(filePath, 'utf8');
|
|
124
|
-
existing = JSON.parse(raw);
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
// Missing or malformed: start fresh rather than failing the save.
|
|
128
|
-
}
|
|
129
|
-
const merged = { ...existing, toolSandbox: sandbox };
|
|
130
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
131
|
-
await writeFile(filePath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
120
|
+
// A malformed file is refused, not replaced: it holds hooks and other settings this save does not own.
|
|
121
|
+
await updateJsonFile(filePath, (existing) => ({ ...existing, toolSandbox: sandbox }));
|
|
132
122
|
}
|
|
@@ -12,15 +12,15 @@ declare const params: z.ZodObject<{
|
|
|
12
12
|
}, "strip", z.ZodTypeAny, {
|
|
13
13
|
pattern: string;
|
|
14
14
|
path?: string | undefined;
|
|
15
|
-
max_results?: number | undefined;
|
|
16
15
|
include?: string | undefined;
|
|
16
|
+
max_results?: number | undefined;
|
|
17
17
|
include_ignored?: boolean | undefined;
|
|
18
18
|
context_lines?: number | undefined;
|
|
19
19
|
}, {
|
|
20
20
|
pattern: string;
|
|
21
21
|
path?: string | undefined;
|
|
22
|
-
max_results?: number | undefined;
|
|
23
22
|
include?: string | undefined;
|
|
23
|
+
max_results?: number | undefined;
|
|
24
24
|
include_ignored?: boolean | undefined;
|
|
25
25
|
context_lines?: number | undefined;
|
|
26
26
|
}>;
|
|
@@ -36,4 +36,10 @@ export interface SearchFilesResult {
|
|
|
36
36
|
filesScanned: number;
|
|
37
37
|
}
|
|
38
38
|
export declare const searchFilesTool: ToolDefinition<z.infer<typeof params>, SearchFilesResult>;
|
|
39
|
+
/**
|
|
40
|
+
* True for a group that contains a repetition and is itself repeated (`(a+)+`, `(\w+\s?)*`, `(x*){2,}`): the shape that
|
|
41
|
+
* makes a backtracking engine take exponential time on a line that almost matches. JavaScript cannot stop a running
|
|
42
|
+
* regex, so such a pattern would freeze the editor; it is refused before it runs.
|
|
43
|
+
*/
|
|
44
|
+
export declare function hasNestedRepetition(source: string): boolean;
|
|
39
45
|
export {};
|
|
@@ -121,7 +121,50 @@ export const searchFilesTool = {
|
|
|
121
121
|
return { pattern: args.pattern, matches, truncated, filesScanned };
|
|
122
122
|
},
|
|
123
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* True for a group that contains a repetition and is itself repeated (`(a+)+`, `(\w+\s?)*`, `(x*){2,}`): the shape that
|
|
126
|
+
* makes a backtracking engine take exponential time on a line that almost matches. JavaScript cannot stop a running
|
|
127
|
+
* regex, so such a pattern would freeze the editor; it is refused before it runs.
|
|
128
|
+
*/
|
|
129
|
+
export function hasNestedRepetition(source) {
|
|
130
|
+
const groups = []; // per open group: does it contain a repetition?
|
|
131
|
+
let inClass = false;
|
|
132
|
+
const isRepeat = (i) => source[i] === '+' || source[i] === '*' || (source[i] === '{' && /^\{\d*,?\d*\}/.test(source.slice(i)) && !/^\{\d+\}/.test(source.slice(i)));
|
|
133
|
+
for (let i = 0; i < source.length; i++) {
|
|
134
|
+
const c = source[i];
|
|
135
|
+
if (c === '\\') {
|
|
136
|
+
i++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (inClass) {
|
|
140
|
+
if (c === ']')
|
|
141
|
+
inClass = false;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (c === '[') {
|
|
145
|
+
inClass = true;
|
|
146
|
+
}
|
|
147
|
+
else if (c === '(') {
|
|
148
|
+
groups.push(false);
|
|
149
|
+
}
|
|
150
|
+
else if (c === ')') {
|
|
151
|
+
const inner = groups.pop() ?? false;
|
|
152
|
+
if (inner && isRepeat(i + 1))
|
|
153
|
+
return true;
|
|
154
|
+
// A repeated group counts as a repetition inside the group around it.
|
|
155
|
+
if ((inner || isRepeat(i + 1)) && groups.length)
|
|
156
|
+
groups[groups.length - 1] = true;
|
|
157
|
+
}
|
|
158
|
+
else if (isRepeat(i) && groups.length) {
|
|
159
|
+
groups[groups.length - 1] = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
124
164
|
function safeRegex(source) {
|
|
165
|
+
if (hasNestedRepetition(source)) {
|
|
166
|
+
throw new Error('Pattern has a repeated group that repeats again (like (a+)+), which can take exponential time on some lines. Simplify it, e.g. a+ instead of (a+)+.');
|
|
167
|
+
}
|
|
125
168
|
try {
|
|
126
169
|
return new RegExp(source, 'gm');
|
|
127
170
|
}
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ import type { ToolContext } from '../types.js';
|
|
|
9
9
|
export declare function resolveSafePath(inputPath: string, ctx: ToolContext, options?: {
|
|
10
10
|
allowOutside?: boolean;
|
|
11
11
|
}): string;
|
|
12
|
+
/**
|
|
13
|
+
* For a `path.relative(root, target)` result: does it lead out of `root`? `..` itself or `../…` does; a name that merely
|
|
14
|
+
* starts with two dots (`..notes.md`) does not. Use this instead of `rel.startsWith('..')`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isOutsideRel(rel: string): boolean;
|
|
12
17
|
/**
|
|
13
18
|
* Convenience: run `resolveSafePath` then enforce the per-tool path policy
|
|
14
19
|
* (allowedPaths / deniedPaths). Throws on either kind of violation. File
|
package/dist/tools/shared.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { checkPolicyViolation } from './policy.js';
|
|
3
4
|
/**
|
|
@@ -12,13 +13,40 @@ export function resolveSafePath(inputPath, ctx, options = {}) {
|
|
|
12
13
|
? path.normalize(inputPath)
|
|
13
14
|
: path.resolve(ctx.cwd, inputPath);
|
|
14
15
|
if (!options.allowOutside) {
|
|
15
|
-
|
|
16
|
-
if (
|
|
16
|
+
// Lexically, then for real: a symlink or junction inside the workspace can point anywhere on disk.
|
|
17
|
+
if (escapes(ctx.cwd, resolved) || escapes(realPath(ctx.cwd), realPath(resolved))) {
|
|
17
18
|
throw new Error(`Path "${inputPath}" resolves outside of the working directory (${ctx.cwd}).`);
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
return resolved;
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* For a `path.relative(root, target)` result: does it lead out of `root`? `..` itself or `../…` does; a name that merely
|
|
25
|
+
* starts with two dots (`..notes.md`) does not. Use this instead of `rel.startsWith('..')`.
|
|
26
|
+
*/
|
|
27
|
+
export function isOutsideRel(rel) {
|
|
28
|
+
return rel === '..' || rel.startsWith(`..${path.sep}`) || rel.startsWith('../') || path.isAbsolute(rel);
|
|
29
|
+
}
|
|
30
|
+
function escapes(root, target) {
|
|
31
|
+
return isOutsideRel(path.relative(root, target));
|
|
32
|
+
}
|
|
33
|
+
/** The real location of `p`: links resolved on the longest part that exists, the rest (a file not yet written) appended. */
|
|
34
|
+
function realPath(p) {
|
|
35
|
+
let existing = p;
|
|
36
|
+
const rest = [];
|
|
37
|
+
for (;;) {
|
|
38
|
+
try {
|
|
39
|
+
return path.join(realpathSync.native(existing), ...rest);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
const parent = path.dirname(existing);
|
|
43
|
+
if (parent === existing)
|
|
44
|
+
return p;
|
|
45
|
+
rest.unshift(path.basename(existing));
|
|
46
|
+
existing = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
22
50
|
/**
|
|
23
51
|
* Convenience: run `resolveSafePath` then enforce the per-tool path policy
|
|
24
52
|
* (allowedPaths / deniedPaths). Throws on either kind of violation. File
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment for a command run through a shell. On Windows, cmd.exe looks for a bare program name in the current
|
|
3
|
+
* directory before PATH, and Forge runs commands in the workspace: a repository could ship `npm.cmd` or `node.cmd`
|
|
4
|
+
* and have it run instead of the real one when the person approves "npm test". `NoDefaultCurrentDirectoryInExePath`
|
|
5
|
+
* tells cmd.exe to skip the current directory; an explicit `.\script.cmd` still works.
|
|
6
|
+
*/
|
|
7
|
+
export declare function shellEnv(env: NodeJS.ProcessEnv, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment for a command run through a shell. On Windows, cmd.exe looks for a bare program name in the current
|
|
3
|
+
* directory before PATH, and Forge runs commands in the workspace: a repository could ship `npm.cmd` or `node.cmd`
|
|
4
|
+
* and have it run instead of the real one when the person approves "npm test". `NoDefaultCurrentDirectoryInExePath`
|
|
5
|
+
* tells cmd.exe to skip the current directory; an explicit `.\script.cmd` still works.
|
|
6
|
+
*/
|
|
7
|
+
export function shellEnv(env, platform = process.platform) {
|
|
8
|
+
return platform === 'win32' ? { ...env, NoDefaultCurrentDirectoryInExePath: '1' } : { ...env };
|
|
9
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ToolDefinition } from '../types.js';
|
|
3
|
+
declare const params: z.ZodObject<{
|
|
4
|
+
ids: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
5
|
+
timeout_seconds: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
}, "strip", z.ZodTypeAny, {
|
|
7
|
+
ids?: string[] | undefined;
|
|
8
|
+
timeout_seconds?: number | undefined;
|
|
9
|
+
}, {
|
|
10
|
+
ids?: string[] | undefined;
|
|
11
|
+
timeout_seconds?: number | undefined;
|
|
12
|
+
}>;
|
|
13
|
+
export interface SubAgentWaitResult {
|
|
14
|
+
agents: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
task: string;
|
|
17
|
+
state: string;
|
|
18
|
+
finalText?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
}>;
|
|
21
|
+
stillRunning: number;
|
|
22
|
+
message?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Collect the answers of sub-agents started with `sub_agent { background: true }`. Waits until they finish or the
|
|
26
|
+
* timeout passes, whichever is first; a run that is still going is reported as running and can be waited for again.
|
|
27
|
+
*/
|
|
28
|
+
export declare const subAgentWaitTool: ToolDefinition<z.infer<typeof params>, SubAgentWaitResult>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const params = z.object({
|
|
3
|
+
ids: z
|
|
4
|
+
.array(z.string().min(1).max(40))
|
|
5
|
+
.max(20)
|
|
6
|
+
.optional()
|
|
7
|
+
.describe('Ids from sub_agent { background: true } to wait for. Omit to wait for every background agent still running.'),
|
|
8
|
+
timeout_seconds: z
|
|
9
|
+
.number()
|
|
10
|
+
.min(0)
|
|
11
|
+
.max(900)
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('How long to wait before giving up and reporting what is still running. Default 300. 0 only looks.'),
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Collect the answers of sub-agents started with `sub_agent { background: true }`. Waits until they finish or the
|
|
17
|
+
* timeout passes, whichever is first; a run that is still going is reported as running and can be waited for again.
|
|
18
|
+
*/
|
|
19
|
+
export const subAgentWaitTool = {
|
|
20
|
+
name: 'sub_agent_wait',
|
|
21
|
+
description: 'Wait for background sub-agents (started with sub_agent { background: true }) and return their answers. A run still going when the timeout passes is reported as running.',
|
|
22
|
+
parameters: params,
|
|
23
|
+
execute: async (args, ctx) => {
|
|
24
|
+
const bg = ctx.background;
|
|
25
|
+
if (!bg)
|
|
26
|
+
throw new Error('Background agents are not available here.');
|
|
27
|
+
const valid = params.parse(args);
|
|
28
|
+
const unknown = (valid.ids ?? []).filter((id) => !bg.get(id));
|
|
29
|
+
const known = (valid.ids ?? []).filter((id) => bg.get(id));
|
|
30
|
+
if (valid.ids && known.length === 0) {
|
|
31
|
+
return { agents: [], stillRunning: 0, message: `No background agent with id ${unknown.join(', ')}.` };
|
|
32
|
+
}
|
|
33
|
+
if (!valid.ids && bg.list().filter((a) => a.state === 'running').length === 0) {
|
|
34
|
+
// Nothing running: report what has finished, if anything has.
|
|
35
|
+
const finished = bg.takeUndelivered();
|
|
36
|
+
return {
|
|
37
|
+
agents: finished.map(view),
|
|
38
|
+
stillRunning: 0,
|
|
39
|
+
...(finished.length === 0 ? { message: 'There are no background agents running.' } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const timeoutMs = (valid.timeout_seconds ?? 300) * 1000;
|
|
43
|
+
const got = await bg.wait(valid.ids ? known : undefined, { timeoutMs, ...(ctx.abortSignal ? { signal: ctx.abortSignal } : {}) });
|
|
44
|
+
bg.markDelivered(got.map((a) => a.id));
|
|
45
|
+
return {
|
|
46
|
+
agents: got.map(view),
|
|
47
|
+
stillRunning: got.filter((a) => a.state === 'running').length,
|
|
48
|
+
...(unknown.length ? { message: `No background agent with id ${unknown.join(', ')}.` } : {}),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
function view(a) {
|
|
53
|
+
return {
|
|
54
|
+
id: a.id,
|
|
55
|
+
task: a.task,
|
|
56
|
+
state: a.state,
|
|
57
|
+
...(a.result?.finalText ? { finalText: a.result.finalText } : {}),
|
|
58
|
+
...(a.error ? { error: a.error } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import type
|
|
2
|
+
import { type AgentDef } from '../agents/agent-files.js';
|
|
3
|
+
import type { AgentConfig, ModelConfig, ToolContext, ToolDefinition } from '../types.js';
|
|
3
4
|
declare const params: z.ZodObject<{
|
|
4
5
|
/** The task for the sub-agent to perform. Free-form text. */
|
|
5
6
|
task: z.ZodString;
|
|
7
|
+
/** Name of an agent file (`.forge/agents/<name>.md`) to run as: its prompt, tools, model and step cap. */
|
|
8
|
+
agent: z.ZodOptional<z.ZodString>;
|
|
6
9
|
/** Hard cap on iterations for the sub-agent. Default 10. */
|
|
7
10
|
max_iterations: z.ZodOptional<z.ZodNumber>;
|
|
8
11
|
/**
|
|
@@ -51,23 +54,39 @@ declare const params: z.ZodObject<{
|
|
|
51
54
|
* won't collide.
|
|
52
55
|
*/
|
|
53
56
|
cleanup_branch: z.ZodOptional<z.ZodBoolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Start the child and return at once. The child keeps working while the parent continues; collect its answer with
|
|
59
|
+
* `sub_agent_wait`. Only where the surface can show background runs; elsewhere the child runs to completion as usual.
|
|
60
|
+
*/
|
|
61
|
+
background: z.ZodOptional<z.ZodBoolean>;
|
|
54
62
|
}, "strip", z.ZodTypeAny, {
|
|
55
63
|
task: string;
|
|
64
|
+
agent?: string | undefined;
|
|
56
65
|
cwd?: string | undefined;
|
|
57
|
-
max_iterations?: number | undefined;
|
|
58
66
|
auto?: boolean | undefined;
|
|
67
|
+
background?: boolean | undefined;
|
|
68
|
+
max_iterations?: number | undefined;
|
|
59
69
|
use_worktree?: boolean | undefined;
|
|
60
70
|
worktree_label?: string | undefined;
|
|
61
71
|
cleanup_branch?: boolean | undefined;
|
|
62
72
|
}, {
|
|
63
73
|
task: string;
|
|
74
|
+
agent?: string | undefined;
|
|
64
75
|
cwd?: string | undefined;
|
|
65
|
-
max_iterations?: number | undefined;
|
|
66
76
|
auto?: boolean | undefined;
|
|
77
|
+
background?: boolean | undefined;
|
|
78
|
+
max_iterations?: number | undefined;
|
|
67
79
|
use_worktree?: boolean | undefined;
|
|
68
80
|
worktree_label?: string | undefined;
|
|
69
81
|
cleanup_branch?: boolean | undefined;
|
|
70
82
|
}>;
|
|
83
|
+
/** What `sub_agent { background: true }` returns instead of a result. */
|
|
84
|
+
export interface SubAgentStarted {
|
|
85
|
+
started: true;
|
|
86
|
+
id: string;
|
|
87
|
+
task: string;
|
|
88
|
+
message: string;
|
|
89
|
+
}
|
|
71
90
|
export interface SubAgentResult {
|
|
72
91
|
task: string;
|
|
73
92
|
finalText: string;
|
|
@@ -103,5 +122,32 @@ export interface SubAgentResult {
|
|
|
103
122
|
* tools unless the parent explicitly opts in via the `auto` parameter.
|
|
104
123
|
* - The sub-agent inherits the parent's cwd and model unless overridden.
|
|
105
124
|
*/
|
|
106
|
-
export declare const subAgentTool: ToolDefinition<z.infer<typeof params>, SubAgentResult>;
|
|
125
|
+
export declare const subAgentTool: ToolDefinition<z.infer<typeof params>, SubAgentResult | SubAgentStarted>;
|
|
126
|
+
/**
|
|
127
|
+
* What a child runs with: the tools it may see, its model, step cap and system prompt. Without an agent file this is
|
|
128
|
+
* the old behaviour (every tool but `sub_agent`, the sub-agent or parent model, default cap). `sub_agent` is never
|
|
129
|
+
* offered to a child, whatever the file says.
|
|
130
|
+
*/
|
|
131
|
+
export declare function childSetupFor<T extends {
|
|
132
|
+
name: string;
|
|
133
|
+
}>(def: AgentDef | undefined, tools: readonly T[], ctx: Pick<ToolContext, 'subAgentModel' | 'parentModel'>, requestedIterations: number | undefined): {
|
|
134
|
+
tools: T[];
|
|
135
|
+
model: ModelConfig;
|
|
136
|
+
maxIterations: number;
|
|
137
|
+
systemPrompt?: string;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* The child's Agent configuration. It carries the limits the person set for the parent (permission rules, tool
|
|
141
|
+
* sandbox): a child started with `auto: true` must not be a way around an `edit: deny` rule or a denied tool.
|
|
142
|
+
*/
|
|
143
|
+
export declare function childAgentConfig(setup: {
|
|
144
|
+
tools: readonly unknown[];
|
|
145
|
+
model: ModelConfig;
|
|
146
|
+
maxIterations: number;
|
|
147
|
+
systemPrompt?: string;
|
|
148
|
+
}, ctx: Pick<ToolContext, 'autoApproveTools' | 'permissionRules' | 'toolSandbox' | 'askBudget'>, cwd: string, auto: boolean): AgentConfig;
|
|
149
|
+
/** What a child runs without asking: what the parent does, else the read-only tools. Destructive tools still never run unasked. */
|
|
150
|
+
export declare function childAutoApprove(ctx: Pick<ToolContext, 'autoApproveTools'>): string[];
|
|
151
|
+
/** The configured sub-agent model, else the parent's, else a last-resort default. */
|
|
152
|
+
export declare function childModelFor(ctx: Pick<ToolContext, 'subAgentModel' | 'parentModel'>): ModelConfig;
|
|
107
153
|
export {};
|