@hasna/economy 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -85,7 +85,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
85
85
  // package.json
86
86
  var package_default = {
87
87
  name: "@hasna/economy",
88
- version: "0.5.4",
88
+ version: "0.5.6",
89
89
  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",
90
90
  repository: {
91
91
  type: "git",
@@ -118,6 +118,8 @@ var package_default = {
118
118
  files: [
119
119
  "dist",
120
120
  "docs",
121
+ "native/Tools/pair-qr.py",
122
+ "native/Tools/pair-qr.swift",
121
123
  "postinstall.js",
122
124
  "LICENSE",
123
125
  "README.md",
@@ -1556,8 +1558,8 @@ function sourceTimeMillis(value) {
1556
1558
 
1557
1559
  // src/ingest/claude.ts
1558
1560
  import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync3, statSync } from "fs";
1559
- import { homedir as homedir2 } from "os";
1560
- import { join as join6, basename } from "path";
1561
+ import { homedir as homedir3 } from "os";
1562
+ import { join as join7, basename } from "path";
1561
1563
 
1562
1564
  // src/lib/accounts-store.ts
1563
1565
  import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
@@ -2164,12 +2166,41 @@ function withAccount(record, account) {
2164
2166
  };
2165
2167
  }
2166
2168
 
2169
+ // src/lib/paths.ts
2170
+ import { homedir as homedir2 } from "os";
2171
+ import { join as join6 } from "path";
2172
+ function getHomeDir() {
2173
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir2();
2174
+ }
2175
+ function agentPaths() {
2176
+ const home = getHomeDir();
2177
+ const claudeHome = process.env["CLAUDE_CONFIG_DIR"] || join6(home, ".claude");
2178
+ const codexHome = process.env["CODEX_HOME"] || join6(home, ".codex");
2179
+ return {
2180
+ claudeProjects: join6(claudeHome, "projects"),
2181
+ claudeCredentials: join6(claudeHome, ".credentials.json"),
2182
+ takumiProjects: join6(home, ".takumi", "projects"),
2183
+ codexDir: codexHome,
2184
+ codexDb: join6(codexHome, "state_5.sqlite"),
2185
+ codexAuth: join6(codexHome, "auth.json"),
2186
+ codexConfig: join6(codexHome, "config.toml"),
2187
+ geminiTmp: join6(home, ".gemini", "tmp"),
2188
+ geminiHistory: join6(home, ".gemini", "history"),
2189
+ opencodeMessages: join6(home, ".local", "share", "opencode", "storage", "message"),
2190
+ piSessions: join6(home, ".pi", "agent", "sessions"),
2191
+ hermesDir: join6(home, ".hermes"),
2192
+ hermesDb: join6(home, ".hermes", "state.db")
2193
+ };
2194
+ }
2195
+
2167
2196
  // src/ingest/claude.ts
2168
2197
  function autoDetectProject(cwd, projects) {
2169
2198
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
2170
2199
  }
2171
- var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join6(homedir2(), ".claude", "projects");
2172
- var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join6(homedir2(), ".takumi", "projects");
2200
+ var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join7(homedir3(), ".takumi", "projects");
2201
+ function claudeProjectsDir() {
2202
+ return process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? agentPaths().claudeProjects;
2203
+ }
2173
2204
  function dirNameToPath(dirName) {
2174
2205
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
2175
2206
  }
@@ -2179,9 +2210,9 @@ function collectJsonlFiles(projectDir) {
2179
2210
  try {
2180
2211
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
2181
2212
  if (entry.isDirectory())
2182
- walk(join6(dir, entry.name));
2213
+ walk(join7(dir, entry.name));
2183
2214
  else if (entry.name.endsWith(".jsonl"))
2184
- files.push(join6(dir, entry.name));
2215
+ files.push(join7(dir, entry.name));
2185
2216
  }
2186
2217
  } catch {
2187
2218
  collectionProblem("failed", "source_unreadable");
@@ -2190,7 +2221,7 @@ function collectJsonlFiles(projectDir) {
2190
2221
  walk(projectDir);
2191
2222
  return files;
2192
2223
  }
2193
- async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
2224
+ async function ingestClaude(db, verbose = false, projectsDir = claudeProjectsDir()) {
2194
2225
  return ingestJsonlProjects(db, projectsDir, "claude", verbose);
2195
2226
  }
2196
2227
  async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
@@ -2212,7 +2243,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2212
2243
  const ingestState = loadIngestState(db, agentName);
2213
2244
  const projectDirs = readdirSync3(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
2214
2245
  for (const projectDirEntry of projectDirs) {
2215
- const projectDirPath = join6(projectsDir, projectDirEntry.name);
2246
+ const projectDirPath = join7(projectsDir, projectDirEntry.name);
2216
2247
  const projectPath = dirNameToPath(projectDirEntry.name);
2217
2248
  const jsonlFiles = collectJsonlFiles(projectDirPath);
2218
2249
  for (const filePath of jsonlFiles) {
@@ -2361,24 +2392,19 @@ function supportsClaudeDataResidencyPricing(model) {
2361
2392
  // src/ingest/codex.ts
2362
2393
  import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
2363
2394
  import { createHash as createHash3 } from "crypto";
2364
- import { homedir as homedir3 } from "os";
2365
- import { join as join7, basename as basename2 } from "path";
2366
- var DEFAULT_CODEX_DB_PATH = join7(homedir3(), ".codex", "state_5.sqlite");
2367
- var DEFAULT_CODEX_CONFIG_PATH = join7(homedir3(), ".codex", "config.toml");
2368
- var DEFAULT_CODEWITH_DB_PATH = join7(homedir3(), ".codewith", "state_5.sqlite");
2369
- var DEFAULT_CODEWITH_CONFIG_PATH = join7(homedir3(), ".codewith", "config.toml");
2395
+ import { join as join8, basename as basename2 } from "path";
2370
2396
  var CODEX_INGEST_VERSION = "rollout-events-v4";
2371
2397
  function codexDbPath() {
2372
- return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
2398
+ return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? agentPaths().codexDb;
2373
2399
  }
2374
2400
  function codexConfigPath() {
2375
- return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
2401
+ return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? agentPaths().codexConfig;
2376
2402
  }
2377
2403
  function codewithDbPath() {
2378
- return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? DEFAULT_CODEWITH_DB_PATH;
2404
+ return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? join8(getHomeDir(), ".codewith", "state_5.sqlite");
2379
2405
  }
2380
2406
  function codewithConfigPath() {
2381
- return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? DEFAULT_CODEWITH_CONFIG_PATH;
2407
+ return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? join8(getHomeDir(), ".codewith", "config.toml");
2382
2408
  }
2383
2409
  function codexSources() {
2384
2410
  const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
@@ -2390,7 +2416,7 @@ function codexSources() {
2390
2416
  sessionPrefix: "codex",
2391
2417
  stateSource: "codex"
2392
2418
  }];
2393
- if (!explicitCodexPath || explicitCodewithPath) {
2419
+ if (!explicitCodexPath && !process.env["CODEX_HOME"] || explicitCodewithPath) {
2394
2420
  sources.push({
2395
2421
  label: "Codewith",
2396
2422
  dbPath: codewithDbPath(),
@@ -2628,9 +2654,9 @@ async function ingestCodex(db, verbose = false) {
2628
2654
  // src/ingest/gemini.ts
2629
2655
  import { readdirSync as readdirSync4, readFileSync as readFileSync6, existsSync as existsSync5, statSync as statSync3 } from "fs";
2630
2656
  import { homedir as homedir4 } from "os";
2631
- import { join as join8, basename as basename3 } from "path";
2632
- var DEFAULT_GEMINI_TMP_DIR = join8(homedir4(), ".gemini", "tmp");
2633
- var DEFAULT_GEMINI_HISTORY_DIR = join8(homedir4(), ".gemini", "history");
2657
+ import { join as join9, basename as basename3 } from "path";
2658
+ var DEFAULT_GEMINI_TMP_DIR = join9(homedir4(), ".gemini", "tmp");
2659
+ var DEFAULT_GEMINI_HISTORY_DIR = join9(homedir4(), ".gemini", "history");
2634
2660
  function geminiTmpDir() {
2635
2661
  return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
2636
2662
  }
@@ -2652,7 +2678,7 @@ function listProjectDirs(...roots) {
2652
2678
  try {
2653
2679
  for (const entry of readdirSync4(root, { withFileTypes: true })) {
2654
2680
  if (entry.isDirectory())
2655
- dirs.add(join8(root, entry.name));
2681
+ dirs.add(join9(root, entry.name));
2656
2682
  }
2657
2683
  } catch {
2658
2684
  collectionProblem("failed", "source_unreadable");
@@ -2665,7 +2691,7 @@ function projectRoot(projectDir, chatData) {
2665
2691
  return chatData.projectPath;
2666
2692
  if (chatData.project_path)
2667
2693
  return chatData.project_path;
2668
- const rootFile = join8(projectDir, ".project_root");
2694
+ const rootFile = join9(projectDir, ".project_root");
2669
2695
  try {
2670
2696
  if (existsSync5(rootFile))
2671
2697
  return readFileSync6(rootFile, "utf-8").trim();
@@ -2690,12 +2716,12 @@ async function ingestGemini(db, verbose) {
2690
2716
  const account = await resolveAccountForAgent("gemini");
2691
2717
  const projectDirs = listProjectDirs(tmpDir, historyDir);
2692
2718
  for (const projectDir of projectDirs) {
2693
- const chatsDir = join8(projectDir, "chats");
2719
+ const chatsDir = join9(projectDir, "chats");
2694
2720
  if (!existsSync5(chatsDir))
2695
2721
  continue;
2696
2722
  let chatFiles = [];
2697
2723
  try {
2698
- chatFiles = readdirSync4(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join8(chatsDir, f));
2724
+ chatFiles = readdirSync4(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join9(chatsDir, f));
2699
2725
  } catch {
2700
2726
  collectionProblem("failed", "source_unreadable");
2701
2727
  continue;
@@ -2805,13 +2831,13 @@ async function ingestGemini(db, verbose) {
2805
2831
  // src/ingest/opencode.ts
2806
2832
  import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
2807
2833
  import { homedir as homedir5 } from "os";
2808
- import { join as join9 } from "path";
2809
- var OPENCODE_STORAGE = join9(homedir5(), ".local", "share", "opencode", "storage");
2834
+ import { join as join10 } from "path";
2835
+ var OPENCODE_STORAGE = join10(homedir5(), ".local", "share", "opencode", "storage");
2810
2836
  function walkJsonFiles(dir, acc = []) {
2811
2837
  if (!existsSync6(dir))
2812
2838
  return acc;
2813
2839
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
2814
- const p = join9(dir, entry.name);
2840
+ const p = join10(dir, entry.name);
2815
2841
  if (entry.isDirectory())
2816
2842
  walkJsonFiles(p, acc);
2817
2843
  else if (entry.name.endsWith(".json"))
@@ -2831,7 +2857,7 @@ function parseSessionIdFromPath(filePath) {
2831
2857
  return null;
2832
2858
  }
2833
2859
  async function ingestOpenCode(db, verbose = false, sourceDir = OPENCODE_STORAGE) {
2834
- const messageDir = join9(sourceDir, "message");
2860
+ const messageDir = join10(sourceDir, "message");
2835
2861
  if (!existsSync6(messageDir))
2836
2862
  collectionProblem("missing", "source_missing");
2837
2863
  const files = walkJsonFiles(messageDir);
@@ -3023,13 +3049,13 @@ async function ingestCursor(db, verbose = false) {
3023
3049
  // src/ingest/pi.ts
3024
3050
  import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
3025
3051
  import { homedir as homedir6 } from "os";
3026
- import { join as join10 } from "path";
3027
- var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join10(homedir6(), ".pi", "agent", "sessions");
3052
+ import { join as join11 } from "path";
3053
+ var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join11(homedir6(), ".pi", "agent", "sessions");
3028
3054
  function walkSessions(dir, acc = []) {
3029
3055
  if (!existsSync7(dir))
3030
3056
  return acc;
3031
3057
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
3032
- const p = join10(dir, entry.name);
3058
+ const p = join11(dir, entry.name);
3033
3059
  if (entry.isDirectory())
3034
3060
  walkSessions(p, acc);
3035
3061
  else if (entry.name.endsWith(".json"))
@@ -3129,8 +3155,8 @@ async function ingestPi(db, verbose = false, sourceDir = PI_SESSION_DIR) {
3129
3155
  // src/ingest/hermes.ts
3130
3156
  import { existsSync as existsSync8, statSync as statSync6 } from "fs";
3131
3157
  import { homedir as homedir7 } from "os";
3132
- import { join as join11 } from "path";
3133
- var HERMES_DB = join11(homedir7(), ".hermes", "state.db");
3158
+ import { join as join12 } from "path";
3159
+ var HERMES_DB = join12(homedir7(), ".hermes", "state.db");
3134
3160
  async function ingestHermes(db, verbose = false, sourcePath = HERMES_DB) {
3135
3161
  if (!existsSync8(sourcePath)) {
3136
3162
  collectionProblem("missing", "source_missing");
@@ -3198,7 +3224,7 @@ function statSyncSafe(path) {
3198
3224
  // src/ingest/loops.ts
3199
3225
  import { existsSync as existsSync9 } from "fs";
3200
3226
  import { homedir as homedir8 } from "os";
3201
- import { join as join12 } from "path";
3227
+ import { join as join13 } from "path";
3202
3228
  import { homedir as pathsResolverHomedir } from "os";
3203
3229
  import { join as pathsResolverJoin } from "path";
3204
3230
  var PATHS_RESOLVER_KIND_ENV = {
@@ -3265,8 +3291,8 @@ function defaultLoopsDbPath() {
3265
3291
  app: "loops",
3266
3292
  home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
3267
3293
  });
3268
- const legacy = join12(process.env["HOME"] || process.env["USERPROFILE"] || homedir8(), ".hasna", "loops", "loops.db");
3269
- return existsSync9(join12(resolved, "loops.db")) ? join12(resolved, "loops.db") : legacy;
3294
+ const legacy = join13(process.env["HOME"] || process.env["USERPROFILE"] || homedir8(), ".hasna", "loops", "loops.db");
3295
+ return existsSync9(join13(resolved, "loops.db")) ? join13(resolved, "loops.db") : legacy;
3270
3296
  }
3271
3297
  function loopsDbPath() {
3272
3298
  return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
@@ -3450,33 +3476,6 @@ async function ingestLoops(db, verbose = false) {
3450
3476
 
3451
3477
  // src/ingest/claude-quota.ts
3452
3478
  import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
3453
-
3454
- // src/lib/paths.ts
3455
- import { homedir as homedir9 } from "os";
3456
- import { join as join13 } from "path";
3457
- function getHomeDir() {
3458
- return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
3459
- }
3460
- function agentPaths() {
3461
- const home = getHomeDir();
3462
- return {
3463
- claudeProjects: join13(home, ".claude", "projects"),
3464
- claudeCredentials: join13(home, ".claude", ".credentials.json"),
3465
- takumiProjects: join13(home, ".takumi", "projects"),
3466
- codexDir: join13(home, ".codex"),
3467
- codexDb: join13(home, ".codex", "state_5.sqlite"),
3468
- codexAuth: join13(home, ".codex", "auth.json"),
3469
- codexConfig: join13(home, ".codex", "config.toml"),
3470
- geminiTmp: join13(home, ".gemini", "tmp"),
3471
- geminiHistory: join13(home, ".gemini", "history"),
3472
- opencodeMessages: join13(home, ".local", "share", "opencode", "storage", "message"),
3473
- piSessions: join13(home, ".pi", "agent", "sessions"),
3474
- hermesDir: join13(home, ".hermes"),
3475
- hermesDb: join13(home, ".hermes", "state.db")
3476
- };
3477
- }
3478
-
3479
- // src/ingest/claude-quota.ts
3480
3479
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
3481
3480
  var OAUTH_BETA = "oauth-2025-04-20";
3482
3481
  function readClaudeToken() {
@@ -2587,33 +2587,26 @@ var BUILTIN_TOOLS = [
2587
2587
  ];
2588
2588
 
2589
2589
  // src/ingest/claude.ts
2590
- var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join3(homedir3(), ".claude", "projects");
2591
2590
  var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
2592
2591
 
2593
2592
  // src/ingest/codex.ts
2594
2593
  init_database();
2595
2594
  init_pricing();
2596
- import { homedir as homedir4 } from "os";
2597
- import { join as join4, basename as basename2 } from "path";
2598
- var DEFAULT_CODEX_DB_PATH = join4(homedir4(), ".codex", "state_5.sqlite");
2599
- var DEFAULT_CODEX_CONFIG_PATH = join4(homedir4(), ".codex", "config.toml");
2600
- var DEFAULT_CODEWITH_DB_PATH = join4(homedir4(), ".codewith", "state_5.sqlite");
2601
- var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir4(), ".codewith", "config.toml");
2602
2595
 
2603
2596
  // src/ingest/gemini.ts
2604
2597
  init_database();
2605
2598
  init_pricing();
2606
- import { homedir as homedir5 } from "os";
2607
- import { join as join5, basename as basename3 } from "path";
2608
- var DEFAULT_GEMINI_TMP_DIR = join5(homedir5(), ".gemini", "tmp");
2609
- 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");
2610
2603
 
2611
2604
  // src/ingest/opencode.ts
2612
2605
  init_database();
2613
2606
  init_pricing();
2614
- import { homedir as homedir6 } from "os";
2615
- import { join as join6 } from "path";
2616
- 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");
2617
2610
 
2618
2611
  // src/ingest/quota-observation.ts
2619
2612
  init_database();
@@ -2621,15 +2614,15 @@ init_observation_provenance();
2621
2614
 
2622
2615
  // src/ingest/pi.ts
2623
2616
  init_database();
2624
- import { homedir as homedir7 } from "os";
2625
- import { join as join7 } from "path";
2626
- 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");
2627
2620
 
2628
2621
  // src/ingest/hermes.ts
2629
2622
  init_database();
2630
- import { homedir as homedir8 } from "os";
2631
- import { join as join8 } from "path";
2632
- 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");
2633
2626
 
2634
2627
  // src/ingest/loops.ts
2635
2628
  init_database();
@@ -2702,7 +2695,7 @@ init_ingest_validation();
2702
2695
  init_database();
2703
2696
  import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync as mkdirSync3, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
2704
2697
  import { createHash as createHash4 } from "crypto";
2705
- import { join as join9, resolve as resolve2 } from "path";
2698
+ import { join as join8, resolve as resolve2 } from "path";
2706
2699
  var MAX_COMMANDS = 1000;
2707
2700
  var MAX_BYTES = 64 * 1024 * 1024;
2708
2701
  var RECEIPT_RESERVE = 4096;
@@ -2730,7 +2723,7 @@ function definitiveIngestRefusal(status, body) {
2730
2723
  };
2731
2724
  }
2732
2725
  function getIngestJournalPath() {
2733
- 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");
2734
2727
  }
2735
2728
  function targetId(cloud) {
2736
2729
  const url = new URL(cloud.client.transport.baseUrl);
@@ -2833,7 +2826,7 @@ class IngestJournal {
2833
2826
  constructor(cloud, path = getIngestJournalPath()) {
2834
2827
  this.target = targetId(cloud);
2835
2828
  secureDirectory(path);
2836
- this.directory = join9(realpathSync(path), this.target);
2829
+ this.directory = join8(realpathSync(path), this.target);
2837
2830
  secureDirectory(this.directory);
2838
2831
  }
2839
2832
  names() {
@@ -2859,7 +2852,7 @@ class IngestJournal {
2859
2852
  throw new Error("Orphaned ingest outcome requires maintenance recovery");
2860
2853
  if (name.endsWith(".refusal.json") && set.has(name.replace(".refusal.", ".receipt.")))
2861
2854
  throw new Error("Conflicting ingest receipt and refusal");
2862
- const stat = lstatSync(join9(this.directory, name));
2855
+ const stat = lstatSync(join8(this.directory, name));
2863
2856
  if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
2864
2857
  throw new Error("Unsafe ingest journal record");
2865
2858
  if (stat.size > MAX_INGEST_BYTES + 4096)
@@ -2871,10 +2864,10 @@ class IngestJournal {
2871
2864
  return names;
2872
2865
  }
2873
2866
  file(key, kind) {
2874
- return join9(this.directory, `${sha(key)}.${kind}.json`);
2867
+ return join8(this.directory, `${sha(key)}.${kind}.json`);
2875
2868
  }
2876
2869
  intent(name) {
2877
- const record = readJson(join9(this.directory, name), MAX_INGEST_BYTES + 4096);
2870
+ const record = readJson(join8(this.directory, name), MAX_INGEST_BYTES + 4096);
2878
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`)
2879
2872
  throw new Error("Invalid ingest journal intent");
2880
2873
  validateIngest(record.command);
@@ -3033,7 +3026,7 @@ class IngestJournal {
3033
3026
  // src/lib/ingest-journal-maintenance.ts
3034
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";
3035
3028
  import { createHash as createHash5 } from "crypto";
3036
- import { join as join10 } from "path";
3029
+ import { join as join9 } from "path";
3037
3030
  var uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
3038
3031
  var digest2 = /^[a-f0-9]{64}$/;
3039
3032
  var sha2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
@@ -3093,7 +3086,7 @@ async function confirm(cloud, anchor, deadline) {
3093
3086
  }
3094
3087
  async function verifyJournalAuthority(journal, cloud) {
3095
3088
  journal.assertCloud(cloud);
3096
- const path = join10(journal.directory, ".authority.json");
3089
+ const path = join9(journal.directory, ".authority.json");
3097
3090
  if (!exists(path)) {
3098
3091
  const names = journal.names();
3099
3092
  const name = names.find((n) => n.endsWith(".intent.json") && names.includes(n.replace(".intent.", ".receipt.")));
@@ -3137,8 +3130,8 @@ class IngestJournalMaintenance {
3137
3130
  this.cloud = cloud;
3138
3131
  this.factory = factory;
3139
3132
  journal.assertCloud(cloud);
3140
- this.control = join10(journal.directory, ".maintenance");
3141
- this.operationRoot = join10(journal.directory, ".recovery");
3133
+ this.control = join9(journal.directory, ".maintenance");
3134
+ this.operationRoot = join9(journal.directory, ".recovery");
3142
3135
  }
3143
3136
  manifest() {
3144
3137
  secureDirectory(this.control);
@@ -3147,10 +3140,10 @@ class IngestJournalMaintenance {
3147
3140
  throw new Error("Unexpected maintenance entry");
3148
3141
  let size = 0;
3149
3142
  for (const name of names)
3150
- size += lstatSync2(join10(this.control, name)).size;
3143
+ size += lstatSync2(join9(this.control, name)).size;
3151
3144
  if (size > MAINTENANCE_RESERVE)
3152
3145
  throw new Error("Maintenance metadata exceeds reserve");
3153
- 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);
3154
3147
  for (const part of [m.intent, m.receipt_file])
3155
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))
3156
3149
  throw new Error("Invalid maintenance snapshot");
@@ -3175,7 +3168,7 @@ class IngestJournalMaintenance {
3175
3168
  if (!operation.receipt || operation.receipt.sha256 !== expected.capture_sha256 || operation.receipt.sizeBytes !== expected.sizeBytes || operation.receipt.mode !== expected.mode)
3176
3169
  throw new Error("Trash snapshot mismatch");
3177
3170
  sameSource(operation.path, expected);
3178
- const path = join10(this.control, `${kind}.operation.json`);
3171
+ const path = join9(this.control, `${kind}.operation.json`);
3179
3172
  createExact(path, { id: operation.id });
3180
3173
  if (readJson(path, 1024).id !== operation.id)
3181
3174
  throw new Error("Conflicting Trash operation");
@@ -3200,7 +3193,7 @@ class IngestJournalMaintenance {
3200
3193
  async pairFile(backend, m, kind, deadline) {
3201
3194
  await confirm(this.cloud, m, deadline);
3202
3195
  const path = this.journal.file(m.key, kind), expected = kind === "receipt" ? m.receipt_file : m.intent;
3203
- const saved = join10(this.control, `${kind}.operation.json`);
3196
+ const saved = join9(this.control, `${kind}.operation.json`);
3204
3197
  let id;
3205
3198
  if (exists(saved))
3206
3199
  id = readJson(saved, 1024).id;
@@ -3306,14 +3299,14 @@ class IngestJournalMaintenance {
3306
3299
  key: intent.command.idempotency_key,
3307
3300
  body_hash: intent.body_hash,
3308
3301
  receipt,
3309
- intent: snapshot(join10(this.journal.directory, name)),
3302
+ intent: snapshot(join9(this.journal.directory, name)),
3310
3303
  receipt_file: snapshot(this.journal.file(intent.command.idempotency_key, "receipt"))
3311
3304
  };
3312
3305
  await confirm(this.cloud, m, deadline);
3313
3306
  m.intent.capture_sha256 = backend.snapshot(this.journal.file(m.key, "intent")).sha256;
3314
3307
  m.receipt_file.capture_sha256 = backend.snapshot(this.journal.file(m.key, "receipt")).sha256;
3315
3308
  secureDirectory(this.control);
3316
- if (!createExact(join10(this.control, "manifest.json"), m))
3309
+ if (!createExact(join9(this.control, "manifest.json"), m))
3317
3310
  throw new Error("Existing maintenance transaction");
3318
3311
  await this.finish(backend, deadline);
3319
3312
  retired++;
@@ -3345,7 +3338,7 @@ class IngestJournalMaintenance {
3345
3338
 
3346
3339
  // src/lib/ingest-journal-lock.ts
3347
3340
  import { constants as constants3, closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3 } from "fs";
3348
- import { join as join11 } from "path";
3341
+ import { join as join10 } from "path";
3349
3342
  async function withIngestJournalLock(directory, action) {
3350
3343
  if (process.platform !== "linux" && process.platform !== "darwin")
3351
3344
  throw new Error("Ingest journal locking requires Linux or macOS");
@@ -3353,7 +3346,7 @@ async function withIngestJournalLock(directory, action) {
3353
3346
  const library = dlopen(process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", {
3354
3347
  flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 }
3355
3348
  });
3356
- const path = join11(directory, ".lease");
3349
+ const path = join10(directory, ".lease");
3357
3350
  let fd;
3358
3351
  try {
3359
3352
  fd = openSync3(path, constants3.O_RDWR | constants3.O_CREAT | constants3.O_NOFOLLOW | constants3.O_NONBLOCK, 384);
@@ -3487,7 +3480,7 @@ async function pushIngestRows(cloud, scratch) {
3487
3480
  // package.json
3488
3481
  var package_default = {
3489
3482
  name: "@hasna/economy",
3490
- version: "0.5.4",
3483
+ version: "0.5.6",
3491
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",
3492
3485
  repository: {
3493
3486
  type: "git",
@@ -3520,6 +3513,8 @@ var package_default = {
3520
3513
  files: [
3521
3514
  "dist",
3522
3515
  "docs",
3516
+ "native/Tools/pair-qr.py",
3517
+ "native/Tools/pair-qr.swift",
3523
3518
  "postinstall.js",
3524
3519
  "LICENSE",
3525
3520
  "README.md",