@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 +266 -7
- package/dist/index.cjs +377 -6
- package/dist/index.d.cts +118 -1
- package/dist/index.d.ts +118 -1
- package/dist/index.js +365 -5
- package/package.json +11 -3
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").
|
|
812
|
-
|
|
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:
|
|
3000
|
-
process.stdout.write(`Logged in. Token saved to ${
|
|
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:
|
|
3258
|
+
const { removed, path: path11 } = handleLogout2();
|
|
3016
3259
|
process.stdout.write(
|
|
3017
|
-
removed ? `Logged out. Removed ${
|
|
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
|
`);
|