humanish 0.141.0 → 0.142.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +1 -0
- package/CHANGELOG.md +11 -0
- package/README.md +2 -0
- package/dist/analysis/admission.d.ts +8 -0
- package/dist/analysis/admission.js +15 -4
- package/dist/analysis/automatic-config.d.ts +5 -2
- package/dist/analysis/automatic-config.js +23 -8
- package/dist/analysis/automatic.js +13 -7
- package/dist/analysis/execute.d.ts +17 -6
- package/dist/analysis/execute.js +36 -18
- package/dist/analysis/gemini-wire.d.ts +8 -0
- package/dist/analysis/gemini-wire.js +165 -0
- package/dist/analysis/image-tokens.d.ts +4 -2
- package/dist/analysis/image-tokens.js +24 -3
- package/dist/analysis/job.d.ts +1 -0
- package/dist/analysis/job.js +1 -0
- package/dist/analysis/openai-wire.d.ts +5 -0
- package/dist/analysis/openai-wire.js +101 -0
- package/dist/analysis/provider.d.ts +15 -7
- package/dist/analysis/provider.js +21 -101
- package/dist/analysis/responses.d.ts +3 -0
- package/dist/analysis/responses.js +19 -0
- package/dist/analysis/service.js +11 -3
- package/dist/analysis/types.d.ts +8 -2
- package/dist/analysis/validation.d.ts +1 -0
- package/dist/analysis/validation.js +13 -12
- package/dist/cli/commands/analyze.js +9 -7
- package/dist/cli/commands/observe.js +26 -1
- package/dist/cli/commands/run-command.d.ts +1 -1
- package/dist/cli/commands/run-command.js +12 -0
- package/dist/cli/commands/study-remote.d.ts +13 -0
- package/dist/cli/commands/study-remote.js +125 -0
- package/dist/cli/commands/study-route-computer-use.js +26 -2
- package/dist/cli/commands/study-route-scripted.js +1 -0
- package/dist/cli/commands/study-route-shared-world.js +1 -0
- package/dist/cli/commands/study-route-terminal.js +1 -0
- package/dist/cli/commands/study-run.js +34 -0
- package/dist/cli/commands/watch.js +5 -0
- package/dist/cli/doctor.js +7 -2
- package/dist/cli/findings.d.ts +8 -1
- package/dist/cli/findings.js +38 -15
- package/dist/cli/io.d.ts +8 -0
- package/dist/cli/io.js +2 -1
- package/dist/keys/key-resolution.d.ts +10 -0
- package/dist/keys/key-resolution.js +33 -5
- package/dist/observer/listen.d.ts +7 -2
- package/dist/observer/listen.js +10 -4
- package/dist/observer/render.d.ts +2 -0
- package/dist/observer/render.js +1 -1
- package/dist/observer-app.html +1 -1
- package/dist/remote/launch.d.ts +53 -0
- package/dist/remote/launch.js +320 -0
- package/dist/remote/observer-proxy.d.ts +11 -0
- package/dist/remote/observer-proxy.js +66 -0
- package/dist/remote/plan.d.ts +35 -0
- package/dist/remote/plan.js +114 -0
- package/dist/remote/reattach.d.ts +17 -0
- package/dist/remote/reattach.js +81 -0
- package/dist/remote/runner.d.ts +68 -0
- package/dist/remote/runner.js +216 -0
- package/dist/routes/computer-use/e2b-desktop/start.js +1 -1
- package/dist/routes/computer-use/types.d.ts +2 -0
- package/dist/routes/scripted/types.d.ts +2 -0
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/routes/terminal/types.d.ts +2 -0
- package/dist/run/costs.d.ts +2 -1
- package/dist/run/pricing.d.ts +13 -1
- package/dist/run/pricing.js +50 -4
- package/dist/run/results.d.ts +1 -1
- package/dist/run/run-cost.d.ts +2 -1
- package/dist/run/run-cost.js +6 -1
- package/dist/run/run.d.ts +13 -0
- package/dist/run/run.js +18 -7
- package/dist/run/sandbox-creates.d.ts +10 -3
- package/dist/run/sandbox-creates.js +48 -6
- package/dist/run-study.d.ts +5 -0
- package/dist/study/doctor.js +14 -1
- package/dist/study/requirements.d.ts +1 -1
- package/dist/study/requirements.js +8 -6
- package/dist/study/route-inputs.js +4 -0
- package/dist/study/run-study-events.d.ts +1 -1
- package/dist/study/run-study-events.js +9 -2
- package/dist/substrates/e2b/base-module.d.ts +3 -0
- package/dist/substrates/e2b/base-module.js +40 -0
- package/dist/substrates/e2b/desktop-browser.d.ts +1 -1
- package/dist/substrates/e2b/desktop-browser.js +24 -6
- package/dist/substrates/e2b/sdk.d.ts +24 -0
- package/dist/substrates/e2b/sdk.js +5 -1
- package/dist/substrates/e2b/shell-only.d.ts +5 -0
- package/dist/substrates/e2b/shell-only.js +27 -0
- package/dist/substrates/modal/sandbox.js +27 -28
- package/dist/substrates/modal/sdk.d.ts +7 -0
- package/dist/tui-app.js +46 -46
- package/docs/contracts/study-analysis.md +51 -3
- package/docs/product/automatic-analysis.md +26 -2
- package/package.json +2 -2
- package/skills/humanish/SKILL.md +10 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// The one runner. `run` and `watch` register a run's flags through addRunOptions, so the two
|
|
2
|
+
// commands cannot drift apart.
|
|
3
|
+
import { Option } from "commander";
|
|
1
4
|
import { runDryRun } from "../../run/dry-run.js";
|
|
2
5
|
import { runStudyCommand } from "./study-run.js";
|
|
3
6
|
import { applyEnvFileOption, CWD_OPTION_DESCRIPTION, DOTENV_OPTION_DESCRIPTION, dotenvPathOf, envFileAliasOption, formatRunHuman, freePortOption, JSON_OPTION_DESCRIPTION, parsePositiveInteger, writeResult, } from "../io.js";
|
|
@@ -17,8 +20,17 @@ export function addRunOptions(command) {
|
|
|
17
20
|
.option("--dotenv <path>", DOTENV_OPTION_DESCRIPTION)
|
|
18
21
|
.addOption(envFileAliasOption())
|
|
19
22
|
.option("--run-id <id>", "Explicit run id for deterministic fixture tests; refused when that run already exists.")
|
|
23
|
+
.option("--remote", "Run the study's own process in a sandbox on its participants' provider (E2B or Modal), so it keeps going when this machine sleeps. This machine follows it, serves its live Observer and copies the run back. Computer-use studies with hosted desktops and an app-url subject.")
|
|
24
|
+
.addOption(ownerKeyOption())
|
|
20
25
|
.option("--json", JSON_OPTION_DESCRIPTION);
|
|
21
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Hidden: set by the process that launches a run on a remote machine, so that machine's run
|
|
29
|
+
* directory and the launcher's own name the same sandboxes.
|
|
30
|
+
*/
|
|
31
|
+
function ownerKeyOption() {
|
|
32
|
+
return new Option("--owner-key <key>").hideHelp();
|
|
33
|
+
}
|
|
22
34
|
/** The flags that only a study file can use, as typed on the command line, when they are set. */
|
|
23
35
|
export function studyOnlyFlags(options) {
|
|
24
36
|
return [
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { type RemoteOutcome } from "../../remote/launch.js";
|
|
3
|
+
import type { StudyConfig } from "../../study/types.js";
|
|
4
|
+
import { type CliIo, type StudyCommandOptions } from "../io.js";
|
|
5
|
+
export declare function runRemoteStudy(args: {
|
|
6
|
+
command: Command;
|
|
7
|
+
io: CliIo;
|
|
8
|
+
options: StudyCommandOptions;
|
|
9
|
+
config: StudyConfig;
|
|
10
|
+
studyPath: string;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
/** The command's result and exit code for a remote run that ended, stopped or detached. */
|
|
13
|
+
export declare function reportRemoteOutcome(command: Command, io: CliIo, cwd: string, outcome: RemoteOutcome): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// `--remote` on run and watch: plan here, refuse what cannot run remotely, then launch and follow
|
|
2
|
+
// the run in a runner sandbox (src/remote/launch.ts). The result and exit code are the run's own.
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { launchRemoteRun } from "../../remote/launch.js";
|
|
5
|
+
import { planRemoteRun } from "../../remote/plan.js";
|
|
6
|
+
import { planStudy } from "../../study/plan.js";
|
|
7
|
+
import { concurrentSandboxes, sandboxCeiling } from "../../substrates/e2b/lifetime.js";
|
|
8
|
+
import { MODAL_MAX_SANDBOX_MS } from "../../substrates/modal/sdk.js";
|
|
9
|
+
import { formatRunHuman, parseObserverPort, wantsJson, writeResult, } from "../io.js";
|
|
10
|
+
export async function runRemoteStudy(args) {
|
|
11
|
+
const { io, options, config } = args;
|
|
12
|
+
const cwd = path.resolve(options.cwd);
|
|
13
|
+
const refuse = (code, message, runId) => {
|
|
14
|
+
const result = {
|
|
15
|
+
schema: "humanish.run-result.v1",
|
|
16
|
+
ok: false,
|
|
17
|
+
cwd,
|
|
18
|
+
...(runId === undefined ? {} : { runId }),
|
|
19
|
+
warnings: [],
|
|
20
|
+
error: { code, message },
|
|
21
|
+
};
|
|
22
|
+
writeResult(args.command, io, result, formatRunHuman);
|
|
23
|
+
io.setExitCode(2);
|
|
24
|
+
};
|
|
25
|
+
if (options.dryRun === true) {
|
|
26
|
+
refuse("HUMANISH_REMOTE_REFUSED", "--remote runs live studies; a dry run needs no runner.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const local = localOnlyFlags(options);
|
|
30
|
+
if (local.length > 0) {
|
|
31
|
+
refuse("HUMANISH_REMOTE_REFUSED", `${local.join(", ")} ${local.length === 1 ? "does" : "do"} not run remotely yet; run without --remote to use ${local.length === 1 ? "it" : "them"}.`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const planned = planStudy(config, { cwd, dryRun: false });
|
|
35
|
+
if (!planned.ok) {
|
|
36
|
+
refuse("HUMANISH_REMOTE_REFUSED", planned.refusal.message);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const plan = planned.planned.plan;
|
|
40
|
+
const e2bCeiling = sandboxCeiling(process.env);
|
|
41
|
+
const provider = plan.route === "computer-use" && plan.runner.desktop === "e2b-desktop"
|
|
42
|
+
? plan.runner.provider
|
|
43
|
+
: "e2b";
|
|
44
|
+
const ceilingMs = provider === "modal" ? MODAL_MAX_SANDBOX_MS : e2bCeiling.ok ? e2bCeiling.ms : 0;
|
|
45
|
+
const sandboxes = concurrentSandboxes(process.env);
|
|
46
|
+
const remote = planRemoteRun({
|
|
47
|
+
config,
|
|
48
|
+
plan,
|
|
49
|
+
cwd,
|
|
50
|
+
studyPath: args.studyPath,
|
|
51
|
+
ceilingMs,
|
|
52
|
+
concurrentSandboxes: sandboxes.ok ? sandboxes.count : 1,
|
|
53
|
+
});
|
|
54
|
+
if (!remote.ok) {
|
|
55
|
+
refuse("HUMANISH_REMOTE_REFUSED", `This study cannot run remotely: ${remote.reason}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const port = parseObserverPort(options.port ?? "0");
|
|
59
|
+
if (port === null) {
|
|
60
|
+
refuse("HUMANISH_REMOTE_REFUSED", "--port must be an integer between 0 and 65535.");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let outcome;
|
|
64
|
+
try {
|
|
65
|
+
outcome = await launchRemoteRun({
|
|
66
|
+
cwd,
|
|
67
|
+
study: path.relative(cwd, path.resolve(cwd, args.studyPath)).split(path.sep).join("/"),
|
|
68
|
+
remote: remote.plan,
|
|
69
|
+
env: process.env,
|
|
70
|
+
port,
|
|
71
|
+
say: (line) => io.writeErr(`${line}\n`),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
refuse("HUMANISH_REMOTE_FAILED", error instanceof Error ? error.message : String(error));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
reportRemoteOutcome(args.command, io, cwd, outcome);
|
|
79
|
+
}
|
|
80
|
+
/** The run options a remote run does not carry to the runner yet. */
|
|
81
|
+
function localOnlyFlags(options) {
|
|
82
|
+
return [
|
|
83
|
+
options.scorer === undefined ? [] : ["--scorer"],
|
|
84
|
+
options.count === undefined ? [] : ["--count"],
|
|
85
|
+
options.rerunFailedFrom === undefined ? [] : ["--rerun-failed-from"],
|
|
86
|
+
options.participants === undefined ? [] : ["--participants"],
|
|
87
|
+
options.expose === true || options.tunnel !== undefined || options.publicUrl !== undefined
|
|
88
|
+
? ["--expose"]
|
|
89
|
+
: [],
|
|
90
|
+
].flat();
|
|
91
|
+
}
|
|
92
|
+
/** The command's result and exit code for a remote run that ended, stopped or detached. */
|
|
93
|
+
export function reportRemoteOutcome(command, io, cwd, outcome) {
|
|
94
|
+
if (outcome.detached === true) {
|
|
95
|
+
io.writeErr(`The terminal closed, so run ${outcome.runId} goes on in its runner without this machine. Watch it again with humanish observe --run ${outcome.runId}, or stop it and its sandboxes with humanish reclaim --run ${outcome.runId}\n`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (outcome.result === undefined || typeof outcome.result !== "object") {
|
|
99
|
+
const failed = {
|
|
100
|
+
schema: "humanish.run-result.v1",
|
|
101
|
+
ok: false,
|
|
102
|
+
cwd,
|
|
103
|
+
runId: outcome.runId,
|
|
104
|
+
warnings: [],
|
|
105
|
+
error: {
|
|
106
|
+
code: "HUMANISH_REMOTE_FAILED",
|
|
107
|
+
message: `Run ${outcome.runId} ${outcome.error ?? "ended without a result"}.`,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
writeResult(command, io, failed, formatRunHuman);
|
|
111
|
+
io.setExitCode(outcome.exitCode === 0 ? 2 : outcome.exitCode);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const result = { ...outcome.result, cwd };
|
|
115
|
+
if (outcome.error !== undefined)
|
|
116
|
+
result.warnings = [...(result.warnings ?? []), outcome.error];
|
|
117
|
+
if (wantsJson(command))
|
|
118
|
+
io.writeOut(`${JSON.stringify(result, null, 2)}\n`);
|
|
119
|
+
else {
|
|
120
|
+
if (outcome.error !== undefined)
|
|
121
|
+
io.writeErr(`warning: ${outcome.error}\n`);
|
|
122
|
+
io.writeErr(`Run ${outcome.runId} ${result.ok && outcome.exitCode === 0 ? "passed" : "did not pass"}; its evidence (${outcome.files ?? 0} files) is in .humanish/runs/${outcome.runId}. Next: humanish review --run ${outcome.runId}\n`);
|
|
123
|
+
}
|
|
124
|
+
io.setExitCode(outcome.exitCode);
|
|
125
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rename, writeFile } from "node:fs/promises";
|
|
1
2
|
import { automaticAnalysisSucceeded } from "../../analysis/automatic-completion.js";
|
|
2
3
|
import { Command } from "commander";
|
|
3
4
|
import {} from "../../run-study.js";
|
|
@@ -90,7 +91,16 @@ function resolveCuaSettings(args) {
|
|
|
90
91
|
refuseCua(args, dryRun, "HUMANISH_WATCH_OPTION_CONFLICT", "--port must be an integer between 0 and 65535.");
|
|
91
92
|
return undefined;
|
|
92
93
|
}
|
|
93
|
-
return {
|
|
94
|
+
return {
|
|
95
|
+
wantsMachine,
|
|
96
|
+
shouldOpen,
|
|
97
|
+
participantIds,
|
|
98
|
+
count,
|
|
99
|
+
dryRun,
|
|
100
|
+
port,
|
|
101
|
+
wantsFollow,
|
|
102
|
+
allInterfaces: args.options.allInterfaces === true,
|
|
103
|
+
};
|
|
94
104
|
}
|
|
95
105
|
/** Validates exposure and plans the finished-run Observer, or returns undefined after refusing. */
|
|
96
106
|
function prepareCuaWatch(args, settings) {
|
|
@@ -147,6 +157,7 @@ function cuaRunOptions(args, settings, prepared, live) {
|
|
|
147
157
|
}
|
|
148
158
|
: {}),
|
|
149
159
|
...(args.options.runId === undefined ? {} : { runId: args.options.runId }),
|
|
160
|
+
...(args.options.ownerKey === undefined ? {} : { ownerKey: args.options.ownerKey }),
|
|
150
161
|
...(args.options.rerunFailedFrom === undefined
|
|
151
162
|
? {}
|
|
152
163
|
: {
|
|
@@ -191,6 +202,7 @@ async function attachLiveObserver(io, settings, exposure, live, observer) {
|
|
|
191
202
|
open: settings.shouldOpen && !exposure.exposed,
|
|
192
203
|
port: settings.port,
|
|
193
204
|
exposed: exposure.exposed,
|
|
205
|
+
allInterfaces: settings.allInterfaces,
|
|
194
206
|
});
|
|
195
207
|
// A signal that stops the run closes the server and any tunnel before the process exits.
|
|
196
208
|
live.releaseShutdown ??= onRunShutdown(() => closeLiveAttachment(io, live));
|
|
@@ -237,7 +249,10 @@ async function reportCuaRun(args, prepared, result, live) {
|
|
|
237
249
|
};
|
|
238
250
|
}
|
|
239
251
|
await writeRunResult(args.command, args.io, args.options.cwd, output, (value) => formatCuaStudyHuman(value, args.config.subject));
|
|
240
|
-
|
|
252
|
+
const exitCode = result.ok && automaticAnalysisSucceeded(result) ? 0 : 2;
|
|
253
|
+
args.io.setExitCode(exitCode);
|
|
254
|
+
if (args.options.resultFile !== undefined)
|
|
255
|
+
await writeResultFile(args.options.resultFile, { exitCode, result: output });
|
|
241
256
|
if (server && (result.observer?.ok || attachedObserver)) {
|
|
242
257
|
const followResult = output.observer?.ok
|
|
243
258
|
? output.observer
|
|
@@ -261,3 +276,12 @@ async function reportCuaRun(args, prepared, result, live) {
|
|
|
261
276
|
});
|
|
262
277
|
}
|
|
263
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Write `value` to `file` as JSON in one rename, so a reader polling for it never sees it half
|
|
281
|
+
* written. A remote run's launcher reads it while this process keeps serving the Observer.
|
|
282
|
+
*/
|
|
283
|
+
async function writeResultFile(file, value) {
|
|
284
|
+
const partial = `${file}.partial`;
|
|
285
|
+
await writeFile(partial, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
286
|
+
await rename(partial, file);
|
|
287
|
+
}
|
|
@@ -28,6 +28,7 @@ export function scriptedRouteRun(args) {
|
|
|
28
28
|
open: observerOpen(args.mode, finishedPlan, shouldOpen),
|
|
29
29
|
...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
|
|
30
30
|
...(args.options.runId === undefined ? {} : { runId: args.options.runId }),
|
|
31
|
+
...(args.options.ownerKey === undefined ? {} : { ownerKey: args.options.ownerKey }),
|
|
31
32
|
},
|
|
32
33
|
present: async (outcome) => {
|
|
33
34
|
if (outcome.route !== "scripted") {
|
|
@@ -88,6 +88,7 @@ export function sharedWorldRouteRun(args) {
|
|
|
88
88
|
}
|
|
89
89
|
: {}),
|
|
90
90
|
...(args.options.runId === undefined ? {} : { runId: args.options.runId }),
|
|
91
|
+
...(args.options.ownerKey === undefined ? {} : { ownerKey: args.options.ownerKey }),
|
|
91
92
|
},
|
|
92
93
|
onRunError: async (error) => {
|
|
93
94
|
await server?.close().catch((cleanupError) => {
|
|
@@ -28,6 +28,7 @@ export function terminalRouteRun(args) {
|
|
|
28
28
|
open: observerOpen(args.mode, finishedPlan, shouldOpen),
|
|
29
29
|
...(args.options.dryRun === undefined ? {} : { dryRun: args.options.dryRun }),
|
|
30
30
|
...(args.options.runId === undefined ? {} : { runId: args.options.runId }),
|
|
31
|
+
...(args.options.ownerKey === undefined ? {} : { ownerKey: args.options.ownerKey }),
|
|
31
32
|
},
|
|
32
33
|
present: async (outcome) => {
|
|
33
34
|
if (outcome.route !== "terminal") {
|
|
@@ -15,11 +15,25 @@ import { sharedWorldRouteRun } from "./study-route-shared-world.js";
|
|
|
15
15
|
import { previewRouteRun } from "./study-route-preview.js";
|
|
16
16
|
import { terminalRouteRun } from "./study-route-terminal.js";
|
|
17
17
|
import { maybeLoadAdapterScorer } from "./study-scorer.js";
|
|
18
|
+
import { runRemoteStudy } from "./study-remote.js";
|
|
18
19
|
import { discoverCliKeys, formatRunHuman, noteRunFacts, writeResult, } from "../io.js";
|
|
19
20
|
import { watchExposeRequested } from "../observer-follow.js";
|
|
20
21
|
import { beginRunSignalPhase } from "./run-signals.js";
|
|
21
22
|
import { WATCH_SAFE_NOT_APPLICABLE_MESSAGE } from "../../observer/exposure.js";
|
|
22
23
|
export async function runStudyCommand(args) {
|
|
24
|
+
const ownerKeyRefusal = ownerKeyConflict(args.options);
|
|
25
|
+
if (ownerKeyRefusal !== undefined) {
|
|
26
|
+
const result = {
|
|
27
|
+
schema: "humanish.run-result.v1",
|
|
28
|
+
ok: false,
|
|
29
|
+
cwd: resolve(args.options.cwd),
|
|
30
|
+
warnings: [],
|
|
31
|
+
error: { code: "HUMANISH_RUN_OPTION_CONFLICT", message: ownerKeyRefusal },
|
|
32
|
+
};
|
|
33
|
+
writeResult(args.command, args.io, result, formatRunHuman);
|
|
34
|
+
args.io.setExitCode(2);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
23
37
|
const resolved = await resolveStudyManifest(args.options.cwd, args.study);
|
|
24
38
|
if (!resolved.ok) {
|
|
25
39
|
writeResult(args.command, args.io, resolved, formatStudyResolveFailureHuman);
|
|
@@ -98,6 +112,16 @@ export async function runStudyCommand(args) {
|
|
|
98
112
|
...(announced === undefined ? {} : { announced }),
|
|
99
113
|
});
|
|
100
114
|
}
|
|
115
|
+
if (args.options.remote === true) {
|
|
116
|
+
await runRemoteStudy({
|
|
117
|
+
command: args.command,
|
|
118
|
+
io: args.io,
|
|
119
|
+
options: args.options,
|
|
120
|
+
config,
|
|
121
|
+
studyPath: resolved.path,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
101
125
|
// The route's CLI setup refuses bad options, runStudyWith's plan refuses bad labs, and the route's local
|
|
102
126
|
// checks (keys, runtime auth, subject env, the local agent, caps) refuse this machine, all before
|
|
103
127
|
// the scorer loads, so none imports the scorer's host code. The scripted route's checks and the
|
|
@@ -191,3 +215,13 @@ function formatStudyResolveFailureHuman(result) {
|
|
|
191
215
|
const warnings = result.warnings.map((warning) => `warning: ${warning}\n`).join("");
|
|
192
216
|
return { ...(warnings ? { stdout: warnings } : {}), error: result.error };
|
|
193
217
|
}
|
|
218
|
+
/** Why `--owner-key` cannot be used as given, or undefined when it can. */
|
|
219
|
+
function ownerKeyConflict(options) {
|
|
220
|
+
if (options.ownerKey === undefined)
|
|
221
|
+
return undefined;
|
|
222
|
+
if (options.runId === undefined)
|
|
223
|
+
return "--owner-key needs --run-id.";
|
|
224
|
+
if (!/^[0-9a-f]{16}$/.test(options.ownerKey))
|
|
225
|
+
return "--owner-key is 16 lowercase hexadecimal characters.";
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
@@ -28,6 +28,11 @@ export function registerWatchCommand(parent, io) {
|
|
|
28
28
|
// Hidden: watch rejects --safe with a pointer to the library filter and to edge auth, which
|
|
29
29
|
// a bare "unknown option" would lose.
|
|
30
30
|
.addOption(new Option("--safe").hideHelp())
|
|
31
|
+
// Hidden: a remote run's launcher starts its runner's watch with these. The runner's Observer
|
|
32
|
+
// is reached only through the provider's authenticated proxy, and the launcher reads the
|
|
33
|
+
// result from the file while the Observer keeps serving.
|
|
34
|
+
.addOption(new Option("--all-interfaces").hideHelp())
|
|
35
|
+
.addOption(new Option("--result-file <path>").hideHelp())
|
|
31
36
|
.addHelpText("after", [
|
|
32
37
|
"",
|
|
33
38
|
"Without a study or --run, watch starts a synthetic run of 4 participants; --count changes it.",
|
package/dist/cli/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile, readdir } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { DOCTOR_KEYS, probeKeySources, } from "../keys/key-resolution.js";
|
|
5
|
+
import { DOCTOR_KEYS, analysisKeyNames, probeKeySources, } from "../keys/key-resolution.js";
|
|
6
6
|
import { desktopPeerAdvice, humanishInstall, } from "../substrates/e2b/peer-install.js";
|
|
7
7
|
import { concurrentSandboxes, describeConcurrentSandboxes, describeSandboxCeiling, sandboxCeiling, } from "../substrates/e2b/lifetime.js";
|
|
8
8
|
import { nodeSupportsTui, terminalSurfaceMessage, TUI_BUNDLE_URL } from "../tui/contract.js";
|
|
@@ -304,7 +304,8 @@ function versionCheck(env) {
|
|
|
304
304
|
},
|
|
305
305
|
];
|
|
306
306
|
}
|
|
307
|
-
/** Probes the live-run keys, plus the receiving-email key a real-comms study names
|
|
307
|
+
/** Probes the live-run keys, plus the receiving-email key a real-comms study names and the Gemini
|
|
308
|
+
* key a study analyzed on Gemini reads. */
|
|
308
309
|
async function probeDoctorKeys(cwd, env, study, keyDeps, dotenv) {
|
|
309
310
|
const keyNames = new Set(DOCTOR_KEYS);
|
|
310
311
|
let receivingKey = null;
|
|
@@ -315,6 +316,10 @@ async function probeDoctorKeys(cwd, env, study, keyDeps, dotenv) {
|
|
|
315
316
|
if (resolved.ok && resolved.config.execution?.target === "modal-terminal")
|
|
316
317
|
for (const name of ["MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET"])
|
|
317
318
|
keyNames.add(name);
|
|
319
|
+
// A study analyzed on Gemini reads the Gemini key, which no other study needs.
|
|
320
|
+
const analysis = resolved.ok ? resolved.config.review?.analysis : undefined;
|
|
321
|
+
if (typeof analysis === "object" && analysis !== null && analysis.provider === "gemini")
|
|
322
|
+
keyNames.add(analysisKeyNames("gemini")[0]);
|
|
318
323
|
if (resolved.ok && resolved.config.comms?.email?.kind === "real") {
|
|
319
324
|
const { receivingRequiredKey } = await import("../comms/setup.js");
|
|
320
325
|
receivingKey = await receivingRequiredKey(cwd, resolved.config.comms.email.connection);
|
package/dist/cli/findings.d.ts
CHANGED
|
@@ -117,6 +117,8 @@ interface FindingsSource {
|
|
|
117
117
|
liveStudy?: LiveStudy;
|
|
118
118
|
/** The run's study, from its bundle. */
|
|
119
119
|
study?: Pick<RunStudyProvenance, "id" | "path">;
|
|
120
|
+
/** The analyst the run's study gets, read only for an analysis skipped for a missing key. */
|
|
121
|
+
analyst?: Analyst;
|
|
120
122
|
/**
|
|
121
123
|
* Whether Codex is signed in with a ChatGPT account on this machine, read only for an analysis
|
|
122
124
|
* skipped for a missing OpenAI key, whose next step it picks.
|
|
@@ -140,7 +142,7 @@ interface FindingsSource {
|
|
|
140
142
|
/** status.json's last heartbeat. */
|
|
141
143
|
updatedAt: string;
|
|
142
144
|
/** The analyst the run's study gets, read only for an analysis whose process stopped. */
|
|
143
|
-
|
|
145
|
+
analyst?: Analyst;
|
|
144
146
|
};
|
|
145
147
|
/** When the view was built, for how long an analysis has run. */
|
|
146
148
|
nowMs?: number;
|
|
@@ -156,6 +158,11 @@ interface LiveStudy {
|
|
|
156
158
|
}
|
|
157
159
|
/** ` --cwd <dir>`, or "" when `cwd` is the current directory. */
|
|
158
160
|
export declare function cwdFlag(cwd: string): string;
|
|
161
|
+
/** An analysis provider and model, as a study or an analysis record names them. */
|
|
162
|
+
interface Analyst {
|
|
163
|
+
provider?: string | undefined;
|
|
164
|
+
model?: string;
|
|
165
|
+
}
|
|
159
166
|
/** The findings view of a run's selected analysis, or why it has none. */
|
|
160
167
|
export declare function analysisFindings(source: FindingsSource): AnalysisFindings;
|
|
161
168
|
/**
|
package/dist/cli/findings.js
CHANGED
|
@@ -22,6 +22,7 @@ import { plural } from "../run/text.js";
|
|
|
22
22
|
import { shellQuote } from "../substrates/shell.js";
|
|
23
23
|
import { analysisOutcomeText } from "./io.js";
|
|
24
24
|
import { cli } from "./invocation.js";
|
|
25
|
+
import { analysisKeyNames } from "../keys/key-resolution.js";
|
|
25
26
|
export const ANALYSIS_FINDINGS_SCHEMA = "humanish.analysis-findings.v1";
|
|
26
27
|
/** ` --cwd <dir>`, or "" when `cwd` is the current directory. */
|
|
27
28
|
export function cwdFlag(cwd) {
|
|
@@ -29,11 +30,20 @@ export function cwdFlag(cwd) {
|
|
|
29
30
|
return "";
|
|
30
31
|
return ` --cwd ${/^[\w@%+=:,./-]+$/.test(cwd) ? cwd : shellQuote(cwd)}`;
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
/** The skip reasons for an analysis whose API key was missing, one per API analyst. */
|
|
34
|
+
const KEY_MISSING_REASONS = new Set([
|
|
35
|
+
"AUTOMATIC_ANALYSIS_KEY_MISSING",
|
|
36
|
+
"AUTOMATIC_ANALYSIS_GEMINI_KEY_MISSING",
|
|
37
|
+
]);
|
|
38
|
+
/** The `analyze` command that runs `analyst` on the run; undefined is the OpenAI default. */
|
|
39
|
+
function analyzeCommand(source, analyst) {
|
|
33
40
|
const run = cli(`analyze --run ${source.runId}${source.cwdFlag}`);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
if (analyst?.provider === "codex")
|
|
42
|
+
return `${run} --provider codex`;
|
|
43
|
+
const gemini = analyst?.provider === "gemini"
|
|
44
|
+
? ` --provider gemini${analyst.model === undefined ? "" : ` --model ${analyst.model}`}`
|
|
45
|
+
: "";
|
|
46
|
+
return `${run}${gemini} --max-cost ${DEFAULT_ANALYSIS_MAX_COST_USD}`;
|
|
37
47
|
}
|
|
38
48
|
/** The cited items the analysis evidence resolves, each once, in citation order. */
|
|
39
49
|
function citedEvidence(ids, analysis, runRoot) {
|
|
@@ -157,16 +167,22 @@ function dryRunFindings(source) {
|
|
|
157
167
|
return withoutFindings(source, "dry_run", null, `${opening} Findings come from live runs: ${howLive}`, cli(`run ${live.run}${source.cwdFlag}`));
|
|
158
168
|
}
|
|
159
169
|
/**
|
|
160
|
-
* An analysis skipped because
|
|
170
|
+
* An analysis skipped because its API key is not set. For OpenAI with Codex signed in, the next step is the
|
|
161
171
|
* Codex account analyst, named as an opt-in: the study chose the OpenAI analyst, and switching
|
|
162
172
|
* the account that pays for it is the operator's choice.
|
|
163
173
|
*/
|
|
164
174
|
function missingKeyFindings(source, reason) {
|
|
175
|
+
// The skip reason records which analyst's key was missing; the study file only adds its model.
|
|
176
|
+
if (reason === "AUTOMATIC_ANALYSIS_GEMINI_KEY_MISSING")
|
|
177
|
+
return withoutFindings(source, "skipped", reason, `The automatic analysis was skipped because ${analysisKeyNames("gemini").join(" and ")} are not set. Store the key with ${cli("keys set gemini")}, then run the next command.`, analyzeCommand(source, {
|
|
178
|
+
provider: "gemini",
|
|
179
|
+
...(source.analyst?.provider === "gemini" ? { model: source.analyst.model } : {}),
|
|
180
|
+
}));
|
|
165
181
|
const opening = "The automatic analysis was skipped because OPENAI_API_KEY is not set.";
|
|
166
182
|
if (source.codexSignedIn !== true)
|
|
167
183
|
return withoutFindings(source, "skipped", reason, `${opening} Set it with ${cli("keys set openai")}, then run the next command.`, analyzeCommand(source, undefined));
|
|
168
184
|
const file = source.study?.path === undefined ? "the study file" : source.study.path;
|
|
169
|
-
return withoutFindings(source, "skipped", reason, `${opening} Codex is signed in on this machine, so its ChatGPT account can analyze the run instead, sending the evidence to OpenAI under that account. The next command does it once; review.analysis.provider: codex in ${file} makes it this study's analyst.`, analyzeCommand(source, "codex"));
|
|
185
|
+
return withoutFindings(source, "skipped", reason, `${opening} Codex is signed in on this machine, so its ChatGPT account can analyze the run instead, sending the evidence to OpenAI under that account. The next command does it once; review.analysis.provider: codex in ${file} makes it this study's analyst.`, analyzeCommand(source, { provider: "codex" }));
|
|
170
186
|
}
|
|
171
187
|
/**
|
|
172
188
|
* The view of a run whose findings are not written yet: participants still running, an analysis
|
|
@@ -194,7 +210,7 @@ function pendingFindings(source, job) {
|
|
|
194
210
|
if (!stopped)
|
|
195
211
|
return undefined;
|
|
196
212
|
const at = job?.updatedAt ?? (progress?.stage === "analysis" ? progress.updatedAt : undefined);
|
|
197
|
-
return withoutFindings(source, "failed", "AUTOMATIC_ANALYSIS_STOPPED", `The automatic analysis stopped before it wrote findings: the process running it stopped updating its record${at === undefined ? "" : ` at ${at}`}, most likely because it ended. Analyze the run again with the next command.`, analyzeCommand(source, progress?.stage === "analysis" ? progress.
|
|
213
|
+
return withoutFindings(source, "failed", "AUTOMATIC_ANALYSIS_STOPPED", `The automatic analysis stopped before it wrote findings: the process running it stopped updating its record${at === undefined ? "" : ` at ${at}`}, most likely because it ended. Analyze the run again with the next command.`, analyzeCommand(source, progress?.stage === "analysis" ? progress.analyst : undefined));
|
|
198
214
|
}
|
|
199
215
|
/**
|
|
200
216
|
* A run that ended, by a signal, a process that left without publishing, or a heartbeat that
|
|
@@ -217,7 +233,9 @@ async function studyAnalyst(cwd, study) {
|
|
|
217
233
|
if (!resolved?.ok || resolved.config.id !== study.id)
|
|
218
234
|
return undefined;
|
|
219
235
|
const analysis = resolveAutomaticAnalysis(resolved.config.review?.analysis, resolved.config.actor);
|
|
220
|
-
return analysis.ok
|
|
236
|
+
return analysis.ok && analysis.config !== undefined
|
|
237
|
+
? { provider: analysis.config.provider, model: analysis.config.model }
|
|
238
|
+
: undefined;
|
|
221
239
|
}
|
|
222
240
|
/** The findings view of a run's selected analysis, or why it has none. */
|
|
223
241
|
export function analysisFindings(source) {
|
|
@@ -236,7 +254,9 @@ export function analysisFindings(source) {
|
|
|
236
254
|
message: stale
|
|
237
255
|
? `The run's evidence changed after analysis ${analysis.id}, so its findings may not match the run.`
|
|
238
256
|
: `Analysis ${analysis.id} is ${analysis.status}, with ${plural(result.findings.length, "finding")}.`,
|
|
239
|
-
next: stale
|
|
257
|
+
next: stale
|
|
258
|
+
? analyzeCommand(source, { provider: analysis.provider, model: analysis.config.model })
|
|
259
|
+
: null,
|
|
240
260
|
analysisId: analysis.id,
|
|
241
261
|
status: analysis.status,
|
|
242
262
|
provider: analysis.provider,
|
|
@@ -259,16 +279,16 @@ export function analysisFindings(source) {
|
|
|
259
279
|
if (pending !== undefined)
|
|
260
280
|
return pending;
|
|
261
281
|
if (analysis && analysis.result === null)
|
|
262
|
-
return withoutFindings(source, "failed", analysis.error, `Analysis attempt ${analysis.id} ${analysis.status === "cancelled" ? "was cancelled" : "failed"}${analysis.error ? ` (${analysis.error})` : ""} and left no findings.`, analyzeCommand(source, analysis.provider));
|
|
282
|
+
return withoutFindings(source, "failed", analysis.error, `Analysis attempt ${analysis.id} ${analysis.status === "cancelled" ? "was cancelled" : "failed"}${analysis.error ? ` (${analysis.error})` : ""} and left no findings.`, analyzeCommand(source, { provider: analysis.provider, model: analysis.config.model }));
|
|
263
283
|
if (job?.state === "failed" || job?.state === "cancelled") {
|
|
264
284
|
const outcome = analysisOutcomeText(job);
|
|
265
|
-
return withoutFindings(source, "failed", job.reason, `The automatic analysis ${outcome.startsWith("failed") ? "" : "was "}${outcome}.`, analyzeCommand(source, job.reason === "AUTOMATIC_ANALYSIS_CODEX_UNAVAILABLE" ? "codex" : undefined));
|
|
285
|
+
return withoutFindings(source, "failed", job.reason, `The automatic analysis ${outcome.startsWith("failed") ? "" : "was "}${outcome}.`, analyzeCommand(source, job.reason === "AUTOMATIC_ANALYSIS_CODEX_UNAVAILABLE" ? { provider: "codex" } : undefined));
|
|
266
286
|
}
|
|
267
287
|
if (job?.state === "skipped" && job.admission) {
|
|
268
288
|
const refusal = costRefusal(job.admission, `--run ${source.runId}${source.cwdFlag}`, cli);
|
|
269
289
|
return withoutFindings(source, "skipped", job.reason, `The automatic analysis was refused before it started. ${refusal.text}`, refusal.command);
|
|
270
290
|
}
|
|
271
|
-
if (job?.state === "skipped" && job.reason
|
|
291
|
+
if (job?.state === "skipped" && job.reason !== null && KEY_MISSING_REASONS.has(job.reason))
|
|
272
292
|
return missingKeyFindings(source, job.reason);
|
|
273
293
|
if (job?.state === "skipped") {
|
|
274
294
|
const refused = job.reason === "AUTOMATIC_ANALYSIS_ADMISSION_REFUSED";
|
|
@@ -339,20 +359,22 @@ export async function readRunAnalysis(cwd, run, analysisId, deps = {}) {
|
|
|
339
359
|
const record = isRunStatusRecord(status) && status.runId === runIdOf(prepared) ? status : null;
|
|
340
360
|
const phase = record === null ? undefined : runAnalysisPhase(record, nowMs);
|
|
341
361
|
const stopped = phase?.state === "stopped" || loaded.automatic?.reason === "AUTOMATIC_ANALYSIS_STOPPED";
|
|
342
|
-
const
|
|
362
|
+
const missingKeyReason = loaded.automatic?.reason ?? null;
|
|
363
|
+
const keyMissing = missingKeyReason !== null && KEY_MISSING_REASONS.has(missingKeyReason);
|
|
364
|
+
const analyst = stopped || keyMissing ? await studyAnalyst(physical, study) : undefined;
|
|
343
365
|
const progress = phase !== undefined && record !== null
|
|
344
366
|
? {
|
|
345
367
|
stage: "analysis",
|
|
346
368
|
phase,
|
|
347
369
|
updatedAt: record.updatedAt,
|
|
348
|
-
...(
|
|
370
|
+
...(analyst === undefined ? {} : { analyst }),
|
|
349
371
|
}
|
|
350
372
|
: bundle?.bundle.outcome === undefined &&
|
|
351
373
|
record !== null &&
|
|
352
374
|
classifyRunStatus(record, nowMs) === "running"
|
|
353
375
|
? { stage: "participants" }
|
|
354
376
|
: cutOff(bundle?.bundle, record, nowMs);
|
|
355
|
-
const codexSignedIn =
|
|
377
|
+
const codexSignedIn = missingKeyReason === "AUTOMATIC_ANALYSIS_KEY_MISSING"
|
|
356
378
|
? await (deps.codexSignedIn ?? codexSignedInHere)().catch(() => false)
|
|
357
379
|
: undefined;
|
|
358
380
|
return {
|
|
@@ -367,6 +389,7 @@ export async function readRunAnalysis(cwd, run, analysisId, deps = {}) {
|
|
|
367
389
|
? {}
|
|
368
390
|
: { study: { id: study.id, ...(study.path === undefined ? {} : { path: study.path }) } }),
|
|
369
391
|
...(codexSignedIn === undefined ? {} : { codexSignedIn }),
|
|
392
|
+
...(keyMissing && analyst !== undefined ? { analyst } : {}),
|
|
370
393
|
...(progress === undefined ? {} : { progress }),
|
|
371
394
|
nowMs,
|
|
372
395
|
};
|
package/dist/cli/io.d.ts
CHANGED
|
@@ -45,6 +45,14 @@ export interface StudyCommandOptions {
|
|
|
45
45
|
port?: string | undefined;
|
|
46
46
|
rerunFailedFrom?: string | undefined;
|
|
47
47
|
runId?: string | undefined;
|
|
48
|
+
/** Run the study in a runner sandbox on its participants' provider. */
|
|
49
|
+
remote?: boolean | undefined;
|
|
50
|
+
/** Hidden: the run key the run's sandboxes carry, chosen by a remote run's launcher. */
|
|
51
|
+
ownerKey?: string | undefined;
|
|
52
|
+
/** Hidden, watch only: bind the served Observer on every interface (a remote run's runner). */
|
|
53
|
+
allInterfaces?: boolean | undefined;
|
|
54
|
+
/** Hidden, watch only: also write the run's result and exit code to this file as JSON. */
|
|
55
|
+
resultFile?: string | undefined;
|
|
48
56
|
/** watch only: serve and follow the Observer even without an interactive terminal. */
|
|
49
57
|
serve?: boolean | undefined;
|
|
50
58
|
/** Repo-relative path to an adopter scorer module; overrides review.scorer.ref when set. */
|
package/dist/cli/io.js
CHANGED
|
@@ -2,7 +2,7 @@ import { automaticAnalysisSucceeded, } from "../analysis/automatic-completion.js
|
|
|
2
2
|
import { costRefusal, refusedCost } from "../analysis/admission.js";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import { loadEnvFile, recordDotenvNames } from "../keys/env-file.js";
|
|
5
|
-
import { discoverProviderKeys } from "../keys/key-resolution.js";
|
|
5
|
+
import { analysisKeyNames, discoverProviderKeys } from "../keys/key-resolution.js";
|
|
6
6
|
import { deriveRunFacts } from "./telemetry.js";
|
|
7
7
|
import { warnAndQueue, withQueuedWarnings } from "./deprecations.js";
|
|
8
8
|
import { forTerminal } from "../routes/terminal/encoding.js";
|
|
@@ -297,6 +297,7 @@ export function wantsJson(command) {
|
|
|
297
297
|
const ANALYSIS_REASON_TEXT = {
|
|
298
298
|
AUTOMATIC_ANALYSIS_DRY_RUN: "skipped for dry runs",
|
|
299
299
|
AUTOMATIC_ANALYSIS_KEY_MISSING: "skipped because OPENAI_API_KEY is not set",
|
|
300
|
+
AUTOMATIC_ANALYSIS_GEMINI_KEY_MISSING: `skipped because ${analysisKeyNames("gemini").join(" and ")} are not set`,
|
|
300
301
|
AUTOMATIC_ANALYSIS_NO_PARTICIPANT_EVIDENCE: "skipped because no participant left evidence",
|
|
301
302
|
AUTOMATIC_ANALYSIS_ACTOR_CANCELLED: "skipped because the run stopped before its participants finished",
|
|
302
303
|
AUTOMATIC_ANALYSIS_SOURCE_UNAVAILABLE: "skipped because the run's evidence could not be read",
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/** The key each API analyst reads, under its first spelling. */
|
|
2
|
+
declare const ANALYSIS_KEYS: {
|
|
3
|
+
readonly openai: "OPENAI_API_KEY";
|
|
4
|
+
readonly gemini: "GEMINI_API_KEY";
|
|
5
|
+
};
|
|
6
|
+
/** The env names an API analyst's key may have, in the order they are read. */
|
|
7
|
+
export declare const analysisKeyNames: (vendor: keyof typeof ANALYSIS_KEYS) => readonly string[];
|
|
8
|
+
/** An API analyst's key from `env`: the first of its names with a non-blank value, else "". */
|
|
9
|
+
export declare function analysisApiKey(vendor: keyof typeof ANALYSIS_KEYS, env: Readonly<Record<string, string | undefined>>): string;
|
|
1
10
|
/** The vendor aliases `humanish keys set` accepts. A raw ENV_NAME is also accepted. */
|
|
2
11
|
export declare const KEY_VENDORS: string[];
|
|
3
12
|
/** The keys `humanish keys` lists, in its order, each with what humanish uses it for. */
|
|
@@ -77,3 +86,4 @@ export declare function setUserKey(name: string, value: string, env: NodeJS.Proc
|
|
|
77
86
|
export declare function unsetUserKey(name: string, env: NodeJS.ProcessEnv, deps?: KeyResolutionDeps): boolean;
|
|
78
87
|
/** The names (never values) currently in the user store. */
|
|
79
88
|
export declare function listUserKeys(env: NodeJS.ProcessEnv, deps?: KeyResolutionDeps): string[];
|
|
89
|
+
export {};
|