@eigenpal/cli 0.5.7 → 0.5.9

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/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @eigenpal/cli
2
2
 
3
- ## 1.0.0
3
+ ## 0.5.9
4
4
 
5
5
  ### Major Changes
6
6
 
@@ -317,6 +317,12 @@ actual) { ... }` declaration around your statements. Parameter order is
317
317
  - **`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.
318
318
  - **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.
319
319
 
320
+ - 809f3d4: `auth list`: the org name is now the primary label (bold, immediately after
321
+ the marker), the profile slug is dimmed beside it (still the value you feed
322
+ to `auth use <name>` or `EIGENPAL_PROFILE=<name>`), and the base URL trails
323
+ on the right. The redundant `(tenantId)` column was dropped — the profile
324
+ slug already disambiguates. `auth use` (interactive picker) now prints the
325
+ non-interactive form on success so you learn the slug you just picked.
320
326
  - 49a8f94: Fixed a stray `undefined` in the `eigenpal auth login` outro. The success message used `dim(...)` (a stderr printer that returns `void`) inside a template literal where `ui.dim(...)` (the inline string formatter) was meant. The dim "Saved profile X — switch with" hint now renders correctly instead of literal `undefined`.
321
327
  - 16c6e83: Updated `@eigenpal/cli` author from `Eigenpal <hello@eigenpal.com>` to `Jedr Blaszyk <jedr@eigenpal.com>`. Surfaces on the npm package page and in `npm view @eigenpal/cli`.
322
328
  - b1d2d77: CLI: default `baseUrl` is now `https://studio.eigenpal.com` instead of `http://localhost:3000`.
@@ -327,6 +333,10 @@ actual) { ... }` declaration around your statements. Parameter order is
327
333
 
328
334
  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.
329
335
 
336
+ - 75caaec: `eigenpal workflow push` and `eigenpal workflow validate` now surface non-fatal **schema-quality warnings** when an output schema is loose enough to hurt downstream typing: categorical fields like `category`/`status`/`kind` declared as plain strings (no `enum`), `transform.script` functions returning `any`/`unknown`, untyped objects (`type: object` with no `properties`), and untyped arrays (`type: array` with no `items`). Warnings print to stderr and never block the push.
337
+
338
+ Updated the bundled CLI skill (`workflow-yaml.md`) with a "Be specific with types" section teaching `enum` fields, per-value meanings inline in `description`, and how `transform.script` TS literal unions auto-produce JSON Schema enums.
339
+
330
340
  - 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).
331
341
 
332
342
  In CI:
@@ -467,6 +477,7 @@ actual) { ... }` declaration around your statements. Parameter order is
467
477
 
468
478
  - 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.
469
479
  - 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.
480
+ - c3bc4e9: Fix Git-backed source release and status edge cases.
470
481
  - 04e4018: Harden hidden Git materialization commands for package-ref installs, frozen lockfile replay, dependency materialization, and source authoring smoke flows.
471
482
  - 71361fd: `install-skill`: multi-tool support via interactive multiselect.
472
483
  - Opens a `@clack/prompts` multiselect picker over supported agent tools
package/dist/cli.js CHANGED
@@ -28107,7 +28107,8 @@ var init_parser = __esm(() => {
28107
28107
  maxConcurrency: exports_external.number().min(1).max(30).default(3),
28108
28108
  pagesPerBatch: exports_external.number().min(1).max(20).default(5),
28109
28109
  prompt: exports_external.string().optional().describe("Custom prompt for LLM-based parsing"),
28110
- languages: exports_external.array(exports_external.string()).optional().describe("OCR language hints")
28110
+ languages: exports_external.array(exports_external.string()).optional().describe("OCR language hints"),
28111
+ tenantId: exports_external.string().optional()
28111
28112
  });
