@plune-ai/cli 0.4.0 → 0.6.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
@@ -808,9 +808,11 @@ var init_embedder = __esm({
808
808
  return rows.map((row) => Float32Array.from(row));
809
809
  }
810
810
  load() {
811
- this.extractor ??= import("@huggingface/transformers").then(
812
- ({ pipeline }) => pipeline("feature-extraction", MODEL_ID)
813
- );
811
+ this.extractor ??= import("@huggingface/transformers").catch(() => {
812
+ throw new Error(
813
+ "The `semantic-similarity` and RAG assertions need local embeddings, which ship separately: install @huggingface/transformers to enable them. Every other assertion works without it."
814
+ );
815
+ }).then(({ pipeline }) => pipeline("feature-extraction", MODEL_ID));
814
816
  return this.extractor;
815
817
  }
816
818
  };
@@ -2796,6 +2798,247 @@ var init_sync = __esm({
2796
2798
  }
2797
2799
  });
2798
2800
 
2801
+ // src/cli/commands/cairn-artifact.ts
2802
+ async function readJson(file) {
2803
+ try {
2804
+ return JSON.parse(await fs11.readFile(file, "utf8"));
2805
+ } catch {
2806
+ return void 0;
2807
+ }
2808
+ }
2809
+ async function defaultReadEvidence(dir) {
2810
+ const raw = await readJson(path10.join(dir, "api-evidence.json"));
2811
+ if (!Array.isArray(raw)) return void 0;
2812
+ return raw.map((r) => {
2813
+ const rec = asRecord(r);
2814
+ return { name: str(rec["name"]), passed: rec["passed"] === true };
2815
+ });
2816
+ }
2817
+ function casesFromDesign(report) {
2818
+ const list = Array.isArray(report["testCases"]) ? report["testCases"] : [];
2819
+ return list.map((raw) => {
2820
+ const c = asRecord(raw);
2821
+ const steps = (Array.isArray(c["steps"]) ? c["steps"] : []).map((s) => str(s)).filter((s) => s !== "");
2822
+ return {
2823
+ stableId: str(c["stableId"]),
2824
+ title: str(c["title"], "untitled case"),
2825
+ execution: c["execution"] === "manual" ? "manual" : "auto",
2826
+ steps: steps.length > 0 ? steps : ["(no steps recorded)"],
2827
+ expected: str(c["expected"], "(no expected result recorded)"),
2828
+ ...typeof c["technique"] === "string" ? { technique: c["technique"] } : {},
2829
+ ...typeof c["priority"] === "string" ? { priority: c["priority"] } : {}
2830
+ };
2831
+ });
2832
+ }
2833
+ function casesFromApi(report, verdicts) {
2834
+ const list = Array.isArray(report["cases"]) ? report["cases"] : [];
2835
+ return list.map((raw) => {
2836
+ const c = asRecord(raw);
2837
+ const name = str(c["name"], "unnamed operation");
2838
+ const request = `${str(c["method"], "GET")} ${str(c["path"], "/")}`;
2839
+ const verdict = verdicts.get(name);
2840
+ return {
2841
+ stableId: str(c["stableId"]),
2842
+ title: name,
2843
+ execution: "auto",
2844
+ steps: [request],
2845
+ expected: `HTTP ${str(c["expectedStatus"], "default")}`,
2846
+ ...verdict !== void 0 ? { verdict } : {},
2847
+ ...typeof c["technique"] === "string" ? { technique: c["technique"] } : {}
2848
+ };
2849
+ });
2850
+ }
2851
+ async function readCairnRun(dir, deps = {}) {
2852
+ const raw = await readJson(path10.join(dir, "report.json"));
2853
+ if (raw === void 0) throw new CairnRunNotFoundError(dir);
2854
+ const report = asRecord(raw);
2855
+ if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
2856
+ throw new CairnRunVersionError(report["schemaVersion"]);
2857
+ }
2858
+ if (report["partial"] === true) {
2859
+ throw new CairnRunPartialError(str(report["error"], "no reason recorded"));
2860
+ }
2861
+ const mode = report["mode"];
2862
+ if (mode !== "design" && mode !== "explore" && mode !== "api") {
2863
+ throw new CairnRunVersionError(`${SUPPORTED_ARTIFACT_VERSION} (unknown run mode ${String(mode)})`);
2864
+ }
2865
+ let cases;
2866
+ if (mode === "api") {
2867
+ const evidence = await (deps.readEvidence ?? defaultReadEvidence)(dir);
2868
+ const verdicts = /* @__PURE__ */ new Map();
2869
+ for (const e of evidence ?? []) {
2870
+ if (verdicts.has(e.name)) throw new CairnEvidenceAmbiguousError(e.name);
2871
+ verdicts.set(e.name, e.passed ? "passed" : "failed");
2872
+ }
2873
+ cases = casesFromApi(report, verdicts);
2874
+ } else {
2875
+ cases = casesFromDesign(report);
2876
+ }
2877
+ return {
2878
+ schemaVersion: INGEST_SCHEMA_VERSION,
2879
+ run: {
2880
+ runId: str(report["runId"], path10.basename(dir)),
2881
+ mode,
2882
+ target: str(report["url"], "unknown")
2883
+ },
2884
+ cases
2885
+ };
2886
+ }
2887
+ var fs11, path10, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
2888
+ var init_cairn_artifact = __esm({
2889
+ "src/cli/commands/cairn-artifact.ts"() {
2890
+ "use strict";
2891
+ init_cjs_shims();
2892
+ fs11 = __toESM(require("fs/promises"), 1);
2893
+ path10 = __toESM(require("path"), 1);
2894
+ SUPPORTED_ARTIFACT_VERSION = 1;
2895
+ INGEST_SCHEMA_VERSION = 1;
2896
+ CairnRunNotFoundError = class extends Error {
2897
+ constructor(dir) {
2898
+ super(`No Cairn run found in ${dir} \u2014 expected a report.json written by "cairn design|explore|api".`);
2899
+ this.name = "CairnRunNotFoundError";
2900
+ }
2901
+ };
2902
+ CairnRunVersionError = class extends Error {
2903
+ constructor(found) {
2904
+ super(
2905
+ found === void 0 ? "This run predates versioned Cairn artifacts. Re-run it with Cairn 0.7.0 or newer, which stamps the format so it can be read safely." : `Unsupported Cairn artifact version ${String(found)} (this CLI reads version ${SUPPORTED_ARTIFACT_VERSION}). Upgrade @plune-ai/cli.`
2906
+ );
2907
+ this.name = "CairnRunVersionError";
2908
+ }
2909
+ };
2910
+ CairnRunPartialError = class extends Error {
2911
+ constructor(reason) {
2912
+ super(`That Cairn run did not finish, so its cases are incomplete: ${reason}`);
2913
+ this.name = "CairnRunPartialError";
2914
+ }
2915
+ };
2916
+ CairnEvidenceAmbiguousError = class extends Error {
2917
+ constructor(name) {
2918
+ super(
2919
+ `Cannot attach results safely: the operation "${name}" appears more than once in this run's evidence, and the evidence file carries no case identity to disambiguate it. Nothing was uploaded.`
2920
+ );
2921
+ this.name = "CairnEvidenceAmbiguousError";
2922
+ }
2923
+ };
2924
+ asRecord = (v) => typeof v === "object" && v !== null ? v : {};
2925
+ str = (v, fallback = "") => typeof v === "string" && v !== "" ? v : fallback;
2926
+ }
2927
+ });
2928
+
2929
+ // src/cli/commands/ingest.ts
2930
+ var ingest_exports = {};
2931
+ __export(ingest_exports, {
2932
+ IngestHttpError: () => IngestHttpError,
2933
+ IngestNetworkError: () => IngestNetworkError,
2934
+ IngestNotLoggedInError: () => IngestNotLoggedInError,
2935
+ IngestTokenRejectedError: () => IngestTokenRejectedError,
2936
+ formatIngestResult: () => formatIngestResult,
2937
+ handleIngest: () => handleIngest,
2938
+ reportIngestFailure: () => reportIngestFailure
2939
+ });
2940
+ function resolveApiUrl2(explicit) {
2941
+ const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL2;
2942
+ return raw.replace(/\/+$/, "");
2943
+ }
2944
+ async function safeErrorDetail2(res) {
2945
+ try {
2946
+ const body = await res.json();
2947
+ return typeof body.error === "string" ? body.error : "";
2948
+ } catch {
2949
+ return "";
2950
+ }
2951
+ }
2952
+ async function handleIngest(deps) {
2953
+ const apiUrl = resolveApiUrl2(deps.apiUrl);
2954
+ const token = (deps.loadToken ?? loadToken)();
2955
+ if (token === null) throw new IngestNotLoggedInError();
2956
+ const payload = await readCairnRun(deps.dir);
2957
+ const doFetch = deps.fetchImpl ?? fetch;
2958
+ const endpoint = `${apiUrl}/v1/ingest/cairn`;
2959
+ let res;
2960
+ try {
2961
+ res = await doFetch(endpoint, {
2962
+ method: "POST",
2963
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
2964
+ body: JSON.stringify(payload)
2965
+ });
2966
+ } catch {
2967
+ throw new IngestNetworkError(apiUrl);
2968
+ }
2969
+ if (!res.ok) {
2970
+ const detail = await safeErrorDetail2(res);
2971
+ if (res.status === 401) throw new IngestTokenRejectedError();
2972
+ throw new IngestHttpError(res.status, detail);
2973
+ }
2974
+ const body = await res.json().catch(() => ({}));
2975
+ return {
2976
+ runId: typeof body.runId === "string" ? body.runId : "",
2977
+ linked: typeof body.linked === "number" ? body.linked : 0,
2978
+ proposed: typeof body.proposed === "number" ? body.proposed : 0,
2979
+ skipped: typeof body.skipped === "number" ? body.skipped : 0
2980
+ };
2981
+ }
2982
+ function reportIngestFailure(err, write) {
2983
+ if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
2984
+ write(`${err.message}
2985
+ `);
2986
+ return 2;
2987
+ }
2988
+ if (err instanceof IngestNetworkError || err instanceof IngestHttpError) {
2989
+ write(`${err.message}
2990
+ `);
2991
+ return 1;
2992
+ }
2993
+ return null;
2994
+ }
2995
+ function formatIngestResult(r) {
2996
+ const lines = [
2997
+ `Ingested Cairn run \u2192 ${r.runId}`,
2998
+ ` ${r.linked} result(s) attached to existing cases`,
2999
+ ` ${r.proposed} case(s) proposed \u2014 review them in Plune to accept`,
3000
+ ` ${r.skipped} skipped (already queued, already rejected, repeated, or never executed)`
3001
+ ];
3002
+ return `${lines.join("\n")}
3003
+ `;
3004
+ }
3005
+ var DEFAULT_API_URL2, IngestNotLoggedInError, IngestTokenRejectedError, IngestNetworkError, IngestHttpError;
3006
+ var init_ingest = __esm({
3007
+ "src/cli/commands/ingest.ts"() {
3008
+ "use strict";
3009
+ init_cjs_shims();
3010
+ init_credentials();
3011
+ init_cairn_artifact();
3012
+ DEFAULT_API_URL2 = "https://beta-api.plune.ai";
3013
+ IngestNotLoggedInError = class extends Error {
3014
+ constructor() {
3015
+ super('Not logged in. Run "plune login" first to save your API token.');
3016
+ this.name = "IngestNotLoggedInError";
3017
+ }
3018
+ };
3019
+ IngestTokenRejectedError = class extends Error {
3020
+ constructor() {
3021
+ super('Your API token was rejected (401). Run "plune login" again with a fresh token.');
3022
+ this.name = "IngestTokenRejectedError";
3023
+ }
3024
+ };
3025
+ IngestNetworkError = class extends Error {
3026
+ constructor(apiUrl) {
3027
+ super(`Could not reach the Plune API at ${apiUrl}. Check your connection, then try again.`);
3028
+ this.name = "IngestNetworkError";
3029
+ }
3030
+ };
3031
+ IngestHttpError = class extends Error {
3032
+ constructor(status, detail) {
3033
+ super(`Plune API returned ${status}${detail !== "" ? ` \u2014 ${detail}` : ""}.`);
3034
+ this.status = status;
3035
+ this.name = "IngestHttpError";
3036
+ }
3037
+ status;
3038
+ };
3039
+ }
3040
+ });
3041
+
2799
3042
  // src/cli.ts
