@eigenpal/cli 0.4.1 → 0.4.3

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +465 -0
  2. package/README.md +34 -24
  3. package/dist/cli.js +159 -18
  4. package/package.json +28 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,465 @@
1
+ # @eigenpal/cli
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - fadb59b: CLI command tree reorganized around the data model. Top-level surface is now
8
+ `workflow`, `agent` (reserved), and `skill`; everything that operates on a
9
+ workflow lives under `workflow`, grouped by sub-entity.
10
+
11
+ Renames (clean break — no aliases):
12
+ - `eigenpal exec <slug>` → `eigenpal workflow execution run <slug>`
13
+ - `eigenpal clear` → `eigenpal workflow clear`
14
+ - `eigenpal install-skill` → `eigenpal skill` (same interactive multiselect UX)
15
+ - `eigenpal execution {get,list,watch,compare}` → `eigenpal workflow execution {…}`
16
+ - `eigenpal workflow set-definition` → `eigenpal workflow push`
17
+ - `eigenpal workflow get-definition` → `eigenpal workflow pull`
18
+ - `eigenpal workflow set-evaluators` / `get-evaluators` → `workflow evaluators {push,pull}`
19
+ - `eigenpal workflow set-dataset` / `get-dataset` / `list-examples` → `workflow dataset {push,pull,list}`
20
+ - `eigenpal workflow run-experiment` / `get-experiment-status` / `get-experiment-results` /
21
+ `get-eval-results` / `list-executions` → `workflow experiment {run,status,results,eval-results,list}`
22
+ - `eigenpal workflow list-versions` / `restore-version` → `workflow version {list,restore}`
23
+ - `eigenpal workflow list-step-types` / `get-step-type` → `workflow step-type {list,get}`
24
+ - `eigenpal workflow get-execution` → `workflow execution get`
25
+ - `eigenpal workflow get-active-tenant` → `workflow active-tenant`
26
+
27
+ Removed:
28
+ - `eigenpal generate-meta` — deleted. The IDs it generated were never read by
29
+ any other CLI command; the server re-mints IDs on every push.
30
+
31
+ Added:
32
+ - `eigenpal agent` — reserved placeholder namespace. Prints a "Coming soon"
33
+ message; agentic-process operations will land here in a future release.
34
+
35
+ ### Minor Changes
36
+
37
+ - c82a215: `eigenpal auth login` now shows an interactive server picker when no `--base-url` flag or `EIGENPAL_BASE_URL` env is set:
38
+ - **Cloud** (`https://studio.eigenpal.com`) is the default highlighted option.
39
+ - Any on-prem URLs already in your credentials file are surfaced as separate options labelled with the profile names that use them — re-login against an existing on-prem deployment without retyping the URL.
40
+ - **Custom URL…** prompts for a URL with strict validation.
41
+
42
+ URL handling is now bulletproof in all three input paths (flag, env, picker):
43
+ - Trims leading/trailing whitespace, strips trailing slashes.
44
+ - Auto-prepends `https://` when you paste a bare hostname (e.g. `eigenpal.example.com` → `https://eigenpal.example.com`).
45
+ - Rejects non-http(s) schemes (`file://`, `ws://`, etc.) with a clear error.
46
+ - Rejects URLs with paths, query strings, or hash fragments — baseUrl is an origin, not a route.
47
+ - Warns when authenticating over `http://` against a non-loopback host (credentials sent unencrypted).
48
+
49
+ Behavior with `--base-url` or `EIGENPAL_BASE_URL` set is unchanged — explicit overrides skip the picker, and an active profile's stored baseUrl is still inherited by all non-login commands.
50
+
51
+ - c330c2a: CLI: bug fixes + new tooling.
52
+ - `success()` / `info()` / `dim()` now write to stderr, not stdout. `cmd | jq` no longer mixes status lines with JSON output. (`error()` and `warn()` already correctly used stderr.)
53
+ - `workflow execution watch <bad-id>` no longer treats 404 as a transient network blip; the loop exits immediately so the caller sees the structured `Execution not found` error and a non-zero exit instead of polling for 30 minutes.
54
+ - `eigenpal status` exits non-zero when authentication fails, so scripts can `eigenpal status && deploy`.
55
+ - New `eigenpal completion <bash|zsh|fish>` command — emits a shell-completion script for the live command tree. Install via `eigenpal completion bash > /usr/local/etc/bash_completion.d/eigenpal` (or your shell's equivalent).
56
+ - New global `--quiet` / `-q` flag — suppresses `success()` / `info()` / `dim()` lines while keeping `error()` and `warn()` (and JSON output) intact.
57
+ - New `addJsonFlagMutation()` helper alongside `addJsonFlag()`. Mutating commands that don't have a curated table view now use it so `--verbose` no longer appears in `--help` where it's a no-op. (Wire-up in mutating handlers happens in a follow-up commit on this branch.)
58
+
59
+ - b2c4308: CLI: per-example dataset CRUD.
60
+
61
+ Three new subcommands under `workflow dataset` mean you no longer have to re-zip and re-push the whole folder when you only want to flip one row:
62
+ - `workflow dataset create-example <wfId> --name <n> [--input-json | --input-file] [--expected-json | --expected-file] [--annotation]` — add one example.
63
+ - `workflow dataset update-example <wfId> <exampleId> [--name] [--input-*] [--expected-*] [--annotation] [--row-order]` — partial PATCH; flags you omit are left alone, `--annotation ""` clears.
64
+ - `workflow dataset delete-example <wfId> <exampleId> [--yes]` — `--yes` required for non-TTY shells.
65
+
66
+ `--input-file` / `--expected-file` accept a path or `-` for stdin. Inline `--*-json` and the `-file` variant are mutually exclusive.
67
+
68
+ Server: the `POST /api/workflows/:id/eval-examples` create schema now accepts the `name` and `annotation` fields the dashboard already exposed via the dashboard's PATCH path. No new endpoints — the v1 mirrors at `/api/v1/eval-examples/[id]` and `/api/v1/workflows/[id]/eval-examples` were already in place.
69
+
70
+ Skill recipes restored: `SKILL.md` and `reference/dataset-format.md` document the three flows (capture corrected output as new GT, add one example, delete one bad row).
71
+
72
+ - d41ee8b: CLI: drop the `--wait` flag from `workflow execution run`. The local-folder runner already waits for every example to reach a terminal state, so the flag was a documented no-op kept "for symmetry with `experiment run --wait`." That symmetry isn't worth a dead flag in the user-facing help screen — `experiment run --wait` is meaningful (it polls batch status), `execution run --wait` never was.
73
+
74
+ Behavior is unchanged. Scripts that passed `--wait` will now see an unknown-option error from Commander; remove the flag.
75
+
76
+ - d41ee8b: CLI: drop `--jq` and `--output-path` from `workflow execution get`. Pipe `--json` through real `jq` instead.
77
+
78
+ Rationale: our `--jq` was dotted-path only (`drillJsonPath`), not real jq syntax. Calling it `--jq` was misleading — `gh --jq '.foo[] | select(.bar)'` works; ours never could. Real `jq` is universally installed on dev machines and supports the full filter language.
79
+
80
+ Migration:
81
+
82
+ | Before | After |
83
+ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
84
+ | `eigenpal workflow execution get exec_… --jq 'output.data'` | `eigenpal workflow execution get exec_… --json \| jq '.output.data'` |
85
+ | `eigenpal workflow execution get exec_… --output-path 'stepExecutions.0.error'` | `eigenpal workflow execution get exec_… --json \| jq -r '.stepExecutions[0].error'` |
86
+
87
+ Updated SKILL.md and reference docs to use the canonical `--json | jq` pattern.
88
+
89
+ - c330c2a: CLI: small DX bundle + 5 audit follow-up fixes.
90
+ - `eigenpal status` (and every other CLI call) now detects HTML-on-the-wire and throws a structured `HtmlResponseError` ("API not reachable at <url> — got HTML response (likely wrong base URL or API not running)") instead of dumping the 100 KB Next.js 404 body.
91
+ - `workflow push --bump <patch|minor|major>` reads the server's current version and computes the next semver, splicing it into the YAML before sending. Pair with `--workflow-id`. Conflicts with a top-level `version:` in the YAML are rejected.
92
+ - `workflow push --set-version <X.Y.Z>` for explicit overrides. (Named `--set-version` to avoid the global `-v, --version` flag.)
93
+ - `workflow evaluator-type {list, get}` — parallel namespace to `step-type` for evaluator schemas (`exact-diff`, `llm-judge`, `custom-script`). Returns JSON Schema instead of 404.
94
+ - `eigenpal init [name]` now works with no name — scaffolds into the current directory using the cwd basename as the workflow slug. Pair with `eigenpal workflow execution run <slug>` in the same dir; `execution run` discovers both flat (`./workflow.yaml` + `./dataset/examples/<name>/`) and nested (`./eigenpal/workflows/<slug>/...`) layouts.
95
+ - `format-error` now appends `Run \`eigenpal auth login\` or set EIGENPAL_API_KEY.`to every 401, and includes the URL we tried +`EIGENPAL_BASE_URL`/`--base-url` hint on connection failures.
96
+ - `execution get --jq <jsonpath>` is the new canonical name for what was `--output-path`. `--output-path` keeps working as a hidden alias; help text shows `--jq` only (matches `gh --jq` / `kubectl --jsonpath`).
97
+ - `workflow push` without `--file` in TTY now prompts for the path; non-TTY (CI) still errors with the existing "Missing --file" message.
98
+ - `transform.script` reference docs include a callout about TDZ on shadowed input names (`const located = located || []` triggers `ReferenceError`).
99
+ - Dropped the half-promise comment in `addJsonFlag` about future `--fields` projection — the canonical answer is `jq` on the raw payload.
100
+
101
+ - c330c2a: CLI: execution + auth polish.
102
+ - **`workflow execution run`** gains `--wait` and `--json`. The local-folder
103
+ runner already polled every example to terminal, so `--wait` is currently a
104
+ no-op accepted for symmetry with `experiment run --wait`. `--json` emits a
105
+ single `{ workflowSlug, passed, failed, total }` summary on stdout (human
106
+ progress lines + the "Results:" footer still go to stderr). Exits 1 when any
107
+ example fails, same as before.
108
+ - **`workflow execution watch`**: rename `--max-minutes <n>` → `--max-wait
109
+ <seconds>` (default 1800 = 30 min). Matches the units used by
110
+ `experiment status --watch --max-wait`. Detach behavior unchanged: still
111
+ exits 2 on timeout so callers distinguish "still running" from "completed".
112
+ - **`workflow execution run`** + **`auth login`** now ship inline `Examples:`
113
+ blocks via `addHelpText('after')`. Mirrors `gh repo create --help` style.
114
+
115
+ - d41ee8b: CLI: `workflow execution cancel <executionId>` — request cancellation of a
116
+ running, queued, or just-created execution.
117
+
118
+ Idempotent against the `POST /api/v1/executions/:executionId/cancel`
119
+ endpoint. created/pending transitions straight to `cancelled`;
120
+ running/waiting stamps `cancellationRequestedAt` for the worker to observe;
121
+ already-terminal (completed / failed / cancelled) is a no-op exit-0 with an
122
+ info line.
123
+
124
+ Single-row destructive op — TTY proceeds silently (matches `gh run cancel`),
125
+ non-TTY shells (CI, pipes) require `--yes`. Pass `--json` to print the raw
126
+ server payload for scripting; 404 → error line + exit 1.
127
+
128
+ - d41ee8b: CLI: `workflow experiment compare <batchIdA> <batchIdB>`.
129
+
130
+ Side-by-side eval-score diff between two experiment batches — no more exporting both result sets to a spreadsheet to spot regressions.
131
+
132
+ ```
133
+ eigenpal workflow experiment compare evb_old evb_new --workflow-id wf_abc
134
+ ```
135
+
136
+ One row per `(example, evaluator)` pair shared by both batches with `A`, `B`, and `Δ` columns. Regressions (Δ below threshold) are flagged with ⚠ and tinted; improvements show in green. Footer aggregates `regressions / improvements / mean Δ / examples shared / only-in-{A,B}` so you can tell at a glance whether the new batch is a net win.
137
+ - `--sort delta-asc | delta-desc | name` — default is `|Δ|` descending so the biggest movers (in either direction) surface at the top.
138
+ - `--regression-threshold <n>` — default `0.05`. Tighten to `0.10` if your evaluators are noisy.
139
+ - `--json` — emits the full diff structure (`{ batchA, batchB, rows, summary }`) for piping into `jq`.
140
+ - `--workflow-id <id>` — required; eval-results is workflow-scoped.
141
+
142
+ Non-overlapping examples are listed in dedicated `only in A` / `only in B` blocks before the table so |Δ| sort stays meaningful.
143
+
144
+ - 3da0612: CLI: pretty default rendering for `list`, `get`, and mutating commands; `--json` toggle for piping.
145
+ - `workflow definition/dataset/experiment/version/step-type list` and `workflow execution list` render aligned ASCII tables. `--json` restores the legacy raw payload.
146
+ - `workflow execution get` (no other flags) prints the vertical step list via `renderFrame` instead of dumping 1000+ lines of JSON. `--json` for the full payload; `--output-path` / `--step` / `--include` continue to print filtered JSON.
147
+ - `workflow experiment status` (without `--watch`) prints a one-line summary plus the per-example failure block. `--json` for raw payload.
148
+ - `workflow definition push`, `workflow evaluators push`, `workflow experiment run`, and `workflow version restore` print one-line `success(...)` summaries instead of dumping the response. `--json` recovers legacy output.
149
+
150
+ No breaking changes: every legacy JSON shape is still available behind `--json`. Status hints route to stderr so `<command> | jq` keeps working.
151
+
152
+ - d41ee8b: **Breaking:** `workflow step exec` is now generic over step type. The two
153
+ hardcoded subcommands (`workflow step exec transform.script` and
154
+ `workflow step exec ai.extract`) become `workflow step exec <type>` with a
155
+ unified flag set, mirroring the kubectl-style noun/verb pattern.
156
+ - `<type>` accepts ANY step type registered in `STEP_SCHEMAS` (`@eigenpal/types`).
157
+ Unknown types exit 2 with a message pointing to `workflow step-type list`;
158
+ registered-but-unsupported types (everything except `transform.script` and
159
+ `ai.extract` today) exit 2 with a "not yet supported locally" message instead
160
+ of the previous "command not found" Commander error.
161
+ - Flag set:
162
+ - `--config-json <json>` — full step config as a JSON literal.
163
+ - `--config-file <path|->` — full step config from JSON file or stdin.
164
+ - `--inputs k=v` — repeatable; `v` may be `@path-to-file` (auto-JSON-parsed
165
+ when applicable) or a literal string. For `transform.script` these merge
166
+ into the config `inputs` map (flag wins); for `ai.extract` `input=…`
167
+ supplies the document text.
168
+ - `--output-schema <path>` — defaults to the step type's built-in
169
+ `outputSchema` from `STEP_SCHEMAS` when omitted.
170
+ - The previous per-type flags (`--code`, `--inputs-json`, `--input`,
171
+ `--prompt`, `--schema`, `--provider`, `--model`, `--base-url`) are gone —
172
+ pass them inside `--config-json`/`--config-file` instead. `--timeout-ms`
173
+ and `--memory-mb` survive as `transform.script`-specific resource overrides.
174
+ - Errors thrown by the handler now route through the same structured-envelope
175
+ rendering used elsewhere in `workflow/*` commands (aligned `field` column +
176
+ `hint` line for `{ issues, hint }` payloads), with a graceful fallback to
177
+ `formatCliError` for plain errors. Schema violations and unknown-type errors
178
+ keep their dedicated exit codes (2).
179
+
180
+ - c330c2a: CLI: workflow surface restructure — DX polish for the `workflow` namespace.
181
+
182
+ Breaking-but-deliberate changes that mirror `gh` / `vercel` conventions:
183
+ - **`dataset {create,update,delete}-example` → `dataset example {create,update,delete}`.** Verbs now live UNDER the `example` noun (matches `gh issue create`, `vercel project add`). No aliases — old form is removed.
184
+ - **New `dataset example get <wfId> <exampleId>`** — single-row read with pretty `Inputs / Expected / Metadata` sections and `--json` for the full payload (`triggerInput`, `expectedOutput`, `annotation`, `rowOrder`, timestamps). Closes the gap between `dataset list` (table subset) and the previous "edit-blind" CRUD verbs.
185
+ - **`dataset push --mode` defaults to `append`.** Was previously a required flag. Destructive `replace` keeps its `--yes` requirement for non-TTY plus typed-slug TTY confirmation.
186
+ - **`experiment run`**: `--poll-interval-s` renamed to `--interval` (seconds), matching `experiment status --watch`'s flag. `--wait` no longer exits 1 when `passRate` is undefined (workflow with no graded examples). Now: graded + non-1.0 → exit 1; ungraded → exit 0.
187
+ - **Mutating commands stop advertising `--verbose`.** `workflow push`, `evaluators push`, `dataset push`, `experiment run`, `versions restore`, and the new `dataset example {create,update,delete,get}` use `addJsonFlagMutation` (only `--json`). `--verbose` was a no-op on these — removing it from `--help` to stop lying.
188
+ - **Command descriptions shortened.** First sentence stays as the description; rich behavioral notes (return types, NOTE warnings) move to `addHelpText('after')` blocks. Examples added to `workflow push`, `dataset push`, `experiment run`, and `dataset example create`.
189
+
190
+ Skill (`SKILL.md` + `reference/dataset-format.md`) updated to use the new
191
+ `dataset example {create,update,delete,get}` shape; recipes mention
192
+ `example get` for single-row inspection.
193
+
194
+ - d41ee8b: CLI: `workflow step exec transform.script` and `workflow step exec ai.extract` for local single-step iteration.
195
+
196
+ Two new commands under a fresh `workflow step exec` namespace let you run one step locally — no server, no queue, no worker — so the script/prompt edit loop is sub-second instead of sub-minute:
197
+ - `workflow step exec transform.script --code <path|-> [--inputs k=v...] [--inputs-json <path|->] [--output-schema <path>] [--timeout-ms 5000] [--memory-mb 10]` — runs JS in QuickJS with the same TDZ + strict-mode + deep-frozen-input semantics the worker uses, so a script that crashes locally crashes the same way in production.
198
+ - `workflow step exec ai.extract --input <path|-> --prompt <path|-> [--schema <path>] [--provider openai] [--model gpt-4o-mini] [--base-url <url>]` — one LLM call, structured output when `--schema` is provided, raw assistant text when omitted. Reads provider/model from `WORKER_LLM_*` env vars (or `OPENAI_API_KEY` for the openai default).
199
+
200
+ Both commands write the step output as JSON to stdout. Schema validation failures exit 2 with `{ code: 'output_schema_violation', issues: [{ field, message }] }` on stderr — agent-readable so the iteration loop is autonomous.
201
+
202
+ `@` path syntax (`--code @file.js`, `--inputs items=@arr.json`) matches `curl -d @file` / `gh --body-file -`. Bare paths and `-` (stdin) work too.
203
+
204
+ - 3da0612: All list commands now support `--limit` / `--offset` pagination consistently.
205
+ - CLI: `workflow execution list`, `workflow version list`, and `workflow step-type list` previously had no `--offset` (or no pagination at all). All six list commands now accept `--limit`/`--offset` with defaults of 50/0.
206
+ - Server: `GET /api/workflows/:id/versions` now reads `?limit=` and `?offset=` query params (defaults 50/0; max limit 100). The `listVersions` repo method gained an optional `{ limit, offset }` argument.
207
+ - `step-type list` paginates locally over the static catalog so the UX is uniform; `total` reflects the full filtered set so callers can see how many rows are hidden by the page window.
208
+
209
+ - 3da0612: API: list endpoints now consistently return `{ data, total }`.
210
+ - `GET /api/workflows/:id/experiments` previously returned `{ experiments }` — now returns `{ data, total }`.
211
+ - `GET /api/workflows/:id/versions` previously returned a bare array — now returns `{ data, total }`.
212
+
213
+ This is a breaking change for direct API consumers. The CLI, frontend, and `listWorkflowVersions` contract are all updated. CLI's `renderListResult` helper is simpler (single envelope, no per-endpoint `dataKey` workaround).
214
+
215
+ - d41ee8b: Add workflow-agnostic eval-results export endpoint
216
+ (`GET /api/v1/eval-results/export?batchId=...&format=csv|json`) that
217
+ resolves the owning workflow from the batch id server-side. The CLI's
218
+ `workflow experiment compare batchA batchB` no longer requires
219
+ `--workflow-id` — the flag is dropped (breaking change) and both batches
220
+ are resolved through the new endpoint instead. The legacy workflow-scoped
221
+ route stays in place for back-compat.
222
+
223
+ Document the CLI exit-code contract (0 = success, 1 = runtime error,
224
+ 2 = misuse / recoverable failure) in `packages/cli/CLAUDE.md`.
225
+
226
+ - 3da0612: CLI: cleaner workflow command tree + matching `--json` shape.
227
+
228
+ **Tree restructure** — the `workflow definition` namespace is gone. Its three verbs are core operations on the workflow itself, so they move up one level:
229
+
230
+ | Before | After |
231
+ | ------------------------------ | ---------------------------------------- |
232
+ | `workflow definition list` | `workflow list` |
233
+ | `workflow definition push` | `workflow push` |
234
+ | `workflow definition pull` | `workflow pull` |
235
+ | `workflow definition validate` | dropped (use `workflow validate [path]`) |
236
+ | `workflow version list` | `workflow versions list` (plural) |
237
+ | `workflow version restore` | `workflow versions restore` |
238
+
239
+ `evaluators`, `dataset`, `experiment`, `execution`, and `step-type` keep their nested structure.
240
+
241
+ **`--json` returns the table's columns, not the server envelope.** Previously `--json` dumped `{ data: [{...full server rows}], total }`. Now it emits an array containing exactly the fields the table renders (e.g. `[{id, name, version, updatedAt}]` for `workflow list`). `total` still goes to stderr in both modes.
242
+
243
+ **`--verbose`** opts into the firehose: `--json --verbose` emits the full server rows for the rare cases you need fields the table doesn't surface.
244
+
245
+ `workflow list --json | jq '.[0].id'` — the natural shape for piping.
246
+
247
+ ### Patch Changes
248
+
249
+ - 71361fd: `auth login`: simplified flow. Removed the dead localhost-callback server (the
250
+ dashboard never POSTed to it — every login fell through to copy-paste anyway).
251
+ The browser now opens to `/developers/api-keys?from=cli` with a clearer prompt
252
+ ("Create a new key, copy it, paste below"), and the API key prompt **hides
253
+ input** as you type or paste — same as `gh auth login`, `stripe login`, etc.
254
+ - d41ee8b: CLI: two cleanups for the audit follow-ups.
255
+ - **`init agent` and bare `agent` now exit 2** instead of 0 when invoked. Both subcommands are reserved-but-unimplemented placeholders, and exiting 0 made it possible for a CI script that misrouted into one of them to silently no-op. POSIX exit 2 ("command exists but isn't usable as invoked") makes the misroute fail loudly. The human-readable "coming soon" stderr message is unchanged.
256
+ - **New `--no-color` global flag.** Disables ANSI colors in all output, matching the convention shared by `gh`, `kubectl`, `npm`, and `terraform`. Equivalent to setting `NO_COLOR=1` in the environment, but takes effect for the current invocation only. Implemented as a lazy picocolors wrapper so the flip propagates to every helper in `lib/ui.ts` (and every downstream caller of `pc`) regardless of when in the command lifecycle the flag is parsed.
257
+
258
+ - b1d2d77: CLI: default `baseUrl` is now `https://studio.eigenpal.com` instead of `http://localhost:3000`.
259
+
260
+ Affects only the fallback when nothing else is set — the priority chain is `--base-url > EIGENPAL_BASE_URL > active profile > default`. Users who ran `eigenpal auth login` against any environment already have a `baseUrl` in their profile and are unaffected. CI scripts that set `EIGENPAL_API_KEY` but relied on the implicit `localhost:3000` will now hit production; set `EIGENPAL_BASE_URL=http://localhost:3000` if you want the old behavior.
261
+
262
+ Aligns with the gh / vercel / kubectl convention of falling back to the public service rather than localhost.
263
+
264
+ Also scrubs internal references from CLI source ahead of open-sourcing — generic placeholder names in docstrings and test fixtures, internal-tracker IDs stripped from comments, and one user-facing error message updated to point at the public issue tracker.
265
+
266
+ - be693a3: `EIGENPAL_API_KEY` is now a complete profile bypass — when set, the active profile's `baseUrl` is also ignored (previously the env key was paired with the profile's URL, which is always wrong because an API key is provisioned for one server).
267
+
268
+ In CI:
269
+ - **Cloud:** set `EIGENPAL_API_KEY=eig_live_…` and you get `https://studio.eigenpal.com`.
270
+ - **On-prem:** set both `EIGENPAL_API_KEY=…` and `EIGENPAL_BASE_URL=https://eigenpal.example.com`.
271
+
272
+ Interactive profiles are unchanged: when you log in via `eigenpal auth login`, the profile stores both apiKey and baseUrl, and every subsequent command derives baseUrl from that profile. Set `EIGENPAL_PROFILE=<name>` to switch profiles per-shell without modifying state.
273
+
274
+ - d41ee8b: CLI: fix `eigenpal status` error UX + harden `formatCliError` for real-world connection failures.
275
+ - `eigenpal status` against an unreachable base URL no longer mis-diagnoses the failure as an auth problem. Previously, `EIGENPAL_BASE_URL=http://localhost:9999 eigenpal status` printed "Authentication check failed; try `eigenpal auth login`" — the wrong rabbit hole. The status command now rethrows non-auth errors (connection refused, DNS NXDOMAIN, timeout, HTML responses, 5xx) so `formatCliError` upstream can surface the right hint: `Could not connect to the server at <url> ... Set EIGENPAL_BASE_URL or pass --base-url <url>.` Only real 401/403 responses are treated as auth failures.
276
+ - `formatCliError` now detects connection failures wrapped in any Error shape — bare `Error('ENOTFOUND ...')`, `Error` with `code: 'ECONNREFUSED'`, and `TypeError('fetch failed')` whose `.cause` carries the underlying error. Previously only `TypeError` whose message contained `fetch`/`URL` was recognized, so `getaddrinfo`-style errors fell through to the generic raw-message branch and lost the URL context.
277
+
278
+ - d41ee8b: CLI: fix `workflow experiment compare --sort` advertised values + thread base URL into error rendering.
279
+ - `workflow experiment compare --sort` now advertises all four valid values (`abs-delta-desc`, `delta-asc`, `delta-desc`, `name`) in `--help` and in the error message you get on a typo. Previously the help/error listed three; the default `abs-delta-desc` was accepted but undocumented.
280
+ - The `action()` decorator that wraps every `workflow ...` handler now resolves the base URL from the parsed opts and forwards it to `printApiError`. Connection failures (`fetch failed`, invalid base URL) now echo the URL the CLI tried to reach — set via `--base-url`, `EIGENPAL_BASE_URL`, the active profile, or the default — so the user can see at a glance whether they pointed at the wrong server.
281
+ - Note: the `--jq` rename / honesty pass for `--output-path` is deferred to a follow-up to avoid a merge conflict with the in-flight `execution cancel` change.
282
+
283
+ - d41ee8b: CLI: validate `workflow experiment results --format` locally; extract `experiment compare` helpers into a sibling file.
284
+ - `workflow experiment results --format` now rejects bad values up-front via a Commander `InvalidArgumentError` parser-fn (modeled after `intArg`). `--format yaml` previously round-tripped to the server and returned a generic HTTP 400; now the command exits 1 with `error: option '--format <csv|json>' argument 'yaml' is invalid. must be 'csv' or 'json'` before any network I/O happens.
285
+ - Internal refactor: `experiment compare`'s pure helpers (`buildBatchDiff`, `renderBatchDiffHuman`, `normalizeCompareSort`, `formatDelta`, `fetchEvalResults`, `exampleLabelOf`, plus their types) moved out of `commands/workflow/index.ts` into a new sibling `commands/workflow/experiment-compare.ts`. The Commander registration stays in `index.ts`. Tests follow the symbols into `experiment-compare.test.ts`. No user-visible behavior change from the refactor; `index.ts` shrinks by ~310 LOC.
286
+
287
+ - c82a215: Polish on `eigenpal auth login` based on DX review:
288
+ - **Browser fallback** — the prompt's note now repeats the API-keys URL prominently, so when `xdg-open`/`open`/`start` silently fails (headless VMs, WSL without xdg-open, restricted containers) the user has a copyable URL right next to the prompt instead of a dim status line above.
289
+ - **Outro is informative** — distinguishes "Saved profile X" (first login to this tenant) from "Updated profile X" (re-login overwrote prior creds in place) and shows the exact `eigenpal auth use X` command to switch to it later. No more raw `(profile org_xxxx)` dead-end.
290
+ - **Path-rejection error** suggests the corrected origin: instead of just "URL must be the server origin only", it now says e.g. "Try https://studio.eigenpal.com instead of '/api/v1'".
291
+ - **Non-TTY error copy** is consistent across CI and pipe-redirect paths — both name the missing piece (interactive terminal) and the workaround (`--base-url` / env var).
292
+
293
+ - be693a3: `eigenpal auth login` no longer opens your browser as the very first thing it does. The flow now mirrors mature CLIs (`gh auth login`, `vercel login`):
294
+ 1. **Pick the server** (existing select prompt).
295
+ 2. **Explain the flow** — a clear step-by-step note tells you what's about to happen and surfaces the dashboard URL prominently so you can copy it manually if you prefer.
296
+ 3. **Confirm browser launch** — "Open the dashboard in your browser?" (default Yes). Users on headless VMs, WSL without xdg-open, or who already have the dashboard open can answer No and skip the best-effort browser launch entirely.
297
+ 4. **Paste the API key** — same as before.
298
+
299
+ No surprise side effects, no "wait, why did a browser pop?" moment. The URL is also in the explanation note so a failed `openBrowser` doesn't strand you.
300
+
301
+ - c82a215: `package.json` now carries full npm metadata: `license: Apache-2.0`, `keywords`, `repository`, `bugs`, `homepage`, and `author`. The published tarball also bundles `LICENSE` and `CHANGELOG.md` so the npm page renders the license badge instead of "License: none" and surfaces real keywords for search.
302
+ - ee32a73: Three fixes around the public CLI mirror and login UX:
303
+ 1. **Public mirror keeps history.** `sync_public_cli` no longer wipes `github.com/eigenpal/cli`'s git history on every release. It now clones the existing public repo, replaces the working tree with the new synth output, and pushes one commit per release. Each release adds `release: @eigenpal/cli@X.Y.Z` on top of the previous one. Tags accumulate too.
304
+ 2. **CHANGELOG no longer starts from `0.0.0`.** The `Pin CLI version + apply pending changesets` step in `sync_public_cli` was running `bunx changeset version` _before_ pinning the source `0.0.0-placeholder` to the real release tag, so changeset wrote CHANGELOG headings like `## 0.0.1` while the actual published version was e.g. `0.4.2`. Pin first, then run changeset version, then sed-rewrite the topmost heading to match the real release tag.
305
+ 3. **Login picker copy + URL cleanup.**
306
+ - Hint `(saved profile)` had double parens because clack auto-wraps hints in parens — was rendering as `((saved profile) from acme)`. Now `saved profile: <names>`.
307
+ - Dropped `?from=cli` from the dashboard URL — the dashboard ignores the param, and a clean URL is easier to copy/paste/visit manually.
308
+
309
+ - c82a215: Public README cleanup:
310
+ - Removed the duplicated `# @eigenpal/cli` heading and redundant install snippet that was being prepended to the public mirror's README. The bundled CLI README is already self-contained, so the synth header was just stacking a second title on top.
311
+ - Reordered sections so users hit "what to install + what's available" first: Install → Commands → Use it → Primitives. Primitives is the conceptual reference; it now sits below the hands-on sections instead of above them.
312
+
313
+ - ac95906: CLI: `eigenpal skill` now installs into 9 popular AI coding tools instead of 2.
314
+
315
+ Previously the picker only listed Claude Code and Cursor. The new set covers:
316
+ `claude`, `cursor`, `codex`, `gemini`, `antigravity`, `opencode`, `pi`,
317
+ `windsurf`, `github-copilot`. Each tool follows the same
318
+ `<root>/skills/eigenpal/SKILL.md` Anthropic-skill convention (paths mirror
319
+ OpenSpec's canonical mapping) so a single skill bundle drops into whichever
320
+ tool you already have installed.
321
+
322
+ Detection heuristics were also broadened — each tool now matches against a
323
+ list of sentinel files (e.g. `.claude` or `CLAUDE.md` for Claude Code; the
324
+ several Copilot config paths for GitHub Copilot) instead of a single
325
+ directory. The picker surfaces installed tools first, then detected, then
326
+ the rest.
327
+
328
+ Power-user flags are unchanged: `--target` for a custom path, `--tools` for
329
+ a comma-separated id list, `--force` / `--yes` for non-interactive flows.
330
+
331
+ - 158d513: **Breaking change.** `eigenpal skill` is now a noun namespace with three explicit verbs: `install`, `uninstall`, `list`. The bare `eigenpal skill` invocation exits 2 with a usage error.
332
+
333
+ Before:
334
+
335
+ ```bash
336
+ eigenpal skill # interactive picker (install + toggle-uninstall in one)
337
+ eigenpal skill --tools claude,cursor --yes # scripted install
338
+ eigenpal skill --tools "" --yes # uninstall everything
339
+ ```
340
+
341
+ After:
342
+
343
+ ```bash
344
+ eigenpal skill install # interactive picker (toggle-uninstall still works here)
345
+ eigenpal skill install --tools claude,cursor --yes
346
+ eigenpal skill uninstall claude cursor # explicit removal
347
+ eigenpal skill uninstall --all --yes # wipe every install (CI / scripts)
348
+ eigenpal skill uninstall # picker showing only installed tools
349
+ eigenpal skill list # table of installed tools + CLI version + timestamp
350
+ eigenpal skill list --json # `{ data, total }` envelope for scripting
351
+ ```
352
+
353
+ Mirrors `eigenpal auth login/logout/list/use` — every other CLI surface follows kind → noun → verb; `skill` was the only one-shot exception. Now uniform.
354
+
355
+ Migration: scripts using `eigenpal skill` should switch to `eigenpal skill install`. The `--target` / `--tools` / `--force` / `--yes` flags moved to `install` unchanged.
356
+
357
+ - c330c2a: CLI skill (installed by `eigenpal skill`) updated to cover the recently-added command surface:
358
+ - `SKILL.md` — three new recipes (local single-step iteration via `workflow step exec`, `workflow execution cancel`, `workflow experiment compare` without `--workflow-id`), an "Output convention" section (stdout=data / stderr=status / `--json`) with the 0/1/2 exit-code contract, `workflow push --bump/--set-version` mention, `workflow evaluator-type {list,get}` parallel to `step-type`.
359
+ - New `reference/step-exec.md` — full deep-dive on `workflow step exec` (the iteration killer for agents): supported step types, `--inputs k=v` grammar, `--config-{json,file}`, `--output-schema` with the `{ code: 'output_schema_violation', issues: [...] }` envelope, resource limits, two worked examples.
360
+ - `reference/debugging.md` — added `skipped` status + `skippedReason` + `resolvedConfig` field to the inspection section, full `execution cancel` section (cooperative; honored inside `control.foreach` / `control.parallel` / `control.block`), full `experiment compare` section, connection-vs-auth error distinction.
361
+ - `reference/workflow-yaml.md` — `--bump` / `--set-version` push flags + `evaluator-type` introspection.
362
+ - New `reference/cli/*.md` — autogen flag reference for every command (auth, status, init, agent, completion, skill, workflow), copied from `packages/cli/docs/` by `generate-skill-reference.ts` so agents have offline flag-level reference without `--help` round-trips. CI's `--check` mode catches drift.
363
+
364
+ Re-run `eigenpal skill` after upgrading to install the new content.
365
+
366
+ - c82a215: Removed all `localhost:3000` references from user-visible help text and source comments. The `auth login --help` examples no longer suggest `--base-url http://localhost:3000` — local development is an internal concern, not something CLI users need to think about. Defaults still target `https://studio.eigenpal.com`; advanced users with their own deployments can still override via `--base-url` or `EIGENPAL_BASE_URL`.
367
+ - c82a215: `eigenpal --version` now prints `dev` for unpublished local builds instead of the confusing `0.0.0-placeholder`. If you ever see `dev`, the binary on your PATH is a local source build (e.g., `bun cli:install:local`), not the npm-published one — run `which -a eigenpal` to find it.
368
+
369
+ CI publish also gained a hard check that fails the release if `dist/cli.js --version` doesn't match the `release_tag`, so a placeholder version can never reach users via npm.
370
+
371
+ - 71361fd: `eigenpal --version` (and `-v`) now reports the actual published package version instead of a hardcoded `0.0.1`. For local dev (`eigenpal-dev`) the placeholder value (`0.0.0-placeholder`) makes it obvious the bundle came from a workspace build, not npm.
372
+ - 71361fd: Dataset folder format made symmetric — `expected/` mirrors `input/` exactly:
373
+ - `input/arguments.json` (scalar args) ↔ `expected/output.json` (raw data ground truth, **no envelope wrapper**)
374
+ - `input/<argName>/<file>` (input file binary) ↔ `expected/<docKey>/<file>` (expected file output binary)
375
+
376
+ `expected/output.json` is now the raw user JSON, mirroring the workflow's `output:` shape 1:1. Drop the `{ data, expectedDocuments }` envelope requirement at the dataset boundary; the server still stores the envelope shape internally but the translation happens at import/export time, just like input file uploads. This is a pre-launch breaking change to the dataset folder layout — re-export your datasets to pick up the cleaner shape.
377
+
378
+ Server import walks `expected/<docKey>/` folders, uploads binaries via `evalExpectedKey`, builds the `expectedDocuments` map server-side. Server export reverses: writes `expectedOutput.data` as raw `output.json`, emits each `expectedDocuments[docKey]` entry as a binary under `expected/<docKey>/<filename>`. Round-trip clean.
379
+
380
+ CLI `validate dataset` accepts any JSON object as `output.json` (drops the envelope check), and validates `expected/<docKey>/` folder names match the same `[a-z0-9][a-z0-9-_]*` pattern as input arg folders. `reference/dataset-format.md` documents the new symmetric layout with a worked example.
381
+
382
+ - c330c2a: DX review — close 8 sharp edges that surfaced during long debugging loops:
383
+ - `execution get --output-path` no longer prints the literal string `"undefined"` and exits 0 on a missed path. Now exits 2 with a structured error listing the top-level keys + step names actually present. (#3)
384
+ - `workflow set-dataset --file <folder>` now works directly — the CLI zips the folder in memory before upload (matches the skill's recipes which always said `--file ./dataset/`). Pre-zipped archives still work. (#5)
385
+ - Skill recipes (`SKILL.md`, `reference/dataset-format.md`) no longer reference unregistered commands (`workflow create-example/update-example/delete-example`). Replaced with the working `set-dataset --mode replace` recipe; per-example CRUD will land later. (#6)
386
+ - `requireApiKey` now renders through the `lib/ui.ts` envelope (red `✗` icon, dimmed hint lines) instead of raw `console.error`. Matches the rest of the CLI's error visual. (#8)
387
+ - `eigenpal exec` prints `→ <example>: <executionId>` to stderr immediately when the execute API returns. If polling or artifact-write fails, the user still has a handle to recover with `execution get <id>`. (#12)
388
+ - `workflow get-experiment-results`, `get-eval-results`, `get-dataset` accept `--out` as optional. When omitted, the binary streams to stdout — pipe to `jq`, `> file.csv`, etc. Status line moved to stderr so stdout stays clean. (#15)
389
+ - `auth list` shows the canonical `org_…` tenant id alongside the display name, so two profiles for the same tenantName are distinguishable. (#19)
390
+ - `execution watch` exits 2 (timeout) on 30-min auto-detach instead of 0, so CI / agents distinguish "still running" from "completed". (#21)
391
+
392
+ Adds `fflate` to `@eigenpal/cli` deps for the in-memory dataset folder zipping.
393
+
394
+ - 71361fd: **Breaking (pre-launch):** `expected/output.json` in dataset folders must follow the system-wide envelope `{ data?: object, expectedDocuments?: object }` — the same shape `eval_examples.expectedOutput` uses everywhere else. Put the workflow output you want to compare under `data`. Raw top-level objects (e.g. `{ header, covenants }`) are now rejected with `code: invalid_expected_output`.
395
+
396
+ `eigenpal validate dataset` enforces this locally; the import endpoint enforces it server-side. Replaces the previous import-time auto-wrap (which silently mutated user input).
397
+
398
+ - 71361fd: `workflow get-experiment-status --watch` now prints a per-example failure summary on stderr when the batch finishes with any failed/cancelled execution: example name, top-level error (truncated), and the exact `eigenpal execution get <exec-id>` command to drill into step-by-step detail. Removes the need for the agent to grep through a 1000-line JSON dump to find which example broke and why. `reference/debugging.md` documents the discovery path.
399
+ - 71361fd: `eigenpal workflow get-experiment-status --watch` now polls until every execution in the batch reaches a terminal state (`completed` / `failed` / `cancelled`), then exits non-zero if any execution failed or was cancelled. Removes the need for agent-side polling loops that didn't notice all-terminal-already and kept ticking. Renders a one-line `N/M terminal (counts)` progress summary on stderr (in-place rewrite when stderr is a TTY), dumps the full JSON on stdout when done. `--interval <s>` and `--max-wait <s>` are configurable; defaults are 5s/30min.
400
+ - 71361fd: `install-skill`: multi-tool support via interactive multiselect.
401
+ - Opens a `@clack/prompts` multiselect picker over supported agent tools
402
+ (Claude Code, Cursor). Toggle on to install, toggle off to uninstall —
403
+ one command covers both directions, so the separate `uninstall-skill`
404
+ command is removed.
405
+ - Picker pre-selects tools that already have an Eigenpal install or
406
+ whose project directory exists in the cwd.
407
+ - Non-interactive: pass `--tools <ids>` (comma-separated) to skip the
408
+ picker; tools not listed get uninstalled. `--yes` keeps the currently
409
+ installed set.
410
+ - Help description and CLI grouping no longer hard-code Claude Code.
411
+
412
+ - f13cd4d: Release commits on the public CLI mirror (`github.com/eigenpal/cli`) are now authored by the **EigenPal Release Pal** GitHub App instead of an anonymous bot identity. The App has its own logo and renders as `eigenpal-release-pal[bot]` next to each commit, with the human who triggered the release shown as co-author. No user-visible CLI behavior change — the install path and tarball contents are identical.
413
+ - 71361fd: Review + simplification pass on the recent eval/dataset/error work:
414
+
415
+ **Bug fixes** surfaced by code review:
416
+ - `workflow get-experiment-status --watch` exited 0 when the first poll raced before the server enqueued executions (`0/0 terminal` was vacuously "done"). Now requires `total > 0` to consider the batch terminal.
417
+ - `workflow set-dataset` exited 0 on an unrecognized terminal NDJSON phase (truncated stream or future server-side phase). Now exits 1 with a clear `Import response had unexpected terminal phase` message.
418
+ - Dataset import silently dropped extra files when a folder under `input/<argName>/` had multiple binaries but the workflow input was declared `type: file` (single). Now rejects up front with `code: single_file_input_overpopulated` and a hint to either trim the folder or change the input to the array form.
419
+ - Misleading import error: entries under `expected/` that didn't fit the `<docKey>/<file>` pattern got an error message saying "entry under input/ ..." with an `input/` example. Message now branches on the actual parent.
420
+ - `validate dataset` could crash on broken symlinks / FIFOs. `statSync` now wrapped in try/catch with a structured "unreadable filesystem entry" issue.
421
+
422
+ **Simplifications** (net −21 LOC after both passes):
423
+ - Extracted a `uploadFolder` helper inside `uploadAllExampleBinaries` — the input and expected file-upload loops share a single body.
424
+ - `set-dataset` extracted `postMultipart` and `parseNdjsonEvents` helpers.
425
+ - Removed the unused `validateOrThrow` shim and the documentary `extractExpectedData` / `extractActualData` aliases.
426
+ - `eval/route.ts`: extracted shared `fromBodyExample` mapper and `loadArtifact` helper.
427
+
428
+ **Skill docs** (the agent's pain point): `reference/dataset-format.md` + `reference/workflow-yaml.md` now spell out the workflow-YAML-input → `triggerInput` shape mapping in a table — `type: file` materializes as `{fileId}` (template `{{ input.<name> }}`), `type: array, items: { type: file }` materializes as `[{fileId}, ...]` (template `{{ input.<name>[0] }}`). Removes the ambiguity that led to incorrect `[0]` indexing on single-file inputs.
429
+
430
+ - 71361fd: Polish + simplify CLI UX.
431
+ - `install-skill`: project-level only (`./.claude/skills/eigenpal/`). The
432
+ `--scope user|project` flag and the user-level (`~/.claude/skills/`)
433
+ install path are removed — workflow projects are the unit of work, the
434
+ skill belongs alongside them.
435
+ - Switch interactive prompts to `@inquirer/prompts`: arrow-key selection
436
+ for `init` template picker and the `install-skill` 3-way conflict
437
+ resolver, hidden-input `password` for `auth login`, proper
438
+ yes/no `confirm` for `uninstall-skill`.
439
+ - Color output via `picocolors`: green checkmarks for success, red for
440
+ errors, cyan for info hints, dim for secondary detail. Honors
441
+ `NO_COLOR` and non-TTY stdout automatically.
442
+
443
+ - 71361fd: Add a "Common recipes" section to the bundled skill (`SKILL.md`) covering the typical end-to-end command sequences agents need: scaffolding a workflow, validating + pushing workflow YAML, validating + pushing evaluators, building + uploading a dataset (and editing examples server-side), running an experiment + collecting results, and debugging a failing execution.
444
+ - 71361fd: Generate the skill reference (step types, evaluators, workflow YAML) from the canonical Zod schemas in `@eigenpal/types` on every build, with a CI gate that blocks PRs whose schema changes don't ship the regenerated docs.
445
+ - 71361fd: Tighten error-message quality across the dataset/eval/execute pipeline so the CLI surfaces actionable feedback instead of opaque dumps.
446
+ - `eigenpal workflow set-dataset` now decodes the import endpoint's NDJSON terminal event: `aborted` renders the structured `issues[]` with an aligned field column and exits non-zero; `done` warns loudly when `expectedSet < created` so missing `expected/output.json` files don't slip through silently. Adds smart hints for `invalid_expected_output` and `argument_name_collision` codes.
447
+ - Two endpoints (`POST /api/v1/execute`, `POST /api/v1/eval`) replaced their legacy `{ error, details }` payloads with the structured envelope (`{ issues, hint, requestId, docsUrl }`) the CLI's `printApiError` already consumes.
448
+ - `exact-diff` evaluator no longer falls back to "compare whole envelope when `.data` is missing." That fallback silently absorbed the import shape bugs we just fixed; it now requires `.data` on both sides (matching the system-wide `WorkflowResult` and `ExpectedOutputSchema` contracts) so any future shape regression fails loudly.
449
+ - Exports `printIssues` from the CLI's `validate` command so other commands reuse the same field-aligned issue formatter.
450
+
451
+ - 71361fd: Structured validation errors with introspection hints. `workflow set-definition`
452
+ (and version creation) used to fail with an opaque `"Invalid workflow definition"`
453
+ that left agents stuck. Now the response carries the canonical `{ issues, hint,
454
+ docsUrl, requestId }` envelope:
455
+ - Each issue has a dotted field path (`steps.2.config.passThreshold`) and a
456
+ human message — the agent can target the exact YAML line.
457
+ - The hint points at `eigenpal workflow get-step-type <type>` when the failure
458
+ is scoped to a single step type, or `eigenpal workflow list-step-types` for
459
+ general schema browsing. Both commands read the authoritative catalog from
460
+ `@eigenpal/types` — no docs scraping, no guessing.
461
+ - YAML syntax errors include line/column when available.
462
+
463
+ CLI renders the envelope with an aligned field column, the hint inline, and
464
+ the request id for correlation. Legacy `{ error, details }` responses still
465
+ render best-effort.
package/README.md CHANGED
@@ -7,14 +7,24 @@ Create, evaluate, and deploy workflows from your terminal. Agent-ready.
7
7
  [![license](https://img.shields.io/badge/license-Apache--2.0-3B5BDB?labelColor=555)](https://github.com/eigenpal/cli/blob/main/LICENSE)
8
8
  [![docs](https://img.shields.io/badge/docs-eigenpal%2Fcli-3B5BDB?labelColor=555)](https://github.com/eigenpal/cli)
9
9
 
10
- ## Primitives
10
+ ## Install
11
11
 
12
- | Primitive | Purpose |
13
- | -------------- | ------------------------------------------------------- |
14
- | **Workflow** | Versioned DAG of steps. The thing that runs in prod. |
15
- | **Dataset** | `(input, expected_output)` examples. Your ground truth. |
16
- | **Evaluator** | Scorers: LLM judge, exact match, custom. |
17
- | **Experiment** | Batch run of a workflow over a dataset, scored. |
12
+ ```bash
13
+ npm i -g @eigenpal/cli
14
+ eigenpal auth login # or set EIGENPAL_API_KEY in CI
15
+ eigenpal skill install # install skill for your agent
16
+ ```
17
+
18
+ ## Commands
19
+
20
+ | Command | Purpose |
21
+ | ----------------------------------------- | ----------------------------------------------------- |
22
+ | [`eigenpal status`](./docs/status.md) | Server, tenant, user, key id, workflow count. |
23
+ | [`eigenpal init`](./docs/init.md) | Scaffold a `workflow` or `agent` project. |
24
+ | [`eigenpal auth`](./docs/auth.md) | Profile management. |
25
+ | [`eigenpal workflow`](./docs/workflow.md) | Workflow, evaluators, dataset, experiment, execution. |
26
+ | [`eigenpal agent`](./docs/agent.md) | Agent ops (coming soon). |
27
+ | [`eigenpal skill`](./docs/skill.md) | Install the agent skill across AI tools. |
18
28
 
19
29
  ## Use it
20
30
 
@@ -35,27 +45,27 @@ eigenpal workflow experiment run <id> # batch eval
35
45
  eigenpal workflow execution list <id> --json | jq # query as JSON
36
46
  ```
37
47
 
38
- ## Install
48
+ ## Primitives
39
49
 
40
- ```bash
41
- npm i -g @eigenpal/cli
42
- eigenpal auth login # or set EIGENPAL_API_KEY in CI
43
- eigenpal skill install # install skill for your agent
44
- ```
50
+ | Primitive | Purpose |
51
+ | -------------- | ------------------------------------------------------- |
52
+ | **Workflow** | Versioned DAG of steps. The thing that runs in prod. |
53
+ | **Dataset** | `(input, expected_output)` examples. Your ground truth. |
54
+ | **Evaluator** | Scorers: LLM judge, exact match, custom. |
55
+ | **Experiment** | Batch run of a workflow over a dataset, scored. |
45
56
 
46
- ## Commands
57
+ ## Environment variables
47
58
 
48
- | Command | Purpose |
49
- | ----------------------------------------- | ----------------------------------------------------- |
50
- | [`eigenpal status`](./docs/status.md) | Server, tenant, user, key id, workflow count. |
51
- | [`eigenpal init`](./docs/init.md) | Scaffold a `workflow` or `agent` project. |
52
- | [`eigenpal auth`](./docs/auth.md) | Profile management. |
53
- | [`eigenpal workflow`](./docs/workflow.md) | Workflow, evaluators, dataset, experiment, execution. |
54
- | [`eigenpal agent`](./docs/agent.md) | Agent ops (coming soon). |
55
- | [`eigenpal skill`](./docs/skill.md) | Install the agent skill across AI tools. |
59
+ Most users never need to set these — `eigenpal auth login` writes a profile to `~/.config/eigenpal/credentials.json` and every command derives its config from there. Reach for env vars when you can't run an interactive login (CI), need to switch context for one shell, or want to override a single field without editing the credentials file.
60
+
61
+ | Variable | Purpose |
62
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
63
+ | `EIGENPAL_API_KEY` | Bypass the profile entirely. Set in CI to skip `auth login`. When set, `EIGENPAL_BASE_URL` (or the cloud default) is used — the active profile is **not** consulted, so a stale profile can't redirect a CI run to the wrong server. |
64
+ | `EIGENPAL_BASE_URL` | Override the server URL for one command or shell. Pairs with `EIGENPAL_API_KEY` to point CI at an on-prem deployment. Without `EIGENPAL_API_KEY`, this overrides whatever the active profile would have used. |
65
+ | `EIGENPAL_PROFILE` | Switch the active profile for one shell without touching `~/.config/eigenpal/credentials.json`. Useful for ad-hoc context switches: `EIGENPAL_PROFILE=staging eigenpal status`. Persistent equivalent: `eigenpal auth use <name>`. |
66
+ | `EIGENPAL_DIR` | Override the workflow project directory used by `init` / `validate`. Defaults to `./eigenpal`. |
56
67
 
57
- Full per-command reference in [`./docs/`](./docs/). `eigenpal --help` for flags.
58
- Stdout is data, stderr is status. Exit `0` ok, `1` runtime, `2` misuse.
68
+ Resolution precedence: command-line flags > env vars > active profile > defaults.
59
69
 
60
70
  ## Support
61
71
 
package/dist/cli.js CHANGED
@@ -155990,8 +155990,31 @@ import { fileURLToPath as fileURLToPath6 } from "url";
155990
155990
  // package.json
155991
155991
  var package_default = {
155992
155992
  name: "@eigenpal/cli",
155993
- version: "0.4.1",
155994
- description: "Eigenpal CLI — author and run document workflows from your terminal.",
155993
+ version: "0.4.3",
155994
+ description: "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
155995
+ keywords: [
155996
+ "eigenpal",
155997
+ "cli",
155998
+ "ai",
155999
+ "agent",
156000
+ "workflow",
156001
+ "llm",
156002
+ "evals",
156003
+ "experiments",
156004
+ "document",
156005
+ "extraction",
156006
+ "ocr"
156007
+ ],
156008
+ homepage: "https://github.com/eigenpal/cli#readme",
156009
+ bugs: {
156010
+ url: "https://github.com/eigenpal/cli/issues"
156011
+ },
156012
+ repository: {
156013
+ type: "git",
156014
+ url: "git+https://github.com/eigenpal/cli.git"
156015
+ },
156016
+ license: "Apache-2.0",
156017
+ author: "Eigenpal <hello@eigenpal.com> (https://eigenpal.com)",
155995
156018
  type: "module",
155996
156019
  main: "./src/index.ts",
155997
156020
  types: "./src/index.ts",
@@ -156034,7 +156057,9 @@ var package_default = {
156034
156057
  },
156035
156058
  files: [
156036
156059
  "dist",
156037
- "README.md"
156060
+ "LICENSE",
156061
+ "README.md",
156062
+ "CHANGELOG.md"
156038
156063
  ]
156039
156064
  };
156040
156065
 
@@ -156523,6 +156548,7 @@ function writeRecordCountHint(shown, total, label) {
156523
156548
  }
156524
156549
 
156525
156550
  // src/commands/auth.ts
156551
+ var CLOUD_BASE_URL = "https://studio.eigenpal.com";
156526
156552
  function openBrowser(url2) {
156527
156553
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
156528
156554
  exec(`${cmd} "${url2}"`);
@@ -156534,19 +156560,130 @@ function exitOnCancel(value) {
156534
156560
  }
156535
156561
  return value;
156536
156562
  }
156537
- async function authLogin(baseUrl) {
156563
+ function normalizeBaseUrl(input) {
156564
+ const trimmed = input.trim();
156565
+ if (!trimmed)
156566
+ return { ok: false, error: "Enter a URL." };
156567
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
156568
+ const candidate = hasScheme ? trimmed : `https://${trimmed}`;
156569
+ let parsed;
156570
+ try {
156571
+ parsed = new URL(candidate);
156572
+ } catch {
156573
+ return { ok: false, error: "Not a valid URL." };
156574
+ }
156575
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
156576
+ return {
156577
+ ok: false,
156578
+ error: `Unsupported scheme "${parsed.protocol.replace(":", "")}" — use http or https.`
156579
+ };
156580
+ }
156581
+ const trailingSlashOnly = /^\/*$/.test(parsed.pathname);
156582
+ if (!trailingSlashOnly || parsed.search || parsed.hash) {
156583
+ return {
156584
+ ok: false,
156585
+ error: `URL must be the server origin only (no path/query). Try ${parsed.origin} instead of "${parsed.pathname}${parsed.search}${parsed.hash}".`
156586
+ };
156587
+ }
156588
+ if (!parsed.hostname) {
156589
+ return { ok: false, error: "URL is missing a hostname." };
156590
+ }
156591
+ const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]";
156592
+ const url2 = parsed.origin.replace(/\/+$/, "");
156593
+ const insecure = parsed.protocol === "http:" && !isLoopback;
156594
+ return { ok: true, url: url2, insecure };
156595
+ }
156596
+ async function pickBaseUrlInteractive() {
156597
+ const onPrem = new Map;
156598
+ for (const [name, creds] of Object.entries(listProfiles().profiles)) {
156599
+ const raw = creds.baseUrl?.trim();
156600
+ if (!raw)
156601
+ continue;
156602
+ const norm2 = normalizeBaseUrl(raw);
156603
+ if (!norm2.ok)
156604
+ continue;
156605
+ if (norm2.url === CLOUD_BASE_URL)
156606
+ continue;
156607
+ const names = onPrem.get(norm2.url) ?? [];
156608
+ names.push(name);
156609
+ onPrem.set(norm2.url, names);
156610
+ }
156611
+ const options = [
156612
+ { value: CLOUD_BASE_URL, label: `EigenPal Studio (${CLOUD_BASE_URL})` }
156613
+ ];
156614
+ for (const [url2, profiles] of onPrem) {
156615
+ options.push({
156616
+ value: url2,
156617
+ label: url2,
156618
+ hint: `saved profile: ${profiles.join(", ")}`
156619
+ });
156620
+ }
156621
+ options.push({ value: "__custom__", label: "Custom URL…", hint: "paste your own" });
156622
+ const picked = exitOnCancel(await ve({
156623
+ message: "Authenticate against",
156624
+ options,
156625
+ initialValue: CLOUD_BASE_URL
156626
+ }));
156627
+ if (picked !== "__custom__")
156628
+ return picked;
156629
+ const custom2 = exitOnCancel(await he({
156630
+ message: "Server URL",
156631
+ placeholder: "https://eigenpal.example.com",
156632
+ validate: (value) => {
156633
+ const norm2 = normalizeBaseUrl(value);
156634
+ return norm2.ok ? undefined : norm2.error;
156635
+ }
156636
+ }));
156637
+ const norm = normalizeBaseUrl(custom2);
156638
+ if (!norm.ok) {
156639
+ error48(norm.error);
156640
+ process.exit(1);
156641
+ }
156642
+ if (norm.insecure)
156643
+ warnInsecure();
156644
+ return norm.url;
156645
+ }
156646
+ function warnInsecure() {
156647
+ M2.warn(`Using ${ui.bold("http://")} against a non-loopback host — credentials and traffic are sent unencrypted.`);
156648
+ }
156649
+ async function authLogin(flagBaseUrl) {
156538
156650
  if (env.CI === "true" && process.stdin.isTTY !== true) {
156539
- error48("Refusing to run `auth login` in CI: set EIGENPAL_API_KEY (and optionally EIGENPAL_BASE_URL) in your CI environment instead.");
156651
+ error48("`auth login` needs an interactive terminal. In CI, set EIGENPAL_API_KEY (and optionally EIGENPAL_BASE_URL) directly instead.");
156540
156652
  process.exit(1);
156541
156653
  }
156542
- const settingsUrl = `${baseUrl}/developers/api-keys?from=cli`;
156543
156654
  Ie(ui.bold("Eigenpal — sign in"));
156544
- M2.step(`Opening browser at ${ui.dim(settingsUrl)}`);
156545
- openBrowser(settingsUrl);
156546
- Me(`Create a new key (suggested name: ${ui.bold('"Eigenpal CLI"')}), copy it,
156547
- and paste it below. Input is hidden.`, "Next");
156655
+ const explicit = flagBaseUrl?.trim() || env.EIGENPAL_BASE_URL?.trim();
156656
+ let baseUrl;
156657
+ if (explicit) {
156658
+ const norm = normalizeBaseUrl(explicit);
156659
+ if (!norm.ok) {
156660
+ error48(`Invalid base URL "${explicit}": ${norm.error}`);
156661
+ process.exit(2);
156662
+ }
156663
+ if (norm.insecure)
156664
+ warnInsecure();
156665
+ baseUrl = norm.url;
156666
+ } else {
156667
+ if (process.stdin.isTTY !== true) {
156668
+ error48("`auth login` needs an interactive terminal to pick a server. Pass --base-url <url> or set EIGENPAL_BASE_URL to skip the picker.");
156669
+ process.exit(2);
156670
+ }
156671
+ baseUrl = await pickBaseUrlInteractive();
156672
+ }
156673
+ const settingsUrl = `${baseUrl}/developers/api-keys`;
156674
+ Me(`${ui.bold("1.")} Open your dashboard:
156675
+ ` + ` ${ui.bold(settingsUrl)}
156676
+ ` + `${ui.bold("2.")} Generate a key (suggested name: ${ui.bold('"Eigenpal CLI"')}).
156677
+ ` + `${ui.bold("3.")} Copy the key and paste it back here.`, "Create an API key");
156678
+ const shouldOpen = exitOnCancel(await ye({
156679
+ message: "Open the dashboard in your browser?",
156680
+ initialValue: true
156681
+ }));
156682
+ if (shouldOpen)
156683
+ openBrowser(settingsUrl);
156684
+ M2.info(`Dashboard URL: ${ui.bold(settingsUrl)}`);
156548
156685
  const key = exitOnCancel(await ge({
156549
- message: "API key (eig_live_…)",
156686
+ message: "Paste your API key (eig_live_…)",
156550
156687
  mask: "*",
156551
156688
  validate: (value) => value.trim().length > 0 ? undefined : "Paste the key from your dashboard, or press Ctrl-C to cancel."
156552
156689
  }));
@@ -156568,13 +156705,18 @@ and paste it below. Input is hidden.`, "Next");
156568
156705
  process.exit(1);
156569
156706
  }
156570
156707
  s.stop(`Validated against ${ui.bold(authCheck.tenantName || authCheck.tenantId)}`);
156708
+ const existingProfiles = listProfiles().profiles;
156571
156709
  const profileName = upsertProfile({
156572
156710
  apiKey: trimmedKey,
156573
156711
  baseUrl,
156574
156712
  tenantId: authCheck.tenantId,
156575
156713
  tenantName: authCheck.tenantName ?? undefined
156576
156714
  });
156577
- Se(`${ui.ok("✓")} Logged in as ${ui.bold(authCheck.tenantName || authCheck.tenantId)} ${ui.dim(`(profile ${profileName})`)}`);
156715
+ const updated = profileName in existingProfiles;
156716
+ const tenantLabel = ui.bold(authCheck.tenantName || authCheck.tenantId);
156717
+ const action = updated ? "Updated profile" : "Saved profile";
156718
+ Se(`${ui.ok("✓")} Logged in as ${tenantLabel}
156719
+ ${dim(`${action} ${ui.bold(profileName)} — switch with`)} ${ui.bold(`eigenpal auth use ${profileName}`)}`);
156578
156720
  }
156579
156721
  async function authLogout(profileName) {
156580
156722
  const removed = deleteProfile(profileName);
@@ -157493,7 +157635,8 @@ function runDiff(source, current) {
157493
157635
  // src/lib/config.ts
157494
157636
  var DEFAULT_BASE_URL = "https://studio.eigenpal.com";
157495
157637
  function resolveConfig(flags) {
157496
- const profile = readActiveCredentials();
157638
+ const usingEnvKey = !!env.EIGENPAL_API_KEY;
157639
+ const profile = usingEnvKey ? null : readActiveCredentials();
157497
157640
  const baseUrl = flags.baseUrl || env.EIGENPAL_BASE_URL || profile?.baseUrl || DEFAULT_BASE_URL;
157498
157641
  const apiKey = env.EIGENPAL_API_KEY || profile?.apiKey || "";
157499
157642
  const dir = flags.dir || env.EIGENPAL_DIR || "./eigenpal";
@@ -162017,7 +162160,8 @@ function commandLineWidth(cmd) {
162017
162160
  // src/cli.ts
162018
162161
  var __filename3 = path18.resolve(fileURLToPath6(import.meta.url));
162019
162162
  var program2 = new Command;
162020
- program2.name("eigenpal").description("Eigenpal CLI — AI workflows, evals, and experiments").version(package_default.version, "-v, --version", "Print the CLI version and exit").option("-q, --quiet", "Suppress informational status output (success/info/dim). Errors and warnings still print; --json output is unaffected.").hook("preAction", () => {
162163
+ var cliVersion = package_default.version === "0.0.0-placeholder" ? "dev" : package_default.version;
162164
+ program2.name("eigenpal").description("Eigenpal CLI — AI workflows, evals, and experiments").version(cliVersion, "-v, --version", "Print the CLI version and exit").option("-q, --quiet", "Suppress informational status output (success/info/dim). Errors and warnings still print; --json output is unaffected.").hook("preAction", () => {
162021
162165
  if (program2.opts().quiet)
162022
162166
  setQuiet(true);
162023
162167
  });
@@ -162053,8 +162197,6 @@ var authCmd = program2.command("auth").description("Manage authentication. Crede
162053
162197
  authCmd.command("login").description("Authenticate with Eigenpal. Opens a browser to create an API key, stores it under a profile named after the tenant, and makes that profile active. Re-running `login` against the same tenant updates the existing profile in place.").option("--base-url <url>", "Server base URL", undefined).addHelpText("after", `
162054
162198
  Examples:
162055
162199
  $ eigenpal auth login # default: https://studio.eigenpal.com
162056
- $ eigenpal auth login --base-url https://staging.eigenpal.com
162057
- $ eigenpal auth login --base-url http://localhost:3000 # local dev
162058
162200
  $ EIGENPAL_API_KEY=eig_live_… eigenpal status # CI: skip login entirely
162059
162201
 
162060
162202
  Refuses to run inside CI (\`CI=true\` + non-TTY); set \`EIGENPAL_API_KEY\` (and
@@ -162062,8 +162204,7 @@ optionally \`EIGENPAL_BASE_URL\`) directly instead. Credentials persist to
162062
162204
  \`~/.config/eigenpal/credentials.json\` as named profiles — switch with
162063
162205
  \`eigenpal auth use <name>\` or set \`EIGENPAL_PROFILE=<name>\` per shell.
162064
162206
  `).action(async (opts) => {
162065
- const config2 = resolveConfig(opts);
162066
- await authLogin(config2.baseUrl);
162207
+ await authLogin(opts.baseUrl);
162067
162208
  });
162068
162209
  authCmd.command("logout [profile]").description("Remove a profile from the credentials file. Defaults to the active profile if no name is given. After removal the next available profile becomes active.").action(async (profile) => {
162069
162210
  await authLogout(profile);
package/package.json CHANGED
@@ -1,7 +1,30 @@
1
1
  {
2
2
  "name": "@eigenpal/cli",
3
- "version": "0.4.1",
4
- "description": "Eigenpal CLI — author and run document workflows from your terminal.",
3
+ "version": "0.4.3",
4
+ "description": "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
5
+ "keywords": [
6
+ "eigenpal",
7
+ "cli",
8
+ "ai",
9
+ "agent",
10
+ "workflow",
11
+ "llm",
12
+ "evals",
13
+ "experiments",
14
+ "document",
15
+ "extraction",
16
+ "ocr"
17
+ ],
18
+ "homepage": "https://github.com/eigenpal/cli#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/eigenpal/cli/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/eigenpal/cli.git"
25
+ },
26
+ "license": "Apache-2.0",
27
+ "author": "Eigenpal <hello@eigenpal.com> (https://eigenpal.com)",
5
28
  "type": "module",
6
29
  "main": "./src/index.ts",
7
30
  "types": "./src/index.ts",
@@ -44,6 +67,8 @@
44
67
  },
45
68
  "files": [
46
69
  "dist",
47
- "README.md"
70
+ "LICENSE",
71
+ "README.md",
72
+ "CHANGELOG.md"
48
73
  ]
49
74
  }