28112
28113
  ParserInputSchema = exports_external.object({
28113
28114
  data: exports_external.instanceof(Buffer).or(exports_external.instanceof(Uint8Array)),
@@ -65788,8 +65789,16 @@ function convertTsTypeToJsonSchema(node, ctx) {
65788
65789
  case "TSVoidKeyword":
65789
65790
  return {};
65790
65791
  case "TSAnyKeyword":
65791
- case "TSUnknownKeyword":
65792
+ case "TSUnknownKeyword": {
65793
+ const keyword = node.type === "TSAnyKeyword" ? "any" : "unknown";
65794
+ ctx.warnings.push({
65795
+ code: "weak-return-type",
65796
+ message: `\`${keyword}\` produces an empty schema, so downstream steps can't autocomplete this field or its descendants. Describe the actual shape (\`{ name: string; total: number }\`) or a literal union (\`"low" | "medium" | "high"\`) for the strongest downstream typing.`,
65797
+ start: node.start ?? undefined,
65798
+ end: node.end ?? undefined
65799
+ });
65792
65800
  return {};
65801
+ }
65793
65802
  case "TSNeverKeyword":
65794
65803
  return { not: {} };
65795
65804
  case "TSArrayType":
@@ -66138,13 +66147,15 @@ function compileTypedScript(opts) {
66138
66147
  });
66139
66148
  }
66140
66149
  const convertIssues = [];
66150
+ const convertWarnings = [];
66141
66151
  const inScopeSchemas = new Map(Object.entries(opts.inScopeSchemas ?? {}));
66142
66152
  const localAliases = collectLocalAliases(aliasDecls);
66143
66153
  const returnSchema = opts.kind === "evaluator" ? { type: "number" } : convertTsTypeToJsonSchema(returnTsType, {
66144
66154
  localAliases,
66145
66155
  inScopeSchemas,
66146
66156
  resolving: new Set,
66147
- issues: convertIssues
66157
+ issues: convertIssues,
66158
+ warnings: convertWarnings
66148
66159
  });
66149
66160
  if (convertIssues.length > 0) {
66150
66161
  return {
@@ -66157,10 +66168,7 @@ function compileTypedScript(opts) {
66157
66168
  }
66158
66169
  let stripped;
66159
66170
  try {
66160
- const out = import_sucrase.transform(opts.source, {
66161
- transforms: ["typescript"]
66162
- });
66163
- stripped = out.code;
66171
+ stripped = import_sucrase.transform(opts.source, { transforms: ["typescript"] }).code;
66164
66172
  } catch (err) {
66165
66173
  const e2 = err;
66166
66174
  return failWith({
@@ -66174,7 +66182,8 @@ function compileTypedScript(opts) {
66174
66182
  js: stripped,
66175
66183
  returnSchema,
66176
66184
  paramNames: actualParamNames
66177
- }
66185
+ },
66186
+ warnings: convertWarnings
66178
66187
  };
66179
66188
  }
66180
66189
  function failWith(issue2) {
@@ -181601,7 +181610,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
181601
181610
  // package.json
181602
181611
  var package_default = {
181603
181612
  name: "@eigenpal/cli",
181604
- version: "0.5.7",
181613
+ version: "0.5.9",
181605
181614
  description: "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
181606
181615
  keywords: [
181607
181616
  "eigenpal",
@@ -181641,6 +181650,8 @@ var package_default = {
181641
181650
  scripts: {
181642
181651
  dev: "bun src/cli.ts",
181643
181652
  build: "bun run generate:skill && bun build src/cli.ts --outdir dist --target node --format esm && rm -rf dist/templates dist/skill && cp -R src/templates dist/templates && cp -R src/skill dist/skill",
181653
+ generate: "bun run generate:cli-docs && bun run generate:skill",
181654
+ "generate:check": "bun run generate:cli-docs:check && bun run generate:skill:check",
181644
181655
  "generate:skill": "bun scripts/generate-skill-reference.ts",
181645
181656
  "generate:skill:check": "bun scripts/generate-skill-reference.ts --check",
181646
181657
  "generate:cli-docs": "bun scripts/generate-cli-docs.ts",
@@ -183847,16 +183858,19 @@ async function authList() {
183847
183858
  dim("No profiles configured. Run `eigenpal auth login` to add one.");
183848
183859
  return;
183849
183860
  }
183850
- const nameWidth = Math.max(16, ...names.map((n) => n.length));
183851
- for (const name of names.sort()) {
183861
+ const rows = names.sort().map((name) => {
183852
183862
  const p2 = profiles[name];
183853
- const marker = name === current ? ui.ok("●") : ui.dim("○");
183854
183863
  const tenant = p2.tenantName ?? p2.tenantId ?? "?";
183855
- const tenantLabel = p2.tenantName && p2.tenantId ? `${tenant} ${ui.dim(`(${p2.tenantId})`)}` : tenant;
183856
- console.log(`${marker} ${ui.bold(name.padEnd(nameWidth))} ${ui.dim(tenantLabel)} ${ui.dim(p2.baseUrl)}`);
183864
+ return { name, baseUrl: p2.baseUrl, tenant };
183865
+ });
183866
+ const tenantWidth = Math.max(16, ...rows.map((r) => r.tenant.length));
183867
+ const nameWidth = Math.max(16, ...rows.map((r) => r.name.length));
183868
+ for (const r of rows) {
183869
+ const marker = r.name === current ? ui.ok("●") : ui.dim("○");
183870
+ console.log(`${marker} ${ui.bold(r.tenant.padEnd(tenantWidth))} ${ui.dim(r.name.padEnd(nameWidth))} ${ui.dim(r.baseUrl)}`);
183857
183871
  }
183858
183872
  console.log("");
183859
- dim(`(● = active. Switch with \`eigenpal auth use <name>\` or \`EIGENPAL_PROFILE=<name>\`.)`);
183873
+ dim(`(● = active. Switch with \`eigenpal auth use <name>\` or \`EIGENPAL_PROFILE=<name>\` for one shell.)`);
183860
183874
  }
183861
183875
  async function authUse(profileName) {
183862
183876
  const { current, profiles } = listProfiles();
@@ -183901,7 +183915,8 @@ async function authUse(profileName) {
183901
183915
  if (profileName) {
183902
183916
  success2(`Switched to ${ui.bold(target)}${tenantSuffix}`);
183903
183917
  } else {
183904
- ye(`${ui.ok("✓")} Switched to ${ui.bold(target)}${tenantSuffix}`);
183918
+ ye(`${ui.ok("✓")} Switched to ${ui.bold(target)}${tenantSuffix}
183919
+ ${ui.dim(`In scripts: \`eigenpal auth use ${target}\` or \`EIGENPAL_PROFILE=${target}\``)}`);
183905
183920
  }
183906
183921
  }
183907
183922
 
@@ -184616,6 +184631,9 @@ function checkRepoVersion(gitRoot, mutation) {
184616
184631
  function resolvePackagePath(target) {
184617
184632
  return target.includes("/") ? SourcePackagePathSchema.parse(target) : dottedPackageNameToPath(DottedPackageNameSchema.parse(target));
184618
184633
  }
184634
+ function isAutomationPackagePath(packagePath) {
184635
+ return packagePath.startsWith("agents/") || packagePath.startsWith("workflows/");
184636
+ }
184619
184637
  function semverParts(version2) {
184620
184638
  const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version2);
184621
184639
  if (!match)
@@ -184876,14 +184894,15 @@ function packageStatus(opts) {
184876
184894
  const context = resolveGitSourceContext(opts);
184877
184895
  if (context.gitRoot)
184878
184896
  checkRepoVersion(context.gitRoot, false);
184879
- const validation = context.packageRoot ? validatePackage(context.packageRoot) : null;
184897
+ const isRepoRootStatus = context.gitRoot && context.packageRoot && context.gitRoot === context.packageRoot;
184898
+ const validation = context.packageRoot && !isRepoRootStatus ? validatePackage(context.packageRoot) : null;
184880
184899
  const gitStatus = context.gitRoot ? gitOutput(["status", "--short"], context.gitRoot) ?? "" : "";
184881
184900
  const payload = {
184882
184901
  gitRoot: context.gitRoot,
184883
184902
  packageRoot: context.packageRoot,
184884
184903
  packagePath: context.packagePath,
184885
184904
  clean: gitStatus.length === 0,
184886
- valid: validation?.valid ?? false,
184905
+ valid: validation?.valid ?? null,
184887
184906
  errors: validation?.errors ?? []
184888
184907
  };
184889
184908
  if (opts.json)
@@ -185061,7 +185080,9 @@ async function release(versionOrBump, dir, opts) {
185061
185080
  throw err;
185062
185081
  }
185063
185082
  success2(`Released ${tag}.`);
185064
- await syncLatestAutomation(client, context.packagePath);
185083
+ if (isAutomationPackagePath(context.packagePath)) {
185084
+ await syncLatestAutomation(client, context.packagePath);
185085
+ }
185065
185086
  }
185066
185087
  async function sync(target, opts) {
185067
185088
  const context = resolveGitSourceContext({ dir: opts.dir });
@@ -185072,6 +185093,9 @@ async function sync(target, opts) {
185072
185093
  if (!packagePath) {
185073
185094
  throw new Error("Pass an automation target or run sync inside a source package.");
185074
185095
  }
185096
+ if (!isAutomationPackagePath(packagePath)) {
185097
+ throw new Error("Sync only supports agent and workflow packages.");
185098
+ }
185075
185099
  await syncLatestAutomation(new ApiClient(resolveConfig(opts)), packagePath);
185076
185100
  }
185077
185101
  async function install(packageRef, opts) {
@@ -188030,6 +188054,155 @@ function parseWorkflow(yaml) {
188030
188054
  }
188031
188055
  return result.data;
188032
188056
  }
188057
+ // ../workflow-yaml/src/validate-quality.ts
188058
+ init_src();
188059
+ var CATEGORICAL_NAMES = new Set([
188060
+ "category",
188061
+ "subcategory",
188062
+ "kind",
188063
+ "status",
188064
+ "type",
188065
+ "subtype",
188066
+ "level",
188067
+ "severity",
188068
+ "priority",
188069
+ "state",
188070
+ "stage",
188071
+ "tier",
188072
+ "classification",
188073
+ "monitoring",
188074
+ "frequency",
188075
+ "reference",
188076
+ "sectionname",
188077
+ "code",
188078
+ "flag",
188079
+ "signal",
188080
+ "mode",
188081
+ "phase"
188082
+ ]);
188083
+ var SEPARATOR_LIST_RE = /(?:^|[\s(])((?:[A-Za-z0-9_-]{1,20})(?:\s*[|,/]\s*[A-Za-z0-9_-]{1,20}){1,7})(?:$|[\s).])/;
188084
+ function validateSchemaQuality(workflow2) {
188085
+ const warnings = [];
188086
+ for (let i5 = 0;i5 < workflow2.steps.length; i5++) {
188087
+ const step = workflow2.steps[i5];
188088
+ if (!step)
188089
+ continue;
188090
+ const basePath = `steps.${i5}`;
188091
+ walkStep(step, basePath, warnings);
188092
+ }
188093
+ return warnings;
188094
+ }
188095
+ function walkStep(step, basePath, out) {
188096
+ if (step.type === "ai.extract") {
188097
+ const schema = step.with?.schema;
188098
+ if (schema && typeof schema === "object") {
188099
+ walkSchema(schema, `${basePath}.with.schema`, out);
188100
+ }
188101
+ return;
188102
+ }
188103
+ if (step.type === "transform.script") {
188104
+ const cfg = step.with;
188105
+ const source2 = typeof cfg?.function === "string" ? cfg.function : null;
188106
+ if (!source2)
188107
+ return;
188108
+ const paramNames = cfg?.inputs && typeof cfg.inputs === "object" && !Array.isArray(cfg.inputs) ? Object.keys(cfg.inputs) : [];
188109
+ const compiled = compileTypedScript({ kind: "transform", source: source2, paramNames });
188110
+ if (!compiled.ok)
188111
+ return;
188112
+ for (const w10 of compiled.warnings) {
188113
+ if (w10.code === "weak-return-type") {
188114
+ out.push({
188115
+ code: "weak-script-return",
188116
+ field: `${basePath}.with.function`,
188117
+ message: `transform.script return type is too loose: ${w10.message}`,
188118
+ hint: 'Replace `unknown`/`any` with a concrete shape, or use a TS literal union (e.g. `"low" | "medium" | "high"`) when the value belongs to a closed set.'
188119
+ });
188120
+ }
188121
+ }
188122
+ const schema = compiled.result.returnSchema;
188123
+ if (schema.type === "object" && (!schema.properties || Object.keys(schema.properties).length === 0) && schema.additionalProperties !== false) {
188124
+ out.push({
188125
+ code: "untyped-object",
188126
+ field: `${basePath}.with.function`,
188127
+ message: "transform.script returns an untyped object. Downstream steps cannot autocomplete its fields.",
188128
+ hint: "Describe the actual shape in the return annotation: `function script(...): { name: string; total: number }`."
188129
+ });
188130
+ }
188131
+ return;
188132
+ }
188133
+ const withAny = step.with;
188134
+ const nestedSteps = withAny?.steps;
188135
+ if (Array.isArray(nestedSteps)) {
188136
+ for (let i5 = 0;i5 < nestedSteps.length; i5++) {
188137
+ const s5 = nestedSteps[i5];
188138
+ if (s5)
188139
+ walkStep(s5, `${basePath}.with.steps.${i5}`, out);
188140
+ }
188141
+ }
188142
+ const nestedBranches = withAny?.branches;
188143
+ if (Array.isArray(nestedBranches)) {
188144
+ for (let b8 = 0;b8 < nestedBranches.length; b8++) {
188145
+ const branch = nestedBranches[b8];
188146
+ if (branch && Array.isArray(branch.steps)) {
188147
+ for (let i5 = 0;i5 < branch.steps.length; i5++) {
188148
+ const s5 = branch.steps[i5];
188149
+ if (s5)
188150
+ walkStep(s5, `${basePath}.with.branches.${b8}.steps.${i5}`, out);
188151
+ }
188152
+ }
188153
+ }
188154
+ }
188155
+ }
188156
+ function walkSchema(schema, basePath, out) {
188157
+ const type = schema.type;
188158
+ if (type === "object" && (!schema.properties || Object.keys(schema.properties).length === 0) && schema.additionalProperties !== false) {
188159
+ out.push({
188160
+ code: "untyped-object",
188161
+ field: basePath,
188162
+ message: "Object schema has no properties. The LLM has no guidance on what fields to emit and downstream autocomplete is empty.",
188163
+ hint: "Add concrete fields under `properties`, or set `additionalProperties: false` if the object is intentionally open."
188164
+ });
188165
+ }
188166
+ if (type === "array" && (schema.items === undefined || isEmptyObject(schema.items))) {
188167
+ out.push({
188168
+ code: "untyped-array-items",
188169
+ field: basePath,
188170
+ message: "Array items are untyped. The LLM does not know what shape each element should take.",
188171
+ hint: "Add an `items:` block describing the element type, e.g. `items: { type: object, properties: { ... } }`."
188172
+ });
188173
+ }
188174
+ if (schema.properties && typeof schema.properties === "object") {
188175
+ for (const [name, raw] of Object.entries(schema.properties)) {
188176
+ if (!raw || typeof raw !== "object")
188177
+ continue;
188178
+ const prop = raw;
188179
+ const propType = prop.type;
188180
+ const propEnum = Array.isArray(prop.enum) && prop.enum.length > 0;
188181
+ const lower = name.toLowerCase();
188182
+ const description = typeof prop.description === "string" ? prop.description : "";
188183
+ const looksCategoricalByName = CATEGORICAL_NAMES.has(lower);
188184
+ const looksCategoricalByDescription = description.length > 0 && SEPARATOR_LIST_RE.test(description);
188185
+ if (propType === "string" && !propEnum && (looksCategoricalByName || looksCategoricalByDescription) && !looksLikeFreeform(prop)) {
188186
+ out.push({
188187
+ code: "categorical-missing-enum",
188188
+ field: `${basePath}.properties.${name}`,
188189
+ message: `Field \`${name}\` looks categorical but has no \`enum\`. Coded values extract more reliably when the LLM is constrained to a closed set.`,
188190
+ hint: "In the workflow builder, switch the field type to `Enum` and list the allowed values. In raw YAML, add `enum: ['value1', 'value2', ...]` to the field, and write the per-value meanings into the field's `description`."
188191
+ });
188192
+ }
188193
+ walkSchema(prop, `${basePath}.properties.${name}`, out);
188194
+ }
188195
+ }
188196
+ if (schema.items && typeof schema.items === "object" && !Array.isArray(schema.items)) {
188197
+ walkSchema(schema.items, `${basePath}.items`, out);
188198
+ }
188199
+ }
188200
+ function isEmptyObject(v11) {
188201
+ return typeof v11 === "object" && v11 !== null && !Array.isArray(v11) && Object.keys(v11).length === 0;
188202
+ }
188203
+ function looksLikeFreeform(prop) {
188204
+ return prop.pattern !== undefined || prop.format !== undefined;
188205
+ }
188033
188206
  // src/commands/validate.ts
188034
188207
  var import_yaml6 = __toESM(require_dist(), 1);
188035
188208
  import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
@@ -188048,24 +188221,46 @@ function isDirSafe(path15, issues, field) {
188048
188221
  }
188049
188222
  function validateWorkflowYaml(path15) {
188050
188223
  if (!existsSync10(path15)) {
188051
- return [{ field: path15, message: "File not found." }];
188224
+ return { issues: [{ field: path15, message: "File not found." }], warnings: [] };
188052
188225
  }
188053
188226
  const text = readFileSync10(path15, "utf-8");
188054
188227
  try {
188055
- parseWorkflow(text);
188056
- return [];
188228
+ const definition = parseWorkflow(text);
188229
+ let warnings = [];
188230
+ try {
188231
+ warnings = validateSchemaQuality(definition);
188232
+ } catch {
188233
+ warnings = [];
188234
+ }
188235
+ return { issues: [], warnings };
188057
188236
  } catch (err2) {
188058
188237
  if (err2 instanceof YamlParseError) {
188059
188238
  const where = err2.line !== undefined ? ` (line ${err2.line})` : "";
188060
- return [{ field: "yaml", message: `${err2.message}${where}` }];
188239
+ return { issues: [{ field: "yaml", message: `${err2.message}${where}` }], warnings: [] };
188061
188240
  }
188062
188241
  if (err2 instanceof WorkflowValidationError) {
188063
- return err2.errors.map((e8) => ({
188064
- field: e8.path.length > 0 ? e8.path.map(String).join(".") : "(root)",
188065
- message: e8.message
188066
- }));
188242
+ return {
188243
+ issues: err2.errors.map((e8) => ({
188244
+ field: e8.path.length > 0 ? e8.path.map(String).join(".") : "(root)",
188245
+ message: e8.message
188246
+ })),
188247
+ warnings: []
188248
+ };
188067
188249
  }
188068
- return [{ field: "(root)", message: err2 instanceof Error ? err2.message : String(err2) }];
188250
+ return {
188251
+ issues: [{ field: "(root)", message: err2 instanceof Error ? err2.message : String(err2) }],
188252
+ warnings: []
188253
+ };
188254
+ }
188255
+ }
188256
+ function printSchemaQualityWarnings(path15, warnings) {
188257
+ if (warnings.length === 0)
188258
+ return;
188259
+ warn(`workflow.yaml ${ui.dim(`(${path15})`)}: ${warnings.length} schema-quality warning${warnings.length === 1 ? "" : "s"}`);
188260
+ for (const w10 of warnings) {
188261
+ warn(` ${ui.dim(w10.field)} ${w10.message}`);
188262
+ if (w10.hint)
188263
+ warn(` ${ui.dim("hint:")} ${w10.hint}`);
188069
188264
  }
188070
188265
  }
188071
188266
  function validateEvaluatorsYaml(path15) {
@@ -188248,9 +188443,23 @@ function rootDir(opts) {
188248
188443
  return resolve6(opts.dir ?? process.cwd());
188249
188444
  }
188250
188445
  function registerAllValidateCommand(parent) {
188251
- parent.command("validate").description("Local-only validation against the templated project layout: ./workflow.yaml + ./evaluators.yaml + ./dataset/. For targeted validation use `evaluators validate` or `dataset validate`.").option("--dir <path>", "Project root (defaults to cwd; resolves the three default paths from here)").action((opts) => {
188446
+ parent.command("validate [path]").description("Local-only validation. Without [path]: runs the templated three-way check (./workflow.yaml + ./evaluators.yaml + ./dataset/). With [path] pointing at a YAML file: validates just that workflow.yaml. For per-noun targeting use `evaluators validate` or `dataset validate`.").option("--dir <path>", "Project root (defaults to cwd; resolves the three default paths from here)").action((path15, opts) => {
188447
+ if (path15) {
188448
+ const target = resolve6(path15);
188449
+ const wfResult2 = validateWorkflowYaml(target);
188450
+ const wfOk2 = printIssues("workflow.yaml", target, wfResult2.issues);
188451
+ if (wfOk2)
188452
+ printSchemaQualityWarnings(target, wfResult2.warnings);
188453
+ if (!wfOk2)
188454
+ process.exit(1);
188455
+ return;
188456
+ }
188252
188457
  const root2 = rootDir(opts);
188253
- const wfOk = printIssues("workflow.yaml", relPath(root2, join12(root2, "workflow.yaml")), validateWorkflowYaml(join12(root2, "workflow.yaml")));
188458
+ const wfResult = validateWorkflowYaml(join12(root2, "workflow.yaml"));
188459
+ const wfPath = relPath(root2, join12(root2, "workflow.yaml"));
188460
+ const wfOk = printIssues("workflow.yaml", wfPath, wfResult.issues);
188461
+ if (wfOk)
188462
+ printSchemaQualityWarnings(wfPath, wfResult.warnings);
188254
188463
  const evOk = printIssues("evaluators.yaml", relPath(root2, join12(root2, "evaluators.yaml")), validateEvaluatorsYaml(join12(root2, "evaluators.yaml")));
188255
188464
  const dsOk = printIssues("dataset/", relPath(root2, join12(root2, "dataset")), validateDatasetFolder(join12(root2, "dataset")));
188256
188465
  if (!wfOk || !evOk || !dsOk)
@@ -188903,6 +189112,7 @@ flag would otherwise shadow it. Validation failures come back as
188903
189112
  return;
188904
189113
  }
188905
189114
  success2(`Pushed ${ui.bold(workflowId)} (v${result.version ?? nextVersion})`);
189115
+ printSchemaQualityWarnings2(result.warnings);
188906
189116
  } else {
188907
189117
  if (opts.bump) {
188908
189118
  throw new Error("--bump requires --workflow-id (need a current version to bump from). For a new workflow, use --set-version <X.Y.Z> or omit both.");
@@ -188920,6 +189130,7 @@ flag would otherwise shadow it. Validation failures come back as
188920
189130
  const id2 = result.workflow?.id ?? result.id ?? "?";
188921
189131
  const ver = result.version?.version ?? result.workflow?.currentVersion?.version ?? result.currentVersion?.version ?? "?";
188922
189132
  success2(`Created ${ui.bold(id2)} (v${ver})`);
189133
+ printSchemaQualityWarnings2(result.warnings);
188923
189134
  }
188924
189135
  }));
188925
189136
  const moveCmd = parent.command("move <workflow-id>").description("Move a workflow to a folder path, creating folders as needed").requiredOption("--folder <path>", "Target folder path (`/` for root)");
@@ -188937,6 +189148,16 @@ flag would otherwise shadow it. Validation failures come back as
188937
189148
  }));
188938
189149
  }
188939
189150
  var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
189151
+ function printSchemaQualityWarnings2(warnings) {
189152
+ if (!warnings || warnings.length === 0)
189153
+ return;
189154
+ warn(`${warnings.length} schema-quality warning${warnings.length === 1 ? "" : "s"} (push succeeded; sharper types make downstream steps easier to wire):`);
189155
+ for (const w10 of warnings) {
189156
+ warn(` ${ui.dim(w10.field)} ${w10.message}`);
189157
+ if (w10.hint)
189158
+ warn(` ${ui.dim("hint:")} ${w10.hint}`);
189159
+ }
189160
+ }
188940
189161
  function isBumpLevel(value) {
188941
189162
  return value === "patch" || value === "minor" || value === "major";
188942
189163
  }
@@ -20,7 +20,7 @@ Manage workflows: push, pull, run, evaluate.
20
20
  - [`eigenpal workflow pull [options] <workflow-id>`](#eigenpal-workflow-pull-options-workflow-id)
21
21
  - [`eigenpal workflow push [options]`](#eigenpal-workflow-push-options)
22
22
  - [`eigenpal workflow move [options] <workflow-id>`](#eigenpal-workflow-move-options-workflow-id)
23
- - [`eigenpal workflow validate [options]`](#eigenpal-workflow-validate-options)
23
+ - [`eigenpal workflow validate [options] [path]`](#eigenpal-workflow-validate-options-path)
24
24
  - [`eigenpal workflow clear-local [options] [examples...]`](#eigenpal-workflow-clear-local-options-examples)
25
25
  - [`eigenpal workflow evaluators pull [options] <workflow-id>`](#eigenpal-workflow-evaluators-pull-options-workflow-id)
26
26
  - [`eigenpal workflow evaluators push [options] <workflow-id>`](#eigenpal-workflow-evaluators-push-options-workflow-id)
@@ -100,7 +100,7 @@ workflow
100
100
  ├── evaluator-type
101
101
  │ ├── list|ls
102
102
  │ └── get <type>
103
- ├── validate
103
+ ├── validate [path]
104
104
  ├── clear-local [examples...]
105
105
  └── step
106
106
  └── exec <type>
@@ -110,14 +110,14 @@ workflow
110
110
 
111
111
  ### Core
112
112
 
113
- | Command | Description |
114
- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
115
- | `eigenpal workflow list | ls [options]` | List workflows the caller can read. |
116
- | `eigenpal workflow pull [options] <workflow-id>` | Download the YAML definition of the workflow at its current version. |
117
- | `eigenpal workflow push [options]` | Create or update a workflow from a YAML file. |
118
- | `eigenpal workflow move [options] <workflow-id>` | Move a workflow to a folder path, creating folders as needed |
119
- | `eigenpal workflow validate [options]` | Local-only validation against the templated project layout: ./workflow.yaml + ./evaluators.yaml + ./dataset/. For targeted validation use `evaluators validate` or `dataset validate`. |
120
- | `eigenpal workflow clear-local [options] [examples...]` | Delete local execution artifacts under ./dataset/examples/. Keeps the latest run per example by default. |
113
+ | Command | Description |
114
+ | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
115
+ | `eigenpal workflow list | ls [options]` | List workflows the caller can read. |
116
+ | `eigenpal workflow pull [options] <workflow-id>` | Download the YAML definition of the workflow at its current version. |
117
+ | `eigenpal workflow push [options]` | Create or update a workflow from a YAML file. |
118
+ | `eigenpal workflow move [options] <workflow-id>` | Move a workflow to a folder path, creating folders as needed |
119
+ | `eigenpal workflow validate [options] [path]` | Local-only validation. Without [path]: runs the templated three-way check (./workflow.yaml + ./evaluators.yaml + ./dataset/). With [path] pointing at a YAML file: validates just that workflow.yaml. For per-noun targeting use `evaluators validate` or `dataset validate`. |
120
+ | `eigenpal workflow clear-local [options] [examples...]` | Delete local execution artifacts under ./dataset/examples/. Keeps the latest run per example by default. |
121
121
 
122
122
  ### Evaluators
123
123
 
@@ -256,9 +256,15 @@ Move a workflow to a folder path, creating folders as needed
256
256
  | `--base-url <url>` | no | | Server base URL |
257
257
  | `--json` | no | | Output the raw server response as JSON |
258
258
 
259
- ### `eigenpal workflow validate [options]`
259
+ ### `eigenpal workflow validate [options] [path]`
260
260
 
261
- Local-only validation against the templated project layout: ./workflow.yaml + ./evaluators.yaml + ./dataset/. For targeted validation use `evaluators validate` or `dataset validate`.
261
+ Local-only validation. Without [path]: runs the templated three-way check (./workflow.yaml + ./evaluators.yaml + ./dataset/). With [path] pointing at a YAML file: validates just that workflow.yaml. For per-noun targeting use `evaluators validate` or `dataset validate`.
262
+
263
+ ### Arguments
264
+
265
+ | Name | Required | Variadic | Description |
266
+ | ------ | -------- | -------- | ----------- |
267
+ | `path` | no | no | |
262
268
 
263
269
  ### Options
264
270
 
@@ -191,6 +191,76 @@ The script runs in a WASM sandbox: 5s wall clock, 10 MB heap, no network,
191
191
  no filesystem. Trips show up as `script_timeout` / `script_memory_limit`
192
192
  on the step execution.
193
193
 
194
+ ## Be specific with types
195
+
196
+ Every output schema you write is read three times: by the LLM (for
197
+ `ai.extract`), by downstream steps (autocomplete + template resolution),
198
+ and by the runtime validator. **The more specific the type, the better
199
+ the workflow runs.**
200
+
201
+ ### Use `enum` for coded values
202
+
203
+ Categorical fields like `category`, `status`, `kind`, `severity`,
204
+ `monitoring`, `frequency` should be enums whenever the value set is
205
+ closed. The LLM is constrained to emit only allowed values, and
206
+ downstream `transform.script` steps know exactly what they'll see.
207
+
208
+ ```yaml
209
+ - name: classify
210
+ type: ai.extract
211
+ with:
212
+ input: '{{ input.text }}'
213
+ schema:
214
+ type: object
215
+ properties:
216
+ category:
217
+ type: string
218
+ enum: ['901', '902']
219
+ description: |
220
+ Covenant category.
221
+ 901 = Odkladacie podmienky (conditions precedent).
222
+ 902 = Následné podmienky (subsequent / ongoing covenants).
223
+ required: [category]
224
+ ```
225
+
226
+ Per-value meanings go inline in the field's `description`. The LLM picks
227
+ them up reliably from there, and the scope browser renders the allowed
228
+ values when downstream steps reference the field.
229
+
230
+ ### `transform.script` — TS literal unions become JSON Schema enums
231
+
232
+ A TS literal-union return type compiles to a JSON-Schema enum
233
+ automatically. Use it when the function returns a value from a closed set:
234
+
235
+ ```yaml
236
+ - name: severity
237
+ type: transform.script
238
+ with:
239
+ inputs:
240
+ score: '{{ steps.score.output.value }}'
241
+ function: |
242
+ function script(score: number): { level: 'low' | 'medium' | 'high' } {
243
+ return { level: score > 0.8 ? 'high' : score > 0.4 ? 'medium' : 'low' };
244
+ }
245
+ ```
246
+
247
+ Downstream steps autocomplete `low | medium | high` for `level`.
248
+
249
+ ### `eigenpal workflow push` warns on weak types
250
+
251
+ Push surfaces non-fatal **schema-quality warnings** when it sees patterns
252
+ that hurt downstream typing. Common ones:
253
+
254
+ | Warning code | What it means |
255
+ | ---------------------------- | -------------------------------------------------------------------------------------------- |
256
+ | `categorical-missing-enum` | A field named like `category` / `status` / `kind` is a plain `string`. Consider declaring an `enum`. |
257
+ | `weak-script-return` | A `transform.script` returns `any` or `unknown`. Replace with a concrete shape or a literal union. |
258
+ | `untyped-object` | A `type: object` field has no `properties`. The LLM has no guidance on what to emit. |
259
+ | `untyped-array-items` | A `type: array` field has no `items:` declared. |
260
+
261
+ Warnings do not block the push. They print to stderr as `! …` lines. Run
262
+ `eigenpal workflow validate` locally to surface them before pushing.
263
+
194
264
  ## Common patterns
195
265
 
196
266
  ### Conditional branches
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eigenpal/cli",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
5
5
  "keywords": [
6
6
  "eigenpal",
@@ -40,6 +40,8 @@
40
40
  "scripts": {
41
41
  "dev": "bun src/cli.ts",
42
42
  "build": "bun run generate:skill && bun build src/cli.ts --outdir dist --target node --format esm && rm -rf dist/templates dist/skill && cp -R src/templates dist/templates && cp -R src/skill dist/skill",
43
+ "generate": "bun run generate:cli-docs && bun run generate:skill",
44
+ "generate:check": "bun run generate:cli-docs:check && bun run generate:skill:check",
43
45
  "generate:skill": "bun scripts/generate-skill-reference.ts",
44
46
  "generate:skill:check": "bun scripts/generate-skill-reference.ts --check",
45
47
  "generate:cli-docs": "bun scripts/generate-cli-docs.ts",