@cruxy/cli 0.18.0 → 0.19.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.
@@ -408,6 +408,138 @@ export declare const MemoryConfigSchema: z.ZodObject<{
408
408
  maxRecallTokens?: number | undefined;
409
409
  }>;
410
410
  export type MemoryConfig = z.infer<typeof MemoryConfigSchema>;
411
+ /** A per-tier price, in the user's own currency, PER MILLION TOKENS (C.22). */
412
+ export declare const TierPriceSchema: z.ZodObject<{
413
+ /** Price per 1,000,000 input tokens. */
414
+ input: z.ZodNumber;
415
+ /** Price per 1,000,000 output tokens. */
416
+ output: z.ZodNumber;
417
+ }, "strict", z.ZodTypeAny, {
418
+ input: number;
419
+ output: number;
420
+ }, {
421
+ input: number;
422
+ output: number;
423
+ }>;
424
+ /**
425
+ * Usage telemetry + cost tracking (C.22): LOCAL usage accounting only — nothing
426
+ * here is ever transmitted. Costs are opt-in and honest: prices are per-TIER and
427
+ * per-million-tokens, configured by the user; with none set, tokens are shown
428
+ * and cost is omitted (cruxy ships no bundled price table). The `prices` keys are
429
+ * the fixed {@link MODEL_TIERS}, so an upstream model name can never appear here
430
+ * (U.8 gag holds by construction) and a mistyped tier is rejected at config load.
431
+ */
432
+ export declare const UsageConfigSchema: z.ZodObject<{
433
+ /** Master switch. When false, no usage is collected, persisted, or shown. */
434
+ enabled: z.ZodDefault<z.ZodBoolean>;
435
+ /** Currency label to prefix costs with (e.g. "$", "€"). Empty ⇒ no symbol is
436
+ * assumed — a bare number is shown, since the user configured the prices. */
437
+ currency: z.ZodDefault<z.ZodString>;
438
+ /** How many past runs to keep in the store; older ones are pruned oldest-first. */
439
+ retention: z.ZodDefault<z.ZodNumber>;
440
+ /** Per-tier prices. Any tier omitted is simply unpriced (cost omitted for it). */
441
+ prices: z.ZodDefault<z.ZodObject<{
442
+ kavi: z.ZodOptional<z.ZodObject<{
443
+ /** Price per 1,000,000 input tokens. */
444
+ input: z.ZodNumber;
445
+ /** Price per 1,000,000 output tokens. */
446
+ output: z.ZodNumber;
447
+ }, "strict", z.ZodTypeAny, {
448
+ input: number;
449
+ output: number;
450
+ }, {
451
+ input: number;
452
+ output: number;
453
+ }>>;
454
+ vaani: z.ZodOptional<z.ZodObject<{
455
+ /** Price per 1,000,000 input tokens. */
456
+ input: z.ZodNumber;
457
+ /** Price per 1,000,000 output tokens. */
458
+ output: z.ZodNumber;
459
+ }, "strict", z.ZodTypeAny, {
460
+ input: number;
461
+ output: number;
462
+ }, {
463
+ input: number;
464
+ output: number;
465
+ }>>;
466
+ mira: z.ZodOptional<z.ZodObject<{
467
+ /** Price per 1,000,000 input tokens. */
468
+ input: z.ZodNumber;
469
+ /** Price per 1,000,000 output tokens. */
470
+ output: z.ZodNumber;
471
+ }, "strict", z.ZodTypeAny, {
472
+ input: number;
473
+ output: number;
474
+ }, {
475
+ input: number;
476
+ output: number;
477
+ }>>;
478
+ }, "strict", z.ZodTypeAny, {
479
+ kavi?: {
480
+ input: number;
481
+ output: number;
482
+ } | undefined;
483
+ vaani?: {
484
+ input: number;
485
+ output: number;
486
+ } | undefined;
487
+ mira?: {
488
+ input: number;
489
+ output: number;
490
+ } | undefined;
491
+ }, {
492
+ kavi?: {
493
+ input: number;
494
+ output: number;
495
+ } | undefined;
496
+ vaani?: {
497
+ input: number;
498
+ output: number;
499
+ } | undefined;
500
+ mira?: {
501
+ input: number;
502
+ output: number;
503
+ } | undefined;
504
+ }>>;
505
+ }, "strict", z.ZodTypeAny, {
506
+ enabled: boolean;
507
+ retention: number;
508
+ currency: string;
509
+ prices: {
510
+ kavi?: {
511
+ input: number;
512
+ output: number;
513
+ } | undefined;
514
+ vaani?: {
515
+ input: number;
516
+ output: number;
517
+ } | undefined;
518
+ mira?: {
519
+ input: number;
520
+ output: number;
521
+ } | undefined;
522
+ };
523
+ }, {
524
+ enabled?: boolean | undefined;
525
+ retention?: number | undefined;
526
+ currency?: string | undefined;
527
+ prices?: {
528
+ kavi?: {
529
+ input: number;
530
+ output: number;
531
+ } | undefined;
532
+ vaani?: {
533
+ input: number;
534
+ output: number;
535
+ } | undefined;
536
+ mira?: {
537
+ input: number;
538
+ output: number;
539
+ } | undefined;
540
+ } | undefined;
541
+ }>;
542
+ export type UsageConfig = z.infer<typeof UsageConfigSchema>;
411
543
  /** MCP server entry — stdio or URL transport (wired up in a later phase). */