2800
3043
  var cli_exports = {};
2801
3044
  __export(cli_exports, {
@@ -2996,8 +3239,8 @@ function createProgram() {
2996
3239
  const { handleLogin: handleLogin2, EmptyTokenError: EmptyTokenError2 } = await Promise.resolve().then(() => (init_login(), login_exports));
2997
3240
  try {
2998
3241
  const token = options.token ?? await readTokenFromStdin();
2999
- const { path: path10 } = handleLogin2({ token });
3000
- process.stdout.write(`Logged in. Token saved to ${path10}
3242
+ const { path: path11 } = handleLogin2({ token });
3243
+ process.stdout.write(`Logged in. Token saved to ${path11}
3001
3244
  `);
3002
3245
  } catch (err) {
3003
3246
  if (err instanceof EmptyTokenError2) {
@@ -3012,9 +3255,9 @@ function createProgram() {
3012
3255
  const verbose = command.optsWithGlobals().verbose === true;
3013
3256
  const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
3014
3257
  try {
3015
- const { removed, path: path10 } = handleLogout2();
3258
+ const { removed, path: path11 } = handleLogout2();
3016
3259
  process.stdout.write(
3017
- removed ? `Logged out. Removed ${path10}
3260
+ removed ? `Logged out. Removed ${path11}
3018
3261
  ` : "Not logged in \u2014 nothing to remove.\n"
3019
3262
  );
3020
3263
  } catch (err) {
@@ -3043,6 +3286,22 @@ function createProgram() {
3043
3286
  failUnexpected(err, verbose);
3044
3287
  }
3045
3288
  });
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) => {
3290
+ const globals = command.optsWithGlobals();
3291
+ const verbose = globals.verbose === true;
3292
+ const { handleIngest: handleIngest2, reportIngestFailure: reportIngestFailure2, formatIngestResult: formatIngestResult2 } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
3293
+ try {
3294
+ process.stdout.write(formatIngestResult2(await handleIngest2({ dir })));
3295
+ } catch (err) {
3296
+ const code = reportIngestFailure2(err, (s) => process.stderr.write(s));
3297
+ if (code !== null) {
3298
+ maybeStack(err, verbose);
3299
+ process.exitCode = code;
3300
+ return;
3301
+ }
3302
+ failUnexpected(err, verbose);
3303
+ }
3304
+ });
3046
3305
  program.on("command:*", (operands) => {
3047
3306
  process.stderr.write(`Unknown command: ${operands.join(" ")}
3048
3307
  `);