@wix/pathgrade 1.0.5 → 1.0.7
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 +25 -5
- package/dist/agents/opencode-contract.d.ts +24 -0
- package/dist/agents/opencode-contract.js +84 -0
- package/dist/agents/opencode.d.ts +27 -0
- package/dist/agents/opencode.js +508 -0
- package/dist/agents/registry.js +2 -0
- package/dist/providers/credentials.js +12 -0
- package/dist/sdk/agent-resolution.d.ts +2 -1
- package/dist/sdk/agent-resolution.js +13 -1
- package/dist/sdk/agent.js +17 -5
- package/dist/sdk/converse.js +3 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/managed-session.d.ts +2 -0
- package/dist/sdk/managed-session.js +2 -0
- package/dist/sdk/runtime-policy.js +1 -0
- package/dist/sdk/snapshots.d.ts +2 -2
- package/dist/sdk/snapshots.js +13 -4
- package/dist/sdk/types.d.ts +7 -3
- package/dist/sdk/types.js +1 -0
- package/dist/tool-events.d.ts +1 -1
- package/dist/types.d.ts +4 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Pathgrade
|
|
2
2
|
|
|
3
|
-
**Evaluate AI coding agents with Vitest or Jest.** Write evals as normal `.eval.ts` files, run Claude Code, Codex, or
|
|
3
|
+
**Evaluate AI coding agents with Vitest or Jest.** Write evals as normal `.eval.ts` files, run Claude Code, Codex, Cursor, or trusted-internal OpenCode fixtures in isolated workspaces, and score the result with deterministic checks, rubric judges, and tool-usage assertions.
|
|
4
4
|
|
|
5
5
|
## Why Pathgrade?
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
## Quick Start
|
|
15
15
|
|
|
16
|
-
**Prerequisites**: Node.js 20.11+, Vitest 4+ or Jest 30+, and at least one configured agent runtime. Claude uses the bundled `@anthropic-ai/claude-agent-sdk` binary by default; Codex requires the `codex` CLI; Cursor requires the `cursor-agent` CLI.
|
|
16
|
+
**Prerequisites**: Node.js 20.11+, Vitest 4+ or Jest 30+, and at least one configured agent runtime. Claude uses the bundled `@anthropic-ai/claude-agent-sdk` binary by default; Codex requires the `codex` CLI; Cursor requires the `cursor-agent` CLI. OpenCode requires the exact v1.18.18 macOS ARM64 or Linux ARM64 executable.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
yarn add -D @wix/pathgrade
|
|
@@ -40,6 +40,9 @@ By default, Pathgrade tries to reuse the agent CLI's native auth before falling
|
|
|
40
40
|
- forwards `CURSOR_API_KEY` when set
|
|
41
41
|
- macOS: reuses `cursor-agent login` OAuth tokens from the login Keychain
|
|
42
42
|
- surfaces a clear error when neither is available (run `cursor-agent login` or set `CURSOR_API_KEY`)
|
|
43
|
+
- **OpenCode**
|
|
44
|
+
- requires an explicit `ANTHROPIC_API_KEY` in `createAgent({ env })`; ambient credentials and login state are never read
|
|
45
|
+
- accepts an optional explicit HTTPS `ANTHROPIC_BASE_URL` ending in `/v1`
|
|
43
46
|
|
|
44
47
|
If you set `ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, or `CURSOR_API_BASE_URL`, set the matching API key too.
|
|
45
48
|
|
|
@@ -231,7 +234,24 @@ toolUsage('expected-workflow', [
|
|
|
231
234
|
|
|
232
235
|
## Conversations
|
|
233
236
|
|
|
234
|
-
Pathgrade
|
|
237
|
+
Pathgrade supports `claude`, `codex`, `cursor`, and `opencode`. Set the backend per test via `createAgent({ agent: 'claude' })`, or omit `agent` and use `PATHGRADE_AGENT` as the process-wide fallback.
|
|
238
|
+
|
|
239
|
+
### OpenCode trusted-internal backend
|
|
240
|
+
|
|
241
|
+
OpenCode v1 is only for fixtures, prompts, skills, and generated MCP mocks controlled by trusted Pathgrade maintainers. It is not a security sandbox: OpenCode and Bash children run with the host user's filesystem, process, and network authority and receive the provider credentials. Do not use it for external repositories, user-supplied prompts, or adversarial fixtures.
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
const agent = await createAgent({
|
|
245
|
+
agent: 'opencode',
|
|
246
|
+
opencodeExecutable: '/absolute/path/to/opencode-v1.18.18',
|
|
247
|
+
env: {
|
|
248
|
+
ANTHROPIC_API_KEY: process.env.APP_ANTHROPIC_API_KEY!,
|
|
249
|
+
ANTHROPIC_BASE_URL: 'https://api.example.com/v1',
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The backend supports only `anthropic/claude-sonnet-5`, generated stdio `mcpMock` servers, and clean hosts without system-managed OpenCode configuration. `PATHGRADE_AGENT=opencode` selects the backend but does not supply the required executable or credentials.
|
|
235
255
|
|
|
236
256
|
### `agent.prompt()` - One shot
|
|
237
257
|
|
|
@@ -409,13 +429,13 @@ Notes:
|
|
|
409
429
|
|
|
410
430
|
| Variable | Purpose |
|
|
411
431
|
|----------|---------|
|
|
412
|
-
| `ANTHROPIC_API_KEY` | Claude auth
|
|
432
|
+
| `ANTHROPIC_API_KEY` | Claude auth; also required explicitly in `AgentOptions.env` for OpenCode |
|
|
413
433
|
| `OPENAI_API_KEY` | Codex auth and the required key when using `OPENAI_BASE_URL` |
|
|
414
434
|
| `CURSOR_API_KEY` | Cursor auth and the required key when using `CURSOR_API_BASE_URL` |
|
|
415
435
|
| `ANTHROPIC_BASE_URL` | Custom Anthropic-compatible endpoint |
|
|
416
436
|
| `OPENAI_BASE_URL` | Custom OpenAI-compatible endpoint |
|
|
417
437
|
| `CURSOR_API_BASE_URL` | Custom Cursor-compatible endpoint |
|
|
418
|
-
| `PATHGRADE_AGENT` | Fallback agent for all tests (`claude`, `codex`, or `
|
|
438
|
+
| `PATHGRADE_AGENT` | Fallback agent for all tests (`claude`, `codex`, `cursor`, or `opencode`). `createAgent({ agent })` wins over this. |
|
|
419
439
|
| `PATHGRADE_CODEX_TRANSPORT` | Fallback Codex transport (`exec` or `app-server`). `createAgent({ transport })` wins over this. |
|
|
420
440
|
| `PATHGRADE_VERBOSE` | `1` enables live per-turn streaming to stderr |
|
|
421
441
|
| `PATHGRADE_DIAGNOSTICS` | `1` prints full diagnostics for passing evals too |
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MockMcpServerDescriptor } from '../core/mcp-mock.types.js';
|
|
2
|
+
import type { AgentName, AgentOptions } from '../sdk/types.js';
|
|
3
|
+
export declare const OPENCODE_MODEL = "anthropic/claude-sonnet-5";
|
|
4
|
+
export declare const OPENCODE_VERSION = "1.18.18";
|
|
5
|
+
export interface OpenCodeRuntimeLockEntry {
|
|
6
|
+
version: typeof OPENCODE_VERSION;
|
|
7
|
+
executableSha256: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const OPENCODE_RUNTIME_LOCK: {
|
|
10
|
+
readonly 'darwin-arm64': {
|
|
11
|
+
readonly version: "1.18.18";
|
|
12
|
+
readonly executableSha256: "4f5979c2dadb06fbff1335335afaaea274e58f92e79aa43cf2ed98618d555422";
|
|
13
|
+
};
|
|
14
|
+
readonly 'linux-arm64': {
|
|
15
|
+
readonly version: "1.18.18";
|
|
16
|
+
readonly executableSha256: "a63ef0c7271383e48ffe36c156b4146087bbbc97929dec40e8be95ed4f4d76ae";
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type OpenCodePlatformKey = keyof typeof OPENCODE_RUNTIME_LOCK;
|
|
20
|
+
export declare function getOpenCodePlatformKey(platform: NodeJS.Platform, arch: string): OpenCodePlatformKey | undefined;
|
|
21
|
+
export declare function currentOpenCodePlatformKey(): OpenCodePlatformKey | undefined;
|
|
22
|
+
export declare function validateOpenCodeDeclaration(agent: AgentName, opts: AgentOptions): void;
|
|
23
|
+
export declare function sanitizeOpenCodeToolName(value: string): string;
|
|
24
|
+
export declare function collectOpenCodeMcpToolNames(declaration: MockMcpServerDescriptor | MockMcpServerDescriptor[] | undefined): string[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
export const OPENCODE_MODEL = 'anthropic/claude-sonnet-5';
|
|
3
|
+
export const OPENCODE_VERSION = '1.18.18';
|
|
4
|
+
export const OPENCODE_RUNTIME_LOCK = {
|
|
5
|
+
'darwin-arm64': {
|
|
6
|
+
version: OPENCODE_VERSION,
|
|
7
|
+
executableSha256: '4f5979c2dadb06fbff1335335afaaea274e58f92e79aa43cf2ed98618d555422',
|
|
8
|
+
},
|
|
9
|
+
'linux-arm64': {
|
|
10
|
+
version: OPENCODE_VERSION,
|
|
11
|
+
executableSha256: 'a63ef0c7271383e48ffe36c156b4146087bbbc97929dec40e8be95ed4f4d76ae',
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export function getOpenCodePlatformKey(platform, arch) {
|
|
15
|
+
const key = `${platform}-${arch}`;
|
|
16
|
+
return key in OPENCODE_RUNTIME_LOCK ? key : undefined;
|
|
17
|
+
}
|
|
18
|
+
export function currentOpenCodePlatformKey() {
|
|
19
|
+
return getOpenCodePlatformKey(process.platform, process.arch);
|
|
20
|
+
}
|
|
21
|
+
function hasOwn(record, key) {
|
|
22
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
23
|
+
}
|
|
24
|
+
function assertOpenCodeBaseUrl(value) {
|
|
25
|
+
let url;
|
|
26
|
+
try {
|
|
27
|
+
url = new URL(value);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error('OpenCode ANTHROPIC_BASE_URL must be an absolute HTTPS API root ending in /v1');
|
|
31
|
+
}
|
|
32
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || !url.pathname.endsWith('/v1')) {
|
|
33
|
+
throw new Error('OpenCode ANTHROPIC_BASE_URL must be an absolute HTTPS API root ending in /v1');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function validateOpenCodeDeclaration(agent, opts) {
|
|
37
|
+
if (agent !== 'opencode') {
|
|
38
|
+
if (opts.opencodeExecutable !== undefined) {
|
|
39
|
+
throw new Error('opencodeExecutable is valid only for agent: "opencode"');
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!currentOpenCodePlatformKey()) {
|
|
44
|
+
throw new Error(`OpenCode is not supported on ${process.platform}-${process.arch}`);
|
|
45
|
+
}
|
|
46
|
+
if (!opts.opencodeExecutable || !path.isAbsolute(opts.opencodeExecutable)) {
|
|
47
|
+
throw new Error('OpenCode requires an absolute opencodeExecutable path');
|
|
48
|
+
}
|
|
49
|
+
if (opts.model !== undefined && opts.model !== OPENCODE_MODEL) {
|
|
50
|
+
throw new Error(`OpenCode v1 supports only model ${OPENCODE_MODEL}`);
|
|
51
|
+
}
|
|
52
|
+
const env = opts.env ?? {};
|
|
53
|
+
if (!hasOwn(env, 'ANTHROPIC_API_KEY') || !env.ANTHROPIC_API_KEY?.trim()) {
|
|
54
|
+
throw new Error('OpenCode requires a nonempty own env.ANTHROPIC_API_KEY');
|
|
55
|
+
}
|
|
56
|
+
if (hasOwn(env, 'ANTHROPIC_BASE_URL')) {
|
|
57
|
+
if (!env.ANTHROPIC_BASE_URL) {
|
|
58
|
+
throw new Error('OpenCode ANTHROPIC_BASE_URL must be a nonempty absolute HTTPS API root ending in /v1');
|
|
59
|
+
}
|
|
60
|
+
assertOpenCodeBaseUrl(env.ANTHROPIC_BASE_URL);
|
|
61
|
+
}
|
|
62
|
+
if (opts.copyFromHome !== undefined)
|
|
63
|
+
throw new Error('OpenCode does not support copyFromHome');
|
|
64
|
+
if (opts.mcpConfigFile !== undefined)
|
|
65
|
+
throw new Error('OpenCode v1 supports generated mcpMock only, not mcpConfigFile');
|
|
66
|
+
if (opts.mcpSafety !== undefined)
|
|
67
|
+
throw new Error('OpenCode v1 does not support mcpSafety');
|
|
68
|
+
if (opts.transport !== undefined)
|
|
69
|
+
throw new Error('OpenCode does not support transport overrides');
|
|
70
|
+
for (const key of Object.keys(env)) {
|
|
71
|
+
if (key === 'HOME' || key.startsWith('XDG_') || key.startsWith('OPENCODE_')) {
|
|
72
|
+
throw new Error(`OpenCode reserves env.${key}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function sanitizeOpenCodeToolName(value) {
|
|
77
|
+
return value.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
78
|
+
}
|
|
79
|
+
export function collectOpenCodeMcpToolNames(declaration) {
|
|
80
|
+
if (!declaration)
|
|
81
|
+
return [];
|
|
82
|
+
const descriptors = Array.isArray(declaration) ? declaration : [declaration];
|
|
83
|
+
return descriptors.flatMap((descriptor) => descriptor.config.tools.map((tool) => `${sanitizeOpenCodeToolName(descriptor.config.name)}_${sanitizeOpenCodeToolName(tool.name)}`));
|
|
84
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BaseAgent, type AgentCommandRunner, type AgentSession, type AgentSessionOptions, type AgentTurnResult, type EnvironmentHandle } from '../types.js';
|
|
2
|
+
interface SpawnResult {
|
|
3
|
+
stdout: string;
|
|
4
|
+
exitCode: number | null;
|
|
5
|
+
signal: NodeJS.Signals | null;
|
|
6
|
+
overflow: boolean;
|
|
7
|
+
aborted: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface SpawnOpenCodeOptions {
|
|
10
|
+
cwd: string;
|
|
11
|
+
env: Record<string, string>;
|
|
12
|
+
stdin: string;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
outputCapBytes?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function spawnOpenCode(executable: string, args: string[], options: SpawnOpenCodeOptions): Promise<SpawnResult>;
|
|
17
|
+
interface ParsedOpenCodeTurn {
|
|
18
|
+
result: AgentTurnResult;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function parseOpenCodeOutput(stdout: string, processResult: Pick<SpawnResult, 'exitCode' | 'overflow' | 'aborted'>, mcpToolNames: ReadonlySet<string>): ParsedOpenCodeTurn;
|
|
22
|
+
export declare function managedOpenCodeConfigPaths(platform?: NodeJS.Platform, username?: string): string[];
|
|
23
|
+
export declare function assertCleanManagedOpenCodeHost(candidates?: string[]): Promise<void>;
|
|
24
|
+
export declare class OpenCodeAgent extends BaseAgent {
|
|
25
|
+
createSession(runtime: EnvironmentHandle, _runCommand: AgentCommandRunner, options?: AgentSessionOptions): Promise<AgentSession>;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createReadStream } from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../types.js';
|
|
8
|
+
import { buildSummary, enrichSkillEvents } from '../tool-events.js';
|
|
9
|
+
import { readStagedMcpServers } from '../providers/mcp-config.js';
|
|
10
|
+
import { currentOpenCodePlatformKey, OPENCODE_MODEL, OPENCODE_RUNTIME_LOCK, } from './opencode-contract.js';
|
|
11
|
+
const OUTPUT_CAP_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
const FIXED_OPENCODE_ENV = {
|
|
13
|
+
OPENCODE_CLIENT: 'pathgrade',
|
|
14
|
+
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
|
15
|
+
OPENCODE_DISABLE_PRUNE: '1',
|
|
16
|
+
OPENCODE_DISABLE_MODELS_FETCH: '1',
|
|
17
|
+
OPENCODE_DISABLE_DEFAULT_PLUGINS: '1',
|
|
18
|
+
OPENCODE_DISABLE_EXTERNAL_SKILLS: '1',
|
|
19
|
+
OPENCODE_DISABLE_LSP_DOWNLOAD: '1',
|
|
20
|
+
OPENCODE_DISABLE_SHARE: '1',
|
|
21
|
+
OPENCODE_PURE: '1',
|
|
22
|
+
};
|
|
23
|
+
const OPENCODE_PERMISSION = JSON.stringify({
|
|
24
|
+
read: 'allow',
|
|
25
|
+
edit: 'allow',
|
|
26
|
+
glob: 'allow',
|
|
27
|
+
grep: 'allow',
|
|
28
|
+
list: 'allow',
|
|
29
|
+
bash: 'allow',
|
|
30
|
+
todowrite: 'allow',
|
|
31
|
+
lsp: 'allow',
|
|
32
|
+
skill: 'allow',
|
|
33
|
+
task: 'deny',
|
|
34
|
+
question: 'deny',
|
|
35
|
+
plan_enter: 'deny',
|
|
36
|
+
plan_exit: 'deny',
|
|
37
|
+
external_directory: 'deny',
|
|
38
|
+
webfetch: 'deny',
|
|
39
|
+
websearch: 'deny',
|
|
40
|
+
});
|
|
41
|
+
const NATIVE_TOOL_ACTIONS = {
|
|
42
|
+
bash: 'run_shell',
|
|
43
|
+
read: 'read_file',
|
|
44
|
+
write: 'write_file',
|
|
45
|
+
edit: 'edit_file',
|
|
46
|
+
grep: 'search_code',
|
|
47
|
+
glob: 'list_files',
|
|
48
|
+
list: 'list_files',
|
|
49
|
+
skill: 'use_skill',
|
|
50
|
+
todowrite: 'update_todos',
|
|
51
|
+
lsp: 'search_code',
|
|
52
|
+
};
|
|
53
|
+
const ALLOWED_EVENT_TYPES = new Set([
|
|
54
|
+
'tool_use',
|
|
55
|
+
'step_start',
|
|
56
|
+
'step_finish',
|
|
57
|
+
'text',
|
|
58
|
+
'reasoning',
|
|
59
|
+
'error',
|
|
60
|
+
]);
|
|
61
|
+
export function spawnOpenCode(executable, args, options) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const stdout = [];
|
|
64
|
+
const cap = options.outputCapBytes ?? OUTPUT_CAP_BYTES;
|
|
65
|
+
let stdoutBytes = 0;
|
|
66
|
+
let stderrBytes = 0;
|
|
67
|
+
let overflow = false;
|
|
68
|
+
let aborted = false;
|
|
69
|
+
let killTimer;
|
|
70
|
+
let settled = false;
|
|
71
|
+
const child = spawn(executable, args, {
|
|
72
|
+
cwd: options.cwd,
|
|
73
|
+
env: options.env,
|
|
74
|
+
shell: false,
|
|
75
|
+
detached: true,
|
|
76
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
77
|
+
});
|
|
78
|
+
const killGroup = (signal) => {
|
|
79
|
+
if (!child.pid)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
process.kill(-child.pid, signal);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// The process may have exited between the state check and kill.
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const terminate = () => {
|
|
89
|
+
killGroup('SIGTERM');
|
|
90
|
+
killTimer ??= setTimeout(() => killGroup('SIGKILL'), 250);
|
|
91
|
+
killTimer.unref();
|
|
92
|
+
};
|
|
93
|
+
const onAbort = () => {
|
|
94
|
+
aborted = true;
|
|
95
|
+
terminate();
|
|
96
|
+
};
|
|
97
|
+
const onOverflow = () => {
|
|
98
|
+
overflow = true;
|
|
99
|
+
terminate();
|
|
100
|
+
};
|
|
101
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
102
|
+
if (options.signal?.aborted)
|
|
103
|
+
onAbort();
|
|
104
|
+
child.stdout.on('data', (chunk) => {
|
|
105
|
+
stdoutBytes += chunk.length;
|
|
106
|
+
if (stdoutBytes <= cap)
|
|
107
|
+
stdout.push(chunk);
|
|
108
|
+
else
|
|
109
|
+
onOverflow();
|
|
110
|
+
});
|
|
111
|
+
child.stderr.on('data', (chunk) => {
|
|
112
|
+
stderrBytes += chunk.length;
|
|
113
|
+
if (stderrBytes > cap)
|
|
114
|
+
onOverflow();
|
|
115
|
+
});
|
|
116
|
+
child.stdin.on('error', () => undefined);
|
|
117
|
+
if (!aborted)
|
|
118
|
+
child.stdin.end(options.stdin);
|
|
119
|
+
else
|
|
120
|
+
child.stdin.destroy();
|
|
121
|
+
child.once('error', (error) => finish(undefined, undefined, error));
|
|
122
|
+
child.once('close', (code, signal) => finish(code, signal));
|
|
123
|
+
function finish(code, signal, error) {
|
|
124
|
+
if (settled)
|
|
125
|
+
return;
|
|
126
|
+
settled = true;
|
|
127
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
128
|
+
if (killTimer)
|
|
129
|
+
clearTimeout(killTimer);
|
|
130
|
+
if (error) {
|
|
131
|
+
reject(error);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
resolve({
|
|
135
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
136
|
+
exitCode: code ?? null,
|
|
137
|
+
signal: signal ?? null,
|
|
138
|
+
overflow,
|
|
139
|
+
aborted,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function record(value, label) {
|
|
145
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
146
|
+
throw new Error(`OpenCode protocol error: ${label} must be an object`);
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
function finiteNumber(value, label) {
|
|
151
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
152
|
+
throw new Error(`OpenCode protocol error: ${label} must be a nonnegative finite number`);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
function requiredString(value, label) {
|
|
157
|
+
if (typeof value !== 'string' || !value) {
|
|
158
|
+
throw new Error(`OpenCode protocol error: ${label} must be a nonempty string`);
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
function sanitizedProviderError(event) {
|
|
163
|
+
const error = record(event.error, 'error');
|
|
164
|
+
const data = error.data && typeof error.data === 'object' && !Array.isArray(error.data)
|
|
165
|
+
? error.data
|
|
166
|
+
: {};
|
|
167
|
+
const message = typeof data.message === 'string' && data.message.trim()
|
|
168
|
+
? data.message.trim().slice(0, 1_000)
|
|
169
|
+
: 'OpenCode provider error';
|
|
170
|
+
const status = typeof data.statusCode === 'number' ? ` status=${data.statusCode}` : '';
|
|
171
|
+
const retryable = typeof data.isRetryable === 'boolean' ? ` retryable=${data.isRetryable}` : '';
|
|
172
|
+
return new Error(`${message}${status}${retryable}`);
|
|
173
|
+
}
|
|
174
|
+
export function parseOpenCodeOutput(stdout, processResult, mcpToolNames) {
|
|
175
|
+
if (processResult.overflow)
|
|
176
|
+
throw new Error('OpenCode output exceeded the 16 MiB limit');
|
|
177
|
+
if (processResult.aborted)
|
|
178
|
+
throw new Error('OpenCode turn aborted');
|
|
179
|
+
const textParts = [];
|
|
180
|
+
const toolEvents = [];
|
|
181
|
+
const sanitizedTrace = [];
|
|
182
|
+
const seenParts = new Map();
|
|
183
|
+
let sessionId;
|
|
184
|
+
let stepFinishCount = 0;
|
|
185
|
+
let inputTokens = 0;
|
|
186
|
+
let outputTokens = 0;
|
|
187
|
+
let cacheCreationInputTokens = 0;
|
|
188
|
+
let cacheReadInputTokens = 0;
|
|
189
|
+
let costUsd = 0;
|
|
190
|
+
let providerError;
|
|
191
|
+
for (const [index, line] of stdout.split('\n').entries()) {
|
|
192
|
+
if (!line.trim())
|
|
193
|
+
continue;
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(line);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
throw new Error(`OpenCode protocol error: invalid JSON on line ${index + 1}`);
|
|
200
|
+
}
|
|
201
|
+
const event = record(parsed, `line ${index + 1}`);
|
|
202
|
+
const type = requiredString(event.type, `line ${index + 1} type`);
|
|
203
|
+
if (!ALLOWED_EVENT_TYPES.has(type)) {
|
|
204
|
+
throw new Error(`OpenCode protocol error: unsupported event type ${type}`);
|
|
205
|
+
}
|
|
206
|
+
const currentSessionId = requiredString(event.sessionID, `line ${index + 1} sessionID`);
|
|
207
|
+
if (sessionId && sessionId !== currentSessionId) {
|
|
208
|
+
throw new Error('OpenCode protocol error: mixed session IDs');
|
|
209
|
+
}
|
|
210
|
+
sessionId = currentSessionId;
|
|
211
|
+
if (type === 'error') {
|
|
212
|
+
providerError = sanitizedProviderError(event);
|
|
213
|
+
sanitizedTrace.push({ type: 'error', message: providerError.message });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const part = record(event.part, `${type} part`);
|
|
217
|
+
const partId = requiredString(part.id, `${type} part.id`);
|
|
218
|
+
const fingerprint = JSON.stringify({ type, part });
|
|
219
|
+
const prior = seenParts.get(partId);
|
|
220
|
+
if (prior !== undefined) {
|
|
221
|
+
if (prior !== fingerprint) {
|
|
222
|
+
throw new Error(`OpenCode protocol error: conflicting duplicate part ID ${partId}`);
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
seenParts.set(partId, fingerprint);
|
|
227
|
+
if (type === 'text') {
|
|
228
|
+
const text = typeof part.text === 'string' ? part.text : undefined;
|
|
229
|
+
const time = record(part.time, 'text part.time');
|
|
230
|
+
if (text === undefined || typeof time.end !== 'number') {
|
|
231
|
+
throw new Error('OpenCode protocol error: incomplete text part');
|
|
232
|
+
}
|
|
233
|
+
textParts.push(text);
|
|
234
|
+
sanitizedTrace.push({ type, text });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (type === 'tool_use') {
|
|
238
|
+
const tool = requiredString(part.tool, 'tool_use part.tool');
|
|
239
|
+
const state = record(part.state, 'tool_use part.state');
|
|
240
|
+
if (state.status !== 'completed' && state.status !== 'error') {
|
|
241
|
+
throw new Error(`OpenCode protocol error: incomplete tool ${tool}`);
|
|
242
|
+
}
|
|
243
|
+
const input = state.input === undefined ? undefined : record(state.input, 'tool_use state.input');
|
|
244
|
+
const action = mcpToolNames.has(tool)
|
|
245
|
+
? 'mcp_tool_call'
|
|
246
|
+
: NATIVE_TOOL_ACTIONS[tool] ?? 'unknown';
|
|
247
|
+
toolEvents.push({
|
|
248
|
+
action,
|
|
249
|
+
provider: 'opencode',
|
|
250
|
+
providerToolName: tool,
|
|
251
|
+
...(input ? { arguments: input } : {}),
|
|
252
|
+
summary: buildSummary(action, tool, input),
|
|
253
|
+
confidence: action === 'unknown' ? 'low' : 'high',
|
|
254
|
+
rawSnippet: JSON.stringify({ tool, status: state.status, input }).slice(0, 2_000),
|
|
255
|
+
});
|
|
256
|
+
sanitizedTrace.push({ type, tool, status: state.status, input });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (type === 'step_finish') {
|
|
260
|
+
const tokens = record(part.tokens, 'step_finish part.tokens');
|
|
261
|
+
const cache = record(tokens.cache, 'step_finish part.tokens.cache');
|
|
262
|
+
const input = finiteNumber(tokens.input, 'tokens.input');
|
|
263
|
+
const output = finiteNumber(tokens.output, 'tokens.output');
|
|
264
|
+
const reasoning = finiteNumber(tokens.reasoning, 'tokens.reasoning');
|
|
265
|
+
const cacheWrite = finiteNumber(cache.write, 'tokens.cache.write');
|
|
266
|
+
const cacheRead = finiteNumber(cache.read, 'tokens.cache.read');
|
|
267
|
+
const cost = finiteNumber(part.cost, 'step_finish part.cost');
|
|
268
|
+
inputTokens += input + cacheWrite + cacheRead;
|
|
269
|
+
outputTokens += output + reasoning;
|
|
270
|
+
cacheCreationInputTokens += cacheWrite;
|
|
271
|
+
cacheReadInputTokens += cacheRead;
|
|
272
|
+
costUsd += cost;
|
|
273
|
+
stepFinishCount++;
|
|
274
|
+
sanitizedTrace.push({
|
|
275
|
+
type,
|
|
276
|
+
tokens: { input, output, reasoning, cache: { write: cacheWrite, read: cacheRead } },
|
|
277
|
+
cost,
|
|
278
|
+
});
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
sanitizedTrace.push({ type });
|
|
282
|
+
}
|
|
283
|
+
if (providerError)
|
|
284
|
+
throw providerError;
|
|
285
|
+
if (processResult.exitCode !== 0) {
|
|
286
|
+
throw new Error(`OpenCode process exited with code ${processResult.exitCode ?? 'unknown'}`);
|
|
287
|
+
}
|
|
288
|
+
if (!sessionId)
|
|
289
|
+
throw new Error('OpenCode protocol error: no events');
|
|
290
|
+
if (stepFinishCount === 0)
|
|
291
|
+
throw new Error('OpenCode protocol error: missing step_finish');
|
|
292
|
+
const assistantMessage = textParts.join('');
|
|
293
|
+
const traceOutput = sanitizedTrace.map((event) => JSON.stringify(event)).join('\n');
|
|
294
|
+
return {
|
|
295
|
+
sessionId,
|
|
296
|
+
result: {
|
|
297
|
+
rawOutput: traceOutput,
|
|
298
|
+
traceOutput,
|
|
299
|
+
assistantMessage,
|
|
300
|
+
visibleAssistantMessage: assistantMessage,
|
|
301
|
+
visibleAssistantMessageSource: 'assistant_message',
|
|
302
|
+
exitCode: 0,
|
|
303
|
+
toolEvents: enrichSkillEvents(toolEvents),
|
|
304
|
+
inputTokens,
|
|
305
|
+
outputTokens,
|
|
306
|
+
cacheCreationInputTokens,
|
|
307
|
+
cacheReadInputTokens,
|
|
308
|
+
costUsd,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function sha256File(filename) {
|
|
313
|
+
return new Promise((resolve, reject) => {
|
|
314
|
+
const hash = createHash('sha256');
|
|
315
|
+
const stream = createReadStream(filename);
|
|
316
|
+
stream.on('error', reject);
|
|
317
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
318
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async function assertNoProjectConfig(workspacePath) {
|
|
322
|
+
for (const name of ['opencode.json', 'opencode.jsonc', '.opencode']) {
|
|
323
|
+
if (await fs.pathExists(path.join(workspacePath, name))) {
|
|
324
|
+
throw new Error(`OpenCode workspace configuration is not allowed: ${name}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
export function managedOpenCodeConfigPaths(platform = process.platform, username = os.userInfo().username) {
|
|
329
|
+
return platform === 'linux'
|
|
330
|
+
? ['/etc/opencode/opencode.json', '/etc/opencode/opencode.jsonc']
|
|
331
|
+
: [
|
|
332
|
+
'/Library/Application Support/opencode/opencode.json',
|
|
333
|
+
'/Library/Application Support/opencode/opencode.jsonc',
|
|
334
|
+
`/Library/Managed Preferences/${username}/ai.opencode.managed.plist`,
|
|
335
|
+
'/Library/Managed Preferences/ai.opencode.managed.plist',
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
export async function assertCleanManagedOpenCodeHost(candidates = managedOpenCodeConfigPaths()) {
|
|
339
|
+
for (const candidate of candidates) {
|
|
340
|
+
if (await fs.pathExists(candidate)) {
|
|
341
|
+
throw new Error(`OpenCode managed host configuration is not supported: ${candidate}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function projectMcpConfig(workspacePath, mcpConfigPath) {
|
|
346
|
+
if (!mcpConfigPath)
|
|
347
|
+
return {};
|
|
348
|
+
const servers = await readStagedMcpServers(workspacePath, mcpConfigPath);
|
|
349
|
+
const config = {};
|
|
350
|
+
for (const [name, entry] of Object.entries(servers ?? {})) {
|
|
351
|
+
if (!('command' in entry)) {
|
|
352
|
+
throw new Error(`OpenCode MCP server "${name}" must remain a stdio server`);
|
|
353
|
+
}
|
|
354
|
+
const stdio = entry;
|
|
355
|
+
config[name] = {
|
|
356
|
+
type: 'local',
|
|
357
|
+
command: [stdio.command, ...(stdio.args ?? [])],
|
|
358
|
+
...(stdio.env ? { environment: stdio.env } : {}),
|
|
359
|
+
cwd: '.',
|
|
360
|
+
enabled: true,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return config;
|
|
364
|
+
}
|
|
365
|
+
class OpenCodeSession {
|
|
366
|
+
workspacePath;
|
|
367
|
+
runtimeEnv;
|
|
368
|
+
executable;
|
|
369
|
+
mcpConfigPath;
|
|
370
|
+
mcpToolNames;
|
|
371
|
+
getAbortSignal;
|
|
372
|
+
xdgDirs;
|
|
373
|
+
resolvedExecutable;
|
|
374
|
+
preflightDone = false;
|
|
375
|
+
running = false;
|
|
376
|
+
failed = false;
|
|
377
|
+
disposed = false;
|
|
378
|
+
sessionId;
|
|
379
|
+
inFlight;
|
|
380
|
+
constructor(runtime, options) {
|
|
381
|
+
this.workspacePath = getWorkspacePath(runtime);
|
|
382
|
+
this.runtimeEnv = getRuntimeEnv(runtime);
|
|
383
|
+
this.executable = options.opencodeExecutable;
|
|
384
|
+
this.mcpConfigPath = options.mcpConfigPath;
|
|
385
|
+
this.mcpToolNames = new Set(options.opencodeMcpToolNames ?? []);
|
|
386
|
+
this.getAbortSignal = options.getAbortSignal ?? (() => options.abortSignal);
|
|
387
|
+
const home = this.runtimeEnv.HOME;
|
|
388
|
+
if (!home)
|
|
389
|
+
throw new Error('OpenCode requires a managed HOME');
|
|
390
|
+
this.xdgDirs = [
|
|
391
|
+
path.join(home, '.local', 'share'),
|
|
392
|
+
path.join(home, '.config'),
|
|
393
|
+
path.join(home, '.local', 'state'),
|
|
394
|
+
path.join(home, '.cache'),
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
start({ message }) {
|
|
398
|
+
if (this.sessionId)
|
|
399
|
+
throw new Error('OpenCode session has already started');
|
|
400
|
+
return this.execute(message);
|
|
401
|
+
}
|
|
402
|
+
reply({ message }) {
|
|
403
|
+
if (!this.sessionId)
|
|
404
|
+
throw new Error('OpenCode session has not started');
|
|
405
|
+
return this.execute(message);
|
|
406
|
+
}
|
|
407
|
+
execute(message) {
|
|
408
|
+
if (this.disposed)
|
|
409
|
+
return Promise.reject(new Error('OpenCode session is disposed'));
|
|
410
|
+
if (this.failed)
|
|
411
|
+
return Promise.reject(new Error('OpenCode session is unusable after a failed turn'));
|
|
412
|
+
if (this.running)
|
|
413
|
+
return Promise.reject(new Error('OpenCode does not allow concurrent turns'));
|
|
414
|
+
this.running = true;
|
|
415
|
+
const task = this.runTurn(message);
|
|
416
|
+
this.inFlight = task;
|
|
417
|
+
void task.finally(() => {
|
|
418
|
+
this.running = false;
|
|
419
|
+
if (this.inFlight === task)
|
|
420
|
+
this.inFlight = undefined;
|
|
421
|
+
}).catch(() => undefined);
|
|
422
|
+
return task;
|
|
423
|
+
}
|
|
424
|
+
async runTurn(message) {
|
|
425
|
+
try {
|
|
426
|
+
await this.ensurePreflight();
|
|
427
|
+
await assertNoProjectConfig(this.workspacePath);
|
|
428
|
+
const mcp = await projectMcpConfig(this.workspacePath, this.mcpConfigPath);
|
|
429
|
+
const env = this.buildEnvironment(mcp);
|
|
430
|
+
const args = [
|
|
431
|
+
'run', '--format', 'json', '--thinking', '--dir', this.workspacePath,
|
|
432
|
+
'--model', OPENCODE_MODEL, '--agent', 'build',
|
|
433
|
+
...(this.sessionId ? ['--session', this.sessionId] : []),
|
|
434
|
+
];
|
|
435
|
+
const processResult = await spawnOpenCode(this.resolvedExecutable, args, {
|
|
436
|
+
cwd: this.workspacePath,
|
|
437
|
+
env,
|
|
438
|
+
stdin: message,
|
|
439
|
+
signal: this.getAbortSignal(),
|
|
440
|
+
});
|
|
441
|
+
const parsed = parseOpenCodeOutput(processResult.stdout, processResult, this.mcpToolNames);
|
|
442
|
+
if (this.sessionId && parsed.sessionId !== this.sessionId) {
|
|
443
|
+
throw new Error('OpenCode protocol error: resumed session ID changed');
|
|
444
|
+
}
|
|
445
|
+
this.sessionId = parsed.sessionId;
|
|
446
|
+
return parsed.result;
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
this.failed = true;
|
|
450
|
+
await this.cleanupState();
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
async ensurePreflight() {
|
|
455
|
+
if (this.preflightDone)
|
|
456
|
+
return;
|
|
457
|
+
const platformKey = currentOpenCodePlatformKey();
|
|
458
|
+
if (!platformKey)
|
|
459
|
+
throw new Error(`OpenCode is not supported on ${process.platform}-${process.arch}`);
|
|
460
|
+
const resolved = await fs.realpath(this.executable);
|
|
461
|
+
const actualHash = await sha256File(resolved);
|
|
462
|
+
const expectedHash = OPENCODE_RUNTIME_LOCK[platformKey].executableSha256;
|
|
463
|
+
if (actualHash !== expectedHash) {
|
|
464
|
+
throw new Error(`OpenCode executable hash mismatch for ${platformKey}`);
|
|
465
|
+
}
|
|
466
|
+
await assertCleanManagedOpenCodeHost();
|
|
467
|
+
await assertNoProjectConfig(this.workspacePath);
|
|
468
|
+
await Promise.all(this.xdgDirs.map((directory) => fs.ensureDir(directory)));
|
|
469
|
+
this.resolvedExecutable = resolved;
|
|
470
|
+
this.preflightDone = true;
|
|
471
|
+
}
|
|
472
|
+
buildEnvironment(mcp) {
|
|
473
|
+
const [data, config, state, cache] = this.xdgDirs;
|
|
474
|
+
return {
|
|
475
|
+
...this.runtimeEnv,
|
|
476
|
+
XDG_DATA_HOME: data,
|
|
477
|
+
XDG_CONFIG_HOME: config,
|
|
478
|
+
XDG_STATE_HOME: state,
|
|
479
|
+
XDG_CACHE_HOME: cache,
|
|
480
|
+
...FIXED_OPENCODE_ENV,
|
|
481
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
482
|
+
share: 'disabled',
|
|
483
|
+
model: OPENCODE_MODEL,
|
|
484
|
+
skills: { paths: ['.agents/skills'], urls: [] },
|
|
485
|
+
mcp,
|
|
486
|
+
}),
|
|
487
|
+
OPENCODE_PERMISSION,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
async cleanupState() {
|
|
491
|
+
await Promise.all(this.xdgDirs.map((directory) => fs.remove(directory)));
|
|
492
|
+
}
|
|
493
|
+
async dispose() {
|
|
494
|
+
if (this.disposed)
|
|
495
|
+
return;
|
|
496
|
+
this.disposed = true;
|
|
497
|
+
await this.inFlight?.catch(() => undefined);
|
|
498
|
+
await this.cleanupState();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
export class OpenCodeAgent extends BaseAgent {
|
|
502
|
+
async createSession(runtime, _runCommand, options = {}) {
|
|
503
|
+
if (!options.opencodeExecutable) {
|
|
504
|
+
throw new Error('OpenCode requires opencodeExecutable');
|
|
505
|
+
}
|
|
506
|
+
return new OpenCodeSession(runtime, options);
|
|
507
|
+
}
|
|
508
|
+
}
|
package/dist/agents/registry.js
CHANGED
|
@@ -2,11 +2,13 @@ import { ClaudeAgent } from './claude.js';
|
|
|
2
2
|
import { CodexAgent } from './codex.js';
|
|
3
3
|
import { CodexAppServerAgent } from './codex-app-server/agent.js';
|
|
4
4
|
import { CursorAgent } from './cursor.js';
|
|
5
|
+
import { OpenCodeAgent } from './opencode.js';
|
|
5
6
|
/** Registry of available agent implementations. Codex routing is transport-aware. */
|
|
6
7
|
const AGENT_REGISTRY = {
|
|
7
8
|
claude: () => new ClaudeAgent(),
|
|
8
9
|
codex: (transport) => transport === 'exec' ? new CodexAgent() : new CodexAppServerAgent(),
|
|
9
10
|
cursor: () => new CursorAgent(),
|
|
11
|
+
opencode: () => new OpenCodeAgent(),
|
|
10
12
|
};
|
|
11
13
|
/** Get the list of supported agent names */
|
|
12
14
|
export function getAgentNames() {
|
|
@@ -60,10 +60,22 @@ export async function resolveCredentials(agent, userEnv, ports) {
|
|
|
60
60
|
return resolveCodex(userEnv, p);
|
|
61
61
|
case 'cursor':
|
|
62
62
|
return resolveCursor(userEnv, p);
|
|
63
|
+
case 'opencode':
|
|
64
|
+
return resolveOpenCode(userEnv);
|
|
63
65
|
default:
|
|
64
66
|
return EMPTY;
|
|
65
67
|
}
|
|
66
68
|
}
|
|
69
|
+
function resolveOpenCode(userEnv) {
|
|
70
|
+
const env = {};
|
|
71
|
+
if (userEnv.ANTHROPIC_API_KEY) {
|
|
72
|
+
env.ANTHROPIC_API_KEY = userEnv.ANTHROPIC_API_KEY;
|
|
73
|
+
}
|
|
74
|
+
if (userEnv.ANTHROPIC_BASE_URL) {
|
|
75
|
+
env.ANTHROPIC_BASE_URL = userEnv.ANTHROPIC_BASE_URL;
|
|
76
|
+
}
|
|
77
|
+
return { env, setupCommands: [], copyFromHome: [] };
|
|
78
|
+
}
|
|
67
79
|
async function resolveClaude(userEnv, ports) {
|
|
68
80
|
// User explicitly provided API key — trust it, nothing to add
|
|
69
81
|
if (userEnv.ANTHROPIC_API_KEY) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentName, AgentTransport, AgentOptions } from './types.js';
|
|
1
|
+
import type { AgentExecutionTransport, AgentName, AgentTransport, AgentOptions } from './types.js';
|
|
2
2
|
export declare class InvalidTransportEnvError extends Error {
|
|
3
3
|
constructor(value: string);
|
|
4
4
|
}
|
|
@@ -10,3 +10,4 @@ export declare function resolveCodexTransport(opts: {
|
|
|
10
10
|
}, env: {
|
|
11
11
|
PATHGRADE_CODEX_TRANSPORT?: string;
|
|
12
12
|
}): AgentTransport;
|
|
13
|
+
export declare function resolveExecutionTransport(agentName: AgentName, codexTransport?: AgentTransport): AgentExecutionTransport;
|
|
@@ -4,8 +4,13 @@ export class InvalidTransportEnvError extends Error {
|
|
|
4
4
|
this.name = 'InvalidTransportEnvError';
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
+
const AGENT_NAMES = ['claude', 'codex', 'cursor', 'opencode'];
|
|
7
8
|
export function resolveAgentName(opts, env) {
|
|
8
|
-
|
|
9
|
+
const value = opts.agent || env.PATHGRADE_AGENT || 'claude';
|
|
10
|
+
if (!AGENT_NAMES.includes(value)) {
|
|
11
|
+
throw new Error(`Unknown agent "${value}". Available agents: ${AGENT_NAMES.join(', ')}`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
9
14
|
}
|
|
10
15
|
export function resolveCodexTransport(opts, env) {
|
|
11
16
|
if (opts.transport)
|
|
@@ -19,3 +24,10 @@ export function resolveCodexTransport(opts, env) {
|
|
|
19
24
|
}
|
|
20
25
|
return 'app-server';
|
|
21
26
|
}
|
|
27
|
+
export function resolveExecutionTransport(agentName, codexTransport) {
|
|
28
|
+
if (agentName === 'claude')
|
|
29
|
+
return 'claude-agent-sdk';
|
|
30
|
+
if (agentName === 'cursor')
|
|
31
|
+
return 'cursor-agent';
|
|
32
|
+
return codexTransport ?? 'app-server';
|
|
33
|
+
}
|
package/dist/sdk/agent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { prepareWorkspace } from '../providers/workspace.js';
|
|
2
|
-
import { resolveAgentName, resolveCodexTransport } from './agent-resolution.js';
|
|
2
|
+
import { resolveAgentName, resolveCodexTransport, resolveExecutionTransport, } from './agent-resolution.js';
|
|
3
3
|
import { lifecycleCore } from './lifecycle.js';
|
|
4
4
|
import { ChatSessionImpl } from './chat.js';
|
|
5
5
|
import { runConversation } from './converse.js';
|
|
@@ -14,6 +14,7 @@ import { getCurrentCaseContext } from './case-context.js';
|
|
|
14
14
|
import { createVerboseEmitter } from '../reporters/verbose-emitter.js';
|
|
15
15
|
import fs from 'fs-extra';
|
|
16
16
|
import * as path from 'path';
|
|
17
|
+
import { collectOpenCodeMcpToolNames, validateOpenCodeDeclaration } from '../agents/opencode-contract.js';
|
|
17
18
|
/**
|
|
18
19
|
* Test-only injection point: override the sink used by the next emitter
|
|
19
20
|
* built inside `createAgent`. Pass `null` to restore the default (stderr).
|
|
@@ -41,7 +42,10 @@ class AgentImpl {
|
|
|
41
42
|
verbose;
|
|
42
43
|
transport;
|
|
43
44
|
mcpSafety;
|
|
44
|
-
|
|
45
|
+
opencodeExecutable;
|
|
46
|
+
opencodeMcpToolNames;
|
|
47
|
+
activeChatSession;
|
|
48
|
+
constructor(ws, agentName, llm, timeoutSetting, conversationWindow, modelOpt, debugOpt, debugName, debugBaseDir, verbose, transport, mcpSafety, opencodeExecutable, opencodeMcpToolNames) {
|
|
45
49
|
this.ws = ws;
|
|
46
50
|
this.agentName = agentName;
|
|
47
51
|
this.llm = llm;
|
|
@@ -54,6 +58,8 @@ class AgentImpl {
|
|
|
54
58
|
this.verbose = verbose;
|
|
55
59
|
this.transport = transport;
|
|
56
60
|
this.mcpSafety = mcpSafety;
|
|
61
|
+
this.opencodeExecutable = opencodeExecutable;
|
|
62
|
+
this.opencodeMcpToolNames = opencodeMcpToolNames;
|
|
57
63
|
}
|
|
58
64
|
get messages() {
|
|
59
65
|
return this._messages;
|
|
@@ -73,7 +79,7 @@ class AgentImpl {
|
|
|
73
79
|
return {
|
|
74
80
|
name: this.agentName,
|
|
75
81
|
...(this.modelOpt ? { requestedModel: this.modelOpt } : {}),
|
|
76
|
-
|
|
82
|
+
transport: resolveExecutionTransport(this.agentName, this.transport),
|
|
77
83
|
...(interactionMode ? { interactionMode } : {}),
|
|
78
84
|
};
|
|
79
85
|
}
|
|
@@ -96,6 +102,8 @@ class AgentImpl {
|
|
|
96
102
|
...(askUserTimeoutMs !== undefined ? { askUserTimeoutMs } : {}),
|
|
97
103
|
...(this.transport !== undefined ? { transport: this.transport } : {}),
|
|
98
104
|
...(this.mcpSafety !== undefined ? { mcpSafety: this.mcpSafety } : {}),
|
|
105
|
+
...(this.opencodeExecutable !== undefined ? { opencodeExecutable: this.opencodeExecutable } : {}),
|
|
106
|
+
...(this.opencodeMcpToolNames !== undefined ? { opencodeMcpToolNames: this.opencodeMcpToolNames } : {}),
|
|
99
107
|
});
|
|
100
108
|
}
|
|
101
109
|
resolveTimeoutSec(mode, maxTurns) {
|
|
@@ -195,6 +203,7 @@ class AgentImpl {
|
|
|
195
203
|
}
|
|
196
204
|
const timeoutSec = this.resolveTimeoutSec('startChat');
|
|
197
205
|
const ms = this.createSession(timeoutSec);
|
|
206
|
+
this.activeChatSession = ms;
|
|
198
207
|
const turnResult = await this.executeLoggedTurnResult(ms, firstMessage, 1, 'agent_start');
|
|
199
208
|
return new ChatSessionImpl(turnResult, {
|
|
200
209
|
messages: this._messages,
|
|
@@ -310,6 +319,8 @@ class AgentImpl {
|
|
|
310
319
|
// Runner-owned agents stay tracked until flush consumes metadata.
|
|
311
320
|
// Manual agents have no runner flush, so dispose releases them.
|
|
312
321
|
lifecycleCore.releaseAgent(this);
|
|
322
|
+
await this.activeChatSession?.dispose?.();
|
|
323
|
+
this.activeChatSession = undefined;
|
|
313
324
|
if (this.debugOpt) {
|
|
314
325
|
const dest = typeof this.debugOpt === 'string'
|
|
315
326
|
? this.debugOpt
|
|
@@ -354,13 +365,14 @@ function resolveCaseDebugContext() {
|
|
|
354
365
|
}
|
|
355
366
|
export async function createAgent(opts) {
|
|
356
367
|
const agentName = resolveAgentName(opts, process.env);
|
|
368
|
+
validateOpenCodeDeclaration(agentName, opts);
|
|
357
369
|
const transport = agentName === 'codex'
|
|
358
370
|
? resolveCodexTransport(opts, process.env)
|
|
359
371
|
: undefined;
|
|
360
372
|
const timeoutSetting = opts.timeout ?? 300;
|
|
361
373
|
// Capture runner context now; adapters own installation and restoration.
|
|
362
374
|
const testCtx = opts.debug ? resolveCaseDebugContext() : { name: '', dir: '' };
|
|
363
|
-
const { timeout: _, mcpMock, mcpConfigFile, agent: __, debug: ___, model: ____, transport: _____, mcpSafety: ______, ...rest } = opts;
|
|
375
|
+
const { timeout: _, mcpMock, mcpConfigFile, agent: __, debug: ___, model: ____, transport: _____, mcpSafety: ______, opencodeExecutable, ...rest } = opts;
|
|
364
376
|
const workspace = await prepareWorkspace({
|
|
365
377
|
...rest,
|
|
366
378
|
agent: agentName,
|
|
@@ -378,7 +390,7 @@ export async function createAgent(opts) {
|
|
|
378
390
|
sink: verboseSinkOverride ?? undefined,
|
|
379
391
|
testName: testCtx.name || undefined,
|
|
380
392
|
});
|
|
381
|
-
const agent = new AgentImpl(workspace, agentName, llm, timeoutSetting, opts.conversationWindow, opts.model, opts.debug, debugName, debugBaseDir, verbose, transport, opts.mcpSafety);
|
|
393
|
+
const agent = new AgentImpl(workspace, agentName, llm, timeoutSetting, opts.conversationWindow, opts.model, opts.debug, debugName, debugBaseDir, verbose, transport, opts.mcpSafety, opencodeExecutable, agentName === 'opencode' ? collectOpenCodeMcpToolNames(mcpMock) : undefined);
|
|
382
394
|
lifecycleCore.registerAgent(agent);
|
|
383
395
|
return agent;
|
|
384
396
|
}
|
package/dist/sdk/converse.js
CHANGED
|
@@ -175,7 +175,9 @@ export async function runConversation(opts, deps) {
|
|
|
175
175
|
const turnStart = Date.now();
|
|
176
176
|
// Under app-server the child process holds thread state; a crashed
|
|
177
177
|
// turn is not safely replayable, so retries are suppressed.
|
|
178
|
-
const maxRetries = deps.transport === 'app-server'
|
|
178
|
+
const maxRetries = deps.transport === 'app-server' || deps.agentName === 'opencode'
|
|
179
|
+
? 0
|
|
180
|
+
: MAX_TURN_RETRIES;
|
|
179
181
|
let lastError;
|
|
180
182
|
let turnResult;
|
|
181
183
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
package/dist/sdk/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { emitEvalResult, resetAllResultObserversForTests, resetUserResultObserve
|
|
|
21
21
|
export { getAgentCapabilities } from './types.js';
|
|
22
22
|
export type { AgentTransport, AgentCapabilities, AgentName, McpRunMode, McpSafetyOptions, McpToolPolicy, McpToolPolicyRule, } from './types.js';
|
|
23
23
|
export type { AskBus, AskBatch, AskQuestion, AskOption, AskAnswer, AskResolution, AskBatchSnapshot, AskAnswerSnapshot, AskResolutionSnapshot, AskHandle, AskHandler, AskSource, AskLifecycle, AskAnswerSource, Unsubscribe as AskBusUnsubscribe, } from './ask-bus/types.js';
|
|
24
|
-
export type { Agent, AgentOptions, Message, Scorer, CheckScorer, ScoreScorer, JudgeScorer, ToolUsageScorer, ScorerContext, EvalResult, ScorerResultEntry, ScorerStatus, ChatSession, ConversationResult, ConverseOptions, UntilPredicate, UntilContext, Reaction, TextReaction, AskUserReaction, AskUserQuestion, AskUserOption, ReactionPreviewEntry, TextReactionPreviewEntry, AskUserReactionPreviewEntry, ReactionPreviewResult, ReactionPreviewTurn, StepScorer, Persona, PersonaConfig, ConversationWindowConfig, TurnDetail, ReactionFiredEntry, PathgradePluginOptions, PathgradeMeta, TurnTiming, TokenUsage, EvaluateOptions, ReactionPreviewStatus, ScoreResult, JudgeInput, CodeJudgeToolName, ToolExpectation, SessionArtifactMatchOptions, SessionArtifactContent, SessionArtifacts, RecordedEvalResult, PathgradeTestMeta, EvaluationResultKind, AgentExecutionMetadata, AgentInteractionMode, } from './types.js';
|
|
24
|
+
export type { Agent, AgentOptions, Message, Scorer, CheckScorer, ScoreScorer, JudgeScorer, ToolUsageScorer, ScorerContext, EvalResult, ScorerResultEntry, ScorerStatus, ChatSession, ConversationResult, ConverseOptions, UntilPredicate, UntilContext, Reaction, TextReaction, AskUserReaction, AskUserQuestion, AskUserOption, ReactionPreviewEntry, TextReactionPreviewEntry, AskUserReactionPreviewEntry, ReactionPreviewResult, ReactionPreviewTurn, StepScorer, Persona, PersonaConfig, ConversationWindowConfig, TurnDetail, ReactionFiredEntry, PathgradePluginOptions, PathgradeMeta, TurnTiming, TokenUsage, EvaluateOptions, ReactionPreviewStatus, ScoreResult, JudgeInput, CodeJudgeToolName, ToolExpectation, SessionArtifactMatchOptions, SessionArtifactContent, SessionArtifacts, RecordedEvalResult, PathgradeTestMeta, EvaluationResultKind, AgentExecutionMetadata, AgentExecutionTransport, AgentInteractionMode, } from './types.js';
|
|
25
25
|
export type { ConversationWindow, ConversationWindowOptions } from './conversation-window.js';
|
|
26
26
|
export type { JudgePipelineOptions } from './judge-pipeline.js';
|
|
27
27
|
export type { RunScorerOptions } from './run-scorer.js';
|
|
@@ -31,6 +31,8 @@ export interface ManagedSessionDeps {
|
|
|
31
31
|
transport?: AgentTransport;
|
|
32
32
|
/** Live MCP Server Safety policy to enforce in supported harnesses. */
|
|
33
33
|
mcpSafety?: McpSafetyOptions;
|
|
34
|
+
opencodeExecutable?: string;
|
|
35
|
+
opencodeMcpToolNames?: string[];
|
|
34
36
|
}
|
|
35
37
|
export interface ManagedSession {
|
|
36
38
|
/** Full lifecycle: log start/result, push messages, check exit code. */
|
|
@@ -24,6 +24,8 @@ export function createManagedSession(deps) {
|
|
|
24
24
|
askBus,
|
|
25
25
|
...(transport !== undefined ? { transport } : {}),
|
|
26
26
|
...(deps.mcpSafety !== undefined ? { mcpSafety: deps.mcpSafety } : {}),
|
|
27
|
+
...(deps.opencodeExecutable !== undefined ? { opencodeExecutable: deps.opencodeExecutable } : {}),
|
|
28
|
+
...(deps.opencodeMcpToolNames !== undefined ? { opencodeMcpToolNames: deps.opencodeMcpToolNames } : {}),
|
|
27
29
|
getAbortSignal: () => currentSignal,
|
|
28
30
|
};
|
|
29
31
|
let session = null;
|
|
@@ -7,6 +7,7 @@ const INTERACTIVE_TOOL_NAMES = {
|
|
|
7
7
|
claude: 'AskUserQuestion',
|
|
8
8
|
codex: 'request_user_input',
|
|
9
9
|
cursor: 'AskQuestion',
|
|
10
|
+
opencode: 'question',
|
|
10
11
|
};
|
|
11
12
|
export function planRuntimePolicies(agent, transport) {
|
|
12
13
|
const capabilities = getAgentCapabilities(agent, transport);
|
package/dist/sdk/snapshots.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { LogEntry } from '../types.js';
|
|
2
2
|
import type { ToolEvent } from '../tool-events.js';
|
|
3
3
|
import type { AgentName, ConversationResult, Message, TurnTiming } from './types.js';
|
|
4
|
-
export declare const RUN_SNAPSHOT_VERSION =
|
|
4
|
+
export declare const RUN_SNAPSHOT_VERSION = 2;
|
|
5
5
|
export interface RunSnapshot {
|
|
6
|
-
version: 1;
|
|
6
|
+
version: 1 | 2;
|
|
7
7
|
timestamp: string;
|
|
8
8
|
agent: AgentName;
|
|
9
9
|
messages: Message[];
|
package/dist/sdk/snapshots.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
export const RUN_SNAPSHOT_VERSION =
|
|
2
|
+
export const RUN_SNAPSHOT_VERSION = 2;
|
|
3
3
|
export function buildRunSnapshot(params) {
|
|
4
4
|
const { agent, messages, log, conversationResult, workspace, timestamp } = params;
|
|
5
5
|
const toolEvents = log
|
|
6
6
|
.filter((entry) => entry.type === 'tool_event' && entry.tool_event)
|
|
7
7
|
.map((entry) => entry.tool_event);
|
|
8
8
|
return {
|
|
9
|
-
version:
|
|
9
|
+
version: agent === 'opencode' ? 2 : 1,
|
|
10
10
|
timestamp: timestamp ?? new Date().toISOString(),
|
|
11
11
|
agent,
|
|
12
12
|
messages: [...messages],
|
|
@@ -102,6 +102,9 @@ function validateRunSnapshot(input) {
|
|
|
102
102
|
if (snapshot.version > RUN_SNAPSHOT_VERSION) {
|
|
103
103
|
throw new SnapshotVersionError(snapshot.version);
|
|
104
104
|
}
|
|
105
|
+
if (snapshot.version !== 1 && snapshot.version !== 2) {
|
|
106
|
+
throw new SnapshotVersionError(snapshot.version);
|
|
107
|
+
}
|
|
105
108
|
if (!Array.isArray(snapshot.messages)) {
|
|
106
109
|
throw new SnapshotParseError('Snapshot is missing required field: messages');
|
|
107
110
|
}
|
|
@@ -127,10 +130,16 @@ function validateRunSnapshot(input) {
|
|
|
127
130
|
const turnTimings = Array.isArray(snapshot.turnTimings)
|
|
128
131
|
? snapshot.turnTimings
|
|
129
132
|
: snapshot.conversationResult.turnTimings;
|
|
133
|
+
const legacyAgents = ['claude', 'codex', 'cursor'];
|
|
134
|
+
const currentAgents = [...legacyAgents, 'opencode'];
|
|
135
|
+
const allowedAgents = snapshot.version === 1 ? legacyAgents : currentAgents;
|
|
136
|
+
if (typeof snapshot.agent !== 'string' || !allowedAgents.includes(snapshot.agent)) {
|
|
137
|
+
throw new SnapshotParseError(`Snapshot version ${snapshot.version} has invalid agent: ${String(snapshot.agent)}`);
|
|
138
|
+
}
|
|
130
139
|
return {
|
|
131
|
-
version:
|
|
140
|
+
version: snapshot.version,
|
|
132
141
|
timestamp: typeof snapshot.timestamp === 'string' ? snapshot.timestamp : new Date(0).toISOString(),
|
|
133
|
-
agent: snapshot.agent
|
|
142
|
+
agent: snapshot.agent,
|
|
134
143
|
messages: snapshot.messages,
|
|
135
144
|
log: snapshot.log,
|
|
136
145
|
toolEvents: snapshot.toolEvents,
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -5,18 +5,22 @@ import type { TrialResult } from '../types.js';
|
|
|
5
5
|
import type { DiagnosticsReport } from './diagnostics.js';
|
|
6
6
|
import type { LLMPort } from '../utils/llm-types.js';
|
|
7
7
|
import type { McpSafetyOptions } from './mcp-safety.js';
|
|
8
|
-
export type AgentName = 'claude' | 'codex' | 'cursor';
|
|
8
|
+
export type AgentName = 'claude' | 'codex' | 'cursor' | 'opencode';
|
|
9
9
|
export type AgentInteractionMode = 'prompt' | 'start_chat' | 'conversation';
|
|
10
|
+
/** Runtime channel that actually executed the agent. */
|
|
11
|
+
export type AgentExecutionTransport = AgentTransport | 'claude-agent-sdk' | 'cursor-agent';
|
|
10
12
|
/** Privacy-safe execution dimensions exposed to result observers. */
|
|
11
13
|
export interface AgentExecutionMetadata {
|
|
12
14
|
name: AgentName;
|
|
13
15
|
/** Model override requested by the caller; not necessarily the provider-resolved model. */
|
|
14
16
|
requestedModel?: string;
|
|
15
|
-
transport?:
|
|
17
|
+
transport?: AgentExecutionTransport;
|
|
16
18
|
interactionMode?: AgentInteractionMode;
|
|
17
19
|
}
|
|
18
20
|
export interface AgentOptions {
|
|
19
21
|
agent?: AgentName;
|
|
22
|
+
/** Absolute path to the pinned OpenCode executable. Valid only for `agent: 'opencode'`. */
|
|
23
|
+
opencodeExecutable?: string;
|
|
20
24
|
model?: string;
|
|
21
25
|
timeout?: number | 'auto';
|
|
22
26
|
workspace?: string;
|
|
@@ -70,7 +74,7 @@ export interface Agent {
|
|
|
70
74
|
readonly messages: Message[];
|
|
71
75
|
readonly log: LogEntry[];
|
|
72
76
|
readonly workspace: string;
|
|
73
|
-
/** Provider/
|
|
77
|
+
/** Provider/runtime metadata only; never contains prompts or paths. */
|
|
74
78
|
readonly executionMetadata?: AgentExecutionMetadata;
|
|
75
79
|
dispose(): Promise<void>;
|
|
76
80
|
}
|
package/dist/sdk/types.js
CHANGED
|
@@ -2,6 +2,7 @@ const BASE_CAPABILITIES = {
|
|
|
2
2
|
claude: { mcp: true, nativeSession: true, interactiveQuestionTransport: 'reliable' },
|
|
3
3
|
codex: { mcp: false, nativeSession: true, interactiveQuestionTransport: 'noninteractive' },
|
|
4
4
|
cursor: { mcp: true, nativeSession: true, interactiveQuestionTransport: 'noninteractive' },
|
|
5
|
+
opencode: { mcp: true, nativeSession: true, interactiveQuestionTransport: 'noninteractive' },
|
|
5
6
|
};
|
|
6
7
|
export function getAgentCapabilities(agent, transport) {
|
|
7
8
|
const base = BASE_CAPABILITIES[agent];
|
package/dist/tool-events.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type ToolAction = 'run_shell' | 'read_file' | 'write_file' | 'edit_file' | 'search_code' | 'list_files' | 'ask_user' | 'web_fetch' | 'use_skill' | 'update_todos' | 'mcp_tool_call' | 'unknown';
|
|
2
2
|
export interface ToolEvent {
|
|
3
3
|
action: ToolAction;
|
|
4
|
-
provider: 'claude' | 'codex' | 'cursor';
|
|
4
|
+
provider: 'claude' | 'codex' | 'cursor' | 'opencode';
|
|
5
5
|
providerToolName: string;
|
|
6
6
|
turnNumber?: number;
|
|
7
7
|
arguments?: Record<string, unknown>;
|
package/dist/types.d.ts
CHANGED
|
@@ -379,6 +379,10 @@ export interface AgentSessionOptions {
|
|
|
379
379
|
abortSignal?: AbortSignal;
|
|
380
380
|
/** Supplies the current timeout/cancellation signal for reused sessions. */
|
|
381
381
|
getAbortSignal?: () => AbortSignal | undefined;
|
|
382
|
+
/** Absolute pinned runtime path for the OpenCode adapter. */
|
|
383
|
+
opencodeExecutable?: string;
|
|
384
|
+
/** Exact generated MCP tool names accepted by the OpenCode event normalizer. */
|
|
385
|
+
opencodeMcpToolNames?: string[];
|
|
382
386
|
}
|
|
383
387
|
export declare abstract class BaseAgent {
|
|
384
388
|
createSession(runtime: EnvironmentHandle, runCommand: AgentCommandRunner, options?: AgentSessionOptions): Promise<AgentSession>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/pathgrade",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"packageManager": "yarn@4.12.0",
|
|
5
5
|
"description": "Evaluate whether AI agents discover and use your skills correctly",
|
|
6
6
|
"exports": {
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"gemini",
|
|
77
77
|
"claude",
|
|
78
78
|
"codex",
|
|
79
|
+
"opencode",
|
|
79
80
|
"openai",
|
|
80
81
|
"llm",
|
|
81
82
|
"testing"
|
|
@@ -127,5 +128,5 @@
|
|
|
127
128
|
"typescript": "^5.9.3",
|
|
128
129
|
"zod": "4.3.6"
|
|
129
130
|
},
|
|
130
|
-
"falconPackageHash": "
|
|
131
|
+
"falconPackageHash": "6dec3f88035544895dd8e99741e3c9a69e73651415b4999e32ddefd6"
|
|
131
132
|
}
|