@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.
@@ -4288,14 +4288,14 @@ function sourceTimeMillis(value) {
4288
4288
  init_database();
4289
4289
  init_pricing();
4290
4290
  import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync5, statSync as statSync2 } from "fs";
4291
- import { homedir as homedir3 } from "os";
4292
- import { join as join4, basename } from "path";
4291
+ import { homedir as homedir4 } from "os";
4292
+ import { join as join5, basename } from "path";
4293
4293
 
4294
4294
  // src/lib/accounts-store.ts
4295
4295
  import { existsSync as existsSync4, readFileSync } from "fs";
4296
4296
  import { homedir as homedir2 } from "os";
4297
4297
  import { join as join3 } from "path";
4298
- import { z } from "zod";
4298
+ import { z as z2 } from "zod";
4299
4299
  import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
4300
4300
 
4301
4301
  // src/lib/cloud-storage.ts
@@ -4314,33 +4314,138 @@ import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
4314
4314
  var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
4315
4315
  var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
4316
4316
 
4317
- // src/lib/accounts-store.ts
4318
- var ACCOUNTS_APP = "accounts";
4319
- 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");
4317
+ // src/lib/subscriptions-registry.ts
4318
+ import { execFile } from "child_process";
4319
+ import { promisify } from "util";
4320
+ import { isAbsolute } from "path";
4321
+ import { z } from "zod";
4322
+ var execute = promisify(execFile);
4323
+ var readSubscriptionsMetadata = async (args, env) => {
4324
+ let stdout;
4325
+ try {
4326
+ const result = await execute("subscriptions", args, {
4327
+ env,
4328
+ encoding: "utf8",
4329
+ timeout: 30000,
4330
+ maxBuffer: 16 * 1024 * 1024,
4331
+ windowsHide: true
4332
+ });
4333
+ stdout = result.stdout;
4334
+ } catch (error) {
4335
+ const code = error?.code;
4336
+ if (code === "ENOENT")
4337
+ throw new Error("subscriptions: CLI not found on PATH; install the Subscriptions CLI to resolve station account metadata");
4338
+ const status = typeof code === "number" && Number.isInteger(code) ? `exit ${code}` : "execution limit or transport failure";
4339
+ throw new Error(`subscriptions: read-only metadata command failed (${status}); verify the installed CLI and its hosted credentials`);
4340
+ }
4341
+ try {
4342
+ return JSON.parse(stdout);
4343
+ } catch {
4344
+ throw new Error("subscriptions: metadata command returned invalid JSON");
4345
+ }
4346
+ };
4347
+ var nonempty = z.string().min(1);
4320
4348
  var profileSchema = z.object({
4321
- name: z.string(),
4322
- tool: z.string(),
4349
+ name: nonempty,
4350
+ tool: nonempty,
4323
4351
  email: z.string().optional(),
4324
4352
  displayName: z.string().optional(),
4325
- dir: z.string(),
4353
+ dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
4354
+ createdAt: nonempty,
4355
+ lastUsedAt: z.string().optional(),
4326
4356
  description: z.string().optional(),
4327
- createdAt: z.string(),
4328
- lastUsedAt: z.string().optional()
4357
+ active: z.boolean(),
4358
+ applied: z.boolean(),
4359
+ switchedAway: z.object({ profile: nonempty, email: z.string().optional() }).optional()
4360
+ });
4361
+ var toolsSchema = z.array(z.object({
4362
+ id: nonempty,
4363
+ label: nonempty,
4364
+ envVar: nonempty,
4365
+ defaultDir: nonempty,
4366
+ bin: nonempty
4367
+ }));
4368
+ function unique(rows, identity) {
4369
+ if (new Set(rows.map(identity)).size !== rows.length) {
4370
+ throw new Error("subscriptions: duplicate identity in metadata response");
4371
+ }
4372
+ return rows;
4373
+ }
4374
+
4375
+ class SubscriptionsRegistry {
4376
+ read;
4377
+ transport = "api";
4378
+ registry = "subscriptions";
4379
+ env;
4380
+ profiles;
4381
+ tools;
4382
+ constructor(baseUrl, env, read = readSubscriptionsMetadata) {
4383
+ this.read = read;
4384
+ this.env = { ...env, HASNA_SUBSCRIPTIONS_API_URL: baseUrl.replace(/\/v1\/?$/, "") };
4385
+ }
4386
+ allProfiles() {
4387
+ return this.profiles ??= this.read(["list", "--json"], this.env).then((raw) => {
4388
+ const parsed = z.array(profileSchema).safeParse(raw);
4389
+ if (!parsed.success)
4390
+ throw new Error("subscriptions: invalid profile metadata response");
4391
+ return unique(parsed.data, (row) => JSON.stringify([row.tool, row.name]));
4392
+ });
4393
+ }
4394
+ async listProfiles(tool) {
4395
+ const rows = await this.allProfiles();
4396
+ return rows.filter((row) => !tool || row.tool === tool);
4397
+ }
4398
+ async findProfile(name, tool) {
4399
+ const matches = (await this.listProfiles(tool)).filter((row) => row.name === name);
4400
+ return matches.length === 1 ? matches[0] : undefined;
4401
+ }
4402
+ async appliedProfile(tool) {
4403
+ const matches = (await this.allProfiles()).filter((row) => row.tool === tool && row.applied);
4404
+ if (matches.length > 1)
4405
+ throw new Error("subscriptions: ambiguous station applied-profile binding");
4406
+ return matches[0];
4407
+ }
4408
+ async currentProfile(_tool) {
4409
+ return;
4410
+ }
4411
+ async listTools() {
4412
+ return this.tools ??= this.read(["tools", "list", "--json"], this.env).then((raw) => {
4413
+ const parsed = toolsSchema.safeParse(raw);
4414
+ if (!parsed.success)
4415
+ throw new Error("subscriptions: invalid tool metadata response");
4416
+ return unique(parsed.data, (row) => row.id);
4417
+ });
4418
+ }
4419
+ }
4420
+
4421
+ // src/lib/accounts-store.ts
4422
+ var ACCOUNTS_APP = "accounts";
4423
+ 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");
4424
+ var profileSchema2 = z2.object({
4425
+ name: z2.string(),
4426
+ tool: z2.string(),
4427
+ email: z2.string().optional(),
4428
+ displayName: z2.string().optional(),
4429
+ dir: z2.string(),
4430
+ description: z2.string().optional(),
4431
+ createdAt: z2.string(),
4432
+ lastUsedAt: z2.string().optional(),
4433
+ switchedAway: z2.object({ profile: z2.string().min(1), email: z2.string().optional() }).optional()
4329
4434
  });