412
544
  export declare const McpServerSchema: z.ZodObject<{
413
545
  command: z.ZodOptional<z.ZodString>;
@@ -767,6 +899,116 @@ export declare const CruxyConfigSchema: z.ZodObject<{
767
899
  enabled?: boolean | undefined;
768
900
  maxRecallTokens?: number | undefined;
769
901
  }>>;
902
+ usage: z.ZodDefault<z.ZodObject<{
903
+ /** Master switch. When false, no usage is collected, persisted, or shown. */
904
+ enabled: z.ZodDefault<z.ZodBoolean>;
905
+ /** Currency label to prefix costs with (e.g. "$", "€"). Empty ⇒ no symbol is
906
+ * assumed — a bare number is shown, since the user configured the prices. */
907
+ currency: z.ZodDefault<z.ZodString>;
908
+ /** How many past runs to keep in the store; older ones are pruned oldest-first. */
909
+ retention: z.ZodDefault<z.ZodNumber>;
910
+ /** Per-tier prices. Any tier omitted is simply unpriced (cost omitted for it). */
911
+ prices: z.ZodDefault<z.ZodObject<{
912
+ kavi: z.ZodOptional<z.ZodObject<{
913
+ /** Price per 1,000,000 input tokens. */
914
+ input: z.ZodNumber;
915
+ /** Price per 1,000,000 output tokens. */
916
+ output: z.ZodNumber;
917
+ }, "strict", z.ZodTypeAny, {
918
+ input: number;
919
+ output: number;
920
+ }, {
921
+ input: number;
922
+ output: number;
923
+ }>>;
924
+ vaani: z.ZodOptional<z.ZodObject<{
925
+ /** Price per 1,000,000 input tokens. */
926
+ input: z.ZodNumber;
927
+ /** Price per 1,000,000 output tokens. */
928
+ output: z.ZodNumber;
929
+ }, "strict", z.ZodTypeAny, {
930
+ input: number;
931
+ output: number;
932
+ }, {
933
+ input: number;
934
+ output: number;
935
+ }>>;
936
+ mira: z.ZodOptional<z.ZodObject<{
937
+ /** Price per 1,000,000 input tokens. */
938
+ input: z.ZodNumber;
939
+ /** Price per 1,000,000 output tokens. */
940
+ output: z.ZodNumber;
941
+ }, "strict", z.ZodTypeAny, {
942
+ input: number;
943
+ output: number;
944
+ }, {
945
+ input: number;
946
+ output: number;
947
+ }>>;
948
+ }, "strict", z.ZodTypeAny, {
949
+ kavi?: {
950
+ input: number;
951
+ output: number;
952
+ } | undefined;
953
+ vaani?: {
954
+ input: number;
955
+ output: number;
956
+ } | undefined;
957
+ mira?: {
958
+ input: number;
959
+ output: number;
960
+ } | undefined;
961
+ }, {
962
+ kavi?: {
963
+ input: number;
964
+ output: number;
965
+ } | undefined;
966
+ vaani?: {
967
+ input: number;
968
+ output: number;
969
+ } | undefined;
970
+ mira?: {
971
+ input: number;
972
+ output: number;
973
+ } | undefined;
974
+ }>>;
975
+ }, "strict", z.ZodTypeAny, {
976
+ enabled: boolean;
977
+ retention: number;
978
+ currency: string;
979
+ prices: {
980
+ kavi?: {
981
+ input: number;
982
+ output: number;
983
+ } | undefined;
984
+ vaani?: {
985
+ input: number;
986
+ output: number;
987
+ } | undefined;
988
+ mira?: {
989
+ input: number;
990
+ output: number;
991
+ } | undefined;
992
+ };
993
+ }, {
994
+ enabled?: boolean | undefined;
995
+ retention?: number | undefined;
996
+ currency?: string | undefined;
997
+ prices?: {
998
+ kavi?: {
999
+ input: number;
1000
+ output: number;
1001
+ } | undefined;
1002
+ vaani?: {
1003
+ input: number;
1004
+ output: number;
1005
+ } | undefined;
1006
+ mira?: {
1007
+ input: number;
1008
+ output: number;
1009
+ } | undefined;
1010
+ } | undefined;
1011
+ }>>;
770
1012
  mcpServers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
771
1013
  command: z.ZodOptional<z.ZodString>;
772
1014
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -789,6 +1031,25 @@ export declare const CruxyConfigSchema: z.ZodObject<{
789
1031
  enabled: boolean;
790
1032
  maxRecallTokens: number;
791
1033
  };
1034
+ usage: {
1035
+ enabled: boolean;
1036
+ retention: number;
1037
+ currency: string;
1038
+ prices: {
1039
+ kavi?: {
1040
+ input: number;
1041
+ output: number;
1042
+ } | undefined;
1043
+ vaani?: {
1044
+ input: number;
1045
+ output: number;
1046
+ } | undefined;
1047
+ mira?: {
1048
+ input: number;
1049
+ output: number;
1050
+ } | undefined;
1051
+ };
1052
+ };
792
1053
  checkpoint: {
793
1054
  enabled: boolean;
794
1055
  retention: number;
@@ -886,6 +1147,25 @@ export declare const CruxyConfigSchema: z.ZodObject<{
886
1147
  enabled?: boolean | undefined;
887
1148
  maxRecallTokens?: number | undefined;
888
1149
  } | undefined;
1150
+ usage?: {
1151
+ enabled?: boolean | undefined;
1152
+ retention?: number | undefined;
1153
+ currency?: string | undefined;
1154
+ prices?: {
1155
+ kavi?: {
1156
+ input: number;
1157
+ output: number;
1158
+ } | undefined;
1159
+ vaani?: {
1160
+ input: number;
1161
+ output: number;
1162
+ } | undefined;
1163
+ mira?: {
1164
+ input: number;
1165
+ output: number;
1166
+ } | undefined;
1167
+ } | undefined;
1168
+ } | undefined;
889
1169
  checkpoint?: {
890
1170
  enabled?: boolean | undefined;
891
1171
  retention?: number | undefined;
@@ -290,6 +290,43 @@ export const MemoryConfigSchema = z
290
290
  maxRecallTokens: z.number().int().positive().default(1000),
291
291
  })
292
292
  .strict();
293
+ /** A per-tier price, in the user's own currency, PER MILLION TOKENS (C.22). */
294
+ export const TierPriceSchema = z
295
+ .object({
296
+ /** Price per 1,000,000 input tokens. */
297
+ input: z.number().nonnegative(),
298
+ /** Price per 1,000,000 output tokens. */
299
+ output: z.number().nonnegative(),
300
+ })
301
+ .strict();
302
+ /**
303
+ * Usage telemetry + cost tracking (C.22): LOCAL usage accounting only — nothing
304
+ * here is ever transmitted. Costs are opt-in and honest: prices are per-TIER and
305
+ * per-million-tokens, configured by the user; with none set, tokens are shown
306
+ * and cost is omitted (cruxy ships no bundled price table). The `prices` keys are
307
+ * the fixed {@link MODEL_TIERS}, so an upstream model name can never appear here
308
+ * (U.8 gag holds by construction) and a mistyped tier is rejected at config load.
309
+ */
310
+ export const UsageConfigSchema = z
311
+ .object({
312
+ /** Master switch. When false, no usage is collected, persisted, or shown. */
313
+ enabled: z.boolean().default(true),
314
+ /** Currency label to prefix costs with (e.g. "$", "€"). Empty ⇒ no symbol is
315
+ * assumed — a bare number is shown, since the user configured the prices. */
316
+ currency: z.string().default(""),
317
+ /** How many past runs to keep in the store; older ones are pruned oldest-first. */
318
+ retention: z.number().int().positive().default(50),
319
+ /** Per-tier prices. Any tier omitted is simply unpriced (cost omitted for it). */
320
+ prices: z
321
+ .object({
322
+ kavi: TierPriceSchema.optional(),
323
+ vaani: TierPriceSchema.optional(),
324
+ mira: TierPriceSchema.optional(),
325
+ })
326
+ .strict()
327
+ .default({}),
328
+ })
329
+ .strict();
293
330
  /** MCP server entry — stdio or URL transport (wired up in a later phase). */
294
331
  export const McpServerSchema = z
295
332
  .object({
@@ -316,6 +353,7 @@ export const CruxyConfigSchema = z
316
353
  hooks: HooksConfigSchema.default({}),
317
354
  routing: RoutingConfigSchema.default({}),
318
355
  memory: MemoryConfigSchema.default({}),
356
+ usage: UsageConfigSchema.default({}),
319
357
  mcpServers: z.record(z.string(), McpServerSchema).default({}),
320
358
  logLevel: z.enum(LOG_LEVELS).default("info"),
321
359
  })
@@ -41,6 +41,13 @@ export declare const MEMORY_FILE_NAME = "entries.json";
41
41
  * (`~/.cruxy/memory-trust.json`) — its own file, independent of hook trust, so
42
42
  * cloning a repo carries zero memory trust (C.29 supply-chain safety). */
43
43
  export declare const MEMORY_TRUST_FILE_NAME = "memory-trust.json";
44
+ /**
45
+ * Usage telemetry + cost tracking (C.22). Per-run/per-session usage records live
46
+ * in the GLOBAL dir only (`~/.cruxy/usage/runs.json`), `0600` — LOCAL accounting
47
+ * of your own usage, never transmitted. Pure data, validated, never eval'd.
48
+ */
49
+ export declare const USAGE_DIR_NAME = "usage";
50
+ export declare const USAGE_FILE_NAME = "runs.json";
44
51
  /**
45
52
  * Absolute path of the shipped builtin skills directory (`<pkg>/skills`).
46
53
  * Anchored the same way as the package.json lookup above: both `dist/` and
package/dist/constants.js CHANGED
@@ -61,6 +61,13 @@ export const MEMORY_FILE_NAME = "entries.json";
61
61
  * (`~/.cruxy/memory-trust.json`) — its own file, independent of hook trust, so
62
62
  * cloning a repo carries zero memory trust (C.29 supply-chain safety). */
63
63
  export const MEMORY_TRUST_FILE_NAME = "memory-trust.json";
64
+ /**
65
+ * Usage telemetry + cost tracking (C.22). Per-run/per-session usage records live
66
+ * in the GLOBAL dir only (`~/.cruxy/usage/runs.json`), `0600` — LOCAL accounting
67
+ * of your own usage, never transmitted. Pure data, validated, never eval'd.
68
+ */
69
+ export const USAGE_DIR_NAME = "usage";
70
+ export const USAGE_FILE_NAME = "runs.json";
64
71
  /**
65
72
  * Absolute path of the shipped builtin skills directory (`<pkg>/skills`).
66
73
  * Anchored the same way as the package.json lookup above: both `dist/` and
@@ -150,6 +150,13 @@ export declare function memorySecretRefused(kind: string): CruxyError;
150
150
  export declare function memoryUntrusted(root: string): CruxyError;
151
151
  /** A malformed memory entry was rejected (C.29) — excluded, never eval'd. */
152
152
  export declare function memoryInvalid(detail: string): CruxyError;
153
+ /**
154
+ * The local usage store (`~/.cruxy/usage/runs.json`) is corrupt or unreadable
155
+ * (C.22). Never fatal: usage accounting is best-effort, so the read is SKIPPED
156
+ * and this is surfaced. The fix is always to reset the file — its only content
157
+ * is your own local usage history, so deleting it loses nothing but history.
158
+ */
159
+ export declare function usageRead(path: string, reason: string, underlying?: unknown): CruxyError;
153
160
  export declare function internal(underlying?: unknown): CruxyError;
154
161
  /**
155
162
  * Map a known provider/transport error (from `@cruxy/sdk`) to a typed
@@ -647,6 +647,25 @@ export function memoryInvalid(detail) {
647
647
  ],
648
648
  });
649
649
  }
650
+ // ── usage telemetry (exit 2) — C.22 ───────────────────────────────────────────
651
+ /**
652
+ * The local usage store (`~/.cruxy/usage/runs.json`) is corrupt or unreadable
653
+ * (C.22). Never fatal: usage accounting is best-effort, so the read is SKIPPED
654
+ * and this is surfaced. The fix is always to reset the file — its only content
655
+ * is your own local usage history, so deleting it loses nothing but history.
656
+ */
657
+ export function usageRead(path, reason, underlying) {
658
+ return new CruxyError({
659
+ code: ErrorCode.UsageRead,
660
+ title: "could not read the local usage store",
661
+ cause: reason,
662
+ nextSteps: [
663
+ `delete ${path} to reset it (it holds only your local usage history)`,
664
+ ],
665
+ underlying,
666
+ meta: { path },
667
+ });
668
+ }
650
669
  // ── internal (exit 1) ─────────────────────────────────────────────────────────
651
670
  export function internal(underlying) {
652
671
  return new CruxyError({
@@ -80,6 +80,9 @@ export declare const ErrorCode: {
80
80
  /** A memory write was refused because the content matched a secret shape —
81
81
  * secrets are never persisted (defense in depth over C.17). */
82
82
  readonly MemorySecret: "CRUXY_E_MEMORY_SECRET";
83
+ /** The local usage store is corrupt/unreadable — the read is SKIPPED and this
84
+ * is surfaced; never fatal to a run (usage display is best-effort). */
85
+ readonly UsageRead: "CRUXY_E_USAGE_READ";
83
86
  };
84
87
  export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
85
88
  /** The process exit code for an error code (defaults to 1 for safety). */
@@ -95,6 +95,10 @@ export const ErrorCode = {
95
95
  /** A memory write was refused because the content matched a secret shape —
96
96
  * secrets are never persisted (defense in depth over C.17). */
97
97
  MemorySecret: "CRUXY_E_MEMORY_SECRET",
98
+ // usage telemetry (exit 2) — C.22
99
+ /** The local usage store is corrupt/unreadable — the read is SKIPPED and this
100
+ * is surfaced; never fatal to a run (usage display is best-effort). */
101
+ UsageRead: "CRUXY_E_USAGE_READ",
98
102
  };
99
103
  /**
100
104
  * Category exit codes. Distinct per category so a caller (CI, a script) can
@@ -162,6 +166,10 @@ const EXIT_CODES = {
162
166
  [ErrorCode.MemoryInvalid]: 14,
163
167
  [ErrorCode.MemoryUntrusted]: 14,
164
168
  [ErrorCode.MemorySecret]: 14,
169
+ // Usage telemetry (C.22). A corrupt store is a usage/data problem the user can
170
+ // fix (delete the file); it shares the usage exit code and is never fatal to a
171
+ // run — the aggregation just skips it.
172
+ [ErrorCode.UsageRead]: 2,
165
173
  };
166
174
  /** The process exit code for an error code (defaults to 1 for safety). */
167
175
  export function exitCodeFor(code) {
@@ -1,4 +1,4 @@
1
- import type { Message, Provider } from "@cruxy/sdk";
1
+ import type { Message, Provider, Usage } from "@cruxy/sdk";
2
2
  import type { CruxyConfig } from "../config/index.js";
3
3
  import type { PromptIO } from "../approval/index.js";
4
4
  import { ToolRegistry, type ToolContext } from "../tools/index.js";
@@ -45,5 +45,14 @@ export interface PlanSessionArgs {
45
45
  * and step execution on `main-turn`; omitted → the provider default.
46
46
  */
47
47
  router?: Router;
48
+ /**
49
+ * Usage telemetry (C.22): forwarded to every model request the plan turn
50
+ * drives (propose + each execution step), so plan-mode usage is captured and
51
+ * tier-attributed exactly like a normal turn.
52
+ */
53
+ onRequestUsage?: (req: {
54
+ tier?: string;
55
+ usage?: Usage;
56
+ }) => void;
48
57
  }
49
58
  export declare function runPlanSession(args: PlanSessionArgs): Promise<AgentResult>;
@@ -78,6 +78,7 @@ export async function runPlanSession(args) {
78
78
  planMode: true,
79
79
  router: args.router,
80
80
  taskClass: "plan",
81
+ onRequestUsage: args.onRequestUsage,
81
82
  }));
82
83
  if (!holder.plan) {
83
84
  throw planInvalid("the model ended its turn without calling submit_plan");
@@ -112,6 +113,7 @@ export async function runPlanSession(args) {
112
113
  renderer: args.renderer,
113
114
  router: args.router,
114
115
  taskClass: "main-turn",
116
+ onRequestUsage: args.onRequestUsage,
115
117
  }));
116
118
  };
117
119
  await executePlan(plan, {
@@ -0,0 +1,40 @@
1
+ import type { Usage } from "@cruxy/sdk";
2
+ import type { UsageRecord } from "./types.js";
3
+ /**
4
+ * Usage collection (C.22). Accumulates per-request usage exactly as the agent
5
+ * loop reports it — one {@link UsageEntry} per completed model request — and
6
+ * emits a {@link UsageRecord} for the run.
7
+ *
8
+ * The one honesty invariant: `usage: undefined` (the loop's signal that the
9
+ * provider returned NO usage event for a request) is recorded as `undefined`
10
+ * token counts — the honest "unknown". A provider-reported `0` arrives as a real
11
+ * `Usage` and is stored as `0`. Nothing is estimated, re-tokenized, or
12
+ * zero-filled, and this module makes ZERO network calls.
13
+ */
14
+ /** What the loop hands over for one completed request. */
15
+ export interface RequestUsage {
16
+ /** The routing tier (C.30) the request ran on, if routing was active. */
17
+ tier?: string;
18
+ /**
19
+ * The provider's usage for THIS request, or `undefined` when the provider
20
+ * emitted no usage event (⇒ tokens are unknown, not zero).
21
+ */
22
+ usage?: Usage;
23
+ }
24
+ /** Wall clock as an injectable seam so tests are deterministic. */
25
+ export type Clock = () => string;
26
+ export declare class UsageCollector {
27
+ private readonly now;
28
+ private readonly entries;
29
+ constructor(now?: Clock);
30
+ /**
31
+ * Record one completed request. When `req.usage` is absent the entry's token
32
+ * counts stay `undefined` — the provider reported nothing, so we assert
33
+ * nothing. A real reported `0` is preserved as `0`.
34
+ */
35
+ record(req: RequestUsage): void;
36
+ /** How many requests have been recorded so far. */
37
+ get count(): number;
38
+ /** Snapshot the collected entries into a persistable {@link UsageRecord}. */
39
+ toRecord(runId: string, sessionId: string | undefined, startedAt: string): UsageRecord;
40
+ }
@@ -0,0 +1,34 @@
1
+ const systemClock = () => new Date().toISOString();
2
+ export class UsageCollector {
3
+ now;
4
+ entries = [];
5
+ constructor(now = systemClock) {
6
+ this.now = now;
7
+ }
8
+ /**
9
+ * Record one completed request. When `req.usage` is absent the entry's token
10
+ * counts stay `undefined` — the provider reported nothing, so we assert
11
+ * nothing. A real reported `0` is preserved as `0`.
12
+ */
13
+ record(req) {
14
+ this.entries.push({
15
+ tier: req.tier,
16
+ inputTokens: req.usage?.input_tokens,
17
+ outputTokens: req.usage?.output_tokens,
18
+ at: this.now(),
19
+ });
20
+ }
21
+ /** How many requests have been recorded so far. */
22
+ get count() {
23
+ return this.entries.length;
24
+ }
25
+ /** Snapshot the collected entries into a persistable {@link UsageRecord}. */
26
+ toRecord(runId, sessionId, startedAt) {
27
+ return {
28
+ runId,
29
+ ...(sessionId !== undefined ? { sessionId } : {}),
30
+ startedAt,
31
+ entries: [...this.entries],
32
+ };
33
+ }
34
+ }
@@ -0,0 +1,19 @@
1
+ import type { PriceTable, TierPrice } from "./types.js";
2
+ /**
3
+ * Token → cost mapping (C.22). The whole discipline lives in one rule: a cost is
4
+ * produced ONLY when the tier has a configured price AND at least one token side
5
+ * is known. Otherwise the result is `undefined` — cost is omitted, tokens are
6
+ * still shown, and NO dollar figure is ever fabricated. Prices are per MILLION
7
+ * tokens (see {@link TierPrice}). Keyed by tier only (U.8 gag). No network.
8
+ */
9
+ /** The configured price for a tier, or `undefined` when the tier is unpriced. */
10
+ export declare function priceForTier(tier: string, prices: PriceTable): TierPrice | undefined;
11
+ /**
12
+ * Compute the cost of a tier's usage, or `undefined` when it cannot be stated
13
+ * honestly:
14
+ * - no configured price for the tier → `undefined` (cost omitted).
15
+ * - both token counts unknown → `undefined` (nothing real to price).
16
+ * A known side is priced; an unknown side contributes nothing (never a
17
+ * fabricated 0-token charge). tokens/1e6 × price, summed.
18
+ */
19
+ export declare function costFor(tier: string, inputTokens: number | undefined, outputTokens: number | undefined, prices: PriceTable): number | undefined;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Token → cost mapping (C.22). The whole discipline lives in one rule: a cost is
3
+ * produced ONLY when the tier has a configured price AND at least one token side
4
+ * is known. Otherwise the result is `undefined` — cost is omitted, tokens are
5
+ * still shown, and NO dollar figure is ever fabricated. Prices are per MILLION
6
+ * tokens (see {@link TierPrice}). Keyed by tier only (U.8 gag). No network.
7
+ */
8
+ /** The configured price for a tier, or `undefined` when the tier is unpriced. */
9
+ export function priceForTier(tier, prices) {
10
+ return prices[tier];
11
+ }
12
+ /**
13
+ * Compute the cost of a tier's usage, or `undefined` when it cannot be stated
14
+ * honestly:
15
+ * - no configured price for the tier → `undefined` (cost omitted).
16
+ * - both token counts unknown → `undefined` (nothing real to price).
17
+ * A known side is priced; an unknown side contributes nothing (never a
18
+ * fabricated 0-token charge). tokens/1e6 × price, summed.
19
+ */
20
+ export function costFor(tier, inputTokens, outputTokens, prices) {
21
+ const price = priceForTier(tier, prices);
22
+ if (!price)
23
+ return undefined;
24
+ if (inputTokens === undefined && outputTokens === undefined)
25
+ return undefined;
26
+ const inCost = inputTokens !== undefined ? (inputTokens / 1_000_000) * price.input : 0;
27
+ const outCost = outputTokens !== undefined ? (outputTokens / 1_000_000) * price.output : 0;
28
+ return inCost + outCost;
29
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Usage telemetry + cost tracking (C.22) — LOCAL usage accounting only.
3
+ *
4
+ * NO-PHONE-HOME GUARANTEE: nothing here transmits anything. This module reads
5
+ * and writes ONE local file under `~/.cruxy/usage` and renders to the terminal;
6
+ * it imports no provider, no transport, and makes no `fetch`/HTTP call. The
7
+ * `@cruxy/sdk` import below is a TYPE-only import (`Usage`), erased at build. A
8
+ * future opt-in remote report would be a new, clearly-named seam — this build
9
+ * ships nothing that sends. Asserted by the runtime + static no-phone-home tests.
10
+ */
11
+ export * from "./types.js";
12
+ export { UsageCollector, type RequestUsage, type Clock } from "./collect.js";
13
+ export { costFor, priceForTier } from "./cost.js";
14
+ export { loadUsage, appendRun, usageStorePath } from "./store.js";
15
+ export { summarizeRuns, renderSummary, formatCost, type SummarizeOptions, } from "./summary.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Usage telemetry + cost tracking (C.22) — LOCAL usage accounting only.
3
+ *
4
+ * NO-PHONE-HOME GUARANTEE: nothing here transmits anything. This module reads
5
+ * and writes ONE local file under `~/.cruxy/usage` and renders to the terminal;
6
+ * it imports no provider, no transport, and makes no `fetch`/HTTP call. The
7
+ * `@cruxy/sdk` import below is a TYPE-only import (`Usage`), erased at build. A
8
+ * future opt-in remote report would be a new, clearly-named seam — this build
9
+ * ships nothing that sends. Asserted by the runtime + static no-phone-home tests.
10
+ */
11
+ export * from "./types.js";
12
+ export { UsageCollector } from "./collect.js";
13
+ export { costFor, priceForTier } from "./cost.js";
14
+ export { loadUsage, appendRun, usageStorePath } from "./store.js";
15
+ export { summarizeRuns, renderSummary, formatCost, } from "./summary.js";