@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.
@@ -4620,7 +4620,7 @@ __export(exports_legacy_plan, {
4620
4620
  });
4621
4621
  import { createHash as createHash6 } from "crypto";
4622
4622
  import { closeSync, constants, fstatSync, openSync, readSync } from "fs";
4623
- import { isAbsolute } from "path";
4623
+ import { isAbsolute as isAbsolute2 } from "path";
4624
4624
  function object(value, fields, label) {
4625
4625
  if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join(",") !== fields.sort().join(","))
4626
4626
  throw Error(`${label} has unsupported or missing fields`);
@@ -4673,7 +4673,7 @@ function parseLegacySelection(value) {
4673
4673
  return { schema: mode.selection, items };
4674
4674
  }
4675
4675
  function readLegacySelectionFile(path) {
4676
- if (!isAbsolute(path) || path.includes("\x00"))
4676
+ if (!isAbsolute2(path) || path.includes("\x00"))
4677
4677
  throw Error("--input must be an absolute path to a regular JSON file");
4678
4678
  let fd;
4679
4679
  try {
@@ -4708,7 +4708,7 @@ function readLegacySelectionFile(path) {
4708
4708
  }
4709
4709
  }
4710
4710
  function parseLegacyPlanOptions(args) {
4711
- if (args.length !== 2 || args[0] !== "--input" || !args[1] || !isAbsolute(args[1]) || args[1].includes("\x00"))
4711
+ if (args.length !== 2 || args[0] !== "--input" || !args[1] || !isAbsolute2(args[1]) || args[1].includes("\x00"))
4712
4712
  throw Error("legacy-plan accepts exactly --input <absolute JSON file>");
4713
4713
  return args[1];
4714
4714
  }
@@ -4837,7 +4837,7 @@ __export(exports_legacy_preservation, {
4837
4837
  });
4838
4838
  import { createHash as createHash7, randomUUID as randomUUID3 } from "crypto";
4839
4839
  import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, lstatSync, openSync as openSync2, readSync as readSync2, realpathSync, writeSync } from "fs";
4840
- import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
4840
+ import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute3 } from "path";
4841
4841
  function parseLegacyPlan(value) {
4842
4842
  const p = record(value);
4843
4843
  const mode = legacyMode("plan", p.schema);
@@ -4988,7 +4988,7 @@ async function preserveLegacyOriginals(client, input) {
4988
4988
  });
4989
4989
  }