4330
- var toolDefSchema = z.object({
4331
- id: z.string(),
4332
- label: z.string(),
4333
- envVar: z.string(),
4334
- defaultDir: z.string(),
4335
- bin: z.string()
4435
+ var toolDefSchema = z2.object({
4436
+ id: z2.string(),
4437
+ label: z2.string(),
4438
+ envVar: z2.string(),
4439
+ defaultDir: z2.string(),
4440
+ bin: z2.string()
4336
4441
  });
4337
- var storeSchema = z.object({
4338
- version: z.literal(1),
4339
- current: z.record(z.string(), z.string()).default({}),
4340
- applied: z.record(z.string(), z.string()).default({}),
4341
- toolLocks: z.record(z.string(), z.string()).default({}),
4342
- profiles: z.array(profileSchema).default([]),
4343
- tools: z.array(toolDefSchema).default([])
4442
+ var storeSchema = z2.object({
4443
+ version: z2.literal(1),
4444
+ current: z2.record(z2.string(), z2.string()).default({}),
4445
+ applied: z2.record(z2.string(), z2.string()).default({}),
4446
+ toolLocks: z2.record(z2.string(), z2.string()).default({}),
4447
+ profiles: z2.array(profileSchema2).default([]),
4448
+ tools: z2.array(toolDefSchema).default([])
4344
4449
  });
4345
4450
  function localRegistryAllowed(env = process.env) {
4346
4451
  return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => {
@@ -4622,25 +4727,34 @@ class ApiStore {
4622
4727
  }
4623
4728
  }
4624
4729
  function resolveStore(env = process.env, options = {}) {
4730
+ const legacy = resolveRegistryTransport(ACCOUNTS_APP, env, options.credentials);
4731
+ if (legacy)
4732
+ return new ApiStore(legacy.baseUrl, legacy.apiKey);
4733
+ const current = resolveRegistryTransport("subscriptions", env, options.subscriptionsCredentials);
4734
+ if (current)
4735
+ return new SubscriptionsRegistry(current.baseUrl, env, options.readSubscriptionsMetadata);
4736
+ return unconfiguredStore(env);
4737
+ }
4738
+ function resolveRegistryTransport(app, env, credentials) {
4625
4739
  const clientEnv = env;
4626
- const credential = resolveCredential(ACCOUNTS_APP, clientEnv, options.credentials);
4740
+ const credential = resolveCredential(app, clientEnv, credentials);
4627
4741
  if (credential === null) {
4628
4742
  try {
4629
- resolveClientTransport(ACCOUNTS_APP, clientEnv, options.credentials ? { credentials: options.credentials } : {});
4743
+ resolveClientTransport(app, clientEnv, credentials ? { credentials } : {});
4630
4744
  } catch (err) {
4631
4745
  if (isNoAccountsCredentialConfigurationError(err))
4632
- return unconfiguredStore(env);
4746
+ return null;
4633
4747
  throw err;
4634
4748
  }
4635
- return unconfiguredStore(env);
4749
+ return null;
4636
4750
  }
4637
4751
  if (credential.tier === "pointer") {
4638
- throw new Error(`accounts: ${credential.source} names a secrets-vault item, and the local accounts API store resolves ` + "credentials synchronously, so it cannot complete the pointer per request. It is a deliberate " + "selection and is not resolved around. Use a literal tier instead \u2014 the Keychain item " + "hasna.credentials.accounts.api-key, ~/.hasna/accounts/config/credentials, or HASNA_ACCOUNTS_API_KEY.");
4752
+ throw new Error(`${app}: ${credential.source} names a secrets-vault item, and the registry adapter resolves ` + "credentials synchronously, so it cannot complete the pointer per request. It is a deliberate " + "selection and is not resolved around. Use a literal tier instead \u2014 the Keychain item " + `hasna.credentials.${app}.api-key, ~/.hasna/${app}/config/credentials, or HASNA_${app.toUpperCase()}_API_KEY.`);
4639
4753
  }
4640
- const resolution = resolveClientTransport(ACCOUNTS_APP, clientEnv, {
4641
- credentials: { ...options.credentials, apiKey: credential.apiKey }
4754
+ const resolution = resolveClientTransport(app, clientEnv, {
4755
+ credentials: { ...credentials, apiKey: credential.apiKey }
4642
4756
  });
4643
- return new ApiStore(resolution.baseUrl, credential.apiKey);
4757
+ return { baseUrl: resolution.baseUrl, apiKey: credential.apiKey };
4644
4758
  }
4645
4759
  function isNoAccountsCredentialConfigurationError(error) {
4646
4760
  if (typeof error !== "object" || error === null)
@@ -4657,10 +4771,10 @@ function unconfiguredStore(env) {
4657
4771
  return new NoRegistryStore;
4658
4772
  }
4659
4773
  function accountsLocalModeNotice() {
4660
- return `accounts: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 no Hasna Accounts credential resolved, so ` + "attribution reads the on-box JSON registry (~/.hasna/accounts/accounts.json) instead of accounts-serve. " + "To go hosted, put the fleet key in the Keychain item hasna.credentials.accounts.api-key, write " + "~/.hasna/accounts/config/credentials, or set HASNA_ACCOUNTS_API_KEY.";
4774
+ return `accounts: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 neither Accounts nor Subscriptions credentials resolved, so ` + "attribution reads the on-box JSON registry (~/.hasna/accounts/accounts.json) instead of accounts-serve. " + "For hosted attribution, configure the Subscriptions CLI through its normal sign-in or credential resolver.";
4661
4775
  }
4662
4776
  function accountsRefusalNotice() {
4663
- return "accounts: no Hasna Accounts credential resolved (Keychain item hasna.credentials.accounts.api-key, " + "~/.hasna/accounts/config/credentials, HASNA_ACCOUNTS_API_KEY), so spend attribution is omitted. The " + `on-box registry (~/.hasna/accounts/accounts.json) is NOT read without ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 \u2014 ` + "economy fails closed instead of attributing hosted spend from an on-box file.";
4777
+ return "accounts: neither Accounts nor Subscriptions credentials resolved (legacy hasna.credentials.accounts.api-key, " + "~/.hasna/accounts/config/credentials, HASNA_ACCOUNTS_API_KEY; current hasna.credentials.subscriptions.api-key, " + "~/.hasna/subscriptions/config/credentials, HASNA_SUBSCRIPTIONS_API_KEY), so spend attribution is omitted. The " + `on-box registry (~/.hasna/accounts/accounts.json) is NOT read without ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 \u2014 ` + "economy fails closed instead of attributing hosted spend from an on-box file.";
4664
4778
  }
4665
4779
  var accountsLocalNoticePrinted = false;
4666
4780
  function announceAccountsLocalMode() {
@@ -4748,7 +4862,10 @@ async function profileForEnvDir(store, tool, env) {
4748
4862
  if (!configuredDir)
4749
4863
  return null;
4750
4864
  const normalized = normalizeDir(configuredDir);
4751
- return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
4865
+ const matches = (await store.listProfiles(tool.id)).filter((profile) => normalizeDir(profile.dir) === normalized);
4866
+ if (matches.length > 1)
4867
+ throw new Error("account attribution: multiple profiles match the configured directory");
4868
+ return matches[0] ?? null;
4752
4869
  }
4753
4870
  async function resolveAccountForAgent(agent, env = process.env, options = {}) {
4754
4871
  const override = envOverride(agent, env);
@@ -4762,8 +4879,15 @@ async function resolveAccountForAgent(agent, env = process.env, options = {}) {
4762
4879
  continue;
4763
4880
  const envProfile = await profileForEnvDir(store, tool, env);
4764
4881
  if (envProfile)
4765
- return fromProfile(envProfile, "env");
4766
- const appliedName = appliedProfileName(toolId);
4882
+ return envProfile.switchedAway ? null : fromProfile(envProfile, "env");
4883
+ if (store.registry === "subscriptions" && env[tool.envVar])
4884
+ return null;
4885
+ if (store.appliedProfile) {
4886
+ const applied = await store.appliedProfile(toolId);
4887
+ if (applied)
4888
+ return applied.switchedAway ? null : fromProfile(applied, "applied");
4889
+ }
4890
+ const appliedName = store.registry === "subscriptions" ? undefined : appliedProfileName(toolId);
4767
4891
  if (appliedName) {
4768
4892
  const applied = await store.findProfile(appliedName, toolId);
4769
4893
  if (applied)
@@ -4788,12 +4912,41 @@ function withAccount(record, account) {
4788
4912
  };
4789
4913
  }
4790
4914
 
4915
+ // src/lib/paths.ts
4916
+ import { homedir as homedir3 } from "os";
4917
+ import { join as join4 } from "path";
4918
+ function getHomeDir2() {
4919
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir3();
4920
+ }
4921
+ function agentPaths() {
4922
+ const home = getHomeDir2();
4923
+ const claudeHome = process.env["CLAUDE_CONFIG_DIR"] || join4(home, ".claude");
4924
+ const codexHome = process.env["CODEX_HOME"] || join4(home, ".codex");
4925
+ return {
4926
+ claudeProjects: join4(claudeHome, "projects"),
4927
+ claudeCredentials: join4(claudeHome, ".credentials.json"),
4928
+ takumiProjects: join4(home, ".takumi", "projects"),
4929
+ codexDir: codexHome,
4930
+ codexDb: join4(codexHome, "state_5.sqlite"),
4931
+ codexAuth: join4(codexHome, "auth.json"),
4932
+ codexConfig: join4(codexHome, "config.toml"),
4933
+ geminiTmp: join4(home, ".gemini", "tmp"),
4934
+ geminiHistory: join4(home, ".gemini", "history"),
4935
+ opencodeMessages: join4(home, ".local", "share", "opencode", "storage", "message"),
4936
+ piSessions: join4(home, ".pi", "agent", "sessions"),
4937
+ hermesDir: join4(home, ".hermes"),
4938
+ hermesDb: join4(home, ".hermes", "state.db")
4939
+ };
4940
+ }
4941
+
4791
4942
  // src/ingest/claude.ts
4792
4943
  function autoDetectProject(cwd, projects) {
4793
4944
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
4794
4945
  }
4795
- var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join4(homedir3(), ".claude", "projects");
4796
- var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join4(homedir3(), ".takumi", "projects");
4946
+ var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join5(homedir4(), ".takumi", "projects");
4947
+ function claudeProjectsDir() {
4948
+ return process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? agentPaths().claudeProjects;
4949
+ }
4797
4950
  function dirNameToPath(dirName) {
4798
4951
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
4799
4952
  }
@@ -4803,9 +4956,9 @@ function collectJsonlFiles(projectDir) {
4803
4956
  try {
4804
4957
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
4805
4958
  if (entry.isDirectory())
4806
- walk(join4(dir, entry.name));
4959
+ walk(join5(dir, entry.name));
4807
4960
  else if (entry.name.endsWith(".jsonl"))
4808
- files.push(join4(dir, entry.name));
4961
+ files.push(join5(dir, entry.name));
4809
4962
  }
4810
4963
  } catch {
4811
4964
  collectionProblem("failed", "source_unreadable");
@@ -4814,7 +4967,7 @@ function collectJsonlFiles(projectDir) {
4814
4967
  walk(projectDir);
4815
4968
  return files;
4816
4969
  }
4817
- async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
4970
+ async function ingestClaude(db, verbose = false, projectsDir = claudeProjectsDir()) {
4818
4971
  return ingestJsonlProjects(db, projectsDir, "claude", verbose);
4819
4972
  }
4820
4973
  async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
@@ -4836,7 +4989,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
4836
4989
  const ingestState = loadIngestState(db, agentName);
4837
4990
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
4838
4991
  for (const projectDirEntry of projectDirs) {
4839
- const projectDirPath = join4(projectsDir, projectDirEntry.name);
4992
+ const projectDirPath = join5(projectsDir, projectDirEntry.name);
4840
4993
  const projectPath = dirNameToPath(projectDirEntry.name);
4841
4994
  const jsonlFiles = collectJsonlFiles(projectDirPath);
4842
4995
  for (const filePath of jsonlFiles) {
@@ -4987,24 +5140,19 @@ init_database();
4987
5140
  init_pricing();
4988
5141
  import { existsSync as existsSync6, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
4989
5142
  import { createHash as createHash4 } from "crypto";
4990
- import { homedir as homedir4 } from "os";
4991
- import { join as join5, basename as basename2 } from "path";
4992
- var DEFAULT_CODEX_DB_PATH = join5(homedir4(), ".codex", "state_5.sqlite");
4993
- var DEFAULT_CODEX_CONFIG_PATH = join5(homedir4(), ".codex", "config.toml");
4994
- var DEFAULT_CODEWITH_DB_PATH = join5(homedir4(), ".codewith", "state_5.sqlite");
4995
- var DEFAULT_CODEWITH_CONFIG_PATH = join5(homedir4(), ".codewith", "config.toml");
5143
+ import { join as join6, basename as basename2 } from "path";
4996
5144
  var CODEX_INGEST_VERSION = "rollout-events-v4";
4997
5145
  function codexDbPath() {
4998
- return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
5146
+ return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? agentPaths().codexDb;
4999
5147
  }
5000
5148
  function codexConfigPath() {
5001
- return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
5149
+ return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? agentPaths().codexConfig;
5002
5150
  }
5003
5151
  function codewithDbPath() {
5004
- return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? DEFAULT_CODEWITH_DB_PATH;
5152
+ return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? join6(getHomeDir2(), ".codewith", "state_5.sqlite");
5005
5153
  }
5006
5154
  function codewithConfigPath() {
5007
- return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? DEFAULT_CODEWITH_CONFIG_PATH;
5155
+ return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? join6(getHomeDir2(), ".codewith", "config.toml");
5008
5156
  }
5009
5157
  function codexSources() {
5010
5158
  const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
@@ -5016,7 +5164,7 @@ function codexSources() {
5016
5164
  sessionPrefix: "codex",
5017
5165
  stateSource: "codex"
5018
5166
  }];
5019
- if (!explicitCodexPath || explicitCodewithPath) {
5167
+ if (!explicitCodexPath && !process.env["CODEX_HOME"] || explicitCodewithPath) {
5020
5168
  sources.push({
5021
5169
  label: "Codewith",
5022
5170
  dbPath: codewithDbPath(),
@@ -5256,9 +5404,9 @@ init_database();
5256
5404
  init_pricing();
5257
5405
  import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync7, statSync as statSync4 } from "fs";
5258
5406
  import { homedir as homedir5 } from "os";
5259
- import { join as join6, basename as basename3 } from "path";
5260
- var DEFAULT_GEMINI_TMP_DIR = join6(homedir5(), ".gemini", "tmp");
5261
- var DEFAULT_GEMINI_HISTORY_DIR = join6(homedir5(), ".gemini", "history");
5407
+ import { join as join7, basename as basename3 } from "path";
5408
+ var DEFAULT_GEMINI_TMP_DIR = join7(homedir5(), ".gemini", "tmp");
5409
+ var DEFAULT_GEMINI_HISTORY_DIR = join7(homedir5(), ".gemini", "history");
5262
5410
  function geminiTmpDir() {
5263
5411
  return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
5264
5412
  }
@@ -5280,7 +5428,7 @@ function listProjectDirs(...roots) {
5280
5428
  try {
5281
5429
  for (const entry of readdirSync3(root, { withFileTypes: true })) {
5282
5430
  if (entry.isDirectory())
5283
- dirs.add(join6(root, entry.name));
5431
+ dirs.add(join7(root, entry.name));
5284
5432
  }
5285
5433
  } catch {
5286
5434
  collectionProblem("failed", "source_unreadable");
@@ -5293,7 +5441,7 @@ function projectRoot(projectDir, chatData) {
5293
5441
  return chatData.projectPath;
5294
5442
  if (chatData.project_path)
5295
5443
  return chatData.project_path;
5296
- const rootFile = join6(projectDir, ".project_root");
5444
+ const rootFile = join7(projectDir, ".project_root");
5297
5445
  try {
5298
5446
  if (existsSync7(rootFile))
5299
5447
  return readFileSync4(rootFile, "utf-8").trim();
@@ -5318,12 +5466,12 @@ async function ingestGemini(db, verbose) {
5318
5466
  const account = await resolveAccountForAgent("gemini");
5319
5467
  const projectDirs = listProjectDirs(tmpDir, historyDir);
5320
5468
  for (const projectDir of projectDirs) {
5321
- const chatsDir = join6(projectDir, "chats");
5469
+ const chatsDir = join7(projectDir, "chats");
5322
5470
  if (!existsSync7(chatsDir))
5323
5471
  continue;
5324
5472
  let chatFiles = [];
5325
5473
  try {
5326
- chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
5474
+ chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join7(chatsDir, f));
5327
5475
  } catch {
5328
5476
  collectionProblem("failed", "source_unreadable");
5329
5477
  continue;
@@ -5435,13 +5583,13 @@ init_database();
5435
5583
  init_pricing();
5436
5584
  import { existsSync as existsSync8, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
5437
5585
  import { homedir as homedir6 } from "os";
5438
- import { join as join7 } from "path";
5439
- var OPENCODE_STORAGE = join7(homedir6(), ".local", "share", "opencode", "storage");
5586
+ import { join as join8 } from "path";
5587
+ var OPENCODE_STORAGE = join8(homedir6(), ".local", "share", "opencode", "storage");
5440
5588
  function walkJsonFiles(dir, acc = []) {
5441
5589
  if (!existsSync8(dir))
5442
5590
  return acc;
5443
5591
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
5444
- const p = join7(dir, entry.name);
5592
+ const p = join8(dir, entry.name);
5445
5593
  if (entry.isDirectory())
5446
5594
  walkJsonFiles(p, acc);
5447
5595
  else if (entry.name.endsWith(".json"))
@@ -5461,7 +5609,7 @@ function parseSessionIdFromPath(filePath) {
5461
5609
  return null;
5462
5610
  }
5463
5611
  async function ingestOpenCode(db, verbose = false, sourceDir = OPENCODE_STORAGE) {
5464
- const messageDir = join7(sourceDir, "message");
5612
+ const messageDir = join8(sourceDir, "message");
5465
5613
  if (!existsSync8(messageDir))
5466
5614
  collectionProblem("missing", "source_missing");
5467
5615
  const files = walkJsonFiles(messageDir);
@@ -5656,13 +5804,13 @@ async function ingestCursor(db, verbose = false) {
5656
5804
  init_database();
5657
5805
  import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
5658
5806
  import { homedir as homedir7 } from "os";
5659
- import { join as join8 } from "path";
5660
- var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join8(homedir7(), ".pi", "agent", "sessions");
5807
+ import { join as join9 } from "path";
5808
+ var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join9(homedir7(), ".pi", "agent", "sessions");
5661
5809
  function walkSessions(dir, acc = []) {
5662
5810
  if (!existsSync9(dir))
5663
5811
  return acc;
5664
5812
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
5665
- const p = join8(dir, entry.name);
5813
+ const p = join9(dir, entry.name);
5666
5814
  if (entry.isDirectory())
5667
5815
  walkSessions(p, acc);
5668
5816
  else if (entry.name.endsWith(".json"))
@@ -5763,8 +5911,8 @@ async function ingestPi(db, verbose = false, sourceDir = PI_SESSION_DIR) {
5763
5911
  init_database();
5764
5912
  import { existsSync as existsSync10, statSync as statSync7 } from "fs";
5765
5913
  import { homedir as homedir8 } from "os";
5766
- import { join as join9 } from "path";
5767
- var HERMES_DB = join9(homedir8(), ".hermes", "state.db");
5914
+ import { join as join10 } from "path";
5915
+ var HERMES_DB = join10(homedir8(), ".hermes", "state.db");
5768
5916
  async function ingestHermes(db, verbose = false, sourcePath = HERMES_DB) {
5769
5917
  if (!existsSync10(sourcePath)) {
5770
5918
  collectionProblem("missing", "source_missing");
@@ -5834,7 +5982,7 @@ init_database();
5834
5982
  init_pricing();
5835
5983
  import { existsSync as existsSync11 } from "fs";
5836
5984
  import { homedir as homedir9 } from "os";
5837
- import { join as join10 } from "path";
5985
+ import { join as join11 } from "path";
5838
5986
  import { homedir as pathsResolverHomedir2 } from "os";
5839
5987
  import { join as pathsResolverJoin2 } from "path";
5840
5988
  var PATHS_RESOLVER_KIND_ENV2 = {
@@ -5901,8 +6049,8 @@ function defaultLoopsDbPath() {
5901
6049
  app: "loops",
5902
6050
  home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
5903
6051
  });
5904
- const legacy = join10(process.env["HOME"] || process.env["USERPROFILE"] || homedir9(), ".hasna", "loops", "loops.db");
5905
- return existsSync11(join10(resolved, "loops.db")) ? join10(resolved, "loops.db") : legacy;
6052
+ const legacy = join11(process.env["HOME"] || process.env["USERPROFILE"] || homedir9(), ".hasna", "loops", "loops.db");
6053
+ return existsSync11(join11(resolved, "loops.db")) ? join11(resolved, "loops.db") : legacy;
5906
6054
  }
5907
6055
  function loopsDbPath() {
5908
6056
  return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
@@ -6086,33 +6234,6 @@ async function ingestLoops(db, verbose = false) {
6086
6234
 
6087
6235
  // src/ingest/claude-quota.ts
6088
6236
  import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
6089
-
6090
- // src/lib/paths.ts
6091
- import { homedir as homedir10 } from "os";
6092
- import { join as join11 } from "path";
6093
- function getHomeDir2() {
6094
- return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir10();
6095
- }
6096
- function agentPaths() {
6097
- const home = getHomeDir2();
6098
- return {
6099
- claudeProjects: join11(home, ".claude", "projects"),
6100
- claudeCredentials: join11(home, ".claude", ".credentials.json"),
6101
- takumiProjects: join11(home, ".takumi", "projects"),
6102
- codexDir: join11(home, ".codex"),
6103
- codexDb: join11(home, ".codex", "state_5.sqlite"),
6104
- codexAuth: join11(home, ".codex", "auth.json"),
6105
- codexConfig: join11(home, ".codex", "config.toml"),
6106
- geminiTmp: join11(home, ".gemini", "tmp"),
6107
- geminiHistory: join11(home, ".gemini", "history"),
6108
- opencodeMessages: join11(home, ".local", "share", "opencode", "storage", "message"),
6109
- piSessions: join11(home, ".pi", "agent", "sessions"),
6110
- hermesDir: join11(home, ".hermes"),
6111
- hermesDb: join11(home, ".hermes", "state.db")
6112
- };
6113
- }
6114
-
6115
- // src/ingest/claude-quota.ts
6116
6237
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
6117
6238
  var OAUTH_BETA = "oauth-2025-04-20";
6118
6239
  function readClaudeToken() {
@@ -7305,7 +7426,7 @@ import { randomUUID } from "crypto";
7305
7426
  // package.json
7306
7427
  var package_default = {
7307
7428
  name: "@hasna/economy",
7308
- version: "0.5.3",
7429
+ version: "0.5.5",
7309
7430
  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",
7310
7431
  repository: {
7311
7432
  type: "git",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
5
5
  "repository": {
6
6
  "type": "git",