agent-relay 3.2.10 → 3.2.12
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 +2 -2
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +858 -519
- package/package.json +8 -8
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/cli-registry.d.ts +42 -0
- package/packages/sdk/dist/cli-registry.d.ts.map +1 -0
- package/packages/sdk/dist/cli-registry.js +126 -0
- package/packages/sdk/dist/cli-registry.js.map +1 -0
- package/packages/sdk/dist/cli-resolver.d.ts +30 -0
- package/packages/sdk/dist/cli-resolver.d.ts.map +1 -0
- package/packages/sdk/dist/cli-resolver.js +132 -0
- package/packages/sdk/dist/cli-resolver.js.map +1 -0
- package/packages/sdk/dist/index.d.ts +2 -0
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js +2 -0
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/spawn-from-env.d.ts.map +1 -1
- package/packages/sdk/dist/spawn-from-env.js +6 -15
- package/packages/sdk/dist/spawn-from-env.js.map +1 -1
- package/packages/sdk/dist/workflows/builder.d.ts +5 -0
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +36 -5
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/collectors/opencode.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/collectors/opencode.js +26 -0
- package/packages/sdk/dist/workflows/collectors/opencode.js.map +1 -1
- package/packages/sdk/dist/workflows/default-logger.d.ts +9 -0
- package/packages/sdk/dist/workflows/default-logger.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/default-logger.js +104 -0
- package/packages/sdk/dist/workflows/default-logger.js.map +1 -0
- package/packages/sdk/dist/workflows/index.d.ts +1 -0
- package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/index.js +1 -0
- package/packages/sdk/dist/workflows/index.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +16 -45
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +2 -2
- package/packages/sdk/src/cli-registry.ts +148 -0
- package/packages/sdk/src/cli-resolver.ts +155 -0
- package/packages/sdk/src/index.ts +2 -0
- package/packages/sdk/src/spawn-from-env.ts +6 -17
- package/packages/sdk/src/workflows/builder.ts +44 -4
- package/packages/sdk/src/workflows/collectors/opencode.ts +26 -0
- package/packages/sdk/src/workflows/default-logger.ts +120 -0
- package/packages/sdk/src/workflows/index.ts +1 -0
- package/packages/sdk/src/workflows/runner.ts +16 -43
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-relay/sdk",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"typescript": "^5.7.3"
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@agent-relay/config": "3.2.
|
|
105
|
+
"@agent-relay/config": "3.2.12",
|
|
106
106
|
"@relaycast/sdk": "^1.0.0",
|
|
107
107
|
"@sinclair/typebox": "^0.34.48",
|
|
108
108
|
"chalk": "^4.1.2",
|
|
@@ -995,10 +995,10 @@ agents:
|
|
|
995
995
|
expect(args).toEqual(['-p', 'Analyze']);
|
|
996
996
|
});
|
|
997
997
|
|
|
998
|
-
it('should build opencode command with
|
|
998
|
+
it('should build opencode command with run subcommand', () => {
|
|
999
999
|
const { cmd, args } = WorkflowRunner.buildNonInteractiveCommand('opencode', 'Fix bug');
|
|
1000
1000
|
expect(cmd).toBe('opencode');
|
|
1001
|
-
expect(args).toEqual(['
|
|
1001
|
+
expect(args).toEqual(['run', 'Fix bug']);
|
|
1002
1002
|
});
|
|
1003
1003
|
|
|
1004
1004
|
it('should build droid command with exec subcommand', () => {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidated CLI registry — single source of truth for all supported
|
|
3
|
+
* agent CLI metadata: binary names, non-interactive args, bypass flags,
|
|
4
|
+
* and well-known install paths.
|
|
5
|
+
*
|
|
6
|
+
* Consumers: runner.ts (buildNonInteractiveCommand, resolveCursorCli),
|
|
7
|
+
* spawn-from-env.ts (BYPASS_FLAGS), cli-resolver.ts (path resolution).
|
|
8
|
+
*
|
|
9
|
+
* NOTE: The Rust PTY spawner (src/pty.rs) maintains its own PATH fallback.
|
|
10
|
+
* When updating `COMMON_SEARCH_PATHS` here, also update the Rust fallback
|
|
11
|
+
* in `resolve_command_path()` at src/pty.rs:53-67.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AgentCli } from './workflows/types.js';
|
|
15
|
+
|
|
16
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface CliDefinition {
|
|
19
|
+
/** Binary name(s) to try, in order of preference */
|
|
20
|
+
binaries: string[];
|
|
21
|
+
/** Build non-interactive mode args for a one-shot task */
|
|
22
|
+
nonInteractiveArgs: (task: string, extraArgs?: string[]) => string[];
|
|
23
|
+
/** Bypass flag for auto-approve / unattended mode */
|
|
24
|
+
bypassFlag?: string;
|
|
25
|
+
/** Bypass flag aliases (alternative forms accepted by the CLI) */
|
|
26
|
+
bypassAliases?: string[];
|
|
27
|
+
/** Extra install paths to check beyond PATH (resolved relative to $HOME) */
|
|
28
|
+
searchPaths?: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Well-known install paths ───────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Common install directories checked when PATH is empty or incomplete.
|
|
35
|
+
* Paths containing `~` are expanded at resolution time.
|
|
36
|
+
*
|
|
37
|
+
* Keep in sync with the Rust fallback in src/pty.rs `resolve_command_path()`.
|
|
38
|
+
*/
|
|
39
|
+
export const COMMON_SEARCH_PATHS = [
|
|
40
|
+
'~/.local/bin',
|
|
41
|
+
'~/.opencode/bin',
|
|
42
|
+
'~/.claude/local',
|
|
43
|
+
'/usr/local/bin',
|
|
44
|
+
'/usr/bin',
|
|
45
|
+
'/bin',
|
|
46
|
+
'/opt/homebrew/bin',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// ── Registry ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const CLI_REGISTRY: Record<AgentCli, CliDefinition> = {
|
|
52
|
+
claude: {
|
|
53
|
+
binaries: ['claude'],
|
|
54
|
+
nonInteractiveArgs: (task, extra = []) => [
|
|
55
|
+
'-p',
|
|
56
|
+
'--dangerously-skip-permissions',
|
|
57
|
+
task,
|
|
58
|
+
...extra,
|
|
59
|
+
],
|
|
60
|
+
bypassFlag: '--dangerously-skip-permissions',
|
|
61
|
+
searchPaths: ['~/.claude/local'],
|
|
62
|
+
},
|
|
63
|
+
codex: {
|
|
64
|
+
binaries: ['codex'],
|
|
65
|
+
nonInteractiveArgs: (task, extra = []) => ['exec', task, ...extra],
|
|
66
|
+
bypassFlag: '--dangerously-bypass-approvals-and-sandbox',
|
|
67
|
+
bypassAliases: ['--full-auto'],
|
|
68
|
+
searchPaths: ['~/.local/bin'],
|
|
69
|
+
},
|
|
70
|
+
gemini: {
|
|
71
|
+
binaries: ['gemini'],
|
|
72
|
+
nonInteractiveArgs: (task, extra = []) => ['-p', task, ...extra],
|
|
73
|
+
bypassFlag: '--yolo',
|
|
74
|
+
bypassAliases: ['-y'],
|
|
75
|
+
},
|
|
76
|
+
opencode: {
|
|
77
|
+
binaries: ['opencode'],
|
|
78
|
+
nonInteractiveArgs: (task, extra = []) => ['run', task, ...extra],
|
|
79
|
+
searchPaths: ['~/.opencode/bin'],
|
|
80
|
+
},
|
|
81
|
+
droid: {
|
|
82
|
+
binaries: ['droid'],
|
|
83
|
+
nonInteractiveArgs: (task, extra = []) => ['exec', task, ...extra],
|
|
84
|
+
},
|
|
85
|
+
aider: {
|
|
86
|
+
binaries: ['aider'],
|
|
87
|
+
nonInteractiveArgs: (task, extra = []) => [
|
|
88
|
+
'--message',
|
|
89
|
+
task,
|
|
90
|
+
'--yes-always',
|
|
91
|
+
'--no-git',
|
|
92
|
+
...extra,
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
goose: {
|
|
96
|
+
binaries: ['goose'],
|
|
97
|
+
nonInteractiveArgs: (task, extra = []) => [
|
|
98
|
+
'run',
|
|
99
|
+
'--text',
|
|
100
|
+
task,
|
|
101
|
+
'--no-session',
|
|
102
|
+
...extra,
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
'cursor-agent': {
|
|
106
|
+
binaries: ['cursor-agent'],
|
|
107
|
+
nonInteractiveArgs: (task, extra = []) => [
|
|
108
|
+
'--force',
|
|
109
|
+
'-p',
|
|
110
|
+
task,
|
|
111
|
+
...extra,
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
agent: {
|
|
115
|
+
binaries: ['agent'],
|
|
116
|
+
nonInteractiveArgs: (task, extra = []) => [
|
|
117
|
+
'--force',
|
|
118
|
+
'-p',
|
|
119
|
+
task,
|
|
120
|
+
...extra,
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
cursor: {
|
|
124
|
+
binaries: ['cursor-agent', 'agent'],
|
|
125
|
+
nonInteractiveArgs: (task, extra = []) => [
|
|
126
|
+
'--force',
|
|
127
|
+
'-p',
|
|
128
|
+
task,
|
|
129
|
+
...extra,
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the CLI definition for a given CLI identifier.
|
|
136
|
+
* Handles `cli:model` variants (e.g. `claude:opus`) by extracting the base CLI.
|
|
137
|
+
*/
|
|
138
|
+
export function getCliDefinition(cli: string): CliDefinition | undefined {
|
|
139
|
+
const baseCli = cli.includes(':') ? cli.split(':')[0] : cli;
|
|
140
|
+
return CLI_REGISTRY[baseCli as AgentCli];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the full registry (read-only).
|
|
145
|
+
*/
|
|
146
|
+
export function getCliRegistry(): Readonly<Record<AgentCli, CliDefinition>> {
|
|
147
|
+
return CLI_REGISTRY;
|
|
148
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI binary resolver — finds the actual binary path for a given agent CLI.
|
|
3
|
+
*
|
|
4
|
+
* Checks PATH first, then falls back to well-known install directories
|
|
5
|
+
* from the CLI registry. Results are memoized.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import { access, constants } from 'node:fs/promises';
|
|
10
|
+
import { accessSync, constants as constantsSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { promisify } from 'node:util';
|
|
14
|
+
import type { AgentCli } from './workflows/types.js';
|
|
15
|
+
import { getCliDefinition, COMMON_SEARCH_PATHS } from './cli-registry.js';
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
|
|
19
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface ResolvedCli {
|
|
22
|
+
/** The binary name that was found */
|
|
23
|
+
binary: string;
|
|
24
|
+
/** The full path to the binary */
|
|
25
|
+
path: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Memoization ────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
// null sentinel means "looked up, not found" — avoids repeating expensive searches
|
|
31
|
+
const resolveCache = new Map<string, ResolvedCli | null>();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Clear the resolution cache. Useful for testing or after PATH changes.
|
|
35
|
+
*/
|
|
36
|
+
export function clearResolveCache(): void {
|
|
37
|
+
resolveCache.clear();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Path expansion ─────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function expandHome(p: string): string {
|
|
43
|
+
if (p.startsWith('~/')) {
|
|
44
|
+
return join(homedir(), p.slice(2));
|
|
45
|
+
}
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Async resolver ─────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a CLI to its binary path. Checks PATH via `which`, then falls
|
|
53
|
+
* back to well-known install directories from the CLI registry.
|
|
54
|
+
*
|
|
55
|
+
* Results are memoized. Returns `undefined` if the binary cannot be found.
|
|
56
|
+
*/
|
|
57
|
+
export async function resolveCli(cli: AgentCli): Promise<ResolvedCli | undefined> {
|
|
58
|
+
if (resolveCache.has(cli)) {
|
|
59
|
+
return resolveCache.get(cli) ?? undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const def = getCliDefinition(cli);
|
|
63
|
+
if (!def) return undefined;
|
|
64
|
+
|
|
65
|
+
for (const binary of def.binaries) {
|
|
66
|
+
// Try PATH first via `which`
|
|
67
|
+
try {
|
|
68
|
+
const { stdout } = await execFileAsync('which', [binary]);
|
|
69
|
+
const path = stdout.trim();
|
|
70
|
+
if (path) {
|
|
71
|
+
const result: ResolvedCli = { binary, path };
|
|
72
|
+
resolveCache.set(cli, result);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// not in PATH
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Try well-known install directories (CLI-specific + common)
|
|
80
|
+
const searchDirs = [...(def.searchPaths ?? []), ...COMMON_SEARCH_PATHS];
|
|
81
|
+
const seen = new Set<string>();
|
|
82
|
+
for (const dir of searchDirs) {
|
|
83
|
+
const expanded = expandHome(dir);
|
|
84
|
+
if (seen.has(expanded)) continue;
|
|
85
|
+
seen.add(expanded);
|
|
86
|
+
|
|
87
|
+
const candidate = join(expanded, binary);
|
|
88
|
+
try {
|
|
89
|
+
await access(candidate, constants.X_OK);
|
|
90
|
+
const result: ResolvedCli = { binary, path: candidate };
|
|
91
|
+
resolveCache.set(cli, result);
|
|
92
|
+
return result;
|
|
93
|
+
} catch {
|
|
94
|
+
// not found here
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
resolveCache.set(cli, null);
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Sync resolver (for hot paths that can't be async) ──────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Synchronous version of `resolveCli`. Uses `which` via execFileSync
|
|
107
|
+
* and synchronous fs.accessSync. Prefer the async version when possible.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveCliSync(cli: AgentCli): ResolvedCli | undefined {
|
|
110
|
+
if (resolveCache.has(cli)) {
|
|
111
|
+
return resolveCache.get(cli) ?? undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const def = getCliDefinition(cli);
|
|
115
|
+
if (!def) return undefined;
|
|
116
|
+
|
|
117
|
+
const { execFileSync } = require('node:child_process') as typeof import('node:child_process');
|
|
118
|
+
|
|
119
|
+
for (const binary of def.binaries) {
|
|
120
|
+
// Try PATH first via `which`
|
|
121
|
+
try {
|
|
122
|
+
const stdout = execFileSync('which', [binary], { stdio: ['pipe', 'pipe', 'ignore'] });
|
|
123
|
+
const path = stdout.toString().trim();
|
|
124
|
+
if (path) {
|
|
125
|
+
const result: ResolvedCli = { binary, path };
|
|
126
|
+
resolveCache.set(cli, result);
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// not in PATH
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Try well-known install directories
|
|
134
|
+
const searchDirs = [...(def.searchPaths ?? []), ...COMMON_SEARCH_PATHS];
|
|
135
|
+
const seen = new Set<string>();
|
|
136
|
+
for (const dir of searchDirs) {
|
|
137
|
+
const expanded = expandHome(dir);
|
|
138
|
+
if (seen.has(expanded)) continue;
|
|
139
|
+
seen.add(expanded);
|
|
140
|
+
|
|
141
|
+
const candidate = join(expanded, binary);
|
|
142
|
+
try {
|
|
143
|
+
accessSync(candidate, constantsSync.X_OK);
|
|
144
|
+
const result: ResolvedCli = { binary, path: candidate };
|
|
145
|
+
resolveCache.set(cli, result);
|
|
146
|
+
return result;
|
|
147
|
+
} catch {
|
|
148
|
+
// not found here
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
resolveCache.set(cli, null);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { AgentRelay } from "./relay.js";
|
|
16
|
+
import { getCliDefinition } from "./cli-registry.js";
|
|
16
17
|
|
|
17
18
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
18
19
|
|
|
@@ -59,33 +60,21 @@ export interface SpawnFromEnvResult {
|
|
|
59
60
|
exitCode?: number;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
// ── Bypass Policy (
|
|
63
|
+
// ── Bypass Policy (delegated to cli-registry) ──────────────────────────────
|
|
63
64
|
|
|
64
65
|
type BypassFlagConfig = {
|
|
65
66
|
flag: string;
|
|
66
67
|
aliases?: string[];
|
|
67
68
|
};
|
|
68
69
|
|
|
69
|
-
/** SDK-owned bypass flag mapping. Cloud must NOT duplicate these. */
|
|
70
|
-
const BYPASS_FLAGS: Record<string, BypassFlagConfig> = {
|
|
71
|
-
claude: { flag: "--dangerously-skip-permissions" },
|
|
72
|
-
codex: {
|
|
73
|
-
flag: "--dangerously-bypass-approvals-and-sandbox",
|
|
74
|
-
aliases: ["--full-auto"],
|
|
75
|
-
},
|
|
76
|
-
gemini: {
|
|
77
|
-
flag: "--yolo",
|
|
78
|
-
aliases: ["-y"],
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
|
|
82
70
|
/**
|
|
83
|
-
* Resolve bypass flag config for a CLI.
|
|
71
|
+
* Resolve bypass flag config for a CLI from the consolidated registry.
|
|
84
72
|
* Handles `claude:model` variants (e.g. `claude:opus`).
|
|
85
73
|
*/
|
|
86
74
|
function getBypassFlagConfig(cli: string): BypassFlagConfig | undefined {
|
|
87
|
-
const
|
|
88
|
-
return
|
|
75
|
+
const def = getCliDefinition(cli);
|
|
76
|
+
if (!def?.bypassFlag) return undefined;
|
|
77
|
+
return { flag: def.bypassFlag, aliases: def.bypassAliases };
|
|
89
78
|
}
|
|
90
79
|
|
|
91
80
|
// ── Env Parsing ────────────────────────────────────────────────────────────
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
} from './types.js';
|
|
23
23
|
import { WorkflowRunner, type WorkflowEventListener, type VariableContext, type StepExecutor } from './runner.js';
|
|
24
24
|
import { formatDryRunReport } from './dry-run-format.js';
|
|
25
|
+
import { createDefaultEventLogger, type LogLevel } from './default-logger.js';
|
|
25
26
|
|
|
26
27
|
// ── Option types for the builder API ────────────────────────────────────────
|
|
27
28
|
|
|
@@ -106,6 +107,10 @@ export interface WorkflowRunOptions {
|
|
|
106
107
|
startFrom?: string;
|
|
107
108
|
/** Previous run ID whose cached outputs are used with startFrom. */
|
|
108
109
|
previousRunId?: string;
|
|
110
|
+
/** Console log verbosity: "verbose" | "normal" (default) | "quiet" | false (silent). */
|
|
111
|
+
logLevel?: LogLevel;
|
|
112
|
+
/** Renderer: "listr" for listr2 UI, "default" for console logger, false to disable. */
|
|
113
|
+
renderer?: 'listr' | 'default' | false;
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
// ── WorkflowBuilder ─────────────────────────────────────────────────────────
|
|
@@ -333,7 +338,11 @@ export class WorkflowBuilder {
|
|
|
333
338
|
if (this._timeoutMs !== undefined) config.swarm.timeoutMs = this._timeoutMs;
|
|
334
339
|
if (this._channel !== undefined) config.swarm.channel = this._channel;
|
|
335
340
|
if (this._idleNudge !== undefined) config.swarm.idleNudge = this._idleNudge;
|
|
336
|
-
|
|
341
|
+
config.errorHandling = this._errorHandling ?? {
|
|
342
|
+
strategy: 'retry',
|
|
343
|
+
maxRetries: 2,
|
|
344
|
+
retryDelayMs: 10_000,
|
|
345
|
+
};
|
|
337
346
|
if (this._coordination !== undefined) config.coordination = this._coordination;
|
|
338
347
|
if (this._state !== undefined) config.state = this._state;
|
|
339
348
|
if (this._trajectories !== undefined) config.trajectories = this._trajectories;
|
|
@@ -367,15 +376,23 @@ export class WorkflowBuilder {
|
|
|
367
376
|
return report;
|
|
368
377
|
}
|
|
369
378
|
|
|
379
|
+
// Wire up default console logger unless explicitly disabled
|
|
380
|
+
// renderer: "listr" owns the terminal — skip console logger to avoid garbled output
|
|
381
|
+
// renderer: false implies no output at all
|
|
382
|
+
const logLevel = options.renderer === 'listr' || options.renderer === false
|
|
383
|
+
? false
|
|
384
|
+
: (options.logLevel ?? 'normal');
|
|
385
|
+
if (logLevel !== false) {
|
|
386
|
+
runner.on(createDefaultEventLogger(logLevel));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Wire up user-provided event handler (additive — does not replace the default logger)
|
|
370
390
|
if (options.onEvent) {
|
|
371
391
|
runner.on(options.onEvent);
|
|
372
392
|
}
|
|
373
393
|
|
|
374
394
|
// Auto-detect RESUME_RUN_ID env var for resuming failed runs
|
|
375
395
|
const resumeRunId = process.env.RESUME_RUN_ID;
|
|
376
|
-
if (resumeRunId) {
|
|
377
|
-
return runner.resume(resumeRunId, options.vars);
|
|
378
|
-
}
|
|
379
396
|
|
|
380
397
|
const startFrom = this._startFrom ?? options.startFrom ?? process.env.START_FROM;
|
|
381
398
|
const previousRunId = this._previousRunId ?? options.previousRunId ?? process.env.PREVIOUS_RUN_ID;
|
|
@@ -383,6 +400,29 @@ export class WorkflowBuilder {
|
|
|
383
400
|
? { startFrom, previousRunId }
|
|
384
401
|
: undefined;
|
|
385
402
|
|
|
403
|
+
// If listr renderer requested, wire it up and run concurrently
|
|
404
|
+
// Must be set up BEFORE the resume check so resume runs also get event output
|
|
405
|
+
if (options.renderer === 'listr') {
|
|
406
|
+
const { createWorkflowRenderer } = await import('./listr-renderer.js');
|
|
407
|
+
const renderer = createWorkflowRenderer();
|
|
408
|
+
runner.on(renderer.onEvent);
|
|
409
|
+
|
|
410
|
+
const runPromise = resumeRunId
|
|
411
|
+
? runner.resume(resumeRunId, options.vars)
|
|
412
|
+
: runner.execute(config, options.workflow, options.vars, executeOptions);
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const [result] = await Promise.all([runPromise, renderer.start()]);
|
|
416
|
+
return result;
|
|
417
|
+
} finally {
|
|
418
|
+
renderer.unmount();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (resumeRunId) {
|
|
423
|
+
return runner.resume(resumeRunId, options.vars);
|
|
424
|
+
}
|
|
425
|
+
|
|
386
426
|
return runner.execute(config, options.workflow, options.vars, executeOptions);
|
|
387
427
|
}
|
|
388
428
|
}
|
|
@@ -73,6 +73,32 @@ interface OpenCodePartData {
|
|
|
73
73
|
function loadDatabaseConstructor(): DatabaseConstructor | null {
|
|
74
74
|
try {
|
|
75
75
|
return require('better-sqlite3') as DatabaseConstructor;
|
|
76
|
+
} catch {
|
|
77
|
+
// fall through
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fall back to Node 22+ native node:sqlite (experimental)
|
|
81
|
+
try {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
83
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
84
|
+
return function NativeSqliteWrapper(filename: string, options?: { readonly?: boolean; fileMustExist?: boolean }) {
|
|
85
|
+
const db = new DatabaseSync(filename, { open: true, readOnly: options?.readonly ?? false });
|
|
86
|
+
return {
|
|
87
|
+
prepare(sql: string) {
|
|
88
|
+
const stmt = db.prepare(sql);
|
|
89
|
+
return {
|
|
90
|
+
get<T>(params?: unknown): T | undefined {
|
|
91
|
+
return params != null ? stmt.get(params) as T | undefined : stmt.get() as T | undefined;
|
|
92
|
+
},
|
|
93
|
+
all<T>(params?: unknown): T[] {
|
|
94
|
+
return (params != null ? stmt.all(params) : stmt.all()) as T[];
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
pragma(source: string) { db.exec(`PRAGMA ${source}`); return undefined; },
|
|
99
|
+
close() { db.close(); },
|
|
100
|
+
};
|
|
101
|
+
} as unknown as DatabaseConstructor;
|
|
76
102
|
} catch {
|
|
77
103
|
return null;
|
|
78
104
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import type { WorkflowEvent, WorkflowEventListener } from './runner.js';
|
|
3
|
+
|
|
4
|
+
export type LogLevel = 'verbose' | 'normal' | 'quiet' | false;
|
|
5
|
+
|
|
6
|
+
const noop: WorkflowEventListener = () => {};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a default event logger that writes workflow progress to the console.
|
|
10
|
+
*
|
|
11
|
+
* @param level - Log verbosity: "verbose" | "normal" (default) | "quiet" | false (no-op)
|
|
12
|
+
*/
|
|
13
|
+
export function createDefaultEventLogger(level: LogLevel = 'normal'): WorkflowEventListener {
|
|
14
|
+
if (level === false) return noop;
|
|
15
|
+
|
|
16
|
+
return (event: WorkflowEvent) => {
|
|
17
|
+
switch (event.type) {
|
|
18
|
+
// ── Run lifecycle ──
|
|
19
|
+
case 'run:started':
|
|
20
|
+
if (level !== 'quiet') {
|
|
21
|
+
console.log(chalk.cyan(`[workflow] run ${event.runId.slice(0, 8)}...`));
|
|
22
|
+
}
|
|
23
|
+
break;
|
|
24
|
+
|
|
25
|
+
case 'run:completed':
|
|
26
|
+
console.log(chalk.green(`[workflow] completed`));
|
|
27
|
+
break;
|
|
28
|
+
|
|
29
|
+
case 'run:failed':
|
|
30
|
+
console.log(chalk.red(`[workflow] FAILED: ${event.error}`));
|
|
31
|
+
break;
|
|
32
|
+
|
|
33
|
+
case 'run:cancelled':
|
|
34
|
+
if (level !== 'quiet') {
|
|
35
|
+
console.log(chalk.yellow(`[workflow] cancelled`));
|
|
36
|
+
}
|
|
37
|
+
break;
|
|
38
|
+
|
|
39
|
+
// ── Step lifecycle ──
|
|
40
|
+
case 'step:started':
|
|
41
|
+
if (level !== 'quiet') {
|
|
42
|
+
console.log(chalk.blue(` ● ${event.stepName} — started`));
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
case 'step:completed':
|
|
47
|
+
if (level !== 'quiet') {
|
|
48
|
+
console.log(chalk.green(` ✓ ${event.stepName} — completed`));
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
|
|
52
|
+
case 'step:failed':
|
|
53
|
+
console.log(chalk.red(` ✗ ${event.stepName} — FAILED: ${event.error}`));
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
case 'step:skipped':
|
|
57
|
+
if (level !== 'quiet') {
|
|
58
|
+
console.log(chalk.gray(` ○ ${event.stepName} — skipped`));
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case 'step:retrying':
|
|
63
|
+
if (level !== 'quiet') {
|
|
64
|
+
console.log(chalk.yellow(` ↻ ${event.stepName} — retrying (attempt ${event.attempt})`));
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
case 'step:nudged':
|
|
69
|
+
if (level !== 'quiet') {
|
|
70
|
+
console.log(chalk.yellow(` ⚡ ${event.stepName} — nudged (${event.nudgeCount})`));
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case 'step:agent-report': {
|
|
75
|
+
if (level !== 'quiet') {
|
|
76
|
+
const r = event.report;
|
|
77
|
+
const parts: string[] = [];
|
|
78
|
+
if (r.model) parts.push(r.model);
|
|
79
|
+
if (r.cost != null) parts.push(`$${r.cost.toFixed(2)}`);
|
|
80
|
+
if (r.tokens) parts.push(`${r.tokens.input}+${r.tokens.output} tokens`);
|
|
81
|
+
parts.push(`${r.errors.length} errors`);
|
|
82
|
+
console.log(chalk.dim(` 📊 ${event.stepName} — ${parts.join(' · ')}`));
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Broker-level events (verbose only) ──
|
|
88
|
+
case 'broker:event':
|
|
89
|
+
if (level === 'verbose') {
|
|
90
|
+
console.log(chalk.dim(` [broker] ${JSON.stringify(event.event)}`));
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
// ── Other events (verbose only) ──
|
|
95
|
+
case 'step:owner-assigned':
|
|
96
|
+
if (level === 'verbose') {
|
|
97
|
+
console.log(chalk.dim(` ${event.stepName} — owner: ${event.ownerName}, specialist: ${event.specialistName}`));
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
case 'step:review-completed':
|
|
102
|
+
if (level === 'verbose') {
|
|
103
|
+
console.log(chalk.dim(` ${event.stepName} — review: ${event.decision} by ${event.reviewerName}`));
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 'step:owner-timeout':
|
|
108
|
+
if (level !== 'quiet') {
|
|
109
|
+
console.log(chalk.yellow(` ⏱ ${event.stepName} — owner timeout (${event.ownerName})`));
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case 'step:force-released':
|
|
114
|
+
if (level === 'verbose') {
|
|
115
|
+
console.log(chalk.dim(` ${event.stepName} — force-released`));
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -25,3 +25,4 @@ export * from './templates.js';
|
|
|
25
25
|
export { WorkflowTrajectory, type StepOutcome } from './trajectory.js';
|
|
26
26
|
export { formatDryRunReport } from './dry-run-format.js';
|
|
27
27
|
export { createWorkflowRenderer, type WorkflowRenderer } from './listr-renderer.js';
|
|
28
|
+
export { createDefaultEventLogger } from './default-logger.js';
|