@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/index.js CHANGED
@@ -315,7 +315,7 @@ function normalizeProviderError(err, secret, envVar) {
315
315
  }
316
316
 
317
317
  // src/providers/retry.ts
318
- var defaultSleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
318
+ var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
319
319
  function retryAfterMs(err) {
320
320
  if (typeof err !== "object" || err === null || !("headers" in err)) return void 0;
321
321
  const raw = readHeader(err.headers, "retry-after");
@@ -595,9 +595,11 @@ var XenovaEmbedder = class {
595
595
  return rows.map((row) => Float32Array.from(row));
596
596
  }
597
597
  load() {
598
- this.extractor ??= import("@huggingface/transformers").then(
599
- ({ pipeline }) => pipeline("feature-extraction", MODEL_ID)
600
- );
598
+ this.extractor ??= import("@huggingface/transformers").catch(() => {
599
+ throw new Error(
600
+ "The `semantic-similarity` and RAG assertions need local embeddings, which ship separately: install @huggingface/transformers to enable them. Every other assertion works without it."
601
+ );
602
+ }).then(({ pipeline }) => pipeline("feature-extraction", MODEL_ID));
601
603
  return this.extractor;
602
604
  }
603
605
  };
@@ -1461,6 +1463,10 @@ async function handleRun(options, depsFactory) {
1461
1463
  }
1462
1464
  }
1463
1465
 
1466
+ // src/cli/commands/sync.ts
1467
+ import * as fs6 from "fs";
1468
+ import * as path5 from "path";
1469
+
1464
1470
  // src/types/run-result-schema.ts
1465
1471
  import { z as z2 } from "zod";
1466
1472
  var RUN_RESULT_SCHEMA_VERSION = 1;
