@plune-ai/cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -41,6 +41,35 @@ var init_cjs_shims = __esm({
41
41
  }
42
42
  });
43
43
 
44
+ // src/cli/api-url.ts
45
+ var api_url_exports = {};
46
+ __export(api_url_exports, {
47
+ FEEDBACK_URL: () => FEEDBACK_URL,
48
+ dashboardUrl: () => dashboardUrl,
49
+ resolveApiUrl: () => resolveApiUrl,
50
+ whereToGetAToken: () => whereToGetAToken
51
+ });
52
+ function resolveApiUrl(explicit) {
53
+ const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
54
+ return raw.replace(/\/+$/, "");
55
+ }
56
+ function dashboardUrl(apiUrl = resolveApiUrl()) {
57
+ return apiUrl === DEFAULT_API_URL ? "https://beta.plune.ai" : void 0;
58
+ }
59
+ function whereToGetAToken(apiUrl = resolveApiUrl()) {
60
+ const web = dashboardUrl(apiUrl);
61
+ return web === void 0 ? `Get one from your Plune dashboard (the web app in front of ${apiUrl}), under Settings \u2192 API tokens.` : `Get one at ${web} \u2192 Settings \u2192 API tokens.`;
62
+ }
63
+ var FEEDBACK_URL, DEFAULT_API_URL;
64
+ var init_api_url = __esm({
65
+ "src/cli/api-url.ts"() {
66
+ "use strict";
67
+ init_cjs_shims();
68
+ FEEDBACK_URL = "https://github.com/plune-ai/feedback/issues/new?template=feedback.yml";
69
+ DEFAULT_API_URL = "https://beta-api.plune.ai";
70
+ }
71
+ });
72
+
44
73
  // src/cli/env.ts
45
74
  var env_exports = {};
46
75
  __export(env_exports, {
@@ -189,9 +218,9 @@ var init_schema = __esm({
189
218
  input_per_1k_usd: import_zod.z.number().nonnegative(),
190
219
  output_per_1k_usd: import_zod.z.number().nonnegative()
191
220
  });
192
- pricingSchema = import_zod.z.record(modelPriceSchema);
221
+ pricingSchema = import_zod.z.record(import_zod.z.string(), modelPriceSchema);
193
222
  datasetRowSchema = import_zod.z.object({
194
- vars: import_zod.z.record(import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.boolean()])),
223
+ vars: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.boolean()])),
195
224
  expected: import_zod.z.string().optional()
196
225
  });