4990
4990
  function readLegacyBytes(path, maxBytes = MAX_BYTES) {
4991
- if (!isAbsolute2(path) || path.includes("\x00"))
4991
+ if (!isAbsolute3(path) || path.includes("\x00"))
4992
4992
  fail("Artifact path must be absolute");
4993
4993
  let fd;
4994
4994
  try {
@@ -5027,7 +5027,7 @@ function readLegacyArtifact(path, maxBytes = MAX_BYTES) {
5027
5027
  }
5028
5028
  function writeLegacyCapsule(path, input) {
5029
5029
  const capsule = parseLegacyCapsule(input);
5030
- if (!isAbsolute2(path) || path.includes("\x00") || realpathSync(dirname4(path)) !== dirname4(path))
5030
+ if (!isAbsolute3(path) || path.includes("\x00") || realpathSync(dirname4(path)) !== dirname4(path))
5031
5031
  fail("Output requires an absolute path with a real parent directory");
5032
5032
  const parent = lstatSync(dirname4(path));
5033
5033
  if (!parent.isDirectory() || parent.uid !== process.getuid?.() || (parent.mode & 63) !== 0)
@@ -5141,7 +5141,7 @@ async function rehearseLegacyRestore(client, input, scope = "transaction") {
5141
5141
  }
5142
5142
  function parseLegacyPreservationOptions(command, args) {
5143
5143
  const preserve = command === "legacy-preserve";
5144
- if (!preserve && command !== "legacy-rehearse" || args.length !== (preserve ? 4 : 2) || args[0] !== "--input" || !args[1] || !isAbsolute2(args[1]) || preserve && (args[2] !== "--output" || !args[3] || !isAbsolute2(args[3])) || args.some((x) => x.includes("\x00")))
5144
+ if (!preserve && command !== "legacy-rehearse" || args.length !== (preserve ? 4 : 2) || args[0] !== "--input" || !args[1] || !isAbsolute3(args[1]) || preserve && (args[2] !== "--output" || !args[3] || !isAbsolute3(args[3])) || args.some((x) => x.includes("\x00")))
5145
5145
  fail("Expected " + command + " --input <absolute JSON file>" + (preserve ? " --output <new file in an owner-only directory>" : ""));
5146
5146
  return { input: args[1], ...preserve ? { output: args[3] } : {} };
5147
5147
  }
@@ -5190,7 +5190,7 @@ __export(exports_legacy_operations, {
5190
5190
  LegacyOperationInputError: () => LegacyOperationInputError
5191
5191
  });
5192
5192
  import { createHash as createHash8 } from "crypto";
5193
- import { isAbsolute as isAbsolute3 } from "path";
5193
+ import { isAbsolute as isAbsolute4 } from "path";
5194
5194
  function parseLegacyIntent(action, value) {
5195
5195
  try {
5196
5196
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -5435,7 +5435,7 @@ function parseLegacyOperationOptions(command, args) {
5435
5435
  const apply = command === "legacy-apply";
5436
5436
  if (!apply && command !== "legacy-rollback")
5437
5437
  fail2("Unknown legacy operation or arguments");
5438
- if (args.length !== (apply ? 4 : 2) || args[0] !== "--input" || !args[1] || !isAbsolute3(args[1]) || apply && (args[2] !== "--capsule" || !args[3] || !isAbsolute3(args[3])) || args.some((s) => s.includes("\x00")))
5438
+ if (args.length !== (apply ? 4 : 2) || args[0] !== "--input" || !args[1] || !isAbsolute4(args[1]) || apply && (args[2] !== "--capsule" || !args[3] || !isAbsolute4(args[3])) || args.some((s) => s.includes("\x00")))
5439
5439
  fail2("Expected " + command + " --input <absolute intent JSON>" + (apply ? " --capsule <absolute preservation JSON>" : ""));
5440
5440
  return { input: args[1], ...apply ? { capsule: args[3] } : {} };
5441
5441
  }
@@ -5529,7 +5529,7 @@ function isAuthorizedRequest(req, path) {
5529
5529
  // package.json
5530
5530
  var package_default = {
5531
5531
  name: "@hasna/economy",
5532
- version: "0.5.3",
5532
+ version: "0.5.5",
5533
5533
  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",
5534
5534
  repository: {
5535
5535
  type: "git",
@@ -8812,14 +8812,14 @@ function sourceTimeMillis(value) {
8812
8812
  init_database();
8813
8813
  init_pricing();
8814
8814
  import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync4, statSync as statSync2 } from "fs";
8815
- import { homedir as homedir3 } from "os";
8816
- import { join as join5, basename } from "path";
8815
+ import { homedir as homedir4 } from "os";
8816
+ import { join as join6, basename } from "path";
8817
8817
 
8818
8818
  // src/lib/accounts-store.ts
8819
8819
  import { existsSync as existsSync3, readFileSync } from "fs";
8820
8820
  import { homedir as homedir2 } from "os";
8821
8821
  import { join as join4 } from "path";
8822
- import { z } from "zod";
8822
+ import { z as z2 } from "zod";
8823
8823
  import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
8824
8824
 
8825
8825
  // src/lib/cloud-storage.ts
@@ -8838,33 +8838,138 @@ import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
8838
8838
  var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
8839
8839
  var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
8840
8840
 
8841
- // src/lib/accounts-store.ts
8842
- var ACCOUNTS_APP = "accounts";
8843
- 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");
8841
+ // src/lib/subscriptions-registry.ts
8842
+ import { execFile } from "child_process";
8843
+ import { promisify } from "util";
8844
+ import { isAbsolute } from "path";
8845
+ import { z } from "zod";
8846
+ var execute = promisify(execFile);
8847
+ var readSubscriptionsMetadata = async (args, env) => {
8848
+ let stdout;
8849
+ try {
8850
+ const result = await execute("subscriptions", args, {
8851
+ env,
8852
+ encoding: "utf8",
8853
+ timeout: 30000,
8854
+ maxBuffer: 16 * 1024 * 1024,
8855
+ windowsHide: true
8856
+ });
8857
+ stdout = result.stdout;
8858
+ } catch (error) {
8859
+ const code = error?.code;
8860
+ if (code === "ENOENT")
8861
+ throw new Error("subscriptions: CLI not found on PATH; install the Subscriptions CLI to resolve station account metadata");
8862
+ const status = typeof code === "number" && Number.isInteger(code) ? `exit ${code}` : "execution limit or transport failure";
8863
+ throw new Error(`subscriptions: read-only metadata command failed (${status}); verify the installed CLI and its hosted credentials`);
8864
+ }
8865
+ try {
8866
+ return JSON.parse(stdout);
8867
+ } catch {
8868
+ throw new Error("subscriptions: metadata command returned invalid JSON");
8869
+ }
8870
+ };
8871
+ var nonempty = z.string().min(1);
8844
8872
  var profileSchema = z.object({
8845
- name: z.string(),
8846
- tool: z.string(),
8873
+ name: nonempty,
8874
+ tool: nonempty,
8847
8875
  email: z.string().optional(),
8848
8876
  displayName: z.string().optional(),
8849
- dir: z.string(),
8877
+ dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
8878
+ createdAt: nonempty,
8879
+ lastUsedAt: z.string().optional(),
8850
8880
  description: z.string().optional(),
8851
- createdAt: z.string(),
8852
- lastUsedAt: z.string().optional()
8881
+ active: z.boolean(),
8882
+ applied: z.boolean(),
8883
+ switchedAway: z.object({ profile: nonempty, email: z.string().optional() }).optional()
8884
+ });
8885
+ var toolsSchema = z.array(z.object({
8886
+ id: nonempty,
8887
+ label: nonempty,
8888
+ envVar: nonempty,
8889
+ defaultDir: nonempty,
8890
+ bin: nonempty
8891
+ }));
8892
+ function unique(rows, identity) {
8893
+ if (new Set(rows.map(identity)).size !== rows.length) {
8894
+ throw new Error("subscriptions: duplicate identity in metadata response");
8895
+ }
8896
+ return rows;
8897
+ }
8898
+
8899
+ class SubscriptionsRegistry {
8900
+ read;
8901
+ transport = "api";
8902
+ registry = "subscriptions";
8903
+ env;
8904
+ profiles;
8905
+ tools;
8906
+ constructor(baseUrl, env, read = readSubscriptionsMetadata) {
8907
+ this.read = read;
8908
+ this.env = { ...env, HASNA_SUBSCRIPTIONS_API_URL: baseUrl.replace(/\/v1\/?$/, "") };
8909
+ }
8910
+ allProfiles() {
8911
+ return this.profiles ??= this.read(["list", "--json"], this.env).then((raw) => {
8912
+ const parsed = z.array(profileSchema).safeParse(raw);
8913
+ if (!parsed.success)
8914
+ throw new Error("subscriptions: invalid profile metadata response");
8915
+ return unique(parsed.data, (row) => JSON.stringify([row.tool, row.name]));
8916
+ });
8917
+ }
8918
+ async listProfiles(tool) {
8919
+ const rows = await this.allProfiles();
8920
+ return rows.filter((row) => !tool || row.tool === tool);
8921
+ }
8922
+ async findProfile(name, tool) {
8923
+ const matches = (await this.listProfiles(tool)).filter((row) => row.name === name);
8924
+ return matches.length === 1 ? matches[0] : undefined;
8925
+ }
8926
+ async appliedProfile(tool) {
8927
+ const matches = (await this.allProfiles()).filter((row) => row.tool === tool && row.applied);
8928
+ if (matches.length > 1)
8929
+ throw new Error("subscriptions: ambiguous station applied-profile binding");
8930
+ return matches[0];
8931
+ }
8932
+ async currentProfile(_tool) {
8933
+ return;
8934
+ }
8935
+ async listTools() {
8936
+ return this.tools ??= this.read(["tools", "list", "--json"], this.env).then((raw) => {
8937
+ const parsed = toolsSchema.safeParse(raw);
8938
+ if (!parsed.success)
8939
+ throw new Error("subscriptions: invalid tool metadata response");
8940
+ return unique(parsed.data, (row) => row.id);
8941
+ });
8942
+ }
8943
+ }
8944
+
8945
+ // src/lib/accounts-store.ts
8946
+ var ACCOUNTS_APP = "accounts";
8947
+ 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");
8948
+ var profileSchema2 = z2.object({
8949
+ name: z2.string(),
8950
+ tool: z2.string(),
8951
+ email: z2.string().optional(),
8952
+ displayName: z2.string().optional(),
8953
+ dir: z2.string(),
8954
+ description: z2.string().optional(),
8955
+ createdAt: z2.string(),
8956
+ lastUsedAt: z2.string().optional(),
8957
+ switchedAway: z2.object({ profile: z2.string().min(1), email: z2.string().optional() }).optional()
8853
8958
  });
8854
- var toolDefSchema = z.object({
8855
- id: z.string(),
8856
- label: z.string(),
8857
- envVar: z.string(),
8858
- defaultDir: z.string(),
8859
- bin: z.string()
8959
+ var toolDefSchema = z2.object({
8960
+ id: z2.string(),
8961
+ label: z2.string(),
8962
+ envVar: z2.string(),
8963
+ defaultDir: z2.string(),
8964
+ bin: z2.string()
8860
8965
  });
8861
- var storeSchema = z.object({
8862
- version: z.literal(1),
8863
- current: z.record(z.string(), z.string()).default({}),
8864
- applied: z.record(z.string(), z.string()).default({}),
8865
- toolLocks: z.record(z.string(), z.string()).default({}),
8866
- profiles: z.array(profileSchema).default([]),
8867
- tools: z.array(toolDefSchema).default([])
8966
+ var storeSchema = z2.object({
8967
+ version: z2.literal(1),
8968
+ current: z2.record(z2.string(), z2.string()).default({}),
8969
+ applied: z2.record(z2.string(), z2.string()).default({}),
8970
+ toolLocks: z2.record(z2.string(), z2.string()).default({}),
8971
+ profiles: z2.array(profileSchema2).default([]),
8972
+ tools: z2.array(toolDefSchema).default([])
8868
8973
  });
8869
8974
  function localRegistryAllowed(env = process.env) {
8870
8975
  return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => {
@@ -9146,25 +9251,34 @@ class ApiStore {
9146
9251
  }
9147
9252
  }
9148
9253
  function resolveStore(env = process.env, options = {}) {
9254
+ const legacy = resolveRegistryTransport(ACCOUNTS_APP, env, options.credentials);
9255
+ if (legacy)
9256
+ return new ApiStore(legacy.baseUrl, legacy.apiKey);
9257
+ const current = resolveRegistryTransport("subscriptions", env, options.subscriptionsCredentials);
9258
+ if (current)
9259
+ return new SubscriptionsRegistry(current.baseUrl, env, options.readSubscriptionsMetadata);
9260
+ return unconfiguredStore(env);
9261
+ }
9262
+ function resolveRegistryTransport(app, env, credentials) {
9149
9263
  const clientEnv = env;
9150
- const credential = resolveCredential(ACCOUNTS_APP, clientEnv, options.credentials);
9264
+ const credential = resolveCredential(app, clientEnv, credentials);
9151
9265
  if (credential === null) {
9152
9266
  try {
9153
- resolveClientTransport(ACCOUNTS_APP, clientEnv, options.credentials ? { credentials: options.credentials } : {});
9267
+ resolveClientTransport(app, clientEnv, credentials ? { credentials } : {});
9154
9268
  } catch (err) {
9155
9269
  if (isNoAccountsCredentialConfigurationError(err))
9156
- return unconfiguredStore(env);
9270
+ return null;
9157
9271
  throw err;
9158
9272
  }
9159
- return unconfiguredStore(env);
9273
+ return null;
9160
9274
  }
9161
9275
  if (credential.tier === "pointer") {
9162
- 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.");
9276
+ 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.`);
9163
9277
  }
9164
- const resolution = resolveClientTransport(ACCOUNTS_APP, clientEnv, {
9165
- credentials: { ...options.credentials, apiKey: credential.apiKey }
9278
+ const resolution = resolveClientTransport(app, clientEnv, {
9279
+ credentials: { ...credentials, apiKey: credential.apiKey }
9166
9280
  });
9167
- return new ApiStore(resolution.baseUrl, credential.apiKey);
9281
+ return { baseUrl: resolution.baseUrl, apiKey: credential.apiKey };
9168
9282
  }
9169
9283
  function isNoAccountsCredentialConfigurationError(error) {
9170
9284
  if (typeof error !== "object" || error === null)
@@ -9181,10 +9295,10 @@ function unconfiguredStore(env) {
9181
9295
  return new NoRegistryStore;
9182
9296
  }
9183
9297
  function accountsLocalModeNotice() {
9184
- 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.";
9298
+ 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.";
9185
9299
  }
9186
9300
  function accountsRefusalNotice() {
9187
- 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.";
9301
+ 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.";
9188
9302
  }
9189
9303
  var accountsLocalNoticePrinted = false;
9190
9304
  function announceAccountsLocalMode() {
@@ -9272,7 +9386,10 @@ async function profileForEnvDir(store, tool, env) {
9272
9386
  if (!configuredDir)
9273
9387
  return null;
9274
9388
  const normalized = normalizeDir(configuredDir);
9275
- return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
9389
+ const matches = (await store.listProfiles(tool.id)).filter((profile) => normalizeDir(profile.dir) === normalized);
9390
+ if (matches.length > 1)
9391
+ throw new Error("account attribution: multiple profiles match the configured directory");
9392
+ return matches[0] ?? null;
9276
9393
  }
9277
9394
  async function resolveAccountForAgent(agent, env = process.env, options = {}) {
9278
9395
  const override = envOverride(agent, env);
@@ -9286,8 +9403,15 @@ async function resolveAccountForAgent(agent, env = process.env, options = {}) {
9286
9403
  continue;
9287
9404
  const envProfile = await profileForEnvDir(store, tool, env);
9288
9405
  if (envProfile)
9289
- return fromProfile(envProfile, "env");
9290
- const appliedName = appliedProfileName(toolId);
9406
+ return envProfile.switchedAway ? null : fromProfile(envProfile, "env");
9407
+ if (store.registry === "subscriptions" && env[tool.envVar])
9408
+ return null;
9409
+ if (store.appliedProfile) {
9410
+ const applied = await store.appliedProfile(toolId);
9411
+ if (applied)
9412
+ return applied.switchedAway ? null : fromProfile(applied, "applied");
9413
+ }
9414
+ const appliedName = store.registry === "subscriptions" ? undefined : appliedProfileName(toolId);
9291
9415
  if (appliedName) {
9292
9416
  const applied = await store.findProfile(appliedName, toolId);
9293
9417
  if (applied)
@@ -9312,12 +9436,41 @@ function withAccount(record, account) {
9312
9436
  };
9313
9437
  }
9314
9438
 
9439
+ // src/lib/paths.ts
9440
+ import { homedir as homedir3 } from "os";
9441
+ import { join as join5 } from "path";
9442
+ function getHomeDir2() {
9443
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir3();
9444
+ }
9445
+ function agentPaths() {
9446
+ const home = getHomeDir2();
9447
+ const claudeHome = process.env["CLAUDE_CONFIG_DIR"] || join5(home, ".claude");
9448
+ const codexHome = process.env["CODEX_HOME"] || join5(home, ".codex");
9449
+ return {
9450
+ claudeProjects: join5(claudeHome, "projects"),
9451
+ claudeCredentials: join5(claudeHome, ".credentials.json"),
9452
+ takumiProjects: join5(home, ".takumi", "projects"),
9453
+ codexDir: codexHome,
9454
+ codexDb: join5(codexHome, "state_5.sqlite"),
9455
+ codexAuth: join5(codexHome, "auth.json"),
9456
+ codexConfig: join5(codexHome, "config.toml"),
9457
+ geminiTmp: join5(home, ".gemini", "tmp"),
9458
+ geminiHistory: join5(home, ".gemini", "history"),
9459
+ opencodeMessages: join5(home, ".local", "share", "opencode", "storage", "message"),
9460
+ piSessions: join5(home, ".pi", "agent", "sessions"),
9461
+ hermesDir: join5(home, ".hermes"),
9462
+ hermesDb: join5(home, ".hermes", "state.db")
9463
+ };
9464
+ }
9465
+
9315
9466
  // src/ingest/claude.ts
9316
9467
  function autoDetectProject(cwd, projects) {
9317
9468
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
9318
9469
  }
9319
- var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join5(homedir3(), ".claude", "projects");
9320
- var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join5(homedir3(), ".takumi", "projects");
9470
+ var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join6(homedir4(), ".takumi", "projects");
9471
+ function claudeProjectsDir() {
9472
+ return process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? agentPaths().claudeProjects;
9473
+ }
9321
9474
  function dirNameToPath(dirName) {
9322
9475
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
9323
9476
  }
@@ -9327,9 +9480,9 @@ function collectJsonlFiles(projectDir) {
9327
9480
  try {
9328
9481
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
9329
9482
  if (entry.isDirectory())
9330
- walk(join5(dir, entry.name));
9483
+ walk(join6(dir, entry.name));
9331
9484
  else if (entry.name.endsWith(".jsonl"))
9332
- files.push(join5(dir, entry.name));
9485
+ files.push(join6(dir, entry.name));
9333
9486
  }
9334
9487
  } catch {
9335
9488
  collectionProblem("failed", "source_unreadable");
@@ -9338,7 +9491,7 @@ function collectJsonlFiles(projectDir) {
9338
9491
  walk(projectDir);
9339
9492
  return files;
9340
9493
  }
9341
- async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
9494
+ async function ingestClaude(db, verbose = false, projectsDir = claudeProjectsDir()) {
9342
9495
  return ingestJsonlProjects(db, projectsDir, "claude", verbose);
9343
9496
  }
9344
9497
  async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
@@ -9360,7 +9513,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
9360
9513
  const ingestState = loadIngestState(db, agentName);
9361
9514
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
9362
9515
  for (const projectDirEntry of projectDirs) {
9363
- const projectDirPath = join5(projectsDir, projectDirEntry.name);
9516
+ const projectDirPath = join6(projectsDir, projectDirEntry.name);
9364
9517
  const projectPath = dirNameToPath(projectDirEntry.name);
9365
9518
  const jsonlFiles = collectJsonlFiles(projectDirPath);
9366
9519
  for (const filePath of jsonlFiles) {
@@ -9511,24 +9664,19 @@ init_database();
9511
9664
  init_pricing();
9512
9665
  import { existsSync as existsSync5, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
9513
9666
  import { createHash as createHash4 } from "crypto";
9514
- import { homedir as homedir4 } from "os";
9515
- import { join as join6, basename as basename2 } from "path";
9516
- var DEFAULT_CODEX_DB_PATH = join6(homedir4(), ".codex", "state_5.sqlite");
9517
- var DEFAULT_CODEX_CONFIG_PATH = join6(homedir4(), ".codex", "config.toml");
9518
- var DEFAULT_CODEWITH_DB_PATH = join6(homedir4(), ".codewith", "state_5.sqlite");
9519
- var DEFAULT_CODEWITH_CONFIG_PATH = join6(homedir4(), ".codewith", "config.toml");
9667
+ import { join as join7, basename as basename2 } from "path";
9520
9668
  var CODEX_INGEST_VERSION = "rollout-events-v4";
9521
9669
  function codexDbPath() {
9522
- return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
9670
+ return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? agentPaths().codexDb;
9523
9671
  }
9524
9672
  function codexConfigPath() {
9525
- return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
9673
+ return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? agentPaths().codexConfig;
9526
9674
  }
9527
9675
  function codewithDbPath() {
9528
- return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? DEFAULT_CODEWITH_DB_PATH;
9676
+ return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? join7(getHomeDir2(), ".codewith", "state_5.sqlite");
9529
9677
  }
9530
9678
  function codewithConfigPath() {
9531
- return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? DEFAULT_CODEWITH_CONFIG_PATH;
9679
+ return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? join7(getHomeDir2(), ".codewith", "config.toml");
9532
9680
  }
9533
9681
  function codexSources() {
9534
9682
  const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
@@ -9540,7 +9688,7 @@ function codexSources() {
9540
9688
  sessionPrefix: "codex",
9541
9689
  stateSource: "codex"
9542
9690
  }];
9543
- if (!explicitCodexPath || explicitCodewithPath) {
9691
+ if (!explicitCodexPath && !process.env["CODEX_HOME"] || explicitCodewithPath) {
9544
9692
  sources.push({
9545
9693
  label: "Codewith",
9546
9694
  dbPath: codewithDbPath(),
@@ -9780,9 +9928,9 @@ init_database();
9780
9928
  init_pricing();
9781
9929
  import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync6, statSync as statSync4 } from "fs";
9782
9930
  import { homedir as homedir5 } from "os";
9783
- import { join as join7, basename as basename3 } from "path";
9784
- var DEFAULT_GEMINI_TMP_DIR = join7(homedir5(), ".gemini", "tmp");
9785
- var DEFAULT_GEMINI_HISTORY_DIR = join7(homedir5(), ".gemini", "history");
9931
+ import { join as join8, basename as basename3 } from "path";
9932
+ var DEFAULT_GEMINI_TMP_DIR = join8(homedir5(), ".gemini", "tmp");
9933
+ var DEFAULT_GEMINI_HISTORY_DIR = join8(homedir5(), ".gemini", "history");
9786
9934
  function geminiTmpDir() {
9787
9935
  return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
9788
9936
  }
@@ -9804,7 +9952,7 @@ function listProjectDirs(...roots) {
9804
9952
  try {
9805
9953
  for (const entry of readdirSync3(root, { withFileTypes: true })) {
9806
9954
  if (entry.isDirectory())
9807
- dirs.add(join7(root, entry.name));
9955
+ dirs.add(join8(root, entry.name));
9808
9956
  }
9809
9957
  } catch {
9810
9958
  collectionProblem("failed", "source_unreadable");
@@ -9817,7 +9965,7 @@ function projectRoot(projectDir, chatData) {
9817
9965
  return chatData.projectPath;
9818
9966
  if (chatData.project_path)
9819
9967
  return chatData.project_path;
9820
- const rootFile = join7(projectDir, ".project_root");
9968
+ const rootFile = join8(projectDir, ".project_root");
9821
9969
  try {
9822
9970
  if (existsSync6(rootFile))
9823
9971
  return readFileSync4(rootFile, "utf-8").trim();
@@ -9842,12 +9990,12 @@ async function ingestGemini(db, verbose) {
9842
9990
  const account = await resolveAccountForAgent("gemini");
9843
9991
  const projectDirs = listProjectDirs(tmpDir, historyDir);
9844
9992
  for (const projectDir of projectDirs) {
9845
- const chatsDir = join7(projectDir, "chats");
9993
+ const chatsDir = join8(projectDir, "chats");
9846
9994
  if (!existsSync6(chatsDir))
9847
9995
  continue;
9848
9996
  let chatFiles = [];
9849
9997
  try {
9850
- chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join7(chatsDir, f));
9998
+ chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join8(chatsDir, f));
9851
9999
  } catch {
9852
10000
  collectionProblem("failed", "source_unreadable");
9853
10001
  continue;
@@ -9959,13 +10107,13 @@ init_database();
9959
10107
  init_pricing();
9960
10108
  import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
9961
10109
  import { homedir as homedir6 } from "os";
9962
- import { join as join8 } from "path";
9963
- var OPENCODE_STORAGE = join8(homedir6(), ".local", "share", "opencode", "storage");
10110
+ import { join as join9 } from "path";
10111
+ var OPENCODE_STORAGE = join9(homedir6(), ".local", "share", "opencode", "storage");
9964
10112
  function walkJsonFiles(dir, acc = []) {
9965
10113
  if (!existsSync7(dir))
9966
10114
  return acc;
9967
10115
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
9968
- const p = join8(dir, entry.name);
10116
+ const p = join9(dir, entry.name);
9969
10117
  if (entry.isDirectory())
9970
10118
  walkJsonFiles(p, acc);
9971
10119
  else if (entry.name.endsWith(".json"))
@@ -9985,7 +10133,7 @@ function parseSessionIdFromPath(filePath) {
9985
10133
  return null;
9986
10134
  }
9987
10135
  async function ingestOpenCode(db, verbose = false, sourceDir = OPENCODE_STORAGE) {
9988
- const messageDir = join8(sourceDir, "message");
10136
+ const messageDir = join9(sourceDir, "message");
9989
10137
  if (!existsSync7(messageDir))
9990
10138
  collectionProblem("missing", "source_missing");
9991
10139
  const files = walkJsonFiles(messageDir);
@@ -10180,13 +10328,13 @@ async function ingestCursor(db, verbose = false) {
10180
10328
  init_database();
10181
10329
  import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
10182
10330
  import { homedir as homedir7 } from "os";
10183
- import { join as join9 } from "path";
10184
- var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join9(homedir7(), ".pi", "agent", "sessions");
10331
+ import { join as join10 } from "path";
10332
+ var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join10(homedir7(), ".pi", "agent", "sessions");
10185
10333
  function walkSessions(dir, acc = []) {
10186
10334
  if (!existsSync8(dir))
10187
10335
  return acc;
10188
10336
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
10189
- const p = join9(dir, entry.name);
10337
+ const p = join10(dir, entry.name);
10190
10338
  if (entry.isDirectory())
10191
10339
  walkSessions(p, acc);
10192
10340
  else if (entry.name.endsWith(".json"))
@@ -10287,8 +10435,8 @@ async function ingestPi(db, verbose = false, sourceDir = PI_SESSION_DIR) {
10287
10435
  init_database();
10288
10436
  import { existsSync as existsSync9, statSync as statSync7 } from "fs";
10289
10437
  import { homedir as homedir8 } from "os";
10290
- import { join as join10 } from "path";
10291
- var HERMES_DB = join10(homedir8(), ".hermes", "state.db");
10438
+ import { join as join11 } from "path";
10439
+ var HERMES_DB = join11(homedir8(), ".hermes", "state.db");
10292
10440
  async function ingestHermes(db, verbose = false, sourcePath = HERMES_DB) {
10293
10441
  if (!existsSync9(sourcePath)) {
10294
10442
  collectionProblem("missing", "source_missing");
@@ -10358,7 +10506,7 @@ init_database();
10358
10506
  init_pricing();
10359
10507
  import { existsSync as existsSync10 } from "fs";
10360
10508
  import { homedir as homedir9 } from "os";
10361
- import { join as join11 } from "path";
10509
+ import { join as join12 } from "path";
10362
10510
  import { homedir as pathsResolverHomedir2 } from "os";
10363
10511
  import { join as pathsResolverJoin2 } from "path";
10364
10512
  var PATHS_RESOLVER_KIND_ENV2 = {
@@ -10425,8 +10573,8 @@ function defaultLoopsDbPath() {
10425
10573
  app: "loops",
10426
10574
  home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
10427
10575
  });
10428
- const legacy = join11(process.env["HOME"] || process.env["USERPROFILE"] || homedir9(), ".hasna", "loops", "loops.db");
10429
- return existsSync10(join11(resolved, "loops.db")) ? join11(resolved, "loops.db") : legacy;
10576
+ const legacy = join12(process.env["HOME"] || process.env["USERPROFILE"] || homedir9(), ".hasna", "loops", "loops.db");
10577
+ return existsSync10(join12(resolved, "loops.db")) ? join12(resolved, "loops.db") : legacy;
10430
10578
  }
10431
10579
  function loopsDbPath() {
10432
10580
  return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
@@ -10610,33 +10758,6 @@ async function ingestLoops(db, verbose = false) {
10610
10758
 
10611
10759
  // src/ingest/claude-quota.ts
10612
10760
  import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
10613
-
10614
- // src/lib/paths.ts
10615
- import { homedir as homedir10 } from "os";
10616
- import { join as join12 } from "path";
10617
- function getHomeDir2() {
10618
- return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir10();
10619
- }
10620
- function agentPaths() {
10621
- const home = getHomeDir2();
10622
- return {
10623
- claudeProjects: join12(home, ".claude", "projects"),
10624
- claudeCredentials: join12(home, ".claude", ".credentials.json"),
10625
- takumiProjects: join12(home, ".takumi", "projects"),
10626
- codexDir: join12(home, ".codex"),
10627
- codexDb: join12(home, ".codex", "state_5.sqlite"),
10628
- codexAuth: join12(home, ".codex", "auth.json"),
10629
- codexConfig: join12(home, ".codex", "config.toml"),
10630
- geminiTmp: join12(home, ".gemini", "tmp"),
10631
- geminiHistory: join12(home, ".gemini", "history"),
10632
- opencodeMessages: join12(home, ".local", "share", "opencode", "storage", "message"),
10633
- piSessions: join12(home, ".pi", "agent", "sessions"),
10634
- hermesDir: join12(home, ".hermes"),
10635
- hermesDb: join12(home, ".hermes", "state.db")
10636
- };
10637
- }
10638
-
10639
- // src/ingest/claude-quota.ts
10640
10761
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
10641
10762
  var OAUTH_BETA = "oauth-2025-04-20";
10642
10763
  function readClaudeToken() {