@@ -1519,12 +1525,366 @@ var runResultSchema = z2.object({
1519
1525
  function parseRunResult(value) {
1520
1526
  return runResultSchema.parse(value);
1521
1527
  }
1528
+
1529
+ // src/cli/credentials.ts
1530
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
1531
+ import { homedir } from "os";
1532
+ import { dirname as dirname3, join as join3 } from "path";
1533
+ function credentialsFile() {
1534
+ const base = process.env["XDG_CONFIG_HOME"]?.trim() || join3(homedir(), ".config");
1535
+ return join3(base, "plune", "credentials.json");
1536
+ }
1537
+ function loadToken() {
1538
+ try {
1539
+ const parsed = JSON.parse(readFileSync3(credentialsFile(), "utf-8"));
1540
+ return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
1541
+ } catch {
1542
+ return null;
1543
+ }
1544
+ }
1545
+
1546
+ // src/cli/commands/sync.ts
1547
+ var DEFAULT_API_URL = "https://beta-api.plune.ai";
1548
+ var NotLoggedInError = class extends Error {
1549
+ constructor() {
1550
+ super('Not logged in. Run "plune login" first to save your API token.');
1551
+ this.name = "NotLoggedInError";
1552
+ }
1553
+ };
1554
+ var TokenRejectedError = class extends Error {
1555
+ constructor() {
1556
+ super(
1557
+ 'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
1558
+ );
1559
+ this.name = "TokenRejectedError";
1560
+ }
1561
+ };
1562
+ var SyncFileError = class extends Error {
1563
+ constructor(message) {
1564
+ super(message);
1565
+ this.name = "SyncFileError";
1566
+ }
1567
+ };
1568
+ var SyncNetworkError = class extends Error {
1569
+ constructor(apiUrl) {
1570
+ super(
1571
+ `Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
1572
+ );
1573
+ this.name = "SyncNetworkError";
1574
+ }
1575
+ };
1576
+ var SyncHttpError = class extends Error {
1577
+ constructor(status, detail) {
1578
+ super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
1579
+ this.status = status;
1580
+ this.name = "SyncHttpError";
1581
+ }
1582
+ status;
1583
+ };
1584
+ function resolveApiUrl(explicit) {
1585
+ const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL;
1586
+ return raw.replace(/\/+$/, "");
1587
+ }
1588
+ function readRun(cwd, file) {
1589
+ const target = file !== void 0 ? path5.resolve(cwd, file) : path5.join(cwd, ".plune", "last-run.json");
1590
+ let raw;
1591
+ try {
1592
+ raw = fs6.readFileSync(target, "utf8");
1593
+ } catch {
1594
+ throw new SyncFileError(
1595
+ file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
1596
+ );
1597
+ }
1598
+ let json;
1599
+ try {
1600
+ json = JSON.parse(raw);
1601
+ } catch {
1602
+ throw new SyncFileError(`Run file is not valid JSON: ${target}`);
1603
+ }
1604
+ try {
1605
+ return parseRunResult(json);
1606
+ } catch {
1607
+ throw new SyncFileError(`Run file does not match the RunResult schema: ${target}`);
1608
+ }
1609
+ }
1610
+ async function safeErrorDetail(res) {
1611
+ try {
1612
+ const body = await res.json();
1613
+ return typeof body.error === "string" ? body.error : "";
1614
+ } catch {
1615
+ return "";
1616
+ }
1617
+ }
1618
+ async function handleSync(deps = {}) {
1619
+ const cwd = deps.cwd ?? process.cwd();
1620
+ const apiUrl = resolveApiUrl(deps.apiUrl);
1621
+ const token = (deps.loadToken ?? loadToken)();
1622
+ if (token === null) throw new NotLoggedInError();
1623
+ const run = readRun(cwd, deps.file);
1624
+ const doFetch = deps.fetchImpl ?? fetch;
1625
+ const endpoint = `${apiUrl}/v1/runs`;
1626
+ let res;
1627
+ try {
1628
+ res = await doFetch(endpoint, {
1629
+ method: "POST",
1630
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
1631
+ body: JSON.stringify(run)
1632
+ });
1633
+ } catch {
1634
+ throw new SyncNetworkError(apiUrl);
1635
+ }
1636
+ if (!res.ok) {
1637
+ const detail = await safeErrorDetail(res);
1638
+ if (res.status === 401) throw new TokenRejectedError();
1639
+ throw new SyncHttpError(res.status, detail);
1640
+ }
1641
+ const body = await res.json().catch(() => ({}));
1642
+ const id = typeof body.id === "string" ? body.id : "";
1643
+ return { id, url: `${endpoint}/${id}` };
1644
+ }
1645
+ function reportSyncFailure(err, write) {
1646
+ if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
1647
+ write(err.message + "\n");
1648
+ return 2;
1649
+ }
1650
+ if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
1651
+ write(err.message + "\n");
1652
+ return 1;
1653
+ }
1654
+ return null;
1655
+ }
1656
+
1657
+ // src/cli/commands/cairn-artifact.ts
1658
+ import * as fs7 from "fs/promises";
1659
+ import * as path6 from "path";
1660
+ var SUPPORTED_ARTIFACT_VERSION = 1;
1661
+ var INGEST_SCHEMA_VERSION = 1;
1662
+ var CairnRunNotFoundError = class extends Error {
1663
+ constructor(dir) {
1664
+ super(`No Cairn run found in ${dir} \u2014 expected a report.json written by "cairn design|explore|api".`);
1665
+ this.name = "CairnRunNotFoundError";
1666
+ }
1667
+ };
1668
+ var CairnRunVersionError = class extends Error {
1669
+ constructor(found) {
1670
+ super(
1671
+ 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.`
1672
+ );
1673
+ this.name = "CairnRunVersionError";
1674
+ }
1675
+ };
1676
+ var CairnRunPartialError = class extends Error {
1677
+ constructor(reason) {
1678
+ super(`That Cairn run did not finish, so its cases are incomplete: ${reason}`);
1679
+ this.name = "CairnRunPartialError";
1680
+ }
1681
+ };
1682
+ var CairnEvidenceAmbiguousError = class extends Error {
1683
+ constructor(name) {
1684
+ super(
1685
+ `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.`
1686
+ );
1687
+ this.name = "CairnEvidenceAmbiguousError";
1688
+ }
1689
+ };
1690
+ var asRecord = (v) => typeof v === "object" && v !== null ? v : {};
1691
+ var str = (v, fallback = "") => typeof v === "string" && v !== "" ? v : fallback;
1692
+ async function readJson(file) {
1693
+ try {
1694
+ return JSON.parse(await fs7.readFile(file, "utf8"));
1695
+ } catch {
1696
+ return void 0;
1697
+ }
1698
+ }
1699
+ async function defaultReadEvidence(dir) {
1700
+ const raw = await readJson(path6.join(dir, "api-evidence.json"));
1701
+ if (!Array.isArray(raw)) return void 0;
1702
+ return raw.map((r) => {
1703
+ const rec = asRecord(r);
1704
+ return { name: str(rec["name"]), passed: rec["passed"] === true };
1705
+ });
1706
+ }
1707
+ function casesFromDesign(report) {
1708
+ const list = Array.isArray(report["testCases"]) ? report["testCases"] : [];
1709
+ return list.map((raw) => {
1710
+ const c = asRecord(raw);
1711
+ const steps = (Array.isArray(c["steps"]) ? c["steps"] : []).map((s) => str(s)).filter((s) => s !== "");
1712
+ return {
1713
+ stableId: str(c["stableId"]),
1714
+ title: str(c["title"], "untitled case"),
1715
+ execution: c["execution"] === "manual" ? "manual" : "auto",
1716
+ steps: steps.length > 0 ? steps : ["(no steps recorded)"],
1717
+ expected: str(c["expected"], "(no expected result recorded)"),
1718
+ ...typeof c["technique"] === "string" ? { technique: c["technique"] } : {},
1719
+ ...typeof c["priority"] === "string" ? { priority: c["priority"] } : {}
1720
+ };
1721
+ });
1722
+ }
1723
+ function casesFromApi(report, verdicts) {
1724
+ const list = Array.isArray(report["cases"]) ? report["cases"] : [];
1725
+ return list.map((raw) => {
1726
+ const c = asRecord(raw);
1727
+ const name = str(c["name"], "unnamed operation");
1728
+ const request = `${str(c["method"], "GET")} ${str(c["path"], "/")}`;
1729
+ const verdict = verdicts.get(name);
1730
+ return {
1731
+ stableId: str(c["stableId"]),
1732
+ title: name,
1733
+ execution: "auto",
1734
+ steps: [request],
1735
+ expected: `HTTP ${str(c["expectedStatus"], "default")}`,
1736
+ ...verdict !== void 0 ? { verdict } : {},
1737
+ ...typeof c["technique"] === "string" ? { technique: c["technique"] } : {}
1738
+ };
1739
+ });
1740
+ }
1741
+ async function readCairnRun(dir, deps = {}) {
1742
+ const raw = await readJson(path6.join(dir, "report.json"));
1743
+ if (raw === void 0) throw new CairnRunNotFoundError(dir);
1744
+ const report = asRecord(raw);
1745
+ if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
1746
+ throw new CairnRunVersionError(report["schemaVersion"]);
1747
+ }
1748
+ if (report["partial"] === true) {
1749
+ throw new CairnRunPartialError(str(report["error"], "no reason recorded"));
1750
+ }
1751
+ const mode = report["mode"];
1752
+ if (mode !== "design" && mode !== "explore" && mode !== "api") {
1753
+ throw new CairnRunVersionError(`${SUPPORTED_ARTIFACT_VERSION} (unknown run mode ${String(mode)})`);
1754
+ }
1755
+ let cases;
1756
+ if (mode === "api") {
1757
+ const evidence = await (deps.readEvidence ?? defaultReadEvidence)(dir);
1758
+ const verdicts = /* @__PURE__ */ new Map();
1759
+ for (const e of evidence ?? []) {
1760
+ if (verdicts.has(e.name)) throw new CairnEvidenceAmbiguousError(e.name);
1761
+ verdicts.set(e.name, e.passed ? "passed" : "failed");
1762
+ }
1763
+ cases = casesFromApi(report, verdicts);
1764
+ } else {
1765
+ cases = casesFromDesign(report);
1766
+ }
1767
+ return {
1768
+ schemaVersion: INGEST_SCHEMA_VERSION,
1769
+ run: {
1770
+ runId: str(report["runId"], path6.basename(dir)),
1771
+ mode,
1772
+ target: str(report["url"], "unknown")
1773
+ },
1774
+ cases
1775
+ };
1776
+ }
1777
+
1778
+ // src/cli/commands/ingest.ts
1779
+ var DEFAULT_API_URL2 = "https://beta-api.plune.ai";
1780
+ var IngestNotLoggedInError = class extends Error {
1781
+ constructor() {
1782
+ super('Not logged in. Run "plune login" first to save your API token.');
1783
+ this.name = "IngestNotLoggedInError";
1784
+ }
1785
+ };
1786
+ var IngestTokenRejectedError = class extends Error {
1787
+ constructor() {
1788
+ super('Your API token was rejected (401). Run "plune login" again with a fresh token.');
1789
+ this.name = "IngestTokenRejectedError";
1790
+ }
1791
+ };
1792
+ var IngestNetworkError = class extends Error {
1793
+ constructor(apiUrl) {
1794
+ super(`Could not reach the Plune API at ${apiUrl}. Check your connection, then try again.`);
1795
+ this.name = "IngestNetworkError";
1796
+ }
1797
+ };
1798
+ var IngestHttpError = class extends Error {
1799
+ constructor(status, detail) {
1800
+ super(`Plune API returned ${status}${detail !== "" ? ` \u2014 ${detail}` : ""}.`);
1801
+ this.status = status;
1802
+ this.name = "IngestHttpError";
1803
+ }
1804
+ status;
1805
+ };
1806
+ function resolveApiUrl2(explicit) {
1807
+ const raw = explicit ?? process.env["PLUNE_API_URL"] ?? DEFAULT_API_URL2;
1808
+ return raw.replace(/\/+$/, "");
1809
+ }
1810
+ async function safeErrorDetail2(res) {
1811
+ try {
1812
+ const body = await res.json();
1813
+ return typeof body.error === "string" ? body.error : "";
1814
+ } catch {
1815
+ return "";
1816
+ }
1817
+ }
1818
+ async function handleIngest(deps) {
1819
+ const apiUrl = resolveApiUrl2(deps.apiUrl);
1820
+ const token = (deps.loadToken ?? loadToken)();
1821
+ if (token === null) throw new IngestNotLoggedInError();
1822
+ const payload = await readCairnRun(deps.dir);
1823
+ const doFetch = deps.fetchImpl ?? fetch;
1824
+ const endpoint = `${apiUrl}/v1/ingest/cairn`;
1825
+ let res;
1826
+ try {
1827
+ res = await doFetch(endpoint, {
1828
+ method: "POST",
1829
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
1830
+ body: JSON.stringify(payload)
1831
+ });
1832
+ } catch {
1833
+ throw new IngestNetworkError(apiUrl);
1834
+ }
1835
+ if (!res.ok) {
1836
+ const detail = await safeErrorDetail2(res);
1837
+ if (res.status === 401) throw new IngestTokenRejectedError();
1838
+ throw new IngestHttpError(res.status, detail);
1839
+ }
1840
+ const body = await res.json().catch(() => ({}));
1841
+ return {
1842
+ runId: typeof body.runId === "string" ? body.runId : "",
1843
+ linked: typeof body.linked === "number" ? body.linked : 0,
1844
+ proposed: typeof body.proposed === "number" ? body.proposed : 0,
1845
+ skipped: typeof body.skipped === "number" ? body.skipped : 0
1846
+ };
1847
+ }
1848
+ function reportIngestFailure(err, write) {
1849
+ if (err instanceof IngestNotLoggedInError || err instanceof IngestTokenRejectedError || err instanceof CairnRunNotFoundError || err instanceof CairnRunVersionError || err instanceof CairnRunPartialError || err instanceof CairnEvidenceAmbiguousError) {
1850
+ write(`${err.message}
1851
+ `);
1852
+ return 2;
1853
+ }
1854
+ if (err instanceof IngestNetworkError || err instanceof IngestHttpError) {
1855
+ write(`${err.message}
1856
+ `);
1857
+ return 1;
1858
+ }
1859
+ return null;
1860
+ }
1861
+ function formatIngestResult(r) {
1862
+ const lines = [
1863
+ `Ingested Cairn run \u2192 ${r.runId}`,
1864
+ ` ${r.linked} result(s) attached to existing cases`,
1865
+ ` ${r.proposed} case(s) proposed \u2014 review them in Plune to accept`,
1866
+ ` ${r.skipped} skipped (already queued, already rejected, repeated, or never executed)`
1867
+ ];
1868
+ return `${lines.join("\n")}
1869
+ `;
1870
+ }
1522
1871
  export {
1872
+ NotLoggedInError,
1523
1873
  RUN_RESULT_SCHEMA_VERSION,
1874
+ SyncFileError,
1875
+ SyncHttpError,
1876
+ SyncNetworkError,
1877
+ TokenRejectedError,
1524
1878
  assertionConfigSchema,
1525
1879
  assertionResultRecordSchema,
1526
1880
  binaryVerdictSchema,
1881
+ formatIngestResult,
1882
+ handleIngest as ingest,
1527
1883
  parseRunResult,
1884
+ readCairnRun,
1885
+ reportIngestFailure,
1886
+ reportSyncFailure,
1528
1887
  handleRun as run,
1529
- runResultSchema
1888
+ runResultSchema,
1889
+ handleSync as sync
1530
1890
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plune-ai/cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "AI-powered assertion testing for LLM apps — write assertions, run them against any provider, get a pass/fail report (local, CI, or diff).",
5
5
  "keywords": [
6
6
  "ai",
@@ -53,8 +53,7 @@
53
53
  "dependencies": {
54
54
  "@anthropic-ai/sdk": "^0.102.0",
55
55
  "@clack/prompts": "^1.5.1",
56
- "@huggingface/transformers": "4.2.0",
57
- "ajv": "^8.20.0",
56
+ "ajv": "^8.17.1",
58
57
  "better-sqlite3": "^12.10.0",
59
58
  "commander": "^12.0.0",
60
59
  "dotenv": "^16.4.0",
@@ -63,6 +62,7 @@
63
62
  "zod": "^3.23.0"
64
63
  },
65
64
  "devDependencies": {
65
+ "@huggingface/transformers": "4.2.0",
66
66
  "@types/better-sqlite3": "^7.6.13",
67
67
  "@types/node": "^25.9.1",
68
68
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -96,5 +96,13 @@
96
96
  "protobufjs",
97
97
  "sharp"
98
98
  ]
99
+ },
100
+ "peerDependencies": {
101
+ "@huggingface/transformers": "4.2.0"
102
+ },
103
+ "peerDependenciesMeta": {
104
+ "@huggingface/transformers": {
105
+ "optional": true
106
+ }
99
107
  }
100
108
  }