197
226
  datasetRefSchema = import_zod.z.union([
@@ -221,7 +250,7 @@ var init_schema = __esm({
221
250
  });
222
251
  jsonSchemaAssertionSchema = import_zod.z.object({
223
252
  type: import_zod.z.literal("json-schema"),
224
- schema: import_zod.z.record(import_zod.z.unknown()),
253
+ schema: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()),
225
254
  extract: import_zod.z.enum(["auto", "strict"]).optional()
226
255
  });
227
256
  llmJudgeAssertionSchema = import_zod.z.object({
@@ -456,7 +485,7 @@ function readHeader(headers, name) {
456
485
  return typeof v === "string" || typeof v === "number" ? v : void 0;
457
486
  }
458
487
  async function withRetry(fn, options) {
459
- const { base_delay_ms = 500, sleep = defaultSleep, random = Math.random } = options;
488
+ const { base_delay_ms = 500, sleep: sleep2 = defaultSleep, random = Math.random } = options;
460
489
  const maxRetries = Math.max(0, Math.floor(options.max_retries));
461
490
  let attempt = 0;
462
491
  for (; ; ) {
@@ -472,7 +501,7 @@ async function withRetry(fn, options) {
472
501
  const backoff = base_delay_ms * 2 ** attempt;
473
502
  const jitter = backoff * 0.1 * random();
474
503
  const delay = Math.max(backoff + jitter, retryAfterMs(err) ?? 0);
475
- await sleep(delay);
504
+ await sleep2(delay);
476
505
  attempt += 1;
477
506
  }
478
507
  }
@@ -886,7 +915,7 @@ var init_cache = __esm({
886
915
  return void 0;
887
916
  }
888
917
  }
889
- set(key, value) {
918
+ set(key, value2) {
890
919
  try {
891
920
  this.db.prepare(
892
921
  `INSERT INTO completions (cache_key, output, input_tokens, output_tokens, created_at)
@@ -896,7 +925,7 @@ var init_cache = __esm({
896
925
  input_tokens = excluded.input_tokens,
897
926
  output_tokens = excluded.output_tokens,
898
927
  created_at = excluded.created_at`
899
- ).run(key, value.output, value.usage.input_tokens, value.usage.output_tokens, this.now());
928
+ ).run(key, value2.output, value2.usage.input_tokens, value2.usage.output_tokens, this.now());
900
929
  } catch {
901
930
  }
902
931
  }
@@ -934,32 +963,32 @@ var init_cache2 = __esm({
934
963
  });
935
964
 
936
965
  // src/util/canonical-json.ts
937
- function normalizeValue(value) {
938
- if (value === null) return null;
939
- if (typeof value === "boolean") return value;
940
- if (typeof value === "number") {
941
- if (!isFinite(value)) {
966
+ function normalizeValue(value2) {
967
+ if (value2 === null) return null;
968
+ if (typeof value2 === "boolean") return value2;
969
+ if (typeof value2 === "number") {
970
+ if (!isFinite(value2)) {
942
971
  throw new Error(
943
- `canonicalJson: non-finite number rejected: ${String(value)}`
972
+ `canonicalJson: non-finite number rejected: ${String(value2)}`
944
973
  );
945
974
  }
946
- return value === 0 ? 0 : value;
975
+ return value2 === 0 ? 0 : value2;
947
976
  }
948
- if (typeof value === "string") {
949
- return value.normalize("NFC");
977
+ if (typeof value2 === "string") {
978
+ return value2.normalize("NFC");
950
979
  }
951
- if (Array.isArray(value)) {
952
- return value.map(normalizeValue);
980
+ if (Array.isArray(value2)) {
981
+ return value2.map(normalizeValue);
953
982
  }
954
- if (typeof value === "object") {
955
- const proto = Object.getPrototypeOf(value);
983
+ if (typeof value2 === "object") {
984
+ const proto = Object.getPrototypeOf(value2);
956
985
  if (proto !== null && proto !== Object.prototype) {
957
- const name = value.constructor?.name ?? "unknown";
986
+ const name = value2.constructor?.name ?? "unknown";
958
987
  throw new Error(
959
988
  `canonicalJson: unsupported non-plain object (${name}) \u2014 only plain objects, arrays, and primitives are serializable`
960
989
  );
961
990
  }
962
- const obj = value;
991
+ const obj = value2;
963
992
  const sorted = {};
964
993
  for (const key of Object.keys(obj).sort()) {
965
994
  sorted[key] = normalizeValue(obj[key]);
@@ -967,11 +996,11 @@ function normalizeValue(value) {
967
996
  return sorted;
968
997
  }
969
998
  throw new Error(
970
- `canonicalJson: unsupported value type: ${typeof value}`
999
+ `canonicalJson: unsupported value type: ${typeof value2}`
971
1000
  );
972
1001
  }
973
- function canonicalJson(value) {
974
- return JSON.stringify(normalizeValue(value));
1002
+ function canonicalJson(value2) {
1003
+ return JSON.stringify(normalizeValue(value2));
975
1004
  }
976
1005
  var init_canonical_json = __esm({
977
1006
  "src/util/canonical-json.ts"() {
@@ -1009,8 +1038,8 @@ function interpolate(template, ctx) {
1009
1038
  }
1010
1039
  if (key.startsWith("vars.")) {
1011
1040
  const name = key.slice("vars.".length);
1012
- const value = ctx.vars[name];
1013
- return value === void 0 ? match : String(value);
1041
+ const value2 = ctx.vars[name];
1042
+ return value2 === void 0 ? match : String(value2);
1014
1043
  }
1015
1044
  return match;
1016
1045
  });
@@ -1063,12 +1092,12 @@ var init_contains = __esm({
1063
1092
  init_interpolate();
1064
1093
  contains = {
1065
1094
  run(ctx) {
1066
- const value = interpolate(ctx.params.value, ctx);
1095
+ const value2 = interpolate(ctx.params.value, ctx);
1067
1096
  const found = norm(ctx.output, ctx.params.ignore_case).includes(
1068
- norm(value, ctx.params.ignore_case)
1097
+ norm(value2, ctx.params.ignore_case)
1069
1098
  );
1070
1099
  return Promise.resolve(
1071
- found ? { passed: true } : { passed: false, reason: `expected output to contain: "${value}"` }
1100
+ found ? { passed: true } : { passed: false, reason: `expected output to contain: "${value2}"` }
1072
1101
  );
1073
1102
  }
1074
1103
  };
@@ -2117,6 +2146,549 @@ var init_reporters = __esm({
2117
2146
  }
2118
2147
  });
2119
2148
 
2149
+ // src/cli/credentials.ts
2150
+ function credentialsFile() {
2151
+ const base = process.env["XDG_CONFIG_HOME"]?.trim() || (0, import_node_path.join)((0, import_node_os.homedir)(), ".config");
2152
+ return (0, import_node_path.join)(base, "plune", "credentials.json");
2153
+ }
2154
+ function saveToken(token) {
2155
+ const file = credentialsFile();
2156
+ (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(file), { recursive: true, mode: 448 });
2157
+ (0, import_node_fs.writeFileSync)(file, JSON.stringify({ token }, null, 2) + "\n", { mode: 384 });
2158
+ (0, import_node_fs.chmodSync)(file, 384);
2159
+ return file;
2160
+ }
2161
+ function loadToken() {
2162
+ try {
2163
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
2164
+ return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
2165
+ } catch {
2166
+ return null;
2167
+ }
2168
+ }
2169
+ function clearToken() {
2170
+ const file = credentialsFile();
2171
+ if (!(0, import_node_fs.existsSync)(file)) return { removed: false, path: file };
2172
+ (0, import_node_fs.rmSync)(file);
2173
+ return { removed: true, path: file };
2174
+ }
2175
+ var import_node_fs, import_node_os, import_node_path;
2176
+ var init_credentials = __esm({
2177
+ "src/cli/credentials.ts"() {
2178
+ "use strict";
2179
+ init_cjs_shims();
2180
+ import_node_fs = require("fs");
2181
+ import_node_os = require("os");
2182
+ import_node_path = require("path");
2183
+ }
2184
+ });
2185
+
2186
+ // src/reporter-core/client.ts
2187
+ async function detailOf(res) {
2188
+ try {
2189
+ const body = await res.json();
2190
+ return typeof body.error === "string" ? body.error : "";
2191
+ } catch {
2192
+ return "";
2193
+ }
2194
+ }
2195
+ function retryAfterMs2(res) {
2196
+ const raw = res.headers.get("retry-after");
2197
+ if (raw === null) return null;
2198
+ const seconds = Number(raw.trim());
2199
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : null;
2200
+ }
2201
+ function classify3(status) {
2202
+ if (status === 401 || status === 403) return "auth";
2203
+ if (status === 409) return "conflict";
2204
+ if (status === 400) return "invalid";
2205
+ if (status === 429 || status >= 500) return "unavailable";
2206
+ return "other";
2207
+ }
2208
+ function createClient(opts) {
2209
+ const doFetch = opts.fetchImpl ?? fetch;
2210
+ const wait = opts.wait ?? sleep;
2211
+ const base = opts.apiUrl.replace(/\/+$/, "");
2212
+ const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
2213
+ async function post(path12, body) {
2214
+ let last = {
2215
+ kind: "unavailable",
2216
+ status: null,
2217
+ detail: "no attempt was made"
2218
+ };
2219
+ for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
2220
+ let res;
2221
+ try {
2222
+ res = await doFetch(`${base}${path12}`, {
2223
+ method: "POST",
2224
+ headers: { "content-type": "application/json", authorization: `Bearer ${opts.token}` },
2225
+ body: JSON.stringify(body)
2226
+ });
2227
+ } catch (err) {
2228
+ last = { kind: "unavailable", status: null, detail: scrub(String(err)) };
2229
+ if (attempt < RETRY_DELAYS_MS.length) await wait(RETRY_DELAYS_MS[attempt]);
2230
+ continue;
2231
+ }
2232
+ if (res.ok) return { ok: true, status: res.status, body: await res.json() };
2233
+ const detail = scrub(await detailOf(res));
2234
+ const kind = classify3(res.status);
2235
+ last = { kind, status: res.status, detail };
2236
+ if (kind !== "unavailable") break;
2237
+ if (attempt < RETRY_DELAYS_MS.length) {
2238
+ await wait(retryAfterMs2(res) ?? RETRY_DELAYS_MS[attempt]);
2239
+ }
2240
+ }
2241
+ return { ok: false, ...last };
2242
+ }
2243
+ return { post };
2244
+ }
2245
+ var RETRY_DELAYS_MS, sleep;
2246
+ var init_client = __esm({
2247
+ "src/reporter-core/client.ts"() {
2248
+ "use strict";
2249
+ init_cjs_shims();
2250
+ RETRY_DELAYS_MS = [300, 900];
2251
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2252
+ }
2253
+ });
2254
+
2255
+ // src/reporter-core/env.ts
2256
+ function value(env, name) {
2257
+ const raw = env[name]?.trim();
2258
+ return raw === void 0 || raw === "" ? void 0 : raw;
2259
+ }
2260
+ function flag(env, name) {
2261
+ const raw = value(env, name)?.toLowerCase();
2262
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
2263
+ }
2264
+ function count(env, name) {
2265
+ const raw = value(env, name);
2266
+ if (raw === void 0) return void 0;
2267
+ const n = Number(raw);
2268
+ return Number.isInteger(n) && n > 0 ? n : void 0;
2269
+ }
2270
+ function readEnv(env = process.env) {
2271
+ const externalKey = value(env, "PLUNE_RUN");
2272
+ const token = value(env, "PLUNE_TOKEN");
2273
+ const apiUrl = value(env, "PLUNE_API_URL");
2274
+ const fallbackPath = value(env, "PLUNE_FALLBACK");
2275
+ const batchSize = count(env, "PLUNE_BATCH_SIZE");
2276
+ return {
2277
+ config: {
2278
+ ...externalKey !== void 0 ? { externalKey } : {},
2279
+ ...token !== void 0 ? { token } : {},
2280
+ ...apiUrl !== void 0 ? { apiUrl } : {},
2281
+ ...fallbackPath !== void 0 ? { fallbackPath } : {},
2282
+ ...batchSize !== void 0 ? { batchSize } : {}
2283
+ },
2284
+ // Two names for two situations that end the same way. `SHARED_RUN` says other processes are
2285
+ // reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
2286
+ // way this process must not be the one to call it finished.
2287
+ keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED"),
2288
+ ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0)
2289
+ };
2290
+ }
2291
+ var UNSUPPORTED_VARS;
2292
+ var init_env2 = __esm({
2293
+ "src/reporter-core/env.ts"() {
2294
+ "use strict";
2295
+ init_cjs_shims();
2296
+ UNSUPPORTED_VARS = [
2297
+ "PLUNE_RUN_TITLE",
2298
+ "PLUNE_ENV",
2299
+ "PLUNE_LABELS",
2300
+ "PLUNE_GROUP"
2301
+ ];
2302
+ }
2303
+ });
2304
+
2305
+ // src/reporter-core/fallback.ts
2306
+ function appendBatch(file, batch) {
2307
+ fs6.mkdirSync(path6.dirname(file), { recursive: true });
2308
+ const line = {
2309
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2310
+ runId: batch.runId,
2311
+ externalKey: batch.externalKey,
2312
+ results: batch.results
2313
+ };
2314
+ fs6.appendFileSync(file, `${JSON.stringify(line)}
2315
+ `, "utf8");
2316
+ }
2317
+ var fs6, path6, DEFAULT_FALLBACK_PATH;
2318
+ var init_fallback = __esm({
2319
+ "src/reporter-core/fallback.ts"() {
2320
+ "use strict";
2321
+ init_cjs_shims();
2322
+ fs6 = __toESM(require("fs"), 1);
2323
+ path6 = __toESM(require("path"), 1);
2324
+ DEFAULT_FALLBACK_PATH = path6.join(".plune", "pending-results.jsonl");
2325
+ }
2326
+ });
2327
+
2328
+ // src/reporter-core/session.ts
2329
+ function chunk(items, size) {
2330
+ const out = [];
2331
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
2332
+ return out;
2333
+ }
2334
+ async function startRun(passed, expected) {
2335
+ const env = readEnv();
2336
+ const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
2337
+ const cfg = { ...env.config, ...stated };
2338
+ const log = cfg.log ?? ((line) => void process.stderr.write(`${line}
2339
+ `));
2340
+ const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
2341
+ const externalKey = cfg.externalKey ?? null;
2342
+ const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
2343
+ const token = cfg.token ?? loadToken() ?? "";
2344
+ const client = createClient({
2345
+ apiUrl: resolveApiUrl(cfg.apiUrl),
2346
+ token,
2347
+ ...cfg.fetchImpl !== void 0 ? { fetchImpl: cfg.fetchImpl } : {}
2348
+ });
2349
+ const stats = {
2350
+ accepted: 0,
2351
+ duplicate: 0,
2352
+ conflict: 0,
2353
+ rejected: 0,
2354
+ unresolved: 0,
2355
+ deferred: 0
2356
+ };
2357
+ const resolved = /* @__PURE__ */ new Map();
2358
+ let buffer = [];
2359
+ let runId = null;
2360
+ let joined = false;
2361
+ let offline = token === "";
2362
+ let done = false;
2363
+ if (offline) {
2364
+ log('plune: no API token \u2014 run "plune login" first. Results will be written to the fallback file.');
2365
+ }
2366
+ if (env.ignored.length > 0) {
2367
+ log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
2368
+ }
2369
+ function defer(results) {
2370
+ appendBatch(fallbackPath, { runId, externalKey, results });
2371
+ stats.deferred += results.length;
2372
+ }
2373
+ async function resolveKeys(values) {
2374
+ const unknown = values.filter((k) => !resolved.has(k.value));
2375
+ if (unknown.length === 0 || offline) return;
2376
+ for (const part of chunk(unknown, RESOLVE_CHUNK)) {
2377
+ const out = await client.post(
2378
+ "/v1/test-cases/resolve",
2379
+ { keys: part }
2380
+ );
2381
+ if (!out.ok) {
2382
+ log(`plune: could not look up test cases (${out.detail || out.kind}).`);
2383
+ return;
2384
+ }
2385
+ for (const row of out.body.results) resolved.set(row.key.value, row.testCaseId);
2386
+ }
2387
+ }
2388
+ if (!offline) {
2389
+ if (expected !== void 0) await resolveKeys(expected.flat());
2390
+ const declared = expected?.slice(0, EXPECTED_MAX) ?? [];
2391
+ const configuration = declared.length === 0 ? void 0 : {
2392
+ expected: declared.flatMap((keys) => {
2393
+ const hit = keys.map((k) => resolved.get(k.value)).find((v) => typeof v === "string");
2394
+ if (typeof hit === "string") return [{ testCaseId: hit }];
2395
+ return keys[0] === void 0 ? [] : [{ externalKey: keys[0] }];
2396
+ })
2397
+ };
2398
+ if (expected !== void 0 && expected.length > EXPECTED_MAX) {
2399
+ log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
2400
+ }
2401
+ const start = await client.post("/v1/runs", {
2402
+ schemaVersion: 2,
2403
+ kind: cfg.kind ?? "automated",
2404
+ ...externalKey !== null ? { externalKey } : {},
2405
+ ...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
2406
+ ...configuration !== void 0 ? { configuration } : {}
2407
+ });
2408
+ if (start.ok) {
2409
+ runId = start.body.run.id;
2410
+ joined = start.status === 200;
2411
+ } else {
2412
+ log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
2413
+ }
2414
+ }
2415
+ function matchFor(result) {
2416
+ if (result.testCaseId !== void 0) return { kind: "found", testCaseId: result.testCaseId };
2417
+ let unanswered = false;
2418
+ for (const key of result.keys) {
2419
+ const hit = resolved.get(key.value);
2420
+ if (typeof hit === "string") return { kind: "found", testCaseId: hit };
2421
+ if (hit === void 0) unanswered = true;
2422
+ }
2423
+ return unanswered ? { kind: "unanswered" } : { kind: "unmatched" };
2424
+ }
2425
+ async function flush() {
2426
+ if (buffer.length === 0) return;
2427
+ const pending = buffer;
2428
+ buffer = [];
2429
+ if (offline || runId === null) {
2430
+ defer(pending);
2431
+ return;
2432
+ }
2433
+ await resolveKeys(pending.flatMap((p) => p.keys));
2434
+ const submissions = [];
2435
+ const unanswered = [];
2436
+ for (const result of pending) {
2437
+ const match = matchFor(result);
2438
+ if (match.kind === "found") {
2439
+ const { keys: _keys, ...rest } = result;
2440
+ submissions.push({ ...rest, testCaseId: match.testCaseId });
2441
+ } else if (match.kind === "unmatched") {
2442
+ stats.unresolved += 1;
2443
+ } else {
2444
+ unanswered.push(result);
2445
+ }
2446
+ }
2447
+ if (unanswered.length > 0) defer(unanswered);
2448
+ if (submissions.length === 0) return;
2449
+ const out = await client.post(
2450
+ `/v1/runs/${runId}/results`,
2451
+ { results: submissions }
2452
+ );
2453
+ if (out.ok) {
2454
+ stats.accepted += out.body.counts.accepted ?? 0;
2455
+ stats.duplicate += out.body.counts.duplicate ?? 0;
2456
+ stats.conflict += out.body.counts.conflict ?? 0;
2457
+ stats.rejected += out.body.counts.rejected ?? 0;
2458
+ return;
2459
+ }
2460
+ if (out.kind === "auth") {
2461
+ log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
2462
+ offline = true;
2463
+ } else if (out.kind === "conflict") {
2464
+ log(`plune: this run is closed \u2014 ${out.detail}`);
2465
+ } else {
2466
+ log(`plune: could not send results (${out.detail || out.kind}).`);
2467
+ }
2468
+ defer(submissions);
2469
+ }
2470
+ function summarise() {
2471
+ const parts = [`${stats.accepted} accepted`];
2472
+ if (stats.duplicate > 0) parts.push(`${stats.duplicate} already reported`);
2473
+ if (stats.conflict > 0) parts.push(`${stats.conflict} conflicting`);
2474
+ if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
2475
+ if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
2476
+ if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
2477
+ log(`plune: ${parts.join(" \xB7 ")}`);
2478
+ }
2479
+ return {
2480
+ get runId() {
2481
+ return runId;
2482
+ },
2483
+ get joined() {
2484
+ return joined;
2485
+ },
2486
+ get stats() {
2487
+ return stats;
2488
+ },
2489
+ async add(result) {
2490
+ buffer.push(result);
2491
+ if (buffer.length >= batchSize) await flush();
2492
+ },
2493
+ flush,
2494
+ async finish(reason) {
2495
+ if (done) return;
2496
+ done = true;
2497
+ await flush();
2498
+ if (runId !== null && !offline) {
2499
+ const out = await client.post(`/v1/runs/${runId}/events`, {
2500
+ event: "finish",
2501
+ ...reason !== void 0 ? { reason } : {}
2502
+ });
2503
+ if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
2504
+ }
2505
+ summarise();
2506
+ },
2507
+ async leaveOpen() {
2508
+ if (done) return;
2509
+ done = true;
2510
+ await flush();
2511
+ if (runId !== null) {
2512
+ log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
2513
+ }
2514
+ summarise();
2515
+ }
2516
+ };
2517
+ }
2518
+ var RESOLVE_CHUNK, BATCH_MAX, BATCH_DEFAULT, EXPECTED_MAX;
2519
+ var init_session = __esm({
2520
+ "src/reporter-core/session.ts"() {
2521
+ "use strict";
2522
+ init_cjs_shims();
2523
+ init_api_url();
2524
+ init_credentials();
2525
+ init_client();
2526
+ init_env2();
2527
+ init_fallback();
2528
+ RESOLVE_CHUNK = 500;
2529
+ BATCH_MAX = 500;
2530
+ BATCH_DEFAULT = 100;
2531
+ EXPECTED_MAX = 1e4;
2532
+ }
2533
+ });
2534
+
2535
+ // src/cli/commands/run-lifecycle.ts
2536
+ var run_lifecycle_exports = {};
2537
+ __export(run_lifecycle_exports, {
2538
+ NoTokenError: () => NoTokenError,
2539
+ RunCommandError: () => RunCommandError,
2540
+ handleRunExec: () => handleRunExec,
2541
+ handleRunFinish: () => handleRunFinish,
2542
+ handleRunReport: () => handleRunReport,
2543
+ handleRunStart: () => handleRunStart
2544
+ });
2545
+ function connect(deps) {
2546
+ const env = readEnv();
2547
+ const apiUrl = resolveApiUrl(deps.apiUrl ?? env.config.apiUrl);
2548
+ const token = deps.token ?? env.config.token ?? loadToken() ?? "";
2549
+ if (token === "") throw new NoTokenError();
2550
+ return {
2551
+ client: createClient({
2552
+ apiUrl,
2553
+ token,
2554
+ ...deps.fetchImpl !== void 0 ? { fetchImpl: deps.fetchImpl } : {}
2555
+ }),
2556
+ apiUrl,
2557
+ write: deps.write ?? ((line) => void process.stdout.write(`${line}
2558
+ `))
2559
+ };
2560
+ }
2561
+ function runUrl(apiUrl, id) {
2562
+ const web = dashboardUrl(apiUrl);
2563
+ return web === void 0 ? `${apiUrl}/v1/runs/${id}` : `${web}/runs/${id}`;
2564
+ }
2565
+ async function handleRunStart(options = {}) {
2566
+ const { client, apiUrl, write } = connect(options);
2567
+ const env = readEnv();
2568
+ const externalKey = options.key ?? env.config.externalKey ?? `plune-${(options.now ?? Date.now)().toString(36)}`;
2569
+ const out = await client.post("/v1/runs", {
2570
+ schemaVersion: 2,
2571
+ kind: options.kind ?? "automated",
2572
+ externalKey
2573
+ });
2574
+ if (!out.ok) throw new RunCommandError(`Could not start the run \u2014 ${out.detail || out.kind}.`);
2575
+ const result = {
2576
+ id: out.body.run.id,
2577
+ joined: out.status === 200,
2578
+ externalKey,
2579
+ url: runUrl(apiUrl, out.body.run.id)
2580
+ };
2581
+ if (options.json === true) {
2582
+ write(JSON.stringify(result));
2583
+ } else {
2584
+ write(`${result.joined ? "Joined" : "Started"} run ${result.id}`);
2585
+ write(result.url);
2586
+ }
2587
+ return result;
2588
+ }
2589
+ async function handleRunFinish(id, options = {}) {
2590
+ const { client, write } = connect(options);
2591
+ const event = options.terminate === true ? "terminate" : "finish";
2592
+ const out = await client.post(`/v1/runs/${id}/events`, {
2593
+ event,
2594
+ ...options.reason !== void 0 ? { reason: options.reason } : {}
2595
+ });
2596
+ if (!out.ok) {
2597
+ throw new RunCommandError(
2598
+ out.status === 404 ? `No run '${id}' \u2014 check the id, or whether the token belongs to the same project.` : `Could not ${event} run ${id} \u2014 ${out.detail || out.kind}.`
2599
+ );
2600
+ }
2601
+ write(`Run ${id} ${event === "finish" ? "finished" : "terminated"}.`);
2602
+ }
2603
+ async function handleRunExec(options) {
2604
+ const [command, ...args] = options.argv;
2605
+ if (command === void 0) throw new RunCommandError("Nothing to run \u2014 pass a command after `--`.");
2606
+ const run = await handleRunStart(options);
2607
+ const spawnImpl = options.spawnImpl ?? import_node_child_process.spawn;
2608
+ const code = await new Promise((resolve6) => {
2609
+ const child = spawnImpl(command, args, {
2610
+ stdio: "inherit",
2611
+ shell: process.platform === "win32",
2612
+ env: { ...process.env, PLUNE_RUN: run.externalKey, PLUNE_PROCEED: "1" }
2613
+ });
2614
+ child.on("close", (exitCode) => resolve6(exitCode ?? 1));
2615
+ child.on("error", () => resolve6(1));
2616
+ });
2617
+ await handleRunFinish(run.id, options);
2618
+ return code;
2619
+ }
2620
+ async function handleRunReport(options = {}) {
2621
+ const file = options.file ?? DEFAULT_FALLBACK_PATH;
2622
+ const write = options.write ?? ((line) => void process.stdout.write(`${line}
2623
+ `));
2624
+ let raw;
2625
+ try {
2626
+ raw = fs7.readFileSync(file, "utf8");
2627
+ } catch {
2628
+ throw new RunCommandError(`Nothing to send \u2014 no file at ${file}.`);
2629
+ }
2630
+ const lines = raw.split("\n").filter((l) => l.trim() !== "").map((l) => JSON.parse(l));
2631
+ if (lines.length === 0) throw new RunCommandError(`Nothing to send \u2014 ${file} is empty.`);
2632
+ const { client } = connect(options);
2633
+ const result = { batches: lines.length, sent: 0, failed: 0 };
2634
+ for (const line of lines) {
2635
+ const pending = line.results.filter(isPending);
2636
+ const ready = line.results.filter((r) => !isPending(r));
2637
+ if (line.runId !== null && ready.length > 0) {
2638
+ const out = await client.post(`/v1/runs/${line.runId}/results`, { results: ready });
2639
+ if (out.ok) result.sent += ready.length;
2640
+ else {
2641
+ result.failed += ready.length;
2642
+ write(`Could not send ${ready.length} result(s) to run ${line.runId} \u2014 ${out.detail || out.kind}.`);
2643
+ }
2644
+ }
2645
+ if (pending.length > 0) {
2646
+ const session = await startRun({
2647
+ ...options.apiUrl !== void 0 ? { apiUrl: options.apiUrl } : {},
2648
+ ...options.token !== void 0 ? { token: options.token } : {},
2649
+ ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
2650
+ ...line.externalKey !== null ? { externalKey: line.externalKey } : {},
2651
+ // The replay does not know whether the run is complete, so it must not say that it is.
2652
+ log: write
2653
+ });
2654
+ for (const r of pending) await session.add(r);
2655
+ await session.leaveOpen();
2656
+ result.sent += session.stats.accepted + session.stats.duplicate;
2657
+ result.failed += session.stats.deferred + session.stats.unresolved;
2658
+ }
2659
+ }
2660
+ write(`Replayed ${result.batches} batch(es) from ${file}: ${result.sent} sent, ${result.failed} not.`);
2661
+ return result;
2662
+ }
2663
+ var import_node_child_process, fs7, NoTokenError, RunCommandError, isPending;
2664
+ var init_run_lifecycle = __esm({
2665
+ "src/cli/commands/run-lifecycle.ts"() {
2666
+ "use strict";
2667
+ init_cjs_shims();
2668
+ import_node_child_process = require("child_process");
2669
+ fs7 = __toESM(require("fs"), 1);
2670
+ init_api_url();
2671
+ init_credentials();
2672
+ init_client();
2673
+ init_env2();
2674
+ init_fallback();
2675
+ init_session();
2676
+ NoTokenError = class extends Error {
2677
+ constructor() {
2678
+ super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
2679
+ this.name = "NoTokenError";
2680
+ }
2681
+ };
2682
+ RunCommandError = class extends Error {
2683
+ constructor(message) {
2684
+ super(message);
2685
+ this.name = "RunCommandError";
2686
+ }
2687
+ };
2688
+ isPending = (r) => "keys" in r;
2689
+ }
2690
+ });
2691
+
2120
2692
  // src/cli/commands/report.ts
2121
2693
  var report_exports = {};
2122
2694
  __export(report_exports, {
@@ -2130,10 +2702,10 @@ function isRunResult(v) {
2130
2702
  }
2131
2703
  function handleReport(opts = {}) {
2132
2704
  const cwd = opts.cwd ?? process.cwd();
2133
- const file = path6.join(cwd, ".plune", "last-run.json");
2705
+ const file = path7.join(cwd, ".plune", "last-run.json");
2134
2706
  let raw;
2135
2707
  try {
2136
- raw = fs6.readFileSync(file, "utf8");
2708
+ raw = fs8.readFileSync(file, "utf8");
2137
2709
  } catch {
2138
2710
  throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
2139
2711
  }
@@ -2148,13 +2720,13 @@ function handleReport(opts = {}) {
2148
2720
  }
2149
2721
  return parsed;
2150
2722
  }
2151
- var fs6, path6, ReportNotFoundError;
2723
+ var fs8, path7, ReportNotFoundError;
2152
2724
  var init_report = __esm({
2153
2725
  "src/cli/commands/report.ts"() {
2154
2726
  "use strict";
2155
2727
  init_cjs_shims();
2156
- fs6 = __toESM(require("fs"), 1);
2157
- path6 = __toESM(require("path"), 1);
2728
+ fs8 = __toESM(require("fs"), 1);
2729
+ path7 = __toESM(require("path"), 1);
2158
2730
  ReportNotFoundError = class extends Error {
2159
2731
  code = "NO_SAVED_RUN";
2160
2732
  constructor(message) {
@@ -2177,7 +2749,7 @@ function evalStatus(e) {
2177
2749
  if (statuses.includes("errored")) return "errored";
2178
2750
  return "passed";
2179
2751
  }
2180
- function classify3(baseline, current) {
2752
+ function classify4(baseline, current) {
2181
2753
  if (current === "errored") return "errored";
2182
2754
  if (baseline === "absent") return current === "passed" ? "new-pass" : "new-fail";
2183
2755
  if (current === "absent") return "removed";
@@ -2191,19 +2763,19 @@ function diffRuns(baseline, current) {
2191
2763
  const evals = ids.map((id) => {
2192
2764
  const baselineP = baseMap.get(id) ?? "absent";
2193
2765
  const currentP = curMap.get(id) ?? "absent";
2194
- return { id, status: classify3(baselineP, currentP), baseline: baselineP, current: currentP };
2766
+ return { id, status: classify4(baselineP, currentP), baseline: baselineP, current: currentP };
2195
2767
  });
2196
- const count = (s) => evals.filter((e) => e.status === s).length;
2768
+ const count2 = (s) => evals.filter((e) => e.status === s).length;
2197
2769
  const summary = {
2198
- regressions: count("regression"),
2199
- improvements: count("improvement"),
2200
- preExistingFails: count("pre-existing-fail"),
2201
- newFails: count("new-fail"),
2202
- newPasses: count("new-pass"),
2203
- stablePasses: count("stable-pass"),
2204
- errored: count("errored"),
2205
- removed: count("removed"),
2206
- hasRegression: count("regression") > 0
2770
+ regressions: count2("regression"),
2771
+ improvements: count2("improvement"),
2772
+ preExistingFails: count2("pre-existing-fail"),
2773
+ newFails: count2("new-fail"),
2774
+ newPasses: count2("new-pass"),
2775
+ stablePasses: count2("stable-pass"),
2776
+ errored: count2("errored"),
2777
+ removed: count2("removed"),
2778
+ hasRegression: count2("regression") > 0
2207
2779
  };
2208
2780
  return { evals, summary };
2209
2781
  }
@@ -2228,7 +2800,7 @@ function isRunResult2(v) {
2228
2800
  function loadRunResult(file, label) {
2229
2801
  let raw;
2230
2802
  try {
2231
- raw = fs7.readFileSync(file, "utf8");
2803
+ raw = fs9.readFileSync(file, "utf8");
2232
2804
  } catch {
2233
2805
  throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
2234
2806
  }
@@ -2248,12 +2820,12 @@ function handleDiff(opts) {
2248
2820
  const current = loadRunResult(opts.currentPath, "current");
2249
2821
  return diffRuns(baseline, current);
2250
2822
  }
2251
- var fs7, DiffInputError;
2823
+ var fs9, DiffInputError;
2252
2824
  var init_diff2 = __esm({
2253
2825
  "src/cli/commands/diff.ts"() {
2254
2826
  "use strict";
2255
2827
  init_cjs_shims();
2256
- fs7 = __toESM(require("fs"), 1);
2828
+ fs9 = __toESM(require("fs"), 1);
2257
2829
  init_diff();
2258
2830
  DiffInputError = class extends Error {
2259
2831
  code = "DIFF_INPUT";
@@ -2378,8 +2950,8 @@ async function runInitWizard(cwd) {
2378
2950
  process.exit(1);
2379
2951
  return;
2380
2952
  }
2381
- const outputPath = path7.join(cwd, "plune.yaml");
2382
- if (fs8.existsSync(outputPath)) {
2953
+ const outputPath = path8.join(cwd, "plune.yaml");
2954
+ if (fs10.existsSync(outputPath)) {
2383
2955
  const overwrite = await clack.confirm({
2384
2956
  message: "plune.yaml already exists. Overwrite?"
2385
2957
  });
@@ -2418,18 +2990,18 @@ async function runInitWizard(cwd) {
2418
2990
  `;
2419
2991
  const spin = clack.spinner();
2420
2992
  spin.start("Writing plune.yaml\u2026");
2421
- fs8.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
2993
+ fs10.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
2422
2994
  spin.stop("plune.yaml created!");
2423
- clack.outro("Done! Run `plune run` to execute your evaluations.");
2995
+ clack.outro("plune.yaml is ready.");
2424
2996
  }
2425
- var clack, fs8, path7, import_yaml2;
2997
+ var clack, fs10, path8, import_yaml2;
2426
2998
  var init_wizard = __esm({
2427
2999
  "src/config/init/wizard.ts"() {
2428
3000
  "use strict";
2429
3001
  init_cjs_shims();
2430
3002
  clack = __toESM(require("@clack/prompts"), 1);
2431
- fs8 = __toESM(require("fs"), 1);
2432
- path7 = __toESM(require("path"), 1);
3003
+ fs10 = __toESM(require("fs"), 1);
3004
+ path8 = __toESM(require("path"), 1);
2433
3005
  import_yaml2 = require("yaml");
2434
3006
  init_errors();
2435
3007
  }
@@ -2437,21 +3009,21 @@ var init_wizard = __esm({
2437
3009
 
2438
3010
  // src/cli/templates/index.ts
2439
3011
  function writeTemplateFile(cwd, rel, content, force) {
2440
- const target = path8.join(cwd, rel);
2441
- if (!force && fs9.existsSync(target)) {
3012
+ const target = path9.join(cwd, rel);
3013
+ if (!force && fs11.existsSync(target)) {
2442
3014
  return "skipped";
2443
3015
  }
2444
- fs9.mkdirSync(path8.dirname(target), { recursive: true });
2445
- fs9.writeFileSync(target, content, "utf8");
3016
+ fs11.mkdirSync(path9.dirname(target), { recursive: true });
3017
+ fs11.writeFileSync(target, content, "utf8");
2446
3018
  return "written";
2447
3019
  }
2448
- var fs9, path8, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
3020
+ var fs11, path9, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
2449
3021
  var init_templates = __esm({
2450
3022
  "src/cli/templates/index.ts"() {
2451
3023
  "use strict";
2452
3024
  init_cjs_shims();
2453
- fs9 = __toESM(require("fs"), 1);
2454
- path8 = __toESM(require("path"), 1);
3025
+ fs11 = __toESM(require("fs"), 1);
3026
+ path9 = __toESM(require("path"), 1);
2455
3027
  ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
2456
3028
  # .env is gitignored; .env.example is safe to commit.
2457
3029
  # Plune reads whichever key matches your configured provider.
@@ -2487,6 +3059,7 @@ evals:
2487
3059
  // src/cli/commands/init.ts
2488
3060
  var init_exports = {};
2489
3061
  __export(init_exports, {
3062
+ NEXT_STEPS: () => NEXT_STEPS,
2490
3063
  initCommand: () => initCommand
2491
3064
  });
2492
3065
  function reportWrite(cwd, rel, content, force) {
@@ -2506,8 +3079,9 @@ async function initCommand({ cwd, force, yes = false }) {
2506
3079
  for (const [rel, content] of TEMPLATES) {
2507
3080
  reportWrite(cwd, rel, content, force);
2508
3081
  }
3082
+ process.stdout.write(NEXT_STEPS);
2509
3083
  }
2510
- var TEMPLATES;
3084
+ var TEMPLATES, NEXT_STEPS;
2511
3085
  var init_init = __esm({
2512
3086
  "src/cli/commands/init.ts"() {
2513
3087
  "use strict";
@@ -2518,43 +3092,14 @@ var init_init = __esm({
2518
3092
  ["datasets/example.jsonl", EXAMPLE_JSONL_TPL],
2519
3093
  [".env.example", ENV_EXAMPLE_TPL]
2520
3094
  ];
2521
- }
2522
- });
2523
-
2524
- // src/cli/credentials.ts
2525
- function credentialsFile() {
2526
- const base = process.env["XDG_CONFIG_HOME"]?.trim() || (0, import_node_path.join)((0, import_node_os.homedir)(), ".config");
2527
- return (0, import_node_path.join)(base, "plune", "credentials.json");
2528
- }
2529
- function saveToken(token) {
2530
- const file = credentialsFile();
2531
- (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(file), { recursive: true, mode: 448 });
2532
- (0, import_node_fs.writeFileSync)(file, JSON.stringify({ token }, null, 2) + "\n", { mode: 384 });
2533
- (0, import_node_fs.chmodSync)(file, 384);
2534
- return file;
2535
- }
2536
- function loadToken() {
2537
- try {
2538
- const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
2539
- return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
2540
- } catch {
2541
- return null;
2542
- }
2543
- }
2544
- function clearToken() {
2545
- const file = credentialsFile();
2546
- if (!(0, import_node_fs.existsSync)(file)) return { removed: false, path: file };
2547
- (0, import_node_fs.rmSync)(file);
2548
- return { removed: true, path: file };
2549
- }
2550
- var import_node_fs, import_node_os, import_node_path;
2551
- var init_credentials = __esm({
2552
- "src/cli/credentials.ts"() {
2553
- "use strict";
2554
- init_cjs_shims();
2555
- import_node_fs = require("fs");
2556
- import_node_os = require("os");
2557
- import_node_path = require("path");
3095
+ NEXT_STEPS = [
3096
+ "",
3097
+ "Next:",
3098
+ " 1. plune run \u2014 runs locally. No account, no token needed.",
3099
+ " 2. plune login \u2014 only if you want history and trends: paste a token from the dashboard.",
3100
+ " 3. plune sync \u2014 sends the last run to the platform.",
3101
+ ""
3102
+ ].join("\n");
2558
3103
  }
2559
3104
  });
2560
3105
 
@@ -2562,25 +3107,81 @@ var init_credentials = __esm({
2562
3107
  var login_exports = {};
2563
3108
  __export(login_exports, {
2564
3109
  EmptyTokenError: () => EmptyTokenError,
2565
- handleLogin: () => handleLogin
3110
+ LoginHttpError: () => LoginHttpError,
3111
+ LoginNetworkError: () => LoginNetworkError,
3112
+ TokenRejectedError: () => TokenRejectedError,
3113
+ handleLogin: () => handleLogin,
3114
+ reportLoginFailure: () => reportLoginFailure
2566
3115
  });
2567
- function handleLogin(opts) {
2568
- const token = opts.token.trim();
2569
- if (token.length === 0) throw new EmptyTokenError();
2570
- return { path: saveToken(token) };
3116
+ async function handleLogin(deps) {
3117
+ const token = deps.token.trim();
3118
+ const apiUrl = resolveApiUrl(deps.apiUrl);
3119
+ if (token.length === 0) throw new EmptyTokenError(apiUrl);
3120
+ if (deps.skipVerify === true) return { path: saveToken(token), verified: false };
3121
+ const doFetch = deps.fetchImpl ?? fetch;
3122
+ let res;
3123
+ try {
3124
+ res = await doFetch(`${apiUrl}/v1/runs?limit=1`, {
3125
+ headers: { authorization: `Bearer ${token}` }
3126
+ });
3127
+ } catch {
3128
+ throw new LoginNetworkError(apiUrl);
3129
+ }
3130
+ await res.text().catch(() => "");
3131
+ if (res.status === 401) throw new TokenRejectedError(apiUrl);
3132
+ if (!res.ok) throw new LoginHttpError(res.status);
3133
+ return { path: saveToken(token), verified: true };
3134
+ }
3135
+ function reportLoginFailure(err, write) {
3136
+ if (err instanceof EmptyTokenError || err instanceof TokenRejectedError) {
3137
+ write(`${err.message}
3138
+ `);
3139
+ return 2;
3140
+ }
3141
+ if (err instanceof LoginNetworkError || err instanceof LoginHttpError) {
3142
+ write(`${err.message}
3143
+ `);
3144
+ return 1;
3145
+ }
3146
+ return null;
2571
3147
  }
2572
- var EmptyTokenError;
3148
+ var EmptyTokenError, TokenRejectedError, LoginNetworkError, LoginHttpError;
2573
3149
  var init_login = __esm({
2574
3150
  "src/cli/commands/login.ts"() {
2575
3151
  "use strict";
2576
3152
  init_cjs_shims();
3153
+ init_api_url();
2577
3154
  init_credentials();
2578
3155
  EmptyTokenError = class extends Error {
2579
- constructor() {
2580
- super("No API token provided. Pass --token <token> or pipe it via stdin.");
3156
+ constructor(apiUrl = resolveApiUrl()) {
3157
+ super(
3158
+ `No API token provided. ${whereToGetAToken(apiUrl)} Then pass --token <token> or pipe it via stdin.`
3159
+ );
2581
3160
  this.name = "EmptyTokenError";
2582
3161
  }
2583
3162
  };
3163
+ TokenRejectedError = class extends Error {
3164
+ constructor(apiUrl = resolveApiUrl()) {
3165
+ super(`That API token was rejected (401). Nothing was saved. ${whereToGetAToken(apiUrl)}`);
3166
+ this.name = "TokenRejectedError";
3167
+ }
3168
+ };
3169
+ LoginNetworkError = class extends Error {
3170
+ constructor(apiUrl) {
3171
+ super(
3172
+ `Could not reach the Plune API at ${apiUrl}, so the token was not checked and NOT saved. Try again, or use --skip-verify to save it without checking.`
3173
+ );
3174
+ this.name = "LoginNetworkError";
3175
+ }
3176
+ };
3177
+ LoginHttpError = class extends Error {
3178
+ constructor(status) {
3179
+ super(
3180
+ `The Plune API returned ${status} while checking the token, so it was NOT saved. Try again.`
3181
+ );
3182
+ this.name = "LoginHttpError";
3183
+ }
3184
+ };
2584
3185
  }
2585
3186
  });
2586
3187
 
@@ -2601,8 +3202,8 @@ var init_logout = __esm({
2601
3202
  });
2602
3203
 
2603
3204
  // src/types/run-result-schema.ts
2604
- function parseRunResult(value) {
2605
- return runResultSchema.parse(value);
3205
+ function parseRunResult(value2) {
3206
+ return runResultSchema.parse(value2);
2606
3207
  }
2607
3208
  var import_zod2, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
2608
3209
  var init_run_result_schema = __esm({
@@ -2632,7 +3233,7 @@ var init_run_result_schema = __esm({
2632
3233
  message: import_zod2.z.string()
2633
3234
  });
2634
3235
  rowResultSchema = import_zod2.z.object({
2635
- vars: import_zod2.z.record(import_zod2.z.unknown()),
3236
+ vars: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()),
2636
3237
  output: import_zod2.z.string().nullable(),
2637
3238
  cached: import_zod2.z.boolean(),
2638
3239
  usage: usageSchema.optional(),
@@ -2673,19 +3274,15 @@ __export(sync_exports, {
2673
3274
  SyncFileError: () => SyncFileError,
2674
3275
  SyncHttpError: () => SyncHttpError,
2675
3276
  SyncNetworkError: () => SyncNetworkError,
2676
- TokenRejectedError: () => TokenRejectedError,
3277
+ TokenRejectedError: () => TokenRejectedError2,
2677
3278
  handleSync: () => handleSync,
2678
3279
  reportSyncFailure: () => reportSyncFailure
2679
3280
  });
2680
- function resolveApiUrl(explicit) {
2681
- const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
2682
- return raw.replace(/\/+$/, "");
2683
- }
2684
3281
  function readRun(cwd, file) {
2685
- const target = file !== void 0 ? path9.resolve(cwd, file) : path9.join(cwd, ".plune", "last-run.json");
3282
+ const target = file !== void 0 ? path10.resolve(cwd, file) : path10.join(cwd, ".plune", "last-run.json");
2686
3283
  let raw;
2687
3284
  try {
2688
- raw = fs10.readFileSync(target, "utf8");
3285
+ raw = fs12.readFileSync(target, "utf8");
2689
3286
  } catch {
2690
3287
  throw new SyncFileError(
2691
3288
  file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
@@ -2731,7 +3328,7 @@ async function handleSync(deps = {}) {
2731
3328
  }
2732
3329
  if (!res.ok) {
2733
3330
  const detail = await safeErrorDetail(res);
2734
- if (res.status === 401) throw new TokenRejectedError();
3331
+ if (res.status === 401) throw new TokenRejectedError2();
2735
3332
  throw new SyncHttpError(res.status, detail);
2736
3333
  }
2737
3334
  const body = await res.json().catch(() => ({}));
@@ -2739,7 +3336,7 @@ async function handleSync(deps = {}) {
2739
3336
  return { id, url: `${endpoint}/${id}` };
2740
3337
  }
2741
3338
  function reportSyncFailure(err, write) {
2742
- if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
3339
+ if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
2743
3340
  write(err.message + "\n");
2744
3341
  return 2;
2745
3342
  }
@@ -2749,23 +3346,23 @@ function reportSyncFailure(err, write) {
2749
3346
  }
2750
3347
  return null;
2751
3348
  }
2752
- var fs10, path9, DEFAULT_API_URL, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
3349
+ var fs12, path10, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
2753
3350
  var init_sync = __esm({
2754
3351
  "src/cli/commands/sync.ts"() {
2755
3352
  "use strict";
2756
3353
  init_cjs_shims();
2757
- fs10 = __toESM(require("fs"), 1);
2758
- path9 = __toESM(require("path"), 1);
3354
+ fs12 = __toESM(require("fs"), 1);
3355
+ path10 = __toESM(require("path"), 1);
2759
3356
  init_run_result_schema();
3357
+ init_api_url();
2760
3358
  init_credentials();
2761
- DEFAULT_API_URL = "https://beta-api.plune.ai";
2762
3359
  NotLoggedInError = class extends Error {
2763
3360
  constructor() {
2764
3361
  super('Not logged in. Run "plune login" first to save your API token.');
2765
3362
  this.name = "NotLoggedInError";
2766
3363
  }
2767
3364
  };
2768
- TokenRejectedError = class extends Error {
3365
+ TokenRejectedError2 = class extends Error {
2769
3366
  constructor() {
2770
3367
  super(
2771
3368
  'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
@@ -2799,15 +3396,34 @@ var init_sync = __esm({
2799
3396
  });
2800
3397
 
2801
3398
  // src/cli/commands/cairn-artifact.ts
3399
+ async function findLatestCairnRun(runsDir = "runs") {
3400
+ const entries = await fs13.readdir(runsDir, { withFileTypes: true }).catch(() => null);
3401
+ if (entries === null) throw new NoCairnRunsError(runsDir);
3402
+ let newest;
3403
+ for (const entry of entries) {
3404
+ if (!entry.isDirectory()) continue;
3405
+ const dir = path11.join(runsDir, entry.name);
3406
+ try {
3407
+ const report = await fs13.lstat(path11.join(dir, "report.json"));
3408
+ if (!report.isFile()) continue;
3409
+ if (newest === void 0 || report.mtimeMs > newest.at) {
3410
+ newest = { dir, at: report.mtimeMs };
3411
+ }
3412
+ } catch {
3413
+ }
3414
+ }
3415
+ if (newest === void 0) throw new NoCairnRunsError(runsDir);
3416
+ return newest.dir;
3417
+ }
2802
3418
  async function readJson(file) {
2803
3419
  try {
2804
- return JSON.parse(await fs11.readFile(file, "utf8"));
3420
+ return JSON.parse(await fs13.readFile(file, "utf8"));
2805
3421
  } catch {
2806
3422
  return void 0;
2807
3423
  }
2808
3424
  }
2809
3425
  async function defaultReadEvidence(dir) {
2810
- const raw = await readJson(path10.join(dir, "api-evidence.json"));
3426
+ const raw = await readJson(path11.join(dir, "api-evidence.json"));
2811
3427
  if (!Array.isArray(raw)) return void 0;
2812
3428
  return raw.map((r) => {
2813
3429
  const rec = asRecord(r);
@@ -2849,7 +3465,7 @@ function casesFromApi(report, verdicts) {
2849
3465
  });
2850
3466
  }
2851
3467
  async function readCairnRun(dir, deps = {}) {
2852
- const raw = await readJson(path10.join(dir, "report.json"));
3468
+ const raw = await readJson(path11.join(dir, "report.json"));
2853
3469
  if (raw === void 0) throw new CairnRunNotFoundError(dir);
2854
3470
  const report = asRecord(raw);
2855
3471
  if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
@@ -2860,7 +3476,9 @@ async function readCairnRun(dir, deps = {}) {
2860
3476
  }
2861
3477
  const mode = report["mode"];
2862
3478
  if (mode !== "design" && mode !== "explore" && mode !== "api") {
2863
- throw new CairnRunVersionError(`${SUPPORTED_ARTIFACT_VERSION} (unknown run mode ${String(mode)})`);
3479
+ throw new CairnRunVersionError(
3480
+ `${SUPPORTED_ARTIFACT_VERSION} (unknown run mode ${String(mode)})`
3481
+ );
2864
3482
  }
2865
3483
  let cases;
2866
3484
  if (mode === "api") {
@@ -2877,25 +3495,33 @@ async function readCairnRun(dir, deps = {}) {
2877
3495
  return {
2878
3496
  schemaVersion: INGEST_SCHEMA_VERSION,
2879
3497
  run: {
2880
- runId: str(report["runId"], path10.basename(dir)),
3498
+ runId: str(report["runId"], path11.basename(dir)),
2881
3499
  mode,
2882
3500
  target: str(report["url"], "unknown")
2883
3501
  },
2884
3502
  cases
2885
3503
  };
2886
3504
  }
2887
- var fs11, path10, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
3505
+ var fs13, path11, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
2888
3506
  var init_cairn_artifact = __esm({
2889
3507
  "src/cli/commands/cairn-artifact.ts"() {
2890
3508
  "use strict";
2891
3509
  init_cjs_shims();
2892
- fs11 = __toESM(require("fs/promises"), 1);
2893
- path10 = __toESM(require("path"), 1);
3510
+ fs13 = __toESM(require("fs/promises"), 1);
3511
+ path11 = __toESM(require("path"), 1);
2894
3512
  SUPPORTED_ARTIFACT_VERSION = 1;
2895
3513
  INGEST_SCHEMA_VERSION = 1;
3514
+ NoCairnRunsError = class extends Error {
3515
+ constructor(runsDir) {
3516
+ super(`No Cairn run found under ${runsDir} \u2014 name the directory instead: plune ingest <dir>.`);
3517
+ this.name = "NoCairnRunsError";
3518
+ }
3519
+ };
2896
3520
  CairnRunNotFoundError = class extends Error {
2897
3521
  constructor(dir) {
2898
- super(`No Cairn run found in ${dir} \u2014 expected a report.json written by "cairn design|explore|api".`);
3522
+ super(
3523
+ `No Cairn run found in ${dir} \u2014 expected a report.json written by "cairn design|explore|api".`
3524
+ );
2899
3525
  this.name = "CairnRunNotFoundError";
2900
3526
  }
2901
3527
  };
@@ -2937,10 +3563,6 @@ __export(ingest_exports, {
2937
3563
  handleIngest: () => handleIngest,
2938
3564
  reportIngestFailure: () => reportIngestFailure
2939
3565
  });
2940
- function resolveApiUrl2(explicit) {
2941
- const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL2;
2942
- return raw.replace(/\/+$/, "");
2943
- }
2944
3566
  async function safeErrorDetail2(res) {
2945
3567
  try {
2946
3568
  const body = await res.json();
@@ -2950,10 +3572,11 @@ async function safeErrorDetail2(res) {
2950
3572
  }
2951
3573
  }
2952
3574
  async function handleIngest(deps) {
2953
- const apiUrl = resolveApiUrl2(deps.apiUrl);
3575
+ const apiUrl = resolveApiUrl(deps.apiUrl);
2954
3576
  const token = (deps.loadToken ?? loadToken)();
2955
3577
  if (token === null) throw new IngestNotLoggedInError();
2956
- const payload = await readCairnRun(deps.dir);
3578
+ const dir = deps.dir ?? await findLatestCairnRun(deps.runsDir);
3579
+ const payload = await readCairnRun(dir);
2957
3580
  const doFetch = deps.fetchImpl ?? fetch;
2958
3581
  const endpoint = `${apiUrl}/v1/ingest/cairn`;
2959
3582
  let res;
@@ -2980,7 +3603,7 @@ async function handleIngest(deps) {
2980
3603
  };
2981
3604
  }
2982
3605
  function reportIngestFailure(err, write) {
2983
- if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
3606
+ if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof NoCairnRunsError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
2984
3607
  write(`${err.message}
2985
3608
  `);
2986
3609
  return 2;
@@ -3002,14 +3625,14 @@ function formatIngestResult(r) {
3002
3625
  return `${lines.join("\n")}
3003
3626
  `;
3004
3627
  }
3005
- var DEFAULT_API_URL2, IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
3628
+ var IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
3006
3629
  var init_ingest = __esm({
3007
3630
  "src/cli/commands/ingest.ts"() {
3008
3631
  "use strict";
3009
3632
  init_cjs_shims();
3633
+ init_api_url();
3010
3634
  init_credentials();
3011
3635
  init_cairn_artifact();
3012
- DEFAULT_API_URL2 = "https://beta-api.plune.ai";
3013
3636
  IngestNotLoggedInError = class extends Error {
3014
3637
  constructor() {
3015
3638
  super('Not logged in. Run "plune login" first to save your API token.');
@@ -3046,6 +3669,7 @@ __export(cli_exports, {
3046
3669
  });
3047
3670
  module.exports = __toCommonJS(cli_exports);
3048
3671
  init_cjs_shims();
3672
+ init_api_url();
3049
3673
  var import_commander = require("commander");
3050
3674
  var import_node_fs2 = require("fs");
3051
3675
  var import_node_path2 = require("path");
@@ -3076,10 +3700,17 @@ function failUnexpected(err, verbose) {
3076
3700
  } else {
3077
3701
  process.stderr.write(String(err) + "\n");
3078
3702
  }
3703
+ process.stderr.write(
3704
+ `This one is on us. Run again with --verbose and send the output: ${FEEDBACK_URL}
3705
+ `
3706
+ );
3079
3707
  process.exit(1);
3080
3708
  }
3081
3709
  async function readTokenFromStdin() {
3082
3710
  if (process.stdin.isTTY === true) {
3711
+ const { resolveApiUrl: resolveApiUrl2, whereToGetAToken: whereToGetAToken2 } = await Promise.resolve().then(() => (init_api_url(), api_url_exports));
3712
+ process.stderr.write(`${whereToGetAToken2(resolveApiUrl2())}
3713
+ `);
3083
3714
  process.stderr.write("Paste your API token, then press Enter:\n");
3084
3715
  }
3085
3716
  const { createInterface } = await import("readline");
@@ -3095,7 +3726,7 @@ function createProgram() {
3095
3726
  const program = new import_commander.Command();
3096
3727
  program.name("plune").description("Plune \u2014 AI-powered assertion testing CLI").version(pkg.version);
3097
3728
  program.option("-c, --config <path>", "Path to plune.yaml config file (applies to all commands)").option("-v, --verbose", "Verbose output, including stack traces on unexpected errors", false).option("--no-color", "Disable colored output regardless of TTY");
3098
- program.command("run").description("Run assertions against a dataset").option("--dry-run", "Estimate cost/tokens; do not call the model", false).option("--config <path>", "Path to plune.yaml config file").option(
3729
+ const runCommand = program.command("run").description("Run assertions against a dataset").option("--dry-run", "Estimate cost/tokens; do not call the model", false).option("--config <path>", "Path to plune.yaml config file").option(
3099
3730
  "--only <selector>",
3100
3731
  "Run a subset by eval id or tag (repeatable)",
3101
3732
  (val, prev) => [...prev, val],
@@ -3171,6 +3802,58 @@ function createProgram() {
3171
3802
  }
3172
3803
  }
3173
3804
  );
3805
+ const failRunCommand = async (err, verbose) => {
3806
+ const { NoTokenError: NoTokenError2, RunCommandError: RunCommandError2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
3807
+ if (err instanceof NoTokenError2) {
3808
+ process.stderr.write(err.message + "\n");
3809
+ process.exit(2);
3810
+ }
3811
+ if (err instanceof RunCommandError2) {
3812
+ process.stderr.write(err.message + "\n");
3813
+ process.exit(1);
3814
+ }
3815
+ failUnexpected(err, verbose);
3816
+ process.exit(1);
3817
+ };
3818
+ const verboseOf = (command) => command.optsWithGlobals().verbose === true;
3819
+ runCommand.command("start").description("Open a platform run (or join one already carrying the key) and print its id").option("--key <externalKey>", "The key several processes share; generated when absent").option("--json", "Print one machine-readable line instead of prose", false).action(async (options, command) => {
3820
+ try {
3821
+ const { handleRunStart: handleRunStart2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
3822
+ await handleRunStart2({ ...options.key !== void 0 ? { key: options.key } : {}, json: options.json });
3823
+ } catch (err) {
3824
+ await failRunCommand(err, verboseOf(command));
3825
+ }
3826
+ });
3827
+ runCommand.command("finish").argument("<id>", "The run to close").description("Close a platform run \u2014 what the reporter tells you to do for a run left open").option("--terminate", "Record the run as cut short rather than finished", false).option("--reason <text>", "Why, for a terminated run").action(async (id, options, command) => {
3828
+ try {
3829
+ const { handleRunFinish: handleRunFinish2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
3830
+ await handleRunFinish2(id, {
3831
+ terminate: options.terminate,
3832
+ ...options.reason !== void 0 ? { reason: options.reason } : {}
3833
+ });
3834
+ } catch (err) {
3835
+ await failRunCommand(err, verboseOf(command));
3836
+ }
3837
+ });
3838
+ runCommand.command("exec").argument("<command...>", "The command to run inside the run, after `--`").description("Open a run, run a command inside it, close the run \u2014 whatever the command returns").option("--key <externalKey>", "The key several processes share; generated when absent").action(async (argv, options, command) => {
3839
+ try {
3840
+ const { handleRunExec: handleRunExec2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
3841
+ process.exitCode = await handleRunExec2({
3842
+ argv,
3843
+ ...options.key !== void 0 ? { key: options.key } : {}
3844
+ });
3845
+ } catch (err) {
3846
+ await failRunCommand(err, verboseOf(command));
3847
+ }
3848
+ });
3849
+ runCommand.command("report").description("Send what the reporter could not \u2014 replays .plune/pending-results.jsonl").option("--file <path>", "The fallback file to replay").action(async (options, command) => {
3850
+ try {
3851
+ const { handleRunReport: handleRunReport2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
3852
+ await handleRunReport2({ ...options.file !== void 0 ? { file: options.file } : {} });
3853
+ } catch (err) {
3854
+ await failRunCommand(err, verboseOf(command));
3855
+ }
3856
+ });
3174
3857
  program.command("report").description("Render the most recent run in a chosen format").option("--format <fmt>", "console | json | markdown", "console").option("-o, --output <path>", "Write the report to a file instead of stdout").action(async (options, command) => {
3175
3858
  const globals = command.optsWithGlobals();
3176
3859
  const [{ loadEnv: loadEnv2 }, { handleReport: handleReport2, ReportNotFoundError: ReportNotFoundError2 }, { renderReport: renderReport2 }] = await Promise.all([
@@ -3234,18 +3917,26 @@ function createProgram() {
3234
3917
  failUnexpected(err, verbose);
3235
3918
  }
3236
3919
  });
3237
- program.command("login").description("Save your Plune API token so the CLI can reach the platform").option("--token <token>", "API token (omit to paste it / pipe it via stdin)").action(async (options, command) => {
3920
+ program.command("login").description("Save your Plune API token (get one in the dashboard: Settings \u2192 API tokens)").option("--token <token>", "API token (omit to paste it / pipe it via stdin)").option(
3921
+ "--skip-verify",
3922
+ "Save without checking the token against the API (offline setup)",
3923
+ false
3924
+ ).action(async (options, command) => {
3238
3925
  const verbose = command.optsWithGlobals().verbose === true;
3239
- const { handleLogin: handleLogin2, EmptyTokenError: EmptyTokenError2 } = await Promise.resolve().then(() => (init_login(), login_exports));
3926
+ const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
3240
3927
  try {
3241
3928
  const token = options.token ?? await readTokenFromStdin();
3242
- const { path: path11 } = handleLogin2({ token });
3243
- process.stdout.write(`Logged in. Token saved to ${path11}
3244
- `);
3929
+ const { path: path12, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
3930
+ process.stdout.write(
3931
+ verified ? `Logged in. Token checked against the API and saved to ${path12}
3932
+ ` : `Token saved to ${path12} WITHOUT being checked (--skip-verify).
3933
+ `
3934
+ );
3245
3935
  } catch (err) {
3246
- if (err instanceof EmptyTokenError2) {
3247
- process.stderr.write(err.message + "\n");
3248
- process.exit(2);
3936
+ const code = reportLoginFailure2(err, (s) => process.stderr.write(s));
3937
+ if (code !== null) {
3938
+ maybeStack(err, verbose);
3939
+ process.exitCode = code;
3249
3940
  return;
3250
3941
  }
3251
3942
  failUnexpected(err, verbose);
@@ -3255,9 +3946,9 @@ function createProgram() {
3255
3946
  const verbose = command.optsWithGlobals().verbose === true;
3256
3947
  const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
3257
3948
  try {
3258
- const { removed, path: path11 } = handleLogout2();
3949
+ const { removed, path: path12 } = handleLogout2();
3259
3950
  process.stdout.write(
3260
- removed ? `Logged out. Removed ${path11}
3951
+ removed ? `Logged out. Removed ${path12}
3261
3952
  ` : "Not logged in \u2014 nothing to remove.\n"
3262
3953
  );
3263
3954
  } catch (err) {
@@ -3286,12 +3977,14 @@ function createProgram() {
3286
3977
  failUnexpected(err, verbose);
3287
3978
  }
3288
3979
  });
3289
- program.command("ingest").argument("<dir>", "A Cairn run directory (the one holding report.json)").description("Record a Cairn run in Plune \u2014 generated cases arrive as review proposals").action(async (dir, _options, command) => {
3980
+ program.command("ingest").argument("[dir]", "A Cairn run directory; omit it for the newest run under ./runs").description("Record a Cairn run in Plune \u2014 generated cases arrive as review proposals").action(async (dir, _options, command) => {
3290
3981
  const globals = command.optsWithGlobals();
3291
3982
  const verbose = globals.verbose === true;
3292
3983
  const { handleIngest: handleIngest2, reportIngestFailure: reportIngestFailure2, formatIngestResult: formatIngestResult2 } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
3293
3984
  try {
3294
- process.stdout.write(formatIngestResult2(await handleIngest2({ dir })));
3985
+ process.stdout.write(
3986
+ formatIngestResult2(await handleIngest2({ ...dir !== void 0 ? { dir } : {} }))
3987
+ );
3295
3988
  } catch (err) {
3296
3989
  const code = reportIngestFailure2(err, (s) => process.stderr.write(s));
3297
3990
  if (code !== null) {