argus-reviewer-e2e 0.1.2
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/LICENSE +21 -0
- package/README.md +80 -0
- package/action/action.yml +147 -0
- package/action/sticky-comment.mjs +376 -0
- package/dist/api.d.ts +121 -0
- package/dist/api.js +256 -0
- package/dist/cache/fingerprint.d.ts +59 -0
- package/dist/cache/fingerprint.js +67 -0
- package/dist/cache/store.d.ts +17 -0
- package/dist/cache/store.js +41 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.js +1355 -0
- package/dist/config.d.ts +130 -0
- package/dist/config.js +163 -0
- package/dist/debug.d.ts +1 -0
- package/dist/debug.js +30 -0
- package/dist/detect.d.ts +50 -0
- package/dist/detect.js +105 -0
- package/dist/driver/browser.d.ts +64 -0
- package/dist/driver/browser.js +200 -0
- package/dist/driver/target.d.ts +23 -0
- package/dist/driver/target.js +97 -0
- package/dist/engine/actions.d.ts +26 -0
- package/dist/engine/actions.js +47 -0
- package/dist/engine/loop.d.ts +118 -0
- package/dist/engine/loop.js +649 -0
- package/dist/engine/prompts.d.ts +22 -0
- package/dist/engine/prompts.js +112 -0
- package/dist/evidence/ci.d.ts +18 -0
- package/dist/evidence/ci.js +61 -0
- package/dist/evidence/link.d.ts +35 -0
- package/dist/evidence/link.js +90 -0
- package/dist/executor/a0.d.ts +29 -0
- package/dist/executor/a0.js +38 -0
- package/dist/fsutil.d.ts +5 -0
- package/dist/fsutil.js +12 -0
- package/dist/index/context.d.ts +17 -0
- package/dist/index/context.js +88 -0
- package/dist/index/diff.d.ts +1 -0
- package/dist/index/diff.js +33 -0
- package/dist/index/invalidate.d.ts +28 -0
- package/dist/index/invalidate.js +56 -0
- package/dist/index/scan.d.ts +26 -0
- package/dist/index/scan.js +209 -0
- package/dist/journal/build.d.ts +15 -0
- package/dist/journal/build.js +47 -0
- package/dist/journal/schema.d.ts +59 -0
- package/dist/journal/schema.js +6 -0
- package/dist/journal/store.d.ts +10 -0
- package/dist/journal/store.js +26 -0
- package/dist/live.d.ts +2 -0
- package/dist/live.js +46 -0
- package/dist/log.d.ts +17 -0
- package/dist/log.js +24 -0
- package/dist/report/comment.d.ts +13 -0
- package/dist/report/comment.js +135 -0
- package/dist/report/junit.d.ts +10 -0
- package/dist/report/junit.js +46 -0
- package/dist/report/run.d.ts +51 -0
- package/dist/report/run.js +36 -0
- package/dist/vision/cost.d.ts +36 -0
- package/dist/vision/cost.js +16 -0
- package/dist/vision/ledger.d.ts +29 -0
- package/dist/vision/ledger.js +65 -0
- package/dist/vision/openrouter.d.ts +70 -0
- package/dist/vision/openrouter.js +134 -0
- package/package.json +65 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export interface ProviderRules {
|
|
2
|
+
only?: string[];
|
|
3
|
+
ignore?: string[];
|
|
4
|
+
order?: string[];
|
|
5
|
+
allow_fallbacks?: boolean;
|
|
6
|
+
require_parameters?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface Target {
|
|
9
|
+
command: string;
|
|
10
|
+
url: string;
|
|
11
|
+
readyTimeoutMs: number;
|
|
12
|
+
}
|
|
13
|
+
export interface Config {
|
|
14
|
+
model: string;
|
|
15
|
+
escalation_model: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional specialist model for grounding-correction retries (e.g. a
|
|
18
|
+
* ui-tars-class model that returns bare coordinates). Used only when the
|
|
19
|
+
* primary model's proposed point resolves to the wrong element.
|
|
20
|
+
*/
|
|
21
|
+
grounding_model: string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Optional code review model. Used by `argus-reviewer code-review` to review
|
|
24
|
+
* PR diffs and post findings. Defaults to the primary `model` if not set.
|
|
25
|
+
*/
|
|
26
|
+
code_model: string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Hard budget for the `argus-reviewer code-review` lane. When set, the
|
|
29
|
+
* review stops early if the cumulative OpenRouter cost exceeds this cap.
|
|
30
|
+
*/
|
|
31
|
+
codeReviewBudgetUsd: number | undefined;
|
|
32
|
+
provider: ProviderRules;
|
|
33
|
+
budgetUsd: number | undefined;
|
|
34
|
+
target: Target | undefined;
|
|
35
|
+
cacheDir: string | undefined;
|
|
36
|
+
/** Directory scanned by `argus-reviewer run` for *.test.* files. */
|
|
37
|
+
testsDir: string | undefined;
|
|
38
|
+
/** Directory for JUnit XML + JSON run report output. */
|
|
39
|
+
reportDir: string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Named secrets for `td.type(name, { secret: true })`. The value is typed
|
|
42
|
+
* locally and never sent to the model — the model only resolves the field.
|
|
43
|
+
*/
|
|
44
|
+
secrets: Record<string, string> | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Optional module path (resolved from cwd) whose default export is invoked
|
|
47
|
+
* with the Playwright `Page` after the driver launches and before any
|
|
48
|
+
* navigation — the seam for `page.route` mocks, tenant seeding, and other
|
|
49
|
+
* pre-navigation setup.
|
|
50
|
+
*/
|
|
51
|
+
pageSetup: string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* OpenRouter request metadata. `trace` is sent in the request body and
|
|
54
|
+
* can be used to attribute spend by repo, PR, or run. `headers` are
|
|
55
|
+
* sent verbatim with every OpenRouter request (e.g. HTTP-Referer, X-Title).
|
|
56
|
+
*/
|
|
57
|
+
openrouter: {
|
|
58
|
+
trace?: Record<string, string>;
|
|
59
|
+
headers?: Record<string, string>;
|
|
60
|
+
} | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Browser engine for Playwright: `chromium`, `firefox`, or `webkit`.
|
|
63
|
+
* Defaults to `chromium`.
|
|
64
|
+
*/
|
|
65
|
+
browser: 'chromium' | 'firefox' | 'webkit' | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Hard limit in milliseconds for Playwright cleanup (context + browser close).
|
|
68
|
+
* Prevents a hung browser from keeping the runner or test suite alive.
|
|
69
|
+
* Defaults to 30 seconds.
|
|
70
|
+
*/
|
|
71
|
+
browserTimeoutMs: number | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Severity levels that block a pre-merge status. Defaults to `['bug']` so
|
|
74
|
+
* `risk`/`nit`/`q` findings are surfaced but do not fail the status.
|
|
75
|
+
*/
|
|
76
|
+
severity: string[] | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Log verbosity — 'debug'|'info'|'warn'|'error'. ARGUS_DEBUG=1 forces
|
|
79
|
+
* 'debug'. Default 'warn'.
|
|
80
|
+
*/
|
|
81
|
+
logLevel: 'debug' | 'info' | 'warn' | 'error' | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Repo globs naming the app surface the tests exercise (e.g. 'ui/src/**').
|
|
84
|
+
* Diff-aware invalidation marks flow caches stale when the diff touches
|
|
85
|
+
* files in this surface's dependency cone.
|
|
86
|
+
*/
|
|
87
|
+
sourceGlobs: string[] | undefined;
|
|
88
|
+
/** Path (repo-relative) for the generated repo index. Default 'argus.index.json'. */
|
|
89
|
+
indexPath: string | undefined;
|
|
90
|
+
/** Base ref for diff invalidation (e.g. 'origin/main'); unset = working tree. */
|
|
91
|
+
diffBase: string | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Max actions `argus-reviewer record` will take before giving up on `done`.
|
|
94
|
+
* Real multi-action flows need headroom — defaults to 40; `record
|
|
95
|
+
* --max-steps <n>` overrides.
|
|
96
|
+
*/
|
|
97
|
+
recordStepCap: number | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Agent Zero instance for delegated tasks (`argus-reviewer delegate`,
|
|
100
|
+
* `heal: 'a0'`). `url` is the instance base URL — leave unset to let the
|
|
101
|
+
* `a0` CLI resolve it (saved host, AGENT_ZERO_HOST, Docker discovery).
|
|
102
|
+
* Least-privilege scoping (browser vs full desktop) is configured on the
|
|
103
|
+
* instance's gateway, not here.
|
|
104
|
+
*/
|
|
105
|
+
a0: {
|
|
106
|
+
url: string | undefined;
|
|
107
|
+
} | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Failure escalation for `run`. 'local' (default) heals via the vision
|
|
110
|
+
* model only. 'a0' additionally sends each failed test to Agent Zero for an
|
|
111
|
+
* autonomous second opinion — it clicks through the app and reports whether
|
|
112
|
+
* the app or the expectation is wrong.
|
|
113
|
+
*/
|
|
114
|
+
heal: 'local' | 'a0' | undefined;
|
|
115
|
+
}
|
|
116
|
+
export type ConfigInput = Partial<Omit<Config, 'provider'>> & {
|
|
117
|
+
provider?: Partial<ProviderRules>;
|
|
118
|
+
};
|
|
119
|
+
export declare const DEFAULT_RECORD_STEP_CAP = 40;
|
|
120
|
+
export declare function defineConfig(input: ConfigInput): ConfigInput;
|
|
121
|
+
export declare function resolveConfig(input?: ConfigInput): Config;
|
|
122
|
+
export declare function loadConfig(cwd: string): Promise<Config>;
|
|
123
|
+
/**
|
|
124
|
+
* Provider slugs the harness recognizes for `provider.only/ignore/order`
|
|
125
|
+
* (KTD4). Unknown slugs warn but do not fail — OpenRouter's catalog changes
|
|
126
|
+
* faster than this list, so validation is fail-open by design.
|
|
127
|
+
*/
|
|
128
|
+
export declare const KNOWN_PROVIDER_SLUGS: ReadonlySet<string>;
|
|
129
|
+
/** Slugs in the provider rules that are not recognized; callers warn, not fail. */
|
|
130
|
+
export declare function unknownProviderSlugs(provider: ProviderRules): string[];
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
export const DEFAULT_RECORD_STEP_CAP = 40;
|
|
3
|
+
const defaults = {
|
|
4
|
+
model: 'google/gemini-2.5-flash-lite',
|
|
5
|
+
escalation_model: 'moonshotai/kimi-k2.5',
|
|
6
|
+
grounding_model: undefined,
|
|
7
|
+
code_model: 'deepseek/deepseek-v4.1-flash',
|
|
8
|
+
codeReviewBudgetUsd: undefined,
|
|
9
|
+
provider: {
|
|
10
|
+
ignore: ['siliconflow', 'novitaai', 'atlascloud', 'streamlake', 'chutes'],
|
|
11
|
+
},
|
|
12
|
+
budgetUsd: undefined,
|
|
13
|
+
target: undefined,
|
|
14
|
+
cacheDir: undefined,
|
|
15
|
+
testsDir: undefined,
|
|
16
|
+
reportDir: undefined,
|
|
17
|
+
secrets: undefined,
|
|
18
|
+
pageSetup: undefined,
|
|
19
|
+
openrouter: undefined,
|
|
20
|
+
browser: 'chromium',
|
|
21
|
+
browserTimeoutMs: 30_000,
|
|
22
|
+
severity: ['bug'],
|
|
23
|
+
logLevel: undefined,
|
|
24
|
+
sourceGlobs: undefined,
|
|
25
|
+
indexPath: undefined,
|
|
26
|
+
diffBase: undefined,
|
|
27
|
+
recordStepCap: DEFAULT_RECORD_STEP_CAP,
|
|
28
|
+
a0: undefined,
|
|
29
|
+
heal: 'local',
|
|
30
|
+
};
|
|
31
|
+
export function defineConfig(input) {
|
|
32
|
+
return input;
|
|
33
|
+
}
|
|
34
|
+
export function resolveConfig(input = {}) {
|
|
35
|
+
const provider = { ...defaults.provider, ...(input.provider ?? {}) };
|
|
36
|
+
const resolved = { ...defaults, ...input, provider };
|
|
37
|
+
const cap = resolved.recordStepCap;
|
|
38
|
+
resolved.recordStepCap =
|
|
39
|
+
cap !== undefined && Number.isFinite(cap) && cap >= 1
|
|
40
|
+
? Math.floor(cap)
|
|
41
|
+
: DEFAULT_RECORD_STEP_CAP;
|
|
42
|
+
if (resolved.heal !== 'a0')
|
|
43
|
+
resolved.heal = 'local';
|
|
44
|
+
return resolved;
|
|
45
|
+
}
|
|
46
|
+
export async function loadConfig(cwd) {
|
|
47
|
+
const fs = await import('node:fs/promises');
|
|
48
|
+
const path = await import('node:path');
|
|
49
|
+
const names = ['argus-reviewer.config', 'vision-e2e.config'];
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
for (const ext of ['.ts', '.json']) {
|
|
52
|
+
const file = path.join(cwd, `${name}${ext}`);
|
|
53
|
+
try {
|
|
54
|
+
const stat = await fs.stat(file);
|
|
55
|
+
if (!stat.isFile())
|
|
56
|
+
continue;
|
|
57
|
+
if (ext === '.json') {
|
|
58
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
59
|
+
return resolveConfig(JSON.parse(raw));
|
|
60
|
+
}
|
|
61
|
+
// Always transpile .ts to a temp .mjs rather than importing natively:
|
|
62
|
+
// Node's built-in type stripping resolves the module type from the
|
|
63
|
+
// *consumer's* package.json, so a CommonJS consumer makes ESM config
|
|
64
|
+
// fail with 'Cannot use import statement'. The transpiled file is
|
|
65
|
+
// written next to the config (removed after import) so relative
|
|
66
|
+
// imports and node_modules resolution behave like the original file;
|
|
67
|
+
// the package self-import is rewritten to this module's own index so
|
|
68
|
+
// global/npx installs resolve it too.
|
|
69
|
+
const ts = await import('typescript');
|
|
70
|
+
const { readFile, writeFile, rm } = await import('node:fs/promises');
|
|
71
|
+
const { join, dirname } = await import('node:path');
|
|
72
|
+
const source = await readFile(file, 'utf8');
|
|
73
|
+
const js = ts
|
|
74
|
+
.transpileModule(source, {
|
|
75
|
+
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
|
76
|
+
})
|
|
77
|
+
.outputText.replace(/(['"])argus-reviewer-e2e\1/g,
|
|
78
|
+
// package.json exports '.' → dist/api.js (sibling of this file)
|
|
79
|
+
JSON.stringify(new URL('./api.js', import.meta.url).href));
|
|
80
|
+
const out = join(dirname(file), `.argus-config-${process.pid}-${Date.now()}.mjs`);
|
|
81
|
+
let mod;
|
|
82
|
+
try {
|
|
83
|
+
await writeFile(out, js, 'utf8');
|
|
84
|
+
mod = (await import(pathToFileURL(out).href));
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await rm(out, { force: true }).catch(() => undefined);
|
|
88
|
+
}
|
|
89
|
+
const exported = mod.default ?? mod;
|
|
90
|
+
return resolveConfig(exported);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
const code = e.code;
|
|
94
|
+
if (code === 'ENOENT')
|
|
95
|
+
continue;
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return resolveConfig();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Provider slugs the harness recognizes for `provider.only/ignore/order`
|
|
104
|
+
* (KTD4). Unknown slugs warn but do not fail — OpenRouter's catalog changes
|
|
105
|
+
* faster than this list, so validation is fail-open by design.
|
|
106
|
+
*/
|
|
107
|
+
export const KNOWN_PROVIDER_SLUGS = new Set([
|
|
108
|
+
'ai21',
|
|
109
|
+
'aion-labs',
|
|
110
|
+
'alibaba',
|
|
111
|
+
'amazon-bedrock',
|
|
112
|
+
'anthropic',
|
|
113
|
+
'atlascloud',
|
|
114
|
+
'azure',
|
|
115
|
+
'bedrock',
|
|
116
|
+
'cerebras',
|
|
117
|
+
'chutes',
|
|
118
|
+
'cloudflare',
|
|
119
|
+
'cohere',
|
|
120
|
+
'coreweave',
|
|
121
|
+
'crusoe',
|
|
122
|
+
'deepinfra',
|
|
123
|
+
'deepseek',
|
|
124
|
+
'featherless',
|
|
125
|
+
'fireworks',
|
|
126
|
+
'friendli',
|
|
127
|
+
'gmicloud',
|
|
128
|
+
'google',
|
|
129
|
+
'google-ai-studio',
|
|
130
|
+
'groq',
|
|
131
|
+
'hyperbolic',
|
|
132
|
+
'inception',
|
|
133
|
+
'inference-net',
|
|
134
|
+
'lambda',
|
|
135
|
+
'mistral',
|
|
136
|
+
'moonshotai',
|
|
137
|
+
'ncompass',
|
|
138
|
+
'nebius',
|
|
139
|
+
'nineteen',
|
|
140
|
+
'novitaai',
|
|
141
|
+
'open-inference',
|
|
142
|
+
'openai',
|
|
143
|
+
'openrouter',
|
|
144
|
+
'parasail',
|
|
145
|
+
'perplexity',
|
|
146
|
+
'phala',
|
|
147
|
+
'relace',
|
|
148
|
+
'sambanova',
|
|
149
|
+
'siliconflow',
|
|
150
|
+
'streamlake',
|
|
151
|
+
'targon',
|
|
152
|
+
'together',
|
|
153
|
+
'ubicloud',
|
|
154
|
+
'venice',
|
|
155
|
+
'wandb',
|
|
156
|
+
'xai',
|
|
157
|
+
'zai',
|
|
158
|
+
]);
|
|
159
|
+
/** Slugs in the provider rules that are not recognized; callers warn, not fail. */
|
|
160
|
+
export function unknownProviderSlugs(provider) {
|
|
161
|
+
const slugs = [...(provider.only ?? []), ...(provider.ignore ?? []), ...(provider.order ?? [])];
|
|
162
|
+
return slugs.filter((slug) => !KNOWN_PROVIDER_SLUGS.has(slug.toLowerCase()));
|
|
163
|
+
}
|
package/dist/debug.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function debug(kind: string, ...args: unknown[]): void;
|
package/dist/debug.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { liveLog } from './live.js';
|
|
3
|
+
const DEBUG = process.env.ARGUS_DEBUG === '1' || process.env.ARGUS_DEBUG === 'true';
|
|
4
|
+
// debug() has no config access — it lives under the default cache dir.
|
|
5
|
+
const LIVE_DIR = join(process.cwd(), '.argus-reviewer-cache');
|
|
6
|
+
function toMsg(arg) {
|
|
7
|
+
if (typeof arg === 'string')
|
|
8
|
+
return arg;
|
|
9
|
+
try {
|
|
10
|
+
const s = JSON.stringify(arg);
|
|
11
|
+
return s === undefined ? String(arg) : s; // JSON.stringify(undefined) -> undefined
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return String(arg); // BigInt, cyclic refs — never throw from a debug call
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function debug(kind, ...args) {
|
|
18
|
+
if (!DEBUG)
|
|
19
|
+
return; // keep debug() free of fs work on the hot path
|
|
20
|
+
for (const arg of args) {
|
|
21
|
+
liveLog(LIVE_DIR, kind, 'debug', toMsg(arg));
|
|
22
|
+
const prefix = `[argus-reviewer:${kind}]`;
|
|
23
|
+
if (typeof arg === 'string') {
|
|
24
|
+
console.error(`${prefix} ${arg}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.error(prefix, arg);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment detection for `init` readiness reporting and Agent Zero
|
|
3
|
+
* resolution. Everything here is best-effort — a missing tool degrades a
|
|
4
|
+
* feature flag, it never breaks the command.
|
|
5
|
+
*/
|
|
6
|
+
export interface ExecResult {
|
|
7
|
+
code: number;
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
}
|
|
11
|
+
export type ExecFn = (cmd: string, args: string[], timeoutMs: number) => Promise<ExecResult>;
|
|
12
|
+
export declare const defaultExec: ExecFn;
|
|
13
|
+
export type ProbeFn = (url: string, timeoutMs: number) => Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* Any HTTP response — including a login redirect — means *something* is up,
|
|
16
|
+
* but port 5080 could be an unrelated service. Require an Agent Zero marker
|
|
17
|
+
* in the served HTML before trusting the probe result.
|
|
18
|
+
*/
|
|
19
|
+
export declare const defaultProbe: ProbeFn;
|
|
20
|
+
export interface A0Info {
|
|
21
|
+
/** `a0` CLI version string when the binary is on PATH. */
|
|
22
|
+
version: string | undefined;
|
|
23
|
+
/** Resolved instance base URL, if one could be found. */
|
|
24
|
+
host: string | undefined;
|
|
25
|
+
hostSource: 'env' | 'dotfile' | 'probe' | undefined;
|
|
26
|
+
}
|
|
27
|
+
export interface EnvironmentReport {
|
|
28
|
+
/** OPENROUTER_API_KEY is set and non-empty. */
|
|
29
|
+
openrouterKey: boolean;
|
|
30
|
+
/** gh CLI authenticated; undefined when gh is not installed at all. */
|
|
31
|
+
ghAuth: boolean | undefined;
|
|
32
|
+
/** Playwright engines with a downloaded executable (e.g. 'chromium'). */
|
|
33
|
+
playwrightBrowsers: string[];
|
|
34
|
+
a0: A0Info;
|
|
35
|
+
}
|
|
36
|
+
export interface DetectOptions {
|
|
37
|
+
exec?: ExecFn;
|
|
38
|
+
home?: string;
|
|
39
|
+
probe?: ProbeFn;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the Agent Zero instance URL the same way the `a0` CLI does:
|
|
43
|
+
* explicit env var, the launcher-managed ~/.agent-zero/.env, then a probe of
|
|
44
|
+
* the default local port (http://localhost:5080).
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveA0Host(env: NodeJS.ProcessEnv, opts?: DetectOptions): Promise<{
|
|
47
|
+
host: string | undefined;
|
|
48
|
+
source: A0Info['hostSource'];
|
|
49
|
+
}>;
|
|
50
|
+
export declare function detectEnvironment(env: NodeJS.ProcessEnv, opts?: DetectOptions): Promise<EnvironmentReport>;
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
5
|
+
export const defaultExec = (cmd, args, timeoutMs) => new Promise((resolve) => {
|
|
6
|
+
execFile(cmd, args, { timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
7
|
+
if (err) {
|
|
8
|
+
// stderr is '' (not undefined) on spawn ENOENT — fall back to the
|
|
9
|
+
// error message so callers can distinguish "missing" from "failed".
|
|
10
|
+
resolve({ code: 1, stdout: String(stdout), stderr: String(stderr) || err.message });
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
resolve({ code: 0, stdout: String(stdout), stderr: String(stderr) });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Any HTTP response — including a login redirect — means *something* is up,
|
|
19
|
+
* but port 5080 could be an unrelated service. Require an Agent Zero marker
|
|
20
|
+
* in the served HTML before trusting the probe result.
|
|
21
|
+
*/
|
|
22
|
+
export const defaultProbe = async (url, timeoutMs) => {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
26
|
+
redirect: 'manual',
|
|
27
|
+
});
|
|
28
|
+
if (res.status >= 500)
|
|
29
|
+
return false;
|
|
30
|
+
const body = (await res.text()).slice(0, 65_536);
|
|
31
|
+
return /agent.?zero/i.test(body);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/** Read `AGENT_ZERO_HOST` (and friends) out of ~/.agent-zero/.env. */
|
|
38
|
+
async function a0EnvFileHost(home) {
|
|
39
|
+
try {
|
|
40
|
+
const raw = await readFile(join(home, '.agent-zero', '.env'), 'utf8');
|
|
41
|
+
for (const line of raw.split('\n')) {
|
|
42
|
+
const m = /^AGENT_ZERO_HOST=(\S+)\s*$/.exec(line.trim());
|
|
43
|
+
if (m?.[1] !== undefined && m[1] !== '')
|
|
44
|
+
return m[1];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// no dotfile — fall through
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the Agent Zero instance URL the same way the `a0` CLI does:
|
|
54
|
+
* explicit env var, the launcher-managed ~/.agent-zero/.env, then a probe of
|
|
55
|
+
* the default local port (http://localhost:5080).
|
|
56
|
+
*/
|
|
57
|
+
export async function resolveA0Host(env, opts = {}) {
|
|
58
|
+
const home = opts.home ?? homedir();
|
|
59
|
+
const probe = opts.probe ?? defaultProbe;
|
|
60
|
+
const fromEnv = env.AGENT_ZERO_HOST;
|
|
61
|
+
if (fromEnv !== undefined && fromEnv !== '')
|
|
62
|
+
return { host: fromEnv, source: 'env' };
|
|
63
|
+
const fromFile = await a0EnvFileHost(home);
|
|
64
|
+
if (fromFile !== undefined)
|
|
65
|
+
return { host: fromFile, source: 'dotfile' };
|
|
66
|
+
const local = 'http://localhost:5080';
|
|
67
|
+
if (await probe(local, 2_000))
|
|
68
|
+
return { host: local, source: 'probe' };
|
|
69
|
+
return { host: undefined, source: undefined };
|
|
70
|
+
}
|
|
71
|
+
/** Playwright engines with a downloaded browser under ~/.cache/ms-playwright. */
|
|
72
|
+
async function playwrightBrowsers(home) {
|
|
73
|
+
try {
|
|
74
|
+
const entries = await readdir(join(home, '.cache', 'ms-playwright'));
|
|
75
|
+
const found = new Set();
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
for (const engine of ['chromium', 'firefox', 'webkit']) {
|
|
78
|
+
if (entry.startsWith(engine))
|
|
79
|
+
found.add(engine);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...found];
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export async function detectEnvironment(env, opts = {}) {
|
|
89
|
+
const exec = opts.exec ?? defaultExec;
|
|
90
|
+
const home = opts.home ?? homedir();
|
|
91
|
+
const [a0Version, gh, browsers, a0Host] = await Promise.all([
|
|
92
|
+
exec('a0', ['--version'], 5_000),
|
|
93
|
+
exec('gh', ['auth', 'status'], 5_000),
|
|
94
|
+
playwrightBrowsers(home),
|
|
95
|
+
resolveA0Host(env, opts),
|
|
96
|
+
]);
|
|
97
|
+
const version = a0Version.code === 0 ? a0Version.stdout.trim() : undefined;
|
|
98
|
+
const ghMissing = gh.code !== 0 && /ENOENT|not found|no such file/i.test(gh.stderr);
|
|
99
|
+
return {
|
|
100
|
+
openrouterKey: env.OPENROUTER_API_KEY !== undefined && env.OPENROUTER_API_KEY !== '',
|
|
101
|
+
ghAuth: gh.code === 0 ? true : ghMissing ? undefined : false,
|
|
102
|
+
playwrightBrowsers: browsers,
|
|
103
|
+
a0: { version, host: a0Host.host, hostSource: a0Host.source },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type Page } from 'playwright';
|
|
2
|
+
export interface Viewport {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
}
|
|
6
|
+
export interface BrowserDriverOptions {
|
|
7
|
+
/** Pinned viewport in CSS pixels. Model coordinates map 1:1 onto this. */
|
|
8
|
+
viewport?: Viewport;
|
|
9
|
+
/** Directory for the per-run webm recording. Defaults to a fresh temp dir. */
|
|
10
|
+
videoDir?: string;
|
|
11
|
+
/** JPEG quality 0-100 for observation screenshots. */
|
|
12
|
+
screenshotQuality?: number;
|
|
13
|
+
/** Optional scale factor for the observation screenshot (<=1 downscales). */
|
|
14
|
+
screenshotScale?: number;
|
|
15
|
+
/** Playwright browser engine: `chromium` (default), `firefox`, or `webkit`. */
|
|
16
|
+
browser?: 'chromium' | 'firefox' | 'webkit' | undefined;
|
|
17
|
+
/** Hard limit in ms for Playwright cleanup. */
|
|
18
|
+
browserTimeoutMs?: number | undefined;
|
|
19
|
+
}
|
|
20
|
+
export interface Observation {
|
|
21
|
+
screenshotJpeg: Buffer;
|
|
22
|
+
a11yYaml: string;
|
|
23
|
+
/** Viewport (CSS pixels) the screenshot was taken at — model coords map 1:1. */
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One Playwright context per run. Viewport and deviceScaleFactor are pinned so
|
|
29
|
+
* vision-model pixel coordinates map 1:1 to viewport pixels (KTD2).
|
|
30
|
+
*/
|
|
31
|
+
export declare class BrowserDriver {
|
|
32
|
+
private readonly browser;
|
|
33
|
+
private readonly context;
|
|
34
|
+
private readonly page;
|
|
35
|
+
private readonly quality;
|
|
36
|
+
private readonly videoDir;
|
|
37
|
+
private readonly viewport;
|
|
38
|
+
private readonly browserTimeoutMs;
|
|
39
|
+
private video;
|
|
40
|
+
private closed;
|
|
41
|
+
private constructor();
|
|
42
|
+
static launch(options?: BrowserDriverOptions): Promise<BrowserDriver>;
|
|
43
|
+
get rawPage(): Page;
|
|
44
|
+
get recordingDir(): string;
|
|
45
|
+
goto(url: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Capture the current observation: a bounded JPEG screenshot plus the page's
|
|
48
|
+
* a11y tree as YAML via ariaSnapshot (not the deprecated accessibility API).
|
|
49
|
+
*
|
|
50
|
+
* `grid: true` paints a temporary coordinate overlay (lines + axis labels
|
|
51
|
+
* every 100px) before the screenshot and removes it immediately after — the
|
|
52
|
+
* set-of-marks trick that measurably improves vision-model pixel grounding.
|
|
53
|
+
*/
|
|
54
|
+
observe(options?: {
|
|
55
|
+
grid?: boolean;
|
|
56
|
+
}): Promise<Observation>;
|
|
57
|
+
private _paintGrid;
|
|
58
|
+
private _removeGrid;
|
|
59
|
+
/** Path of the recorded webm, available after close(). */
|
|
60
|
+
videoPath(): string | undefined;
|
|
61
|
+
/** Close the context and browser; resolves the video artifact path. Idempotent. */
|
|
62
|
+
close(): Promise<string | undefined>;
|
|
63
|
+
private _withTimeout;
|
|
64
|
+
}
|