@hasna/economy 0.5.3 → 0.5.5

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.
@@ -2384,7 +2384,7 @@ import { join as join3, basename } from "path";
2384
2384
  // src/lib/accounts-store.ts
2385
2385
  import { homedir as homedir2 } from "os";
2386
2386
  import { join as join2 } from "path";
2387
- import { z } from "zod";
2387
+ import { z as z2 } from "zod";
2388
2388
  import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
2389
2389
 
2390
2390
  // src/lib/cloud-storage.ts
@@ -2515,32 +2515,61 @@ function economyCloudStorage(env = process.env) {
2515
2515
  return cache;
2516
2516
  }
2517
2517
 
2518
- // src/lib/accounts-store.ts
2519
- var profileNameSchema = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
2518
+ // src/lib/subscriptions-registry.ts
2519
+ import { execFile } from "child_process";
2520
+ import { promisify } from "util";
2521
+ import { isAbsolute } from "path";
2522
+ import { z } from "zod";
2523
+ var execute = promisify(execFile);
2524
+ var nonempty = z.string().min(1);
2520
2525
  var profileSchema = z.object({
2521
- name: z.string(),
2522
- tool: z.string(),
2526
+ name: nonempty,
2527
+ tool: nonempty,
2523
2528
  email: z.string().optional(),
2524
2529
  displayName: z.string().optional(),
2525
- dir: z.string(),
2530
+ dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
2531
+ createdAt: nonempty,
2532
+ lastUsedAt: z.string().optional(),
2526
2533
  description: z.string().optional(),
2527
- createdAt: z.string(),
2528
- lastUsedAt: z.string().optional()
2534
+ active: z.boolean(),
2535
+ applied: z.boolean(),
2536
+ switchedAway: z.object({ profile: nonempty, email: z.string().optional() }).optional()
2537
+ });
2538
+ var toolsSchema = z.array(z.object({
2539
+ id: nonempty,
2540
+ label: nonempty,
2541
+ envVar: nonempty,
2542
+ defaultDir: nonempty,
2543
+ bin: nonempty
2544
+ }));
2545
+
2546
+ // src/lib/accounts-store.ts
2547
+ var profileNameSchema = z2.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
2548
+ var profileSchema2 = z2.object({
2549
+ name: z2.string(),
2550
+ tool: z2.string(),
2551
+ email: z2.string().optional(),
2552
+ displayName: z2.string().optional(),
2553
+ dir: z2.string(),
2554
+ description: z2.string().optional(),
2555
+ createdAt: z2.string(),
2556
+ lastUsedAt: z2.string().optional(),
2557
+ switchedAway: z2.object({ profile: z2.string().min(1), email: z2.string().optional() }).optional()
2529
2558
  });
