@trazum/cli 1.40.0 → 1.41.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
@@ -2,13 +2,14 @@
2
2
  import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
3
  import { join, resolve as resolvePath } from 'node:path';
4
4
  import { gunzipSync } from 'node:zlib';
5
- import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, buildHistory, buildPlan, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, detectFromSource, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
5
+ import { applyRewrites, BASELINE_FILENAME, BASELINE_VERSION, breaches, cacheableMinimum, analyzeCachePrefix, billLevers, bucketedCacheEconomics, bucketedProfile, buildHistory, buildPlan, connectorFor, CONNECTORS, normalizeAnthropicUsage, normalizeOpenAIUsage, storedReportFrom, verifyPlan, cacheEconomics, cacheHitRate, contextPressure, comparePrompts, compareToBaseline, computeSavings, countTokensAnthropic, DEFAULT_USAGE, detectFromSource, coverageDrift, driversBetween, explainGateFailure, assignSources, fleetRollup, labelCoverage, measuredUsage, gateMargin, GATE_MARGIN_TIGHT, estimateTokens, evaluate, extractPrompts, findExamples, formatBaseline, formatSignedUsd, formatUsd, getMessages, getModel, hasMarker, LOCALES, MAX_BASELINE_BYTES, moneyIsComparable, mostSpecificMatch, nearestName, optimize, parseBaseline, PHRASE_LANGUAGES, plannedCalls, profilePrompt, profileToCsv, profileUsage, promptId, providerFromEnv, pruneExamples, refineWithLlm, rejectionText, reorderForCache, repriceProfile, reviewAgeDays, reviewExamples, RULES, sharedPrefixes, sharesOf, SOURCE_EXTENSIONS, suggestRewrites, toOtlpMetrics, toPromptfoo, TTL_1H_MS, UNLABELLED, withExactTokenCounts, } from '@trazum/core';
6
6
  import { cacheDir, cacheStats, cachingProvider, clearCache } from './suggest-cache.js';
7
7
  import { dayOf, formatGap, median, spanDays } from './time.js';
8
8
  // Everything that reads the filesystem, on its own entry point so the web
9
9
  // bundle cannot reach it. See packages/core/src/node.ts.
10
10
  import { CONFIG_FILENAME, DEFAULT_EXTENSIONS, budgetFor, BUNDLED_CATALOGUE, SAFE_FETCH_INIT, applyPricingOverlay, catalogueFromOverlay, checkedEndpoint, openrouterOverlay, detectHost, loadConfig, walkPrompts, } from '@trazum/core/node';
11
11
  import { contentAt, gitAvailable, namesByRevision, pathInRepository, repositoryRoot, revisionsFor, } from './git.js';
12
+ import { fetchProviderUsage } from './connect.js';
12
13
  import { detectLocale, getCliMessages } from './i18n/index.js';
13
14
  import { MAX_SUMMARY_CHARS, fitWithin, renderBlameMarkdown, renderCheckMarkdown, renderDiffMarkdown, renderRankMarkdown, renderProfileMarkdown, } from './markdown.js';
14
15
  // --------------------------------------------------------------------------
@@ -27,6 +28,7 @@ const VALUE_FLAGS = new Set([
27
28
  'against',
28
29
  'from-log',
29
30
  'min-usd',
31
+ 'payload',
30
32
  // `route` takes a path here, and the flag is deliberately not `--prompt`:
31
33
  // everywhere else in this tool `--prompt` names a marked prompt *inside* a
32
34
  // source file, and reusing it for a path would be a trap laid for the reader.
@@ -289,6 +291,7 @@ const COMMAND_FLAGS = {
289
291
  plan: ['json', 'out', 'markdown-out', 'min-usd', 'pricing', 'pricing-live'],
290
292
  verify: ['against', 'gate', 'json', 'markdown-out', 'pricing', 'pricing-live'],
291
293
  history: ['json', 'markdown-out'],
294
+ connect: ['since', 'until', 'payload', 'json', 'out', 'markdown-out', 'pricing', 'pricing-live', 'dry-run'],
292
295
  route: ['prompt-file', 'cases', 'label', 'concurrency', 'json', 'yes', 'pricing', 'pricing-live'],
293
296
  eval: ['cases', 'level', 'concurrency', 'export', 'out', 'o', 'model'],
294
297
  prune: ['cases', 'concurrency', 'json', 'yes'],
@@ -1646,6 +1649,164 @@ function isoDate() {
1646
1649
  * metered API calls somebody was actually billed for — the bill exists wherever
1647
1650
  * Trazum happens to be running, so the host has no bearing on it.
1648
1651
  */
1652
+ /**
1653
+ * One end of a time window, from a flag.
1654
+ *
1655
+ * A UTC day (`2026-08-14`), a full ISO 8601 timestamp, a relative window
1656
+ * (`7d`, `24h`) or `now`. A bare day means the whole of it — since its first
1657
+ * instant, until its last — because `--until 2026-08-14` excluding the named
1658
+ * day is a trap sprung on everyone who reads dates the way humans do.
1659
+ *
1660
+ * `relative` comes back so the caller can state the caveat: a relative window
1661
+ * is measured against **the machine's clock, not the data's**, and a log
1662
+ * exported last month answers `--since 7d` with nothing.
1663
+ */
1664
+ function parseWhen(args, flag, endOfDay, t, now) {
1665
+ const value = stringFlag(args, flag);
1666
+ if (value === undefined)
1667
+ return { ms: undefined, relative: false };
1668
+ const relative = /^(\d+)([dh])$/.exec(value);
1669
+ if (relative) {
1670
+ const amount = Number(relative[1]);
1671
+ if (amount > 0) {
1672
+ const span = relative[2] === 'd' ? 86_400_000 : 3_600_000;
1673
+ return { ms: now - amount * span, relative: true };
1674
+ }
1675
+ }
1676
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1677
+ const midnight = Date.parse(`${value}T00:00:00Z`);
1678
+ if (Number.isFinite(midnight)) {
1679
+ return { ms: endOfDay ? midnight + 86_400_000 : midnight, relative: false };
1680
+ }
1681
+ }
1682
+ if (value === 'now')
1683
+ return { ms: now, relative: false };
1684
+ const exact = Date.parse(value);
1685
+ if (Number.isFinite(exact))
1686
+ return { ms: exact, relative: false };
1687
+ throw new Error(t.profile.badWhen(flag, value));
1688
+ }
1689
+ /**
1690
+ * `trazum connect <provider>` — the bill, read from the provider.
1691
+ *
1692
+ * The pull and the pricing live elsewhere; this owns the window, the
1693
+ * rendering and the refusals. The report it prints is deliberately a
1694
+ * *restricted* one: a usage API serves sums, so every per-call finding is
1695
+ * listed as unavailable rather than computed from a zero nobody measured.
1696
+ */
1697
+ async function commandConnect(args, pricing, t) {
1698
+ const id = args.positional[0];
1699
+ if (id === undefined) {
1700
+ throw new Error(t.connect.noTarget(CONNECTORS.map((c) => c.id).join(', ')));
1701
+ }
1702
+ const descriptor = connectorFor(id);
1703
+ if (descriptor === null) {
1704
+ throw new Error(t.connect.unknownProvider(id, CONNECTORS.map((c) => c.id).join(', ')));
1705
+ }
1706
+ const now = Date.now();
1707
+ const since = parseWhen(args, 'since', false, t, now);
1708
+ const until = parseWhen(args, 'until', true, t, now);
1709
+ // A month back by default: long enough to be a bill, short enough that a
1710
+ // first run against a busy organisation does not walk fifty pages.
1711
+ const fromMs = since.ms ?? now - 30 * 86_400_000;
1712
+ const toMs = until.ms ?? now;
1713
+ if (fromMs >= toMs)
1714
+ throw new Error(t.profile.sinceAfterUntil());
1715
+ const day = (msValue) => new Date(msValue).toISOString().slice(0, 10);
1716
+ if (boolFlag(args, 'dry-run')) {
1717
+ console.log(wrap(t.connect.dryRun(descriptor.displayName, day(fromMs), day(toMs), descriptor.credentialEnv.join(' or '), descriptor.keyKind), 76, ' '));
1718
+ return;
1719
+ }
1720
+ /**
1721
+ * A payload somebody already has is priced without a pull.
1722
+ *
1723
+ * People save API responses — from a support thread, from a curl in a
1724
+ * runbook, from a colleague who has the admin key and they do not. Pricing
1725
+ * one needs no credential and no network, and it is the same arithmetic on
1726
+ * the same shape, so refusing it would be ceremony rather than safety.
1727
+ */
1728
+ const payloadPath = stringFlag(args, 'payload');
1729
+ const pulled = payloadPath === undefined
1730
+ ? await fetchProviderUsage({ descriptor, fromMs, toMs, env: process.env })
1731
+ : {
1732
+ pull: (descriptor.id === 'anthropic' ? normalizeAnthropicUsage : normalizeOpenAIUsage)(JSON.parse(await readFile(payloadPath, 'utf8'))),
1733
+ source: { variable: payloadPath },
1734
+ };
1735
+ const { pull, source } = pulled;
1736
+ const report = bucketedProfile(pull, { catalogue: pricing });
1737
+ const cache = bucketedCacheEconomics(report);
1738
+ const n = (value) => value.toLocaleString(t.numberLocale);
1739
+ const outPath = stringFlag(args, 'out');
1740
+ if (outPath !== undefined) {
1741
+ await writeFile(outPath, `${JSON.stringify({ ...report, pulledFrom: source.variable }, null, 2)}\n`);
1742
+ }
1743
+ if (boolFlag(args, 'json')) {
1744
+ console.log(JSON.stringify(report, null, 2));
1745
+ return;
1746
+ }
1747
+ const lines = (md) => {
1748
+ const out = [];
1749
+ const heading = t.connect.heading(descriptor.displayName, report.span === null ? day(fromMs) : day(report.span.fromMs), report.span === null ? day(toMs) : day(report.span.toMs), formatUsd(report.total.totalUsd), report.total.calls === null ? null : n(report.total.calls));
1750
+ out.push(md ? `## ${heading}` : heading);
1751
+ const modelWidth = Math.max(0, ...report.byModel.map((s) => s.model.length));
1752
+ for (const slice of report.byModel) {
1753
+ const share = report.total.totalUsd > 0 ? slice.totalUsd / report.total.totalUsd : 0;
1754
+ const row = t.connect.modelRow(md ? slice.model : slice.model.padEnd(modelWidth), formatUsd(slice.totalUsd).padStart(9), `${(share * 100).toFixed(1)}%`.padStart(6), slice.calls === null ? null : n(slice.calls));
1755
+ out.push(md ? `- ${row}` : row);
1756
+ }
1757
+ if (report.byModel.length === 0) {
1758
+ out.push(md ? `_${t.connect.nothingBilled()}_` : t.connect.nothingBilled());
1759
+ }
1760
+ if (cache.verdict !== 'no-cache') {
1761
+ out.push('');
1762
+ const line = cache.verdict === 'paid-off'
1763
+ ? t.connect.cachePaid(formatUsd(-cache.deltaUsd))
1764
+ : t.connect.cacheLost(formatUsd(cache.deltaUsd));
1765
+ out.push(line);
1766
+ if (cache.worstCaseVerdict !== cache.verdict) {
1767
+ const unsettled = t.connect.cacheUnsettled();
1768
+ out.push(md ? `_${unsettled}_` : unsettled);
1769
+ }
1770
+ }
1771
+ if (report.total.calls === null) {
1772
+ out.push('');
1773
+ const line = t.connect.noCallCount(descriptor.displayName);
1774
+ out.push(md ? `_${line}_` : line);
1775
+ }
1776
+ for (const model of report.unpricedModels) {
1777
+ out.push('');
1778
+ const line = t.connect.unpriced(model.model, n(model.inputTokens + model.outputTokens));
1779
+ out.push(md ? `- ${line}` : `! ${line}`);
1780
+ }
1781
+ if (report.gaps.length > 0)
1782
+ out.push('');
1783
+ for (const gap of report.gaps) {
1784
+ const line = t.connect.gap(gap.detail);
1785
+ out.push(md ? `- ${line}` : `! ${line}`);
1786
+ }
1787
+ out.push('');
1788
+ const unavailable = t.connect.unavailable(report.unavailable.map((u) => u.finding).join(', '));
1789
+ out.push(md ? `_${unavailable}_` : unavailable);
1790
+ out.push('');
1791
+ out.push(md ? `_${t.connect.footer()}_` : t.connect.footer());
1792
+ return out;
1793
+ };
1794
+ await writeMarkdown(args, () => lines(true).join('\n'));
1795
+ const [head, ...rest] = lines(false);
1796
+ console.log(c.bold(head));
1797
+ for (const row of rest) {
1798
+ // Short rows print as written so the columns stay aligned; `wrap` collapses
1799
+ // runs of spaces, which is right for prose and wrong for a table.
1800
+ if (row === '')
1801
+ console.log('');
1802
+ else if (row.length <= 74)
1803
+ console.log(` ${row}`);
1804
+ else
1805
+ console.log(` ${wrap(row, 74, ' ')}`);
1806
+ }
1807
+ if (outPath !== undefined)
1808
+ console.log(c.dim(t.connect.wrote(outPath)));
1809
+ }
1649
1810
  /**
1650
1811
  * `trazum history <dir>` — many reports over many periods, as one series.
1651
1812
  *
@@ -2074,45 +2235,11 @@ async function commandProfile(args, config, pricing, t) {
2074
2235
  * no record.
2075
2236
  */
2076
2237
  const now = Date.now();
2077
- let relativeWindow = false;
2078
- const parseWhen = (flag, endOfDay) => {
2079
- const value = stringFlag(args, flag);
2080
- if (value === undefined)
2081
- return undefined;
2082
- /**
2083
- * A relative window — `7d`, `24h` — because "the last week" is what a
2084
- * nightly job actually wants, and computing a date in a shell to say it
2085
- * is the step that gets skipped.
2086
- *
2087
- * Relative to **the machine's clock, not the log's**, which is a real
2088
- * difference: a log exported last month answers `--since 7d` with
2089
- * nothing, and the report says so rather than reporting $0. That caveat
2090
- * is stated beside the window line, because a period the reader did not
2091
- * name is a period they will misread.
2092
- */
2093
- const relative = /^(\d+)([dh])$/.exec(value);
2094
- if (relative) {
2095
- const amount = Number(relative[1]);
2096
- if (amount > 0) {
2097
- relativeWindow = true;
2098
- const span = relative[2] === 'd' ? 86_400_000 : 3_600_000;
2099
- return now - amount * span;
2100
- }
2101
- }
2102
- if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
2103
- const midnight = Date.parse(`${value}T00:00:00Z`);
2104
- if (Number.isFinite(midnight))
2105
- return endOfDay ? midnight + 86_400_000 : midnight;
2106
- }
2107
- if (value === 'now')
2108
- return now;
2109
- const exact = Date.parse(value);
2110
- if (Number.isFinite(exact))
2111
- return exact;
2112
- throw new Error(t.profile.badWhen(flag, value));
2113
- };
2114
- const sinceMs = parseWhen('since', false);
2115
- const untilMs = parseWhen('until', true);
2238
+ const sinceWhen = parseWhen(args, 'since', false, t, now);
2239
+ const untilWhen = parseWhen(args, 'until', true, t, now);
2240
+ const relativeWindow = sinceWhen.relative || untilWhen.relative;
2241
+ const sinceMs = sinceWhen.ms;
2242
+ const untilMs = untilWhen.ms;
2116
2243
  if (sinceMs !== undefined && untilMs !== undefined && sinceMs >= untilMs) {
2117
2244
  throw new Error(t.profile.sinceAfterUntil());
2118
2245
  }
@@ -5161,6 +5288,9 @@ async function main() {
5161
5288
  case 'history':
5162
5289
  await commandHistory(args, t);
5163
5290
  break;
5291
+ case 'connect':
5292
+ await commandConnect(args, pricing, t);
5293
+ break;
5164
5294
  case 'route':
5165
5295
  await commandRoute(args, pricing, t);
5166
5296
  break;