evals 2.5.0 → 2.7.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/README.md +12 -5
- package/bin.js +43 -0
- package/cli.js +226 -60
- package/onboarding-prompt.md +118 -68
- package/package.json +6 -3
- package/start.ps1 +168 -66
- package/start.sh +99 -17
- package/vendor/MANIFEST +1 -1
- package/vendor/coding_harness_tracing-0.1.0-py3-none-any.whl +0 -0
- package/vendor/harness-pin.lock.json +1 -1
package/README.md
CHANGED
|
@@ -8,13 +8,17 @@ Go from zero to your first [Arize AX](https://arize.com/docs/ax) traces in one c
|
|
|
8
8
|
npx evals
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
You'll see a picker of the coding agents installed on your machine (Claude Code, Codex, Cursor, GitHub Copilot,
|
|
11
|
+
You'll see a picker of the coding agents installed on your machine (Claude Code, Codex, Cursor, GitHub Copilot, Antigravity CLI). Pick one and it launches in your current directory, seeded with the onboarding prompt — so run `npx evals` from the project you want to instrument.
|
|
12
|
+
|
|
13
|
+
**Pasted `npx evals` into a coding agent?** If an agent runs it as a shell command there is no interactive terminal, so nothing is launched. It stages the onboarding prompt and prints two lines asking you to re-run in a terminal, with the staged prompt path in case you would rather point your agent at it yourself. Agents treat command output as data rather than instructions, so this deliberately asks *you* rather than trying to hand the flow to them.
|
|
12
14
|
|
|
13
15
|
No coding agent installed? The picker shows install links instead.
|
|
14
16
|
|
|
17
|
+
Needs **Node.js 20 or newer** (the Ink UI does). On anything older, `npx evals` says so and points you at the shell launcher below rather than failing obscurely.
|
|
18
|
+
|
|
15
19
|
### Without Node
|
|
16
20
|
|
|
17
|
-
If you don't have Node
|
|
21
|
+
If you don't have Node — or have one older than 20 — use the shell launchers (they detect an agent and fetch the prompt from this published package via jsDelivr):
|
|
18
22
|
|
|
19
23
|
```bash
|
|
20
24
|
# macOS / Linux
|
|
@@ -26,7 +30,7 @@ bash <(curl -fsSL https://cdn.jsdelivr.net/npm/evals/start.sh)
|
|
|
26
30
|
irm https://cdn.jsdelivr.net/npm/evals/start.ps1 | iex
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
When Node
|
|
33
|
+
When **Node 20+** is present, these hand off to `npx evals` for the richer UI. On an older Node they say so and carry on in the shell, which needs no Node at all.
|
|
30
34
|
|
|
31
35
|
## What it does
|
|
32
36
|
|
|
@@ -34,7 +38,7 @@ When Node **is** present, these hand off to `npx evals` for the richer UI.
|
|
|
34
38
|
2. Launches the agent in your current directory, seeded with the bundled onboarding prompt ([`onboarding-prompt.md`](./onboarding-prompt.md)).
|
|
35
39
|
3. The agent walks you through: create/sign in to Arize AX → pick what to trace → instrument it → verify your first traces.
|
|
36
40
|
|
|
37
|
-
You can trace an app in the current folder, a starter app the agent creates, or **the coding agent itself** — so every session you run, in any project, shows up in Arize AX. Agent tracing is machine-wide and captures prompts and tool output by default, so the prompt asks for that explicitly and offers per-category opt-outs.
|
|
41
|
+
You can trace an existing app (the one in the current folder, or another one you point it at by path), a starter app the agent creates, or **the coding agent itself** — so every session you run, in any project, shows up in Arize AX. Agent tracing is machine-wide and captures prompts and tool output by default, so the prompt asks for that explicitly and offers per-category opt-outs.
|
|
38
42
|
|
|
39
43
|
The agent runs with its **normal permission model** — `evals` never passes skip-permissions, so you approve each step, and the prompt itself gates real changes on your confirmation.
|
|
40
44
|
|
|
@@ -42,12 +46,15 @@ The agent runs with its **normal permission model** — `evals` never passes ski
|
|
|
42
46
|
|
|
43
47
|
| Variable | Applies to | Effect |
|
|
44
48
|
|----------|-----------|--------|
|
|
45
|
-
| `ARIZE_AGENT=<id>` | `start.sh` / `start.ps1` | Skip the picker and use this agent (`claude`, `codex`, `cursor-agent`, `copilot`, `
|
|
49
|
+
| `ARIZE_AGENT=<id>` | `start.sh` / `start.ps1` | Skip the picker and use this agent (`claude`, `codex`, `cursor-agent`, `copilot`, `agy`). |
|
|
46
50
|
| `ARIZE_SKIP_NPX=1` | `start.sh` / `start.ps1` | Force the shell path even when Node/npx is available. |
|
|
47
51
|
| `ARIZE_PROMPT_URL=<url>` | `start.sh` / `start.ps1` | Fetch the onboarding prompt from a custom URL (supports `file://`). |
|
|
52
|
+
| `ARIZE_ONBOARDING_DIR=<dir>` | all three | Stage the prompt somewhere other than `~/.arize/onboarding`. |
|
|
48
53
|
|
|
49
54
|
The shell launchers also accept `--agent <id>`.
|
|
50
55
|
|
|
56
|
+
The prompt is staged in `~/.arize/onboarding` and handed to your agent by path, since it's too large to pass as a command-line argument. That directory is cleared at the start of each launch and holds nothing but the prompt and the offline tracing bundle. If your home directory isn't writable, the launcher says so and falls back to a temp directory — instrumenting an app still works, but tracing the coding agent itself needs a writable home.
|
|
57
|
+
|
|
51
58
|
## About Arize
|
|
52
59
|
|
|
53
60
|
[Arize AX](https://arize.com/docs/ax) is the AI engineering platform for tracing, evaluating, and observing LLM and agent applications. Learn more at [arize.com](https://arize.com).
|
package/bin.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// Node version gate for `npx evals`.
|
|
4
|
+
//
|
|
5
|
+
// This is a separate file from cli.js on purpose. ESM `import` statements are
|
|
6
|
+
// hoisted and evaluated before any of the importing module's own code runs, so a
|
|
7
|
+
// check at the top of cli.js would execute *after* Ink's dependency graph loads —
|
|
8
|
+
// too late. On Node 18 that graph dies while parsing string-width with
|
|
9
|
+
// "SyntaxError: Invalid regular expression flags" (the regex `v` flag is Node 20+),
|
|
10
|
+
// which tells the user nothing about Node versions. So: check here, where nothing
|
|
11
|
+
// but Node builtins have loaded, then reach cli.js through a dynamic import.
|
|
12
|
+
//
|
|
13
|
+
// Keep this file dependency-free and conservative in syntax — it has to parse and
|
|
14
|
+
// run on the very versions it exists to reject.
|
|
15
|
+
//
|
|
16
|
+
// MIN_MAJOR tracks ink's own `engines.node`, and package.json's `engines` field
|
|
17
|
+
// must say the same thing; test/cli.test.js asserts they agree.
|
|
18
|
+
|
|
19
|
+
const MIN_MAJOR = 20;
|
|
20
|
+
|
|
21
|
+
const major = Number.parseInt(process.versions.node.split('.')[0], 10);
|
|
22
|
+
|
|
23
|
+
if (!Number.isInteger(major) || major < MIN_MAJOR) {
|
|
24
|
+
const shell = process.platform === 'win32'
|
|
25
|
+
? 'irm https://cdn.jsdelivr.net/npm/evals/start.ps1 | iex'
|
|
26
|
+
: 'bash <(curl -fsSL https://cdn.jsdelivr.net/npm/evals/start.sh)';
|
|
27
|
+
|
|
28
|
+
console.error(`evals needs Node.js ${MIN_MAJOR} or newer — this is ${process.version}.`);
|
|
29
|
+
console.error('');
|
|
30
|
+
console.error('Either upgrade Node (https://nodejs.org), or use the launcher that');
|
|
31
|
+
console.error('needs no Node at all — it runs the same onboarding flow:');
|
|
32
|
+
console.error('');
|
|
33
|
+
console.error(` ${shell}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// cli.js only auto-runs when it is the entry point, so call main() explicitly.
|
|
38
|
+
import('./cli.js')
|
|
39
|
+
.then((cli) => cli.main())
|
|
40
|
+
.catch((err) => {
|
|
41
|
+
console.error(`Could not start evals: ${err && err.message ? err.message : err}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
package/cli.js
CHANGED
|
@@ -2,31 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect } from 'react';
|
|
4
4
|
import { render, Box, Text, useInput, useApp, Static } from 'ink';
|
|
5
|
-
import Gradient from 'ink-gradient';
|
|
6
5
|
import { exec, spawn } from 'child_process';
|
|
7
6
|
import {
|
|
8
7
|
existsSync,
|
|
9
8
|
readFileSync,
|
|
10
9
|
writeFileSync,
|
|
11
10
|
mkdtempSync,
|
|
11
|
+
mkdirSync,
|
|
12
12
|
realpathSync,
|
|
13
13
|
readdirSync,
|
|
14
14
|
copyFileSync,
|
|
15
15
|
chmodSync,
|
|
16
16
|
renameSync,
|
|
17
17
|
rmSync,
|
|
18
|
+
rmdirSync,
|
|
19
|
+
unlinkSync,
|
|
18
20
|
} from 'fs';
|
|
19
|
-
import { join } from 'path';
|
|
20
|
-
import { tmpdir } from 'os';
|
|
21
|
+
import { join, isAbsolute, resolve } from 'path';
|
|
22
|
+
import { tmpdir, homedir } from 'os';
|
|
21
23
|
import { fileURLToPath } from 'url';
|
|
22
24
|
|
|
23
25
|
const e = React.createElement;
|
|
24
26
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
27
|
+
// Arize brand magenta, from the 2026 rebrand design system. The mark is a single
|
|
28
|
+
// flat fill (`--brand-accent-ink`, the hex in arize-mark.svg) — the brand has no
|
|
29
|
+
// gradient anywhere, so the logo doesn't get one either. Interactive accents use
|
|
30
|
+
// Pink 400 (`--brand-accent`), which the palette keeps distinct from the mark.
|
|
31
|
+
const BRAND_MARK = '#EC2088';
|
|
32
|
+
const BRAND_ACCENT = '#FF3CA8';
|
|
33
|
+
|
|
34
|
+
// The onboarding prompt is bundled with this package (onboarding-prompt.md).
|
|
35
|
+
// This repo is its only home — there is no docs original to sync against, so
|
|
36
|
+
// edit it here. We read it, write it to a temp file, and tell the agent to read
|
|
37
|
+
// that file — a ~35 KB prompt is too large to pass reliably as a command-line
|
|
38
|
+
// argument.
|
|
30
39
|
const BUNDLED_PROMPT_PATH = fileURLToPath(new URL('./onboarding-prompt.md', import.meta.url));
|
|
31
40
|
|
|
32
41
|
// Wheels for the coding-agent tracing harness, built by scripts/build-harness-wheel.mjs.
|
|
@@ -41,6 +50,106 @@ const OFFLINE_DIR_NAME = 'arize-offline';
|
|
|
41
50
|
// would send the agent down the offline path with no wheel to install.
|
|
42
51
|
const OFFLINE_FILES = ['harness-install.sh', 'harness-install.bat', 'LICENSE-coding-harness-tracing'];
|
|
43
52
|
|
|
53
|
+
const PROMPT_FILE_NAME = 'onboarding-prompt.md';
|
|
54
|
+
|
|
55
|
+
// Step 4A writes the harness credentials here and deletes them itself. We name it
|
|
56
|
+
// only so a stray one — left by a run that died between writing and deleting —
|
|
57
|
+
// gets cleared at the next launch instead of sitting around holding a live key.
|
|
58
|
+
const ENV_FILE_NAME = 'harness.env';
|
|
59
|
+
|
|
60
|
+
// MANIFEST only lands in the shell launchers' bundles, never in ours. Naming it
|
|
61
|
+
// anyway keeps one allowlist correct for every implementation, so a directory
|
|
62
|
+
// staged by start.sh can be cleared by `npx evals` and vice versa.
|
|
63
|
+
const BUNDLE_EXTRA_FILES = ['MANIFEST'];
|
|
64
|
+
|
|
65
|
+
// Where the prompt and the offline bundle get staged: `~/.arize/onboarding`.
|
|
66
|
+
// Deliberately stable rather than a fresh temp directory. The agent is asked to
|
|
67
|
+
// read a file outside its workspace, and that is an approval a human can grant
|
|
68
|
+
// with confidence for `~/.arize/onboarding/onboarding-prompt.md`, where a
|
|
69
|
+
// `/var/folders/_s/3_t5nrxs…/T/arize-onboarding-c5uxb8/` path just looks alarming.
|
|
70
|
+
//
|
|
71
|
+
// This sits beside Step 4A's own `~/.arize/harness` install, which the launcher
|
|
72
|
+
// must never touch — clearing our staging directory cannot uninstall a working
|
|
73
|
+
// harness, and that boundary is why we only ever name files under `onboarding/`.
|
|
74
|
+
export function resolveOnboardingDir({
|
|
75
|
+
home = homedir(),
|
|
76
|
+
override = process.env.ARIZE_ONBOARDING_DIR,
|
|
77
|
+
} = {}) {
|
|
78
|
+
if (override) return resolve(override);
|
|
79
|
+
return join(home, '.arize', 'onboarding');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Clear a staged bundle directory by naming every entry we could have put in it,
|
|
83
|
+
// then rmdir. Bails out — deleting nothing further — the moment it meets an entry
|
|
84
|
+
// it doesn't recognise. Wheel names carry a version we can't know ahead of time,
|
|
85
|
+
// so `.whl` is matched by extension, non-recursively, inside this one directory.
|
|
86
|
+
function clearBundleDir(dir) {
|
|
87
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
88
|
+
const known =
|
|
89
|
+
entry.name.endsWith('.whl') ||
|
|
90
|
+
OFFLINE_FILES.includes(entry.name) ||
|
|
91
|
+
BUNDLE_EXTRA_FILES.includes(entry.name);
|
|
92
|
+
if (entry.isDirectory() || !known) return false;
|
|
93
|
+
unlinkSync(join(dir, entry.name));
|
|
94
|
+
}
|
|
95
|
+
rmdirSync(dir);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Empty the staging directory by deleting the exact files we create — never with
|
|
100
|
+
// a recursive delete. Two things fall out of that: a bad path can at worst try to
|
|
101
|
+
// unlink a handful of names that won't exist, and `rmdir` refuses a non-empty
|
|
102
|
+
// directory, so anything unexpected in there stops us instead of being destroyed.
|
|
103
|
+
// Unlinking a symlink removes the link and never the target.
|
|
104
|
+
//
|
|
105
|
+
// Returns true when the directory is gone or empty and safe to stage into.
|
|
106
|
+
export function clearOnboardingDir(dir) {
|
|
107
|
+
if (!isAbsolute(dir)) return false;
|
|
108
|
+
if (!existsSync(dir)) return true;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
112
|
+
const path = join(dir, entry.name);
|
|
113
|
+
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
// arize-offline/ is ours; .staging-* is the leftover of a crashed run.
|
|
116
|
+
const ours = entry.name === OFFLINE_DIR_NAME || entry.name.startsWith('.staging-');
|
|
117
|
+
if (!ours || !clearBundleDir(path)) return false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (entry.name !== PROMPT_FILE_NAME && entry.name !== ENV_FILE_NAME) return false;
|
|
122
|
+
unlinkSync(path);
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Clear and create the staging directory, degrading to a temp directory when the
|
|
131
|
+
// home path can't be used: a read-only home, a directory owned by another user
|
|
132
|
+
// after a past `sudo npx evals`, a full disk, an offline redirected profile on
|
|
133
|
+
// Windows, or an unrecognised file we refuse to delete. We attempt the work and
|
|
134
|
+
// degrade rather than probing for writability first — a probe lies on NFS and
|
|
135
|
+
// ACL filesystems, races, and can't see ENOSPC at all.
|
|
136
|
+
//
|
|
137
|
+
// Mode 0700 because Step 4A writes credentials into this directory.
|
|
138
|
+
export function prepareOnboardingDir(dir = resolveOnboardingDir()) {
|
|
139
|
+
if (clearOnboardingDir(dir)) {
|
|
140
|
+
try {
|
|
141
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
142
|
+
// mkdir's mode is umask-filtered, so set it outright. Windows has no POSIX
|
|
143
|
+
// mode; there the file inherits the profile's own ACL, as it does today.
|
|
144
|
+
if (process.platform !== 'win32') chmodSync(dir, 0o700);
|
|
145
|
+
return { dir, fellBack: false };
|
|
146
|
+
} catch {
|
|
147
|
+
// Fall through to the temp directory.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { dir: mkdtempSync(join(tmpdir(), 'arize-onboarding-')), fellBack: true };
|
|
151
|
+
}
|
|
152
|
+
|
|
44
153
|
// Copy the bundled wheels next to the prompt. Returns the staged directory, or
|
|
45
154
|
// null when this package has no usable vendor/ (a git checkout that hasn't run
|
|
46
155
|
// the build). The shell launchers stage the same bundle themselves, fetching it
|
|
@@ -77,15 +186,22 @@ export function stageOfflineHarness(dir, vendorDir = VENDOR_DIR) {
|
|
|
77
186
|
}
|
|
78
187
|
}
|
|
79
188
|
|
|
80
|
-
//
|
|
81
|
-
//
|
|
189
|
+
// Stage the bundled prompt and return the seed instruction that points the agent
|
|
190
|
+
// at it, plus whether we had to fall back off the home directory.
|
|
82
191
|
function prepareSeedPrompt() {
|
|
83
192
|
const promptText = readFileSync(BUNDLED_PROMPT_PATH, 'utf8');
|
|
84
|
-
const dir =
|
|
85
|
-
const promptFile = join(dir,
|
|
193
|
+
const { dir, fellBack } = prepareOnboardingDir();
|
|
194
|
+
const promptFile = join(dir, PROMPT_FILE_NAME);
|
|
86
195
|
writeFileSync(promptFile, promptText, 'utf8');
|
|
87
196
|
stageOfflineHarness(dir);
|
|
88
|
-
|
|
197
|
+
// Single quotes, not double: a home path is likelier to contain a space than a
|
|
198
|
+
// temp path was, but Windows spawns through the shell and embedded double
|
|
199
|
+
// quotes don't survive that reliably. cmd.exe leaves single quotes alone.
|
|
200
|
+
// No "in this project": the launcher may well be run from a home directory, and
|
|
201
|
+
// Step 4 is what scopes the work — an app here, an app at another path, a starter
|
|
202
|
+
// app, or the coding agent itself. Keep this wording in step with start.{sh,ps1}.
|
|
203
|
+
const seed = `Read the file '${promptFile}' and follow it to set up Arize AX tracing, walking me through each step and asking me questions as needed.`;
|
|
204
|
+
return { seed, dir, fellBack };
|
|
89
205
|
}
|
|
90
206
|
|
|
91
207
|
// Coding agents we can launch interactively, seeded with the prompt.
|
|
@@ -95,7 +211,7 @@ export const AGENTS = [
|
|
|
95
211
|
{ id: 'codex', label: 'OpenAI Codex', bin: 'codex', args: (s) => [s], installUrl: 'https://developers.openai.com/codex/cli' },
|
|
96
212
|
{ id: 'cursor-agent', label: 'Cursor', bin: 'cursor-agent', args: (s) => [s], installUrl: 'https://docs.cursor.com/en/cli/overview' },
|
|
97
213
|
{ id: 'copilot', label: 'GitHub Copilot', bin: 'copilot', args: (s) => ['-i', s], installUrl: 'https://github.com/features/copilot/cli' },
|
|
98
|
-
{ id: '
|
|
214
|
+
{ id: 'agy', label: 'Antigravity CLI', bin: 'agy', args: (s) => ['-i', s], installUrl: 'https://antigravity.google/docs/cli/getting-started' }
|
|
99
215
|
];
|
|
100
216
|
|
|
101
217
|
// PATH scan — detects an agent without executing it (running it could hang).
|
|
@@ -116,33 +232,21 @@ export function isInstalled(bin) {
|
|
|
116
232
|
return false;
|
|
117
233
|
}
|
|
118
234
|
|
|
119
|
-
// "ARIZE
|
|
235
|
+
// "ARIZE AX" on one line — 66 columns wide, so Header() drops to the compact
|
|
236
|
+
// two-row art below 68 and to plain text below 30.
|
|
120
237
|
const largeLogo = `
|
|
121
|
-
█████╗ ██████╗ ██╗ ███████╗ ███████╗
|
|
122
|
-
██╔══██╗ ██╔══██╗ ██║ ╚══███╔╝ ██╔════╝
|
|
123
|
-
███████║ ██████╔╝ ██║ ███╔╝ █████╗
|
|
124
|
-
██╔══██║ ██╔══██╗ ██║ ███╔╝ ██╔══╝
|
|
125
|
-
██║ ██║ ██║ ██║ ██║ ███████╗ ███████╗
|
|
126
|
-
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝
|
|
127
|
-
███████╗ ██╗ ██╗ █████╗ ██╗ ███████╗
|
|
128
|
-
██╔════╝ ██║ ██║ ██╔══██╗ ██║ ██╔════╝
|
|
129
|
-
█████╗ ██║ ██║ ███████║ ██║ ███████╗
|
|
130
|
-
██╔══╝ ╚██╗ ██╔╝ ██╔══██║ ██║ ╚════██║
|
|
131
|
-
███████╗ ╚████╔╝ ██║ ██║ ███████╗███████║
|
|
132
|
-
╚══════╝ ╚═══╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝`;
|
|
238
|
+
█████╗ ██████╗ ██╗ ███████╗ ███████╗ █████╗ ██╗ ██╗
|
|
239
|
+
██╔══██╗ ██╔══██╗ ██║ ╚══███╔╝ ██╔════╝ ██╔══██╗ ╚██╗██╔╝
|
|
240
|
+
███████║ ██████╔╝ ██║ ███╔╝ █████╗ ███████║ ╚███╔╝
|
|
241
|
+
██╔══██║ ██╔══██╗ ██║ ███╔╝ ██╔══╝ ██╔══██║ ██╔██╗
|
|
242
|
+
██║ ██║ ██║ ██║ ██║ ███████╗ ███████╗ ██║ ██║ ██╔╝ ██╗
|
|
243
|
+
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝`;
|
|
133
244
|
|
|
134
245
|
const mediumLogo = `
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
██║ ██║ ██║ ██║ ██║ ███████╗ ███████╗
|
|
140
|
-
╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝
|
|
141
|
-
█▀▀ █ █ ▄▀█ █ █▀
|
|
142
|
-
██▄ ▀▄▀ █▀█ █▄▄ ▄█`;
|
|
143
|
-
|
|
144
|
-
const smallLogo = `ARIZE
|
|
145
|
-
EVALS`;
|
|
246
|
+
▄▀█ █▀▄ █ ▀█ █▀▀ ▄▀█ ▀▄▀
|
|
247
|
+
█▀█ █▀▄ █ █▄ ██▄ █▀█ ▄▀▄`;
|
|
248
|
+
|
|
249
|
+
const smallLogo = `ARIZE AX`;
|
|
146
250
|
|
|
147
251
|
// Tag every URL the app opens with utm_source=npmevals (idempotent). Applied
|
|
148
252
|
// centrally in openUrlInBrowser so all opened links (the agent install links)
|
|
@@ -175,31 +279,30 @@ function openUrlInBrowser(rawUrl) {
|
|
|
175
279
|
});
|
|
176
280
|
}
|
|
177
281
|
|
|
178
|
-
// Header
|
|
282
|
+
// Header — the brand mark in flat brand magenta
|
|
179
283
|
function Header() {
|
|
180
284
|
const terminalWidth = process.stdout.columns || 80;
|
|
181
285
|
|
|
182
286
|
let logo;
|
|
183
|
-
if (terminalWidth >=
|
|
287
|
+
if (terminalWidth >= 68) {
|
|
184
288
|
logo = largeLogo;
|
|
185
|
-
} else if (terminalWidth >=
|
|
289
|
+
} else if (terminalWidth >= 30) {
|
|
186
290
|
logo = mediumLogo;
|
|
187
291
|
} else {
|
|
188
292
|
logo = smallLogo;
|
|
189
293
|
}
|
|
190
294
|
|
|
191
295
|
return e(Box, { flexDirection: 'column', marginBottom: 1 },
|
|
192
|
-
e(
|
|
193
|
-
|
|
194
|
-
),
|
|
195
|
-
e(Text, { color: 'gray', italic: true }, 'Evals and Observability for Agentic AI')
|
|
296
|
+
e(Text, { color: BRAND_MARK }, logo),
|
|
297
|
+
e(Text, { color: 'gray', italic: true }, 'Observability and Evals for Agentic AI')
|
|
196
298
|
);
|
|
197
299
|
}
|
|
198
300
|
|
|
199
301
|
// Character-by-character shimmer component
|
|
200
302
|
function ShimmerText({ text, shimmerPos }) {
|
|
201
|
-
const baseColor =
|
|
202
|
-
|
|
303
|
+
const baseColor = BRAND_ACCENT;
|
|
304
|
+
// Pink 300 / 200 / 100 off the brand ramp, peaking at white.
|
|
305
|
+
const shimmerColors = ['#FF6BBA', '#FFB5D6', '#FEEDF3', '#FFFFFF', '#FEEDF3', '#FFB5D6', '#FF6BBA'];
|
|
203
306
|
const shimmerWidth = shimmerColors.length;
|
|
204
307
|
|
|
205
308
|
const chars = text.split('').map((char, i) => {
|
|
@@ -216,19 +319,22 @@ function ShimmerText({ text, shimmerPos }) {
|
|
|
216
319
|
return e(Box, null, ...chars);
|
|
217
320
|
}
|
|
218
321
|
|
|
219
|
-
// Menu item component - single line,
|
|
322
|
+
// Menu item component - single line, brand pink selected with character shimmer
|
|
220
323
|
function MenuItem({ name, subtext, isSelected, shimmerPos }) {
|
|
324
|
+
// subtext is optional: the detected-agent items are just the agent's name.
|
|
325
|
+
const tail = subtext ? [e(Text, { color: 'gray', key: 'sub' }, ' — ' + subtext)] : [];
|
|
326
|
+
|
|
221
327
|
if (isSelected) {
|
|
222
328
|
return e(Box, null,
|
|
223
|
-
e(Text, { bold: true, color:
|
|
329
|
+
e(Text, { bold: true, color: BRAND_ACCENT }, '❯ '),
|
|
224
330
|
e(ShimmerText, { text: name, shimmerPos }),
|
|
225
|
-
|
|
331
|
+
...tail
|
|
226
332
|
);
|
|
227
333
|
}
|
|
228
334
|
|
|
229
335
|
return e(Box, null,
|
|
230
336
|
e(Text, { color: 'white' }, ' ' + name),
|
|
231
|
-
|
|
337
|
+
...tail
|
|
232
338
|
);
|
|
233
339
|
}
|
|
234
340
|
|
|
@@ -241,7 +347,6 @@ export function buildAgentItems() {
|
|
|
241
347
|
return {
|
|
242
348
|
items: detected.map(a => ({
|
|
243
349
|
name: a.label,
|
|
244
|
-
subtext: 'Launch and walk me through setup',
|
|
245
350
|
launch: a
|
|
246
351
|
})),
|
|
247
352
|
none: false
|
|
@@ -351,17 +456,26 @@ function App({ onDone }) {
|
|
|
351
456
|
function launchAgent(agent) {
|
|
352
457
|
const isWin = process.platform === 'win32';
|
|
353
458
|
|
|
354
|
-
let
|
|
459
|
+
let prepared;
|
|
355
460
|
try {
|
|
356
|
-
|
|
461
|
+
prepared = prepareSeedPrompt();
|
|
357
462
|
} catch (err) {
|
|
358
|
-
console.error(`Could not
|
|
463
|
+
console.error(`Could not prepare the onboarding prompt: ${err.message}`);
|
|
359
464
|
process.exit(1);
|
|
360
465
|
}
|
|
361
466
|
|
|
467
|
+
// Say so when we couldn't use the home directory. Otherwise the seed path
|
|
468
|
+
// silently reverts to the /var/folders/… form this change exists to avoid, and
|
|
469
|
+
// Step 4A is going to fail later anyway — it writes the harness and its
|
|
470
|
+
// credentials under ~/.arize regardless of where the prompt was staged.
|
|
471
|
+
if (prepared.fellBack) {
|
|
472
|
+
console.log(`\nCouldn't use ${resolveOnboardingDir()} — staged in ${prepared.dir} instead.`);
|
|
473
|
+
console.log('Instrumenting an app still works; tracing this coding agent (Step 4A) needs a writable home directory.');
|
|
474
|
+
}
|
|
475
|
+
|
|
362
476
|
console.log(`\nLaunching ${agent.label}…\n`);
|
|
363
477
|
|
|
364
|
-
const child = spawn(agent.bin, agent.args(seed), {
|
|
478
|
+
const child = spawn(agent.bin, agent.args(prepared.seed), {
|
|
365
479
|
stdio: 'inherit',
|
|
366
480
|
shell: isWin // .cmd/.bat shims on Windows need the shell to resolve
|
|
367
481
|
});
|
|
@@ -378,14 +492,66 @@ function launchAgent(agent) {
|
|
|
378
492
|
}
|
|
379
493
|
|
|
380
494
|
// Render the interactive app and act on the user's choice. Exported so it can
|
|
495
|
+
// No terminal to render into. Stage the prompt anyway and tell whoever is reading
|
|
496
|
+
// this output what to do with it — an agent can follow it directly, and a human who
|
|
497
|
+
// piped us at least learns where the prompt is. Only the framing and the exit code
|
|
498
|
+
// differ: an agent gets 0 so its shell tool doesn't report a failed command.
|
|
499
|
+
function handOffToCallingAgent() {
|
|
500
|
+
let staged = null;
|
|
501
|
+
let stagingError = null;
|
|
502
|
+
try {
|
|
503
|
+
staged = prepareSeedPrompt();
|
|
504
|
+
} catch (error) {
|
|
505
|
+
stagingError = error;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Nothing writable anywhere — Codex's workspace-write sandbox denies both $HOME
|
|
509
|
+
// and the temp directory. A sandbox that tight blocks the network too, so every
|
|
510
|
+
// path in the prompt would die at its first install: stop instead of pointing at a
|
|
511
|
+
// flow that cannot finish. Explicitly retry-proof, because a bare error is what
|
|
512
|
+
// sent a tester's Codex into 90 seconds of inventing npm cache workarounds.
|
|
513
|
+
if (!staged) {
|
|
514
|
+
console.error('evals cannot run here: no writable directory to stage the onboarding prompt in.');
|
|
515
|
+
console.error(` ${stagingError.message}`);
|
|
516
|
+
console.error('');
|
|
517
|
+
console.error('Do not retry and do not work around this — a sandbox this tight blocks the');
|
|
518
|
+
console.error('network as well, so the setup could not finish. Tell the user to allow writes');
|
|
519
|
+
console.error('to their home directory and network access, or to run `npx evals` in a terminal.');
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Same notice the TUI path gives when it couldn't use the home directory.
|
|
524
|
+
if (staged.fellBack) {
|
|
525
|
+
console.log(`Couldn't use ${resolveOnboardingDir()} — staged in ${staged.dir} instead.`);
|
|
526
|
+
console.log('Instrumenting an app still works; tracing this coding agent (Step 4A) needs a writable home directory.');
|
|
527
|
+
console.log('');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Addressed to the person, not to the agent that ran us. Agents treat tool output as
|
|
531
|
+
// data, not instructions: Codex said so outright — "I did not follow that additional
|
|
532
|
+
// instruction" — and Copilot silently did the same. That's prompt-injection defence,
|
|
533
|
+
// and not something to design around. What both did reliably was relay this text to
|
|
534
|
+
// the user, so the user is who it talks to.
|
|
535
|
+
//
|
|
536
|
+
// Two lines, recommendation first: Copilot twice passed a longer message on with the
|
|
537
|
+
// middle dropped, keeping only the first line and the last. At two lines there is no
|
|
538
|
+
// middle to lose. Don't grow this, and don't name the picker — describing a UI the
|
|
539
|
+
// agent can't draw invites "I can't do this" instead of the one instruction that
|
|
540
|
+
// matters. Exit 0: a failure status is what starts the workaround-hunting.
|
|
541
|
+
console.log('Run `npx evals` in a terminal to set up Arize AX tracing — this is not a terminal, so nothing was set up.');
|
|
542
|
+
console.log(`The prompt is at ${join(staged.dir, PROMPT_FILE_NAME)} if you'd rather have your agent follow it.`);
|
|
543
|
+
process.exit(0);
|
|
544
|
+
}
|
|
545
|
+
|
|
381
546
|
// be driven explicitly; only auto-runs when this file is the entry point (see
|
|
382
547
|
// the guard below), so importing it in tests doesn't launch the TUI.
|
|
383
548
|
export async function main() {
|
|
384
|
-
// Ink needs an interactive terminal
|
|
549
|
+
// Ink needs an interactive terminal — but erroring out is the wrong answer when the
|
|
550
|
+
// caller is a coding agent that ran `npx evals` as a shell command, which is how
|
|
551
|
+
// testers keep reaching us. Stage the prompt and tell the user where to go instead.
|
|
385
552
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
process.exit(1);
|
|
553
|
+
handOffToCallingAgent();
|
|
554
|
+
return;
|
|
389
555
|
}
|
|
390
556
|
|
|
391
557
|
// Handle uncaught errors
|