2530
- var toolDefSchema = z.object({
2531
- id: z.string(),
2532
- label: z.string(),
2533
- envVar: z.string(),
2534
- defaultDir: z.string(),
2535
- bin: z.string()
2559
+ var toolDefSchema = z2.object({
2560
+ id: z2.string(),
2561
+ label: z2.string(),
2562
+ envVar: z2.string(),
2563
+ defaultDir: z2.string(),
2564
+ bin: z2.string()
2536
2565
  });
2537
- var storeSchema = z.object({
2538
- version: z.literal(1),
2539
- current: z.record(z.string(), z.string()).default({}),
2540
- applied: z.record(z.string(), z.string()).default({}),
2541
- toolLocks: z.record(z.string(), z.string()).default({}),
2542
- profiles: z.array(profileSchema).default([]),
2543
- tools: z.array(toolDefSchema).default([])
2566
+ var storeSchema = z2.object({
2567
+ version: z2.literal(1),
2568
+ current: z2.record(z2.string(), z2.string()).default({}),
2569
+ applied: z2.record(z2.string(), z2.string()).default({}),
2570
+ toolLocks: z2.record(z2.string(), z2.string()).default({}),
2571
+ profiles: z2.array(profileSchema2).default([]),
2572
+ tools: z2.array(toolDefSchema).default([])
2544
2573
  });
2545
2574
  var BUILTIN_TOOLS = [
2546
2575
  { id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
@@ -2558,33 +2587,26 @@ var BUILTIN_TOOLS = [
2558
2587
  ];
2559
2588
 
2560
2589
  // src/ingest/claude.ts
2561
- var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join3(homedir3(), ".claude", "projects");
2562
2590
  var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
2563
2591
 
2564
2592
  // src/ingest/codex.ts
2565
2593
  init_database();
2566
2594
  init_pricing();
2567
- import { homedir as homedir4 } from "os";
2568
- import { join as join4, basename as basename2 } from "path";
2569
- var DEFAULT_CODEX_DB_PATH = join4(homedir4(), ".codex", "state_5.sqlite");
2570
- var DEFAULT_CODEX_CONFIG_PATH = join4(homedir4(), ".codex", "config.toml");
2571
- var DEFAULT_CODEWITH_DB_PATH = join4(homedir4(), ".codewith", "state_5.sqlite");
2572
- var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir4(), ".codewith", "config.toml");
2573
2595
 
2574
2596
  // src/ingest/gemini.ts
2575
2597
  init_database();
2576
2598
  init_pricing();
2577
- import { homedir as homedir5 } from "os";
2578
- import { join as join5, basename as basename3 } from "path";
2579
- var DEFAULT_GEMINI_TMP_DIR = join5(homedir5(), ".gemini", "tmp");
2580
- var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir5(), ".gemini", "history");
2599
+ import { homedir as homedir4 } from "os";
2600
+ import { join as join4, basename as basename2 } from "path";
2601
+ var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
2602
+ var DEFAULT_GEMINI_HISTORY_DIR = join4(homedir4(), ".gemini", "history");
2581
2603
 
2582
2604
  // src/ingest/opencode.ts
2583
2605
  init_database();
2584
2606
  init_pricing();
2585
- import { homedir as homedir6 } from "os";
2586
- import { join as join6 } from "path";
2587
- var OPENCODE_STORAGE = join6(homedir6(), ".local", "share", "opencode", "storage");
2607
+ import { homedir as homedir5 } from "os";
2608
+ import { join as join5 } from "path";
2609
+ var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
2588
2610
 
2589
2611
  // src/ingest/quota-observation.ts
2590
2612
  init_database();
@@ -2592,15 +2614,15 @@ init_observation_provenance();
2592
2614
 
2593
2615
  // src/ingest/pi.ts
2594
2616
  init_database();
2595
- import { homedir as homedir7 } from "os";
2596
- import { join as join7 } from "path";
2597
- var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join7(homedir7(), ".pi", "agent", "sessions");
2617
+ import { homedir as homedir6 } from "os";
2618
+ import { join as join6 } from "path";
2619
+ var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
2598
2620
 
2599
2621
  // src/ingest/hermes.ts
2600
2622
  init_database();
2601
- import { homedir as homedir8 } from "os";
2602
- import { join as join8 } from "path";
2603
- var HERMES_DB = join8(homedir8(), ".hermes", "state.db");
2623
+ import { homedir as homedir7 } from "os";
2624
+ import { join as join7 } from "path";
2625
+ var HERMES_DB = join7(homedir7(), ".hermes", "state.db");
2604
2626
 
2605
2627
  // src/ingest/loops.ts
2606
2628
  init_database();
@@ -2673,7 +2695,7 @@ init_ingest_validation();
2673
2695
  init_database();
2674
2696
  import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync as mkdirSync3, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
2675
2697
  import { createHash as createHash4 } from "crypto";
2676
- import { join as join9, resolve as resolve2 } from "path";
2698
+ import { join as join8, resolve as resolve2 } from "path";
2677
2699
  var MAX_COMMANDS = 1000;
2678
2700
  var MAX_BYTES = 64 * 1024 * 1024;
2679
2701
  var RECEIPT_RESERVE = 4096;
@@ -2701,7 +2723,7 @@ function definitiveIngestRefusal(status, body) {
2701
2723
  };
2702
2724
  }
2703
2725
  function getIngestJournalPath() {
2704
- return process.env.HASNA_ECONOMY_INGEST_JOURNAL ?? join9(cacheDir({ app: "economy", home: process.env.HOME || process.env.USERPROFILE }), "pending-ingest");
2726
+ return process.env.HASNA_ECONOMY_INGEST_JOURNAL ?? join8(cacheDir({ app: "economy", home: process.env.HOME || process.env.USERPROFILE }), "pending-ingest");
2705
2727
  }
2706
2728
  function targetId(cloud) {
2707
2729
  const url = new URL(cloud.client.transport.baseUrl);
@@ -2804,7 +2826,7 @@ class IngestJournal {
2804
2826
  constructor(cloud, path = getIngestJournalPath()) {
2805
2827
  this.target = targetId(cloud);
2806
2828
  secureDirectory(path);
2807
- this.directory = join9(realpathSync(path), this.target);
2829
+ this.directory = join8(realpathSync(path), this.target);
2808
2830
  secureDirectory(this.directory);
2809
2831
  }
2810
2832
  names() {
@@ -2830,7 +2852,7 @@ class IngestJournal {
2830
2852
  throw new Error("Orphaned ingest outcome requires maintenance recovery");
2831
2853
  if (name.endsWith(".refusal.json") && set.has(name.replace(".refusal.", ".receipt.")))
2832
2854
  throw new Error("Conflicting ingest receipt and refusal");
2833
- const stat = lstatSync(join9(this.directory, name));
2855
+ const stat = lstatSync(join8(this.directory, name));
2834
2856
  if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
2835
2857
  throw new Error("Unsafe ingest journal record");
2836
2858
  if (stat.size > MAX_INGEST_BYTES + 4096)
@@ -2842,10 +2864,10 @@ class IngestJournal {
2842
2864
  return names;
2843
2865
  }
2844
2866
  file(key, kind) {
2845
- return join9(this.directory, `${sha(key)}.${kind}.json`);
2867
+ return join8(this.directory, `${sha(key)}.${kind}.json`);
2846
2868
  }
2847
2869
  intent(name) {
2848
- const record = readJson(join9(this.directory, name), MAX_INGEST_BYTES + 4096);
2870
+ const record = readJson(join8(this.directory, name), MAX_INGEST_BYTES + 4096);
2849
2871
  if (record.version !== 1 || record.target !== this.target || !record.command || typeof record.command.idempotency_key !== "string" || record.body_hash !== canonicalIngestBodyHash(record.command) || name !== `${sha(record.command.idempotency_key)}.intent.json`)
2850
2872
  throw new Error("Invalid ingest journal intent");
2851
2873
  validateIngest(record.command);
@@ -3004,7 +3026,7 @@ class IngestJournal {
3004
3026
  // src/lib/ingest-journal-maintenance.ts
3005
3027
  import { constants as constants2, closeSync as closeSync2, fstatSync as fstatSync2, lstatSync as lstatSync2, openSync as openSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
3006
3028
  import { createHash as createHash5 } from "crypto";
3007
- import { join as join10 } from "path";
3029
+ import { join as join9 } from "path";
3008
3030
  var uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
3009
3031
  var digest2 = /^[a-f0-9]{64}$/;
3010
3032
  var sha2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
@@ -3064,7 +3086,7 @@ async function confirm(cloud, anchor, deadline) {
3064
3086
  }
3065
3087
  async function verifyJournalAuthority(journal, cloud) {
3066
3088
  journal.assertCloud(cloud);
3067
- const path = join10(journal.directory, ".authority.json");
3089
+ const path = join9(journal.directory, ".authority.json");
3068
3090
  if (!exists(path)) {
3069
3091
  const names = journal.names();
3070
3092
  const name = names.find((n) => n.endsWith(".intent.json") && names.includes(n.replace(".intent.", ".receipt.")));
@@ -3108,8 +3130,8 @@ class IngestJournalMaintenance {
3108
3130
  this.cloud = cloud;
3109
3131
  this.factory = factory;
3110
3132
  journal.assertCloud(cloud);
3111
- this.control = join10(journal.directory, ".maintenance");
3112
- this.operationRoot = join10(journal.directory, ".recovery");
3133
+ this.control = join9(journal.directory, ".maintenance");
3134
+ this.operationRoot = join9(journal.directory, ".recovery");
3113
3135
  }
3114
3136
  manifest() {
3115
3137
  secureDirectory(this.control);
@@ -3118,10 +3140,10 @@ class IngestJournalMaintenance {
3118
3140
  throw new Error("Unexpected maintenance entry");
3119
3141
  let size = 0;
3120
3142
  for (const name of names)
3121
- size += lstatSync2(join10(this.control, name)).size;
3143
+ size += lstatSync2(join9(this.control, name)).size;
3122
3144
  if (size > MAINTENANCE_RESERVE)
3123
3145
  throw new Error("Maintenance metadata exceeds reserve");
3124
- const m = validAnchor(readJson(join10(this.control, "manifest.json"), 8192), this.journal.target);
3146
+ const m = validAnchor(readJson(join9(this.control, "manifest.json"), 8192), this.journal.target);
3125
3147
  for (const part of [m.intent, m.receipt_file])
3126
3148
  if (!part || !digest2.test(part.sha256) || !digest2.test(part.capture_sha256 ?? "") || !Number.isSafeInteger(part.sizeBytes) || part.sizeBytes < 1 || part.sizeBytes > 4198400 || part.mode !== 384 || !/^\d+$/.test(part.device) || !/^\d+$/.test(part.inode) || !Number.isFinite(part.ctime))
3127
3149
  throw new Error("Invalid maintenance snapshot");
@@ -3146,7 +3168,7 @@ class IngestJournalMaintenance {
3146
3168
  if (!operation.receipt || operation.receipt.sha256 !== expected.capture_sha256 || operation.receipt.sizeBytes !== expected.sizeBytes || operation.receipt.mode !== expected.mode)
3147
3169
  throw new Error("Trash snapshot mismatch");
3148
3170
  sameSource(operation.path, expected);
3149
- const path = join10(this.control, `${kind}.operation.json`);
3171
+ const path = join9(this.control, `${kind}.operation.json`);
3150
3172
  createExact(path, { id: operation.id });
3151
3173
  if (readJson(path, 1024).id !== operation.id)
3152
3174
  throw new Error("Conflicting Trash operation");
@@ -3171,7 +3193,7 @@ class IngestJournalMaintenance {
3171
3193
  async pairFile(backend, m, kind, deadline) {
3172
3194
  await confirm(this.cloud, m, deadline);
3173
3195
  const path = this.journal.file(m.key, kind), expected = kind === "receipt" ? m.receipt_file : m.intent;
3174
- const saved = join10(this.control, `${kind}.operation.json`);
3196
+ const saved = join9(this.control, `${kind}.operation.json`);
3175
3197
  let id;
3176
3198
  if (exists(saved))
3177
3199
  id = readJson(saved, 1024).id;
@@ -3277,14 +3299,14 @@ class IngestJournalMaintenance {
3277
3299
  key: intent.command.idempotency_key,
3278
3300
  body_hash: intent.body_hash,
3279
3301
  receipt,
3280
- intent: snapshot(join10(this.journal.directory, name)),
3302
+ intent: snapshot(join9(this.journal.directory, name)),
3281
3303
  receipt_file: snapshot(this.journal.file(intent.command.idempotency_key, "receipt"))
3282
3304
  };
3283
3305
  await confirm(this.cloud, m, deadline);
3284
3306
  m.intent.capture_sha256 = backend.snapshot(this.journal.file(m.key, "intent")).sha256;
3285
3307
  m.receipt_file.capture_sha256 = backend.snapshot(this.journal.file(m.key, "receipt")).sha256;
3286
3308
  secureDirectory(this.control);
3287
- if (!createExact(join10(this.control, "manifest.json"), m))
3309
+ if (!createExact(join9(this.control, "manifest.json"), m))
3288
3310
  throw new Error("Existing maintenance transaction");
3289
3311
  await this.finish(backend, deadline);
3290
3312
  retired++;
@@ -3316,7 +3338,7 @@ class IngestJournalMaintenance {
3316
3338
 
3317
3339
  // src/lib/ingest-journal-lock.ts
3318
3340
  import { constants as constants3, closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3 } from "fs";
3319
- import { join as join11 } from "path";
3341
+ import { join as join10 } from "path";
3320
3342
  async function withIngestJournalLock(directory, action) {
3321
3343
  if (process.platform !== "linux" && process.platform !== "darwin")
3322
3344
  throw new Error("Ingest journal locking requires Linux or macOS");
@@ -3324,7 +3346,7 @@ async function withIngestJournalLock(directory, action) {
3324
3346
  const library = dlopen(process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", {
3325
3347
  flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 }
3326
3348
  });
3327
- const path = join11(directory, ".lease");
3349
+ const path = join10(directory, ".lease");
3328
3350
  let fd;
3329
3351
  try {
3330
3352
  fd = openSync3(path, constants3.O_RDWR | constants3.O_CREAT | constants3.O_NOFOLLOW | constants3.O_NONBLOCK, 384);
@@ -3345,11 +3367,11 @@ async function withIngestJournalLock(directory, action) {
3345
3367
  }
3346
3368
 
3347
3369
  // src/lib/ingest-journal-operation.ts
3348
- import { z as z2 } from "zod";
3349
- var ingestJournalOperationSchema = z2.discriminatedUnion("action", [
3350
- z2.object({ action: z2.literal("status") }).strict(),
3351
- z2.object({ action: z2.literal("reconcile") }).strict(),
3352
- z2.object({ action: z2.literal("maintain"), confirm: z2.literal(true), limit: z2.number().int().min(1).max(32).default(32) }).strict()
3370
+ import { z as z3 } from "zod";
3371
+ var ingestJournalOperationSchema = z3.discriminatedUnion("action", [
3372
+ z3.object({ action: z3.literal("status") }).strict(),
3373
+ z3.object({ action: z3.literal("reconcile") }).strict(),
3374
+ z3.object({ action: z3.literal("maintain"), confirm: z3.literal(true), limit: z3.number().int().min(1).max(32).default(32) }).strict()
3353
3375
  ]);
3354
3376
 
3355
3377
  // src/lib/cloud-ingest.ts
@@ -3458,7 +3480,7 @@ async function pushIngestRows(cloud, scratch) {
3458
3480
  // package.json
3459
3481
  var package_default = {
3460
3482
  name: "@hasna/economy",
3461
- version: "0.5.3",
3483
+ version: "0.5.5",
3462
3484
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
3463
3485
  repository: {
3464
3486
  type